Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ This guide owns how to work in the repository: documentation ownership, the repo
| Core implementation constraints beyond the public contracts | [Implementation constraints](services/core/IMPLEMENTATION.md) |
| Environment ownership, preparation, Skills, Plugins, packages and MCP bindings | [Environments](contracts/agents-api/environments.md) |
| Built-in Harness identifiers and display names | `internal/harnessconfig/builtin/catalog.json` and its [generated reference](contracts/agents-api/harness-catalog.md) |
| Harness registration, service qualification and acceptance | [Harness onboarding](contracts/agents-api/harness-onboarding.md) |
| Harness capabilities by placement | [Harness capabilities](contracts/agents-api/harness-capabilities.md) |
| Harness registration, support declaration and acceptance | [Harness onboarding](contracts/agents-api/harness-onboarding.md) |
| Harness selection, model providers and native parameters | [Model execution](contracts/agents-api/model-execution.md) |
| Provider registration and lifecycle | [Sandbox Provider guide](docs/sandbox-provider.md) |
| Sandbox deployment, selection and administrative transitions | [Sandbox deployment](contracts/agents-api/sandbox-deployment.md) |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ The [installation guide](https://openagentcore.dev/docs/getting-started/install)
| Build an application on the API | [Quickstart](https://openagentcore.dev/docs/getting-started/quickstart), then the [Agents API guide](https://openagentcore.dev/docs/api/public-agent-api) |
| See a complete application | [Examples](https://openagentcore.dev/docs/examples) |
| Run agents on my own machine | [Self-hosted execution](https://openagentcore.dev/docs/getting-started/self-hosted) |
| Check Harness capabilities and limits | [Harness capabilities](https://openagentcore.dev/contracts/agents-api/harness-capabilities) |
| Check Harness differences and limits | [Known gaps](https://openagentcore.dev/contracts/agents-api/#known-gaps) |
| Understand the design | [Architecture](https://openagentcore.dev/docs/architecture) |
| Add a sandbox, harness or other component | [Developer guide](https://openagentcore.dev/docs/development) |

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ irm https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/install
| 基于 API 开发应用 | [快速开始](https://openagentcore.dev/zh/docs/getting-started/quickstart),然后看 [Agents API 指南](https://openagentcore.dev/zh/docs/api/public-agent-api) |
| 看一个完整的应用 | [示例](https://openagentcore.dev/zh/docs/examples) |
| 在自己的机器上运行 Agent | [自托管执行](https://openagentcore.dev/zh/docs/getting-started/self-hosted) |
| 查看 Harness 能力和限制 | [Harness 能力](https://openagentcore.dev/zh/contracts/agents-api/harness-capabilities) |
| 查看 Harness 差异和限制 | [已知缺口](https://openagentcore.dev/zh/contracts/agents-api/#known-gaps) |
| 了解设计 | [架构说明](https://openagentcore.dev/zh/docs/architecture) |
| 接入新的沙箱、Harness 或其他组件 | [开发指南](https://openagentcore.dev/zh/docs/development) |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func TestLiveClaudeSDKCancelResume(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
out := make(chan proto.Envelope, 64)
request := proto.PromptRequestPayload{RunID: uuid.NewString(), Input: proto.TextInput(prompt), AgentSessionID: resume, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, Model: "MiniMax-M3", ModelProvider: provider, SystemPrompt: "Follow the user's requested format. Preserve the exact verification value in conversation history. Use no tools."}
request := proto.PromptRequestPayload{RunID: uuid.NewString(), Input: proto.TextInput(prompt), AgentSessionID: resume, DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, Model: "MiniMax-M3", ModelProvider: provider, SystemPrompt: "Follow the user's requested format. Preserve the exact verification value in conversation history. Use no tools."}
running, err := startSingleTurn(ctx, config, request, out)
if err != nil {
t.Fatal(err)
Expand Down
36 changes: 9 additions & 27 deletions apps/daemon/internal/agent/claudesdk/declaration.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,11 @@ import (
const claudeSDKEntrypointEnv = "OAC_RUNTIME_CLAUDE_SDK_ENTRYPOINT"
const claudeSDKNodeEnv = "OAC_RUNTIME_CLAUDE_SDK_NODE"

// Declaration owns Claude SDK discovery, configuration and execution factories.
var Declaration = agent.Declaration{Info: proto.SupportedAgentKind{Kind: "claude_sdk", Capabilities: proto.AgentKindCapabilities{
SubagentObservations: proto.CapabilityUnsupported,
NativeSessionRecovery: proto.CapabilityUnsupported,
EnvironmentNone: proto.CapabilitySupported,
LocalEnvironment: proto.CapabilityUnsupported,
WorkspaceReadPreparation: proto.CapabilityUnsupported,
WorkspaceOutputExport: proto.CapabilityUnsupported,
ProgrammaticToolCallingDisable: proto.CapabilitySupported,
WebSearchControl: proto.CapabilityUnsupported,
TextVerbosity: proto.CapabilityUnsupported,
StructuredOutput: proto.CapabilityUnsupported,
ToolSearch: proto.CapabilityUnsupported,
MessageImages: proto.CapabilityUnsupported,
FunctionResultImages: proto.CapabilityUnsupported,
SubagentControl: proto.CapabilitySupported,
FunctionTools: proto.CapabilitySupported,
MCPHTTPTools: proto.CapabilityUnsupported,
MCPHTTPRequired: proto.CapabilityUnsupported,
MCPHTTPBearerAuth: proto.CapabilityUnsupported,
}}, Configuration: configuration.Configuration(), Discover: discover}
// Declaration owns Claude SDK discovery, configuration and execution
// factories. Discovery narrows the declared support to what the installed
// bundle serves.
var Declaration = agent.Declaration{Info: proto.SupportedAgentKind{Kind: "claude_sdk", Capabilities: configuration.Configuration().Declaration.Capabilities},
Configuration: configuration.Configuration(), Discover: discover}

func discover(ctx context.Context, options agent.DiscoveryOptions, info proto.SupportedAgentKind) *agent.Runtime {
return discoverWithCheck(ctx, options, info, CheckRuntime)
Expand Down Expand Up @@ -100,13 +84,11 @@ func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, d
if err != nil {
return fail(err)
}
caps := &out.Info.Capabilities
if config.Workspace != nil {
if !info.SupportsLocalRuntime() {
return fail(fmt.Errorf("Claude SDK bundle does not support the local Runtime contract"))
}
caps.NativeSessionRecovery = proto.CapabilitySupported
if config.Workspace != nil && !info.SupportsLocalRuntime() {
return fail(fmt.Errorf("Claude SDK bundle does not support the local Runtime contract"))
}
caps := &out.Info.Capabilities
caps.NativeSessionRecovery = proto.CapabilityFromBool(config.Workspace != nil)
// One declaration holds for every Executor of the install: the workspace
// bridge, the agent-host view and a Runtime without a workspace, so each
// feature is its workspace variant, which the others also support.
Expand Down
23 changes: 1 addition & 22 deletions apps/daemon/internal/agent/claudesdk/declaration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ import (
"io"
"os"
"path/filepath"
"reflect"
"slices"
"strings"
"testing"

"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent"
"github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto"
)

func TestClaudeSDKInvalidPathsFailBeforeProbe(t *testing.T) {
Expand Down Expand Up @@ -66,25 +64,6 @@ func TestClaudeSDKFeatureDiscovery(t *testing.T) {
}
}

// The declaration must retain the complete baseline capability descriptor.
func TestDeclaredCapabilityBaseline(t *testing.T) {
expected := map[string]bool{"EnvironmentNone": true, "ProgrammaticToolCallingDisable": true, "SubagentControl": true, "FunctionTools": true}
value := reflect.ValueOf(Declaration.Info.Capabilities)
for i := 0; i < value.NumField(); i++ {
name := value.Type().Field(i).Name
want := proto.CapabilityUnsupported
if expected[name] {
want = proto.CapabilitySupported
}
if got := value.Field(i).Interface(); got != want {
t.Errorf("%s = %v, want %v", name, got, want)
}
}
if err := Declaration.Info.ValidateDeclaration(); err != nil {
t.Fatal(err)
}
}

func TestRuntimeDiscoveryConfigurationAndRegistration(t *testing.T) {
root := t.TempDir()
t.Setenv("OAC_RUNTIME_HOME", root)
Expand Down Expand Up @@ -118,7 +97,7 @@ func TestRuntimeDiscoveryConfigurationAndRegistration(t *testing.T) {
}
continue
}
if calls != 1 || runtime.Info.Available != ready || (runtime.Executor != nil) != ready || runtime.Info.Capabilities.LocalEnvironment.IsSupported() {
if calls != 1 || runtime.Info.Available != ready || (runtime.Executor != nil) != ready || ready && runtime.Info.Capabilities.LocalEnvironment.IsSupported() {
t.Fatalf("runtime: %+v", runtime)
}
}
Expand Down
59 changes: 10 additions & 49 deletions apps/daemon/internal/agent/claudesdk/execution_controls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func TestExecutionControlsPreserveNativeDefaultsAndInstructions(t *testing.T) {
if err != nil {
t.Fatal(err)
}
request.ExecutionControls = &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}
request.ExecutionControls = &proto.ExecutionControls{TextVerbosity: "medium"}
before, _ := json.Marshal(request)
controlled, _, err := prepareConfiguration(config, request)
if err != nil {
Expand All @@ -34,33 +34,6 @@ func TestExecutionControlsPreserveNativeDefaultsAndInstructions(t *testing.T) {
}
}

func TestExecutionControlsRejectUnsupportedProfilesBeforeLaunch(t *testing.T) {
cases := map[string]proto.ExecutionControls{
"empty": {}, "missing-search": {TextVerbosity: "medium"}, "missing-verbosity": {WebSearch: "disabled"},
"cached-search": {WebSearch: "cached", TextVerbosity: "medium"},
"live-search": {WebSearch: "live", TextVerbosity: "medium"},
"unknown-search": {WebSearch: "invalid", TextVerbosity: "medium"},
"low-verbosity": {WebSearch: "disabled", TextVerbosity: "low"},
"high-verbosity": {WebSearch: "disabled", TextVerbosity: "high"},
"unknown-verbosity": {WebSearch: "disabled", TextVerbosity: "invalid"},
}
for name, controls := range cases {
t.Run(name, func(t *testing.T) {
root := t.TempDir()
t.Setenv("OAC_RUNTIME_HOME", root)
config := Config{Node: "must-not-run", Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")}
request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), RunID: "run", Input: proto.TextInput("Original input."), ExecutionControls: &controls, Model: "native-model"}
_, err := startSingleTurn(t.Context(), config, request, make(chan proto.Envelope, 1))
if err == nil || !strings.Contains(err.Error(), "execution controls require") {
t.Fatal("unsupported controls did not fail at admission", err)
}
if _, err := os.Stat(config.StateDir); !os.IsNotExist(err) {
t.Fatal("unsupported controls reached native setup", err)
}
})
}
}

func TestMCPWithoutEnvironmentNoneRejectedBeforeSetup(t *testing.T) {
root := t.TempDir()
t.Setenv("OAC_RUNTIME_HOME", root)
Expand All @@ -81,44 +54,32 @@ func TestStructuredOutputConfigurationReachesNativeUnchanged(t *testing.T) {
t.Setenv("OAC_RUNTIME_HOME", root)
config := Config{Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")}
schema := json.RawMessage(`{"type":"object","properties":{"n":{"const":9007199254740992}}}`)
request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), DisableSubagents: true, Model: "model", SystemPrompt: "Original instructions.", ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium", OutputFormat: &proto.OutputFormat{Type: "json_schema", Schema: schema}}}
request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), DisableSubagents: true, Model: "model", SystemPrompt: "Original instructions.", ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium", OutputFormat: &proto.OutputFormat{Type: "json_schema", Schema: schema}}}
start, _, err := prepareConfiguration(config, request)
if err != nil {
t.Fatal(err)
}
if start.OutputFormat == nil || string(start.OutputFormat.Schema) != string(schema) || start.SystemPrompt != "Original instructions." {
t.Fatal("native configuration changed")
}
request.ExecutionControls.OutputFormat.Schema = json.RawMessage(`{"type":"object","const":9007199254740993}`)
if _, _, err := prepareConfiguration(config, request); err == nil {
t.Fatal("lossy schema accepted")
}
request.ExecutionControls.OutputFormat.Schema = schema
request.DisableSubagents = false
if _, _, err := prepareConfiguration(config, request); err == nil {
t.Fatal("unqualified subagent combination accepted")
}
}

func TestToolDiscoveryPreservesFrozenFunctionsAndRejectsOtherProfiles(t *testing.T) {
// Tool discovery keeps the frozen definitions; a typeless parameters root
// becomes an object root, which admits the same arguments.
func TestToolDiscoveryPreservesFrozenFunctions(t *testing.T) {
root := t.TempDir()
t.Setenv("OAC_RUNTIME_HOME", root)
config := Config{Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")}
request := proto.PromptRequestPayload{ModelProvider: fixtureProvider(), DisableSubagents: true, ToolSearch: true, Model: "model", FunctionTools: []proto.FunctionTool{
{Name: "lookup", Description: "Lookup", Parameters: json.RawMessage(`{"type":"object","properties":{"ticket":{"const":"original"}}}`), DeferLoading: true},
{Name: "clock", Description: "Clock", Parameters: json.RawMessage(`{"type":"object"}`)},
{Name: "clock", Description: "Clock", Parameters: json.RawMessage(`{"properties":{}}`)},
{Name: "note", Description: "Note", Parameters: json.RawMessage(`{"type":["object","null"]}`)},
}}
start, _, err := prepareConfiguration(config, request)
if err != nil || !start.ToolSearch || !reflect.DeepEqual(start.Functions, request.FunctionTools) {
if err != nil || !start.ToolSearch || !reflect.DeepEqual(start.Functions[0], request.FunctionTools[0]) || string(start.Functions[1].Parameters) != `{"properties":{},"type":"object"}` || string(start.Functions[2].Parameters) != `{"type":"object"}` {
t.Fatal("function discovery changed native definitions", err)
}
request.DisableSubagents = false
if _, _, err := prepareConfiguration(config, request); err == nil {
t.Fatal("unqualified combination admitted")
}
request.DisableSubagents = true
request.ToolSearch = false
if _, _, err := prepareConfiguration(config, request); err == nil {
t.Fatal("deferred definitions became eager")
if string(request.FunctionTools[1].Parameters) != `{"properties":{}}` {
t.Fatal("typing the root changed the frozen request")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func TestLiveClaudeExecutorReuseAndCancel(t *testing.T) {
_ = os.WriteFile(filepath.Join(proof, "executor-evidence.json"), raw, 0600)
}
defer persist()
request := proto.PromptRequestPayload{DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{WebSearch: "disabled", TextVerbosity: "medium"}, Model: model, ModelProvider: provider, SystemPrompt: "Follow requested formats briefly. Remember the exact verification marker across the conversation. Use no tools."}
request := proto.PromptRequestPayload{DisableExecutionEnvironment: true, DisableSubagents: true, ExecutionControls: &proto.ExecutionControls{TextVerbosity: "medium"}, Model: model, ModelProvider: provider, SystemPrompt: "Follow requested formats briefly. Remember the exact verification marker across the conversation. Use no tools."}
factory := NewExecutorFactory(config)
prepared := time.Now()
owner, err := factory(ctx, request)
Expand Down
41 changes: 25 additions & 16 deletions apps/daemon/internal/agent/claudesdk/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"slices"
"strings"
"sync"
"time"
Expand All @@ -25,18 +26,34 @@ type functionState struct {
closed bool
}

func validateFunctions(tools []proto.FunctionTool) error {
// functionTools types each parameters root as an object, which the native SDK
// requires. A typeless root or a union with object admits the same tool
// arguments, since these are always objects.
func functionTools(tools []proto.FunctionTool) ([]proto.FunctionTool, error) {
names := map[string]bool{}
for _, tool := range tools {
var schema struct {
Type string `json:"type"`
}
if strings.TrimSpace(tool.Name) == "" || names[tool.Name] || json.Unmarshal(tool.Parameters, &schema) != nil || schema.Type != "object" {
return fmt.Errorf("claudesdk: functions require unique names and object-root JSON schemas")
typed := slices.Clone(tools)
for i, tool := range typed {
var schema map[string]json.RawMessage
var root string
var union []string
if strings.TrimSpace(tool.Name) == "" || names[tool.Name] || json.Unmarshal(tool.Parameters, &schema) != nil || schema == nil {
return nil, fmt.Errorf("claudesdk: functions require unique names and object JSON schemas")
}
names[tool.Name] = true
switch t := schema["type"]; {
case json.Unmarshal(t, &root) == nil && root == "object":
case t == nil || json.Unmarshal(t, &union) == nil && slices.Contains(union, "object"):
schema["type"] = json.RawMessage(`"object"`)
parameters, err := json.Marshal(schema)
if err != nil {
return nil, err
}
typed[i].Parameters = parameters
default:
return nil, fmt.Errorf("claudesdk: functions require unique names and object JSON schemas")
}
}
return nil
return typed, nil
}

func (s *session) receiveFunction(event bridgeEvent, start startRequest, emit func(string, any)) error {
Expand Down Expand Up @@ -104,14 +121,6 @@ func (s *session) SubmitFunctionResult(ctx context.Context, result proto.Functio
if err := result.ValidateContent(); err != nil {
return err
}
for _, part := range result.Content {
if part.Type == "input_image" && !result.Success {
return fmt.Errorf("claudesdk: native error results cannot retain images")
}
}
if err := (proto.MessageInput{{Content: result.Content}}).ValidateInlineImages(); err != nil {
return err
}
data, err := json.Marshal(struct {
Type string `json:"type"`
TurnID string `json:"turn_id"`
Expand Down
5 changes: 0 additions & 5 deletions apps/daemon/internal/agent/claudesdk/functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,6 @@ func TestFunctionTurnNativeReceipts(t *testing.T) {
if err := running.SubmitFunctionResult(ctx, invalid); err == nil {
t.Fatal("missing content consumed call")
}
image := "https://example.invalid/image"
invalid.Content = []proto.InputContent{{Type: "input_image", ImageURL: &image}}
if err := running.SubmitFunctionResult(ctx, invalid); err == nil {
t.Fatal("image should fail before delivery")
}
first, second := "first-"+call.CallID, "second-"+call.CallID
value := proto.FunctionResultPayload{DeliveryID: "delivery-" + call.CallID, CallID: call.CallID, Success: call.CallID == "b", Content: []proto.InputContent{{Type: "input_text", Text: &first}, {Type: "input_text", Text: &second}}}
go func() { submissions <- running.SubmitFunctionResult(ctx, value) }()
Expand Down
Loading
Loading