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
2 changes: 1 addition & 1 deletion apps/daemon/internal/agent/claudesdk/declaration.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func discoverWithCheck(parent context.Context, options agent.DiscoveryOptions, d
for _, capability := range []*proto.CapabilitySupport{&caps.LocalEnvironment, &caps.WorkspaceReadPreparation, &caps.WorkspaceOutputExport, &caps.NativeSessionRecovery} {
agent.Narrow(capability, local)
}
agent.Narrow(&caps.RetainedNativeHistory, local && info.SDK == "0.3.269" && info.Native == "2.1.269 (Claude Code)")
agent.Narrow(&caps.RetainedNativeHistory, local && info.SDK == declaration.Declaration.Native.Version)
agent.Narrow(&caps.FunctionTools, !local || info.SupportsWorkspaceFunctions())
agent.Narrow(&caps.MessageImages, info.SupportsMessageImages())
agent.Narrow(&caps.FunctionResultImages, info.SupportsFunctionResultImages())
Expand Down
5 changes: 3 additions & 2 deletions apps/daemon/internal/agent/claudesdk/declaration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent"
"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/contracttest"
declaration "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/claudesdk"
)

func TestClaudeSDKInvalidPathsFailBeforeProbe(t *testing.T) {
Expand Down Expand Up @@ -47,7 +48,7 @@ func TestClaudeSDKFeatureDiscovery(t *testing.T) {
t.Setenv(claudeSDKNodeEnv, node)
for _, features := range [][]string{nil, {"mcp_http_tools"}, {"mcp_http_bearer_auth"}, {"mcp_http_tools", "mcp_http_bearer_auth"}, {"mcp_http_required"}, {"mcp_http_tools", "mcp_http_required"}, {"subagent_resources"}, {"structured_output"}} {
out := discoverWithCheck(t.Context(), agent.DiscoveryOptions{RuntimeRoot: root, StateRoot: t.TempDir(), Profile: "default", Stdout: &strings.Builder{}, Stderr: &strings.Builder{}}, Declaration.Info(), func(context.Context, Config) (RuntimeInfo, error) {
info := RuntimeInfo{SDK: "0.3.269", Native: "2.1.269 (Claude Code)", Features: features}
info := RuntimeInfo{SDK: declaration.Declaration.Native.Version, Native: "native", Features: features}
return info, nil
})
supported := len(features) > 0 && features[0] == "mcp_http_tools"
Expand Down Expand Up @@ -75,7 +76,7 @@ func TestDiscoveryOnlyNarrowsDeclaration(t *testing.T) {
t.Setenv(claudeSDKNodeEnv, node)
features := []string{"message_images", "function_result_images", "tool_search", "structured_output", "subagent_resources", "mcp_http_tools", "mcp_http_bearer_auth", "mcp_http_required"}
out := discoverWithCheck(t.Context(), agent.DiscoveryOptions{RuntimeRoot: root, StateRoot: t.TempDir(), Profile: "default", Stdout: io.Discard, Stderr: io.Discard}, Declaration.Info(), func(context.Context, Config) (RuntimeInfo, error) {
return RuntimeInfo{SDK: "0.3.269", Native: "2.1.269 (Claude Code)", Features: features}, nil
return RuntimeInfo{SDK: declaration.Declaration.Native.Version, Native: "native", Features: features}, nil
})
if out == nil || !out.Info.Available {
t.Fatal("runtime unavailable")
Expand Down
8 changes: 4 additions & 4 deletions apps/daemon/internal/agent/claudesdk/executor_fixture_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ func helperTurn(scanner *bufio.Scanner, request *startRequest) (func(bridgeEvent
os.Exit(4)
}
var start struct {
Type string `json:"type"`
TurnID string `json:"turn_id"`
Input json.RawMessage `json:"input"`
Type string `json:"type"`
TurnID string `json:"turn_id"`
Input proto.MessageInput `json:"input"`
}
if json.Unmarshal(scanner.Bytes(), &start) != nil || start.Type != "turn_start" || start.TurnID == "" || json.Unmarshal(start.Input, &request.Input) != nil {
if json.Unmarshal(scanner.Bytes(), &start) != nil || start.Type != "turn_start" || start.TurnID == "" {
os.Exit(5)
}
return helperTurnOutput(scanner, start.TurnID)
Expand Down
4 changes: 2 additions & 2 deletions apps/daemon/internal/agent/claudesdk/installation.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (
)

func Installation() agent.Installation {
return agent.Installation{AgentKind: "claude_sdk", Version: declaration.Declaration.Native.Version, Supported: func() bool { return runtime.GOOS == "linux" || runtime.GOOS == "darwin" || runtime.GOOS == "windows" },
return agent.Installation{AgentKind: declaration.Declaration.Kind, Version: declaration.Declaration.Native.Version, Supported: func() bool { return runtime.GOOS == "linux" || runtime.GOOS == "darwin" || runtime.GOOS == "windows" },
Environment: func(dir, node string) map[string]string {
return map[string]string{"OAC_RUNTIME_CLAUDE_SDK_ENTRYPOINT": filepath.Join(dir, "dist", "main.js"), "OAC_RUNTIME_CLAUDE_SDK_NODE": node}
return map[string]string{claudeSDKEntrypointEnv: filepath.Join(dir, "dist", "main.js"), claudeSDKNodeEnv: node}
},
Check: func(ctx context.Context, dir, node string, env []string) error {
got, err := CheckRuntime(ctx, Config{Node: node, Entrypoint: filepath.Join(dir, "dist", "main.js"), Env: env})
Expand Down
3 changes: 1 addition & 2 deletions apps/daemon/internal/agent/claudesdk/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ type startRequest struct {
Subagents *subagentOptions `json:"subagents,omitempty"`
OutputFormat *proto.OutputFormat `json:"output_format,omitempty"`
Type string `json:"type"`
Input proto.MessageInput `json:"input,omitempty"`
Model string `json:"model"`
SystemPrompt string `json:"system_prompt"`
Cwd string `json:"cwd"`
Expand All @@ -42,7 +41,7 @@ type startRequest struct {
}

func prepareConfiguration(config Config, req proto.PromptRequestPayload) (startRequest, []string, error) {
start := startRequest{Type: "start", Resume: req.AgentSessionID, RequireHistory: req.RequireExistingNativeSession, Functions: req.FunctionTools}
start := startRequest{Resume: req.AgentSessionID, RequireHistory: req.RequireExistingNativeSession, Functions: req.FunctionTools}
fail := func(reason string) (startRequest, []string, error) {
return startRequest{}, nil, fmt.Errorf("claudesdk: %s", reason)
}
Expand Down
2 changes: 1 addition & 1 deletion apps/daemon/internal/agent/claudesdk/preparation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func TestPreparationWaitsForReceiptAndRetainsConfiguration(t *testing.T) {
if err := json.Unmarshal(raw, &frozen); err != nil {
t.Fatal(err)
}
if frozen.Model != "fixture" || frozen.Resume != "native-session" || frozen.Workspace == nil || len(frozen.Input) != 0 {
if frozen.Model != "fixture" || frozen.Resume != "native-session" || frozen.Workspace == nil {
t.Fatal("configuration-only request was not retained")
}
config.Env[0] = "ANTHROPIC_AUTH_TOKEN=changed"
Expand Down
2 changes: 1 addition & 1 deletion apps/daemon/internal/agent/codex/installation.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func Installation() agent.Installation {
}
return filepath.Join(dir, "bin", name)
}
return agent.Installation{AgentKind: "codex", Version: declaration.Declaration.Native.Version, Supported: func() bool { return runtime.GOOS == "linux" || runtime.GOOS == "darwin" || runtime.GOOS == "windows" },
return agent.Installation{AgentKind: declaration.Declaration.Kind, Version: declaration.Declaration.Native.Version, Supported: func() bool { return runtime.GOOS == "linux" || runtime.GOOS == "darwin" || runtime.GOOS == "windows" },
Environment: func(dir, node string) map[string]string {
return map[string]string{binaryEnv: binary(dir)}
},
Expand Down
2 changes: 1 addition & 1 deletion apps/daemon/internal/agent/mcode/discovery_workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func discoverWorkspace(parent context.Context, options agent.DiscoveryOptions, r
fail(err)
return nil
}
c, err := ConfigureLocal(binary, node, os.Getenv("OAC_RUNTIME_MCODE_COMPANION"), binding.Workspace())
c, err := ConfigureLocal(binary, node, os.Getenv(companionEnv), binding.Workspace())
if err == nil {
err = CheckWorkspace(parent, c)
}
Expand Down
5 changes: 3 additions & 2 deletions apps/daemon/internal/agent/mcode/installation.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import (
"fmt"
"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent"
"github.com/MiniMax-AI/OpenAgentCore/apps/daemon/internal/agent/installroot"
declaration "github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig/mcode"
"path/filepath"
"runtime"
)

func Installation() agent.Installation {
return agent.Installation{AgentKind: "mcode", Version: SupportedVersion, Supported: func() bool { return runtime.GOOS == "linux" || runtime.GOOS == "darwin" },
return agent.Installation{AgentKind: declaration.Declaration.Kind, Version: SupportedVersion, Supported: func() bool { return runtime.GOOS == "linux" || runtime.GOOS == "darwin" },
Environment: func(dir, node string) map[string]string {
return map[string]string{binaryEnv: filepath.Join(dir, "native", "cli.js"), nodeEnv: node, "OAC_RUNTIME_MCODE_COMPANION": dir}
return map[string]string{binaryEnv: filepath.Join(dir, "native", "cli.js"), nodeEnv: node, companionEnv: dir}
},
Check: func(ctx context.Context, dir, node string, env []string) error {
got, err := installroot.Probe(ctx, node, []string{filepath.Join(dir, "native", "cli.js"), "--version"}, env, dir)
Expand Down
3 changes: 2 additions & 1 deletion apps/daemon/internal/agent/mcode/subagents.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,14 @@ type nativeSubagentTask struct {
}

const nodeEnv = "OAC_RUNTIME_MCODE_NODE"
const companionEnv = "OAC_RUNTIME_MCODE_COMPANION"

// runtimeNode is the Node executable the installation or image provides for the companion scripts.
func runtimeNode() string { return os.Getenv(nodeEnv) }

func subagentReader() (string, string, error) {
node := runtimeNode()
reader := filepath.Join(os.Getenv("OAC_RUNTIME_MCODE_COMPANION"), "subagent-snapshot.mjs")
reader := filepath.Join(os.Getenv(companionEnv), "subagent-snapshot.mjs")
for _, path := range []string{node, reader} {
resolved, err := filepath.EvalSymlinks(path)
if err != nil || !filepath.IsAbs(path) || resolved != path {
Expand Down
4 changes: 2 additions & 2 deletions contracts/agents-api/core.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1351,7 +1351,7 @@ definitions:
compute_phase:
type: string
compute_phase_changed_at:
description: The time the allocation entered its current compute_phase, or null when unknown; an allocation that existed before Core recorded it reports null until its next phase change. For a suspended microsandbox allocation, this time plus the deployment's snapshot retention tells roughly when Core reclaims it.
description: The time the allocation entered its current compute_phase, or null when unknown; an allocation that existed before Core recorded it reports null until its next phase change. For a suspended allocation, this time plus the deployment's snapshot retention tells roughly when Core reclaims it.
type: string
x-nullable: true
created_at:
Expand Down Expand Up @@ -1545,7 +1545,7 @@ definitions:
max_active:
type: integer
max_retained:
description: Docker never suspends, so Core replaces this with max_active; microsandbox uses both limits.
description: A Provider that does not checkpoint never suspends, so Core replaces this with max_active; a Provider that checkpoints uses both limits.
type: integer
name:
type: string
Expand Down
31 changes: 29 additions & 2 deletions packages/claude-sdk-adapter/tests/executor.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ globalThis.startupFixture=async({options})=>{
const [a,b]=InMemoryTransport.createLinkedPair();
await Promise.all([client.connect(a),options.mcpServers.functions.instance.connect(b)]);
}
let queried=false,interrupt,number=0;
let queried=false,interrupt,number=0,later;
const close=()=>{child.stdin.end();interrupt?.()};
options.abortController.signal.addEventListener('abort',close);
return {close,query(prompt){assert.equal(queried,false);queried=true;process.send({type:'query'});return {
Expand All @@ -32,6 +32,18 @@ globalThis.startupFixture=async({options})=>{
const text=user.message.content[0].text;
process.send({type:'input',text});
yield {type:'system',subtype:'init',session_id:'native',tools:client ? ['mcp__functions__lookup'] : [],mcp_servers:client ? [{name:'functions',status:'connected'}] : []};
if(process.argv[1]==='later-function'){
// The call is dispatched independently of the result stream and belongs to the next native turn.
if(number===1){
later=client.callTool({name:'lookup',arguments:{text},_meta:{'claudecode/toolUseId':'later-call'}});
await new Promise(resolve=>process.once('message',resolve));
}else{
const called=await later;
yield {type:'user',session_id:'native',parent_tool_use_id:null,message:{content:[{type:'tool_result',tool_use_id:'later-call',content:called.content,is_error:called.isError}]}};
}
yield {type:'result',uuid:'result-'+number,session_id:'native',user_message_uuids:[user.uuid],subtype:'success',is_error:false,result:'answer-'+number,usage:{input_tokens:1,output_tokens:1},modelUsage:{},total_cost_usd:0};
continue;
}
if(client){
if(text==='pending-function'){
void client.callTool({name:'lookup',arguments:{text},_meta:{'claudecode/toolUseId':'pending-call'}}).catch(()=>{});
Expand Down Expand Up @@ -79,7 +91,7 @@ async function launch(t,mode="normal") {
const send=value=>child.stdin.write(JSON.stringify(value)+"\n");
const start=(id,text)=>send({type:"turn_start",turn_id:id,input:[{content:[{type:"input_text",text}]}]});
send({type:"executor_prepare",cwd:"/tmp",model:"fixture",system_prompt:"",
...(mode==="features" || mode.startsWith("pending-function") ? {functions:[{name:"lookup",description:"lookup",parameters:{type:"object",properties:{text:{type:"string"}}}}]} : {})});
...(mode==="features" || mode==="later-function" || mode.startsWith("pending-function") ? {functions:[{name:"lookup",description:"lookup",parameters:{type:"object",properties:{text:{type:"string"}}}}]} : {})});
await wait(()=>events.some(event=>event.type==="executor_ready"));
assert.equal(observations.filter(event=>event.type==="input").length,0);
return {child,events,observations,closed,wait,send,start};
Expand Down Expand Up @@ -203,3 +215,18 @@ test("executor preparation reports missing native history before readiness",{tim
assert.equal(await closed,0);
assert.deepEqual(stdout.trim().split("\n").map(line=>JSON.parse(line)),[{type:"error",code:"history_unavailable"}]);
});

test("a function call outstanding across a native result is applied by the next native turn of the same Turn", {timeout:10000}, async t => {
const {child,events,closed,wait,send,start}=await launch(t,"later-function");
start("only","later-function");
await wait(()=>events.some(event=>event.type==="function_call"));
send({type:"steer",turn_id:"only",input_id:"extra",input:[{content:[{type:"input_text",text:"later text"}]}]});
child.send({type:"go"});
await wait(()=>events.filter(event=>event.type==="usage").length===1);
send({type:"function_result",turn_id:"only",call_id:"later-call",delivery_id:"delivery",success:true,content:[{type:"input_text",text:"value"}]});
await wait(()=>events.some(event=>event.type==="turn_settled"));
assert.equal(events.filter(event=>event.type==="function_applied").length,1);
assert.equal(events.filter(event=>event.type==="usage").length,2);
assert.equal(events.find(event=>event.type==="turn_settled").confirmed,true);
child.stdin.end();assert.deepEqual(await closed,{code:0,signal:null});
});
2 changes: 1 addition & 1 deletion scripts/build-native-installer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export async function validateComponents(root, names, daemonVersion) {
await requireFile(join(component, 'pnpm-lock.yaml'));
await requireFile(join(component, 'dist/main.js'));
const info = JSON.parse(probe(node, [join(component, 'dist/runtime_check.js')], component, env));
if (info.type !== 'runtime_ready' || info.protocol !== 3 || info.sdk !== pins.claude || info.native !== '2.1.269 (Claude Code)' || !info.features?.includes('local_runtime_v2')) throw new Error('Claude runtime compatibility failed');
if (info.type !== 'runtime_ready' || info.protocol !== 3 || info.sdk !== pins.claude || !info.features?.includes('local_runtime_v2')) throw new Error('Claude runtime compatibility failed');
} else if (name === 'minimax') {
if (windows) throw new Error('MiniMax is not supported on Windows');
const upstream = await json(join(component, 'source.json'));
Expand Down
1 change: 1 addition & 0 deletions scripts/generate-harness-catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ def render_docs(harnesses):
"claude": {
"scripts/native-harness-smoke.mjs": ["!== '{v}'", "info.sdk !== '{v}'"],
"packages/claude-sdk-adapter/package.json": ['"@anthropic-ai/claude-agent-sdk": "{v}"'],
"services/core/deploy/claude/README.md": ["Agent SDK `{v}`"],
},
"minimax": {
"scripts/build-mcode-runtime.sh": ["= {v}\n"],
Expand Down
2 changes: 1 addition & 1 deletion scripts/native-harness-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ async function claudeSmoke() {
if (result.error || result.status !== 0) throw failure('claude_runtime_unavailable');
let info;
try { info = JSON.parse(result.stdout); } catch { throw failure('claude_invalid_receipt'); }
if (info.type !== 'runtime_ready' || info.sdk !== '0.3.269' || info.native !== '2.1.269 (Claude Code)' || !info.features?.includes('local_runtime_v2')) throw failure('claude_runtime_identity');
if (info.type !== 'runtime_ready' || info.sdk !== '0.3.269' || !info.features?.includes('local_runtime_v2')) throw failure('claude_runtime_identity');
return { status: 'passed', sdk: info.sdk, native: info.native };
}

Expand Down
4 changes: 2 additions & 2 deletions services/core/deploy/claude/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Native tools run with the daemon user's permissions; the outer sandbox provides

## Native pin and readiness

The bundle pins Claude Agent SDK `0.3.269` ([`package.json`](../../../../packages/claude-sdk-adapter/package.json)), which reports native Claude Code `2.1.269`. The bridge uses protocol 3 for a prepared Executor with separately identified Turns; the Go adapter's readiness and Executor checks reject any other protocol version ([`readiness.go`](../../../../apps/daemon/internal/agent/claudesdk/readiness.go), [`executor.go`](../../../../apps/daemon/internal/agent/claudesdk/executor.go)).
The bundle pins Claude Agent SDK `0.3.269` ([`package.json`](../../../../packages/claude-sdk-adapter/package.json)), which fixes the native Claude Code it bundles. The bridge uses protocol 3 for a prepared Executor with separately identified Turns; the Go adapter's readiness and Executor checks reject any other protocol version ([`readiness.go`](../../../../apps/daemon/internal/agent/claudesdk/readiness.go), [`executor.go`](../../../../apps/daemon/internal/agent/claudesdk/executor.go)).

Workspace execution requires the bundle to report the `workspace_tools`, `workspace_prepare`, `workspace_command_observations` and `local_runtime_v2` features; matching SDK versions alone do not establish compatibility. Function tools in workspace execution additionally require `workspace_functions`. The native installer rejects a bundle that lacks the workspace features ([`installation.go`](../../../../apps/daemon/internal/agent/claudesdk/installation.go)).

Expand All @@ -33,7 +33,7 @@ The bridge ([`native_failure.ts`](../../../../packages/claude-sdk-adapter/src/na
| `server_error` right after an `api_retry` whose `error_status` is null, with no model output between them | `connection_failed` |
| any other `server_error` | `server_error` |

Claude Code `2.1.269` reports both a request that got no HTTP response, such as a failed DNS lookup, a refused connection or a timeout, and a server error in a response that already started streaming as `server_error` without a status. Only the first comes right after an `api_retry` whose `error_status` is null, with no model output in between. Claude Code retries such a request up to 10 times, with backoff that grows to about 35 seconds, so a model host that does not resolve or refuses connections fails the Turn after about three minutes. The adapter leaves that bound as it is.
The pinned Claude Code reports both a request that got no HTTP response, such as a failed DNS lookup, a refused connection or a timeout, and a server error in a response that already started streaming as `server_error` without a status. Only the first comes right after an `api_retry` whose `error_status` is null, with no model output in between. Claude Code retries such a request up to 10 times, with backoff that grows to about 35 seconds, so a model host that does not resolve or refuses connections fails the Turn after about three minutes. The adapter leaves that bound as it is.

`unknown`, `max_output_tokens` and other results stay unclassified. The bridge drops the classification when any other error ends the Turn, when cleanup fails or when no native process ran. The Go adapter keeps it only for the reported failed result of the same native Session, and only after it received the Turn settlement ([`executor_turn.go`](../../../../apps/daemon/internal/agent/claudesdk/executor_turn.go)).

Expand Down
Loading
Loading