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
89 changes: 89 additions & 0 deletions apps/daemon/internal/agent/claudesdk/subagents_live_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//go:build linux

package claudesdk

import (
"bytes"
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"

"github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto"
"github.com/google/uuid"
)

// TestLiveClaudeSDKNativeSubagent runs one Turn in which the model spawns a native general-purpose Subagent with a per-call model.
func TestLiveClaudeSDKNativeSubagent(t *testing.T) {
entrypoint, keyFile, proofRoot := os.Getenv("OAC_RUNTIME_CLAUDE_SDK_ENTRYPOINT"), os.Getenv("OAC_TEST_CLAUDE_SDK_MINIMAX_KEY_FILE"), os.Getenv("OAC_TEST_CLAUDE_SDK_PROOF_DIR")
endpoint, model := os.Getenv("OAC_TEST_CLAUDE_SDK_BASE_URL"), os.Getenv("OAC_TEST_CLAUDE_SDK_MODEL")
if entrypoint == "" || keyFile == "" || proofRoot == "" || endpoint == "" || model == "" {
t.Skip("real SDK/provider acceptance requires explicit entrypoint, key file, proof directory, base URL and model")
}
key, err := os.ReadFile(keyFile)
if err != nil {
t.Fatal(err)
}
root, err := os.MkdirTemp(proofRoot, "claude-subagent-")
if err != nil {
t.Fatal(err)
}
t.Setenv("OAC_RUNTIME_HOME", root)
config := Config{Entrypoint: entrypoint, StateDir: filepath.Join(root, "state"), Env: []string{
"ANTHROPIC_BASE_URL=" + endpoint, "ANTHROPIC_AUTH_TOKEN=" + strings.TrimSpace(string(key)), "ANTHROPIC_API_KEY=", "CLAUDE_CODE_OAUTH_TOKEN=",
"CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1", "ANTHROPIC_DEFAULT_SONNET_MODEL=" + model, "ANTHROPIC_DEFAULT_OPUS_MODEL=" + model, "ANTHROPIC_DEFAULT_HAIKU_MODEL=" + model,
}}
ctx, cancel := context.WithTimeout(context.Background(), 150*time.Second)
defer cancel()
out := make(chan proto.Envelope, 256)
request := proto.PromptRequestPayload{RunID: uuid.NewString(), ObserveMessages: true, ObserveSubagentIdentities: true,
Input: proto.TextInput("Call the Agent tool exactly once with subagent_type \"general-purpose\", model \"sonnet\", name \"helper\" and a prompt asking it to reply with the single word PONG. Then report what it replied."),
AgentOptions: map[string]any{"model": model}}
turn, err := startSingleTurn(ctx, config, request, out)
if err != nil {
t.Fatal(err)
}
defer turn.Cancel(context.Background())
counts := map[string]int{}
var coordination []map[string]any
var events []proto.Envelope
defer func() {
encoded, _ := json.MarshalIndent(map[string]any{"counts": counts, "coordination": coordination, "events": events}, "", " ")
if bytes.Contains(encoded, bytes.TrimSpace(key)) {
t.Error("evidence contains the provider key")
return
}
_ = os.WriteFile(filepath.Join(root, "evidence.json"), encoded, 0o600)
t.Logf("evidence: %s/evidence.json counts=%v", root, counts)
}()
for event := range out {
events = append(events, event)
counts[event.Type]++
switch event.Type {
case proto.TypeError:
t.Fatalf("native failure: %s", event.Payload)
case proto.TypeSubagentCoordination:
var fact map[string]any
_ = json.Unmarshal(event.Payload, &fact)
coordination = append(coordination, fact)
}
}
if _, err := turn.AwaitSettlement(ctx); err != nil {
t.Fatal(err)
}
if counts[proto.TypeSubagentIdentity] != 1 || counts[proto.TypeSubagentTurn] < 2 || counts[proto.TypeSubagentItem] == 0 {
t.Fatal("native Subagent was not observed")
}
var spawn map[string]any
for _, fact := range coordination {
if fact["kind"] == "create_subagent_call" {
spawn = fact
}
}
if spawn == nil || spawn["status"] != "completed" || spawn["model"] != "sonnet" {
t.Fatalf("spawn coordination did not report the requested model: %v", spawn)
}
}
2 changes: 1 addition & 1 deletion contracts/agents-api/subagents.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ An adapter freezes root output before child settlement, keeps its native owner a

**Codex.** The adapter enables the native `multi_agent` feature and maps the concurrency limit to `agents.max_threads`; nesting depth and other native features keep their Codex defaults. It refuses to start if managed requirements force code mode or `multi_agent_v2`, whose collaboration calls leave no persisted receipt. Child Items without a neutral Item type, such as image views and context compaction, are not projected. Close and reopen facts come from direct tool output correlated with the same call's persisted completion, so they need native persisted receipts. Native Turn times have second precision. Child file work can finish under the same owner after the root Turn finishes. Cancellation continues under the same owner after a caller deadline; a later call can confirm settlement without repeating the native interrupt.

**Claude Code.** The pinned SDK's native Agent and SendMessage calls run the single child type `oac_worker`, which inherits the model and, on a workspace placement, the parent's native tools, and otherwise has only Agent and SendMessage; the bridge's `subagent_resources` feature gates it. Children use native Bash with the parent's launching-user permissions. Private child records establish parentage, the first own input time and later own Turns; inherited parent context is excluded. The query owner admits children before start and keeps their history through settlement. Confirmed cancellation writes an immutable effect receipt because a native abort can leave no terminal record. There is no close operation: completed or cancelled children stay active. Messages to running children, background work, other child profiles and per-call model overrides are rejected. The [Claude SDK adapter](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/packages/claude-sdk-adapter/README.md#subagents) documents the details.
**Claude Code.** The pinned SDK's native Agent and SendMessage calls run the native agent types with their native prompts, per-call `model`, `name` and worktree isolation; a child inherits the parent's tools, which on `none` are Agent and SendMessage. The bridge's `subagent_resources` feature gates it. Children use native Bash with the parent's launching-user permissions. Private child records establish parentage, the first own input time and later own Turns; inherited parent context is excluded. The query owner admits children before start and keeps their history through settlement. Confirmed cancellation writes an immutable effect receipt because a native abort can leave no terminal record. There is no close operation: completed or cancelled children stay active. A SendMessage `to` is a child ID or the `name` given at spawn. Messages to running children and remote isolation are rejected, and the native setting `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS` removes background work. The [Claude SDK adapter](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/packages/claude-sdk-adapter/README.md#subagents) documents the details.

**MiniMax Code.** Native ACP delegation (`task`, `task_append`, `task_stop`) creates children. Session-private SQLite records supply child identity, accepted inputs, terminal times and own messages. The native task-create transaction enforces the concurrency limit before start, and native preparation must acknowledge that limit and that background Bash is off before any model input. There is no close or reopen, and native workers do not delegate nested work.

Expand Down
4 changes: 2 additions & 2 deletions contracts/agents-api/zh/subagents.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "子 Agent"
source: contracts/agents-api/subagents.md
source_hash: 2f66b6d6e9bb613eec6fcf07eb15638e46b8ac4bc8933e33e750b6e5685fd44b
source_hash: 9257e25d1b4d10de9643157510f6695c0a16cc7ee1b21393cf9589f985c577f8
---

启用 `multi_agent.enabled` 后,Harness 可以启动原生子 Agent。Core 通过锁定版本的 SDK(见 [`upstream.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/contracts/agents-api/upstream.json))中的六项 Subagent 读取操作公开它们,并根据适配器观察结果记录它们。当 `multi_agent.enabled=false` 时,Runtime 会移除原生子 Agent 工具。[Harness capabilities](harness-capabilities.md) 列出各 Harness 在哪些组合中支持子 Agent。
Expand Down Expand Up @@ -56,7 +56,7 @@ Core 根据已授权的 Session 绑定关系分配合公开 ID 和所有权。

**Codex.** 适配器会启用原生 `multi_agent` 特性,并把并发限制映射到 `agents.max_threads`;嵌套深度和其他原生特性保持 Codex 默认值。如果托管配置要求强制启用代码模式或 `multi_agent_v2`(它们的协作调用不留下持久化回执),则拒绝启动。没有中立 Item 类型的子级 Item(例如图像查看和上下文压缩)不会被投影。关闭和重新打开的事实来自直接工具输出,并与同一次调用的持久化完成记录相关联,因此需要原生持久化回执。原生 Turn 时间具有秒级精度。根级 Turn 完成后,子级文件工作可以在同一所有者下完成。超过调用方截止时间后,取消操作仍会在同一所有者下继续;后续调用可以确认已经收敛,而无需重复执行原生中断。

**Claude Code.** 锁定版本 SDK 的原生 Agent 和 SendMessage 调用会运行唯一的子级类型 `oac_worker`;该类型继承模型;在工作区位置上继承父级的原生工具,否则只有 Agent 和 SendMessage;bridge 的 `subagent_resources` 特性控制是否启用它。子级使用原生 Bash,权限继承自父级的启动用户。私有子级记录用于确定父子关系、首次自身输入时间及后续自身 Turns;继承的父级上下文会被排除。查询所有者会在启动前接纳子级,并将子级历史保留至工作收敛。已确认的取消操作会写入不可变的操作回执,因为原生中止可能不会留下终态记录。不存在关闭操作:已完成或已取消的子级保持 active 状态。向运行中的子级发送消息、使用后台工作、采用其他子级配置以及按次覆盖模型都会被拒绝。[Claude SDK adapter](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/packages/claude-sdk-adapter/README.md#subagents) 记录了详细信息。
**Claude Code.** 锁定版本 SDK 的原生 Agent 和 SendMessage 调用会运行原生的 Agent 类型及其原生提示词,支持按次指定 `model`、`name` 和 worktree 隔离;子级继承父级的工具,在 `none` 上即 Agent 和 SendMessage。bridge 的 `subagent_resources` 特性控制是否启用。子级使用原生 Bash,权限继承自父级的启动用户。私有子级记录用于确定父子关系、首次自身输入时间及后续自身 Turns;继承的父级上下文会被排除。查询所有者会在启动前接纳子级,并将子级历史保留至工作收敛。已确认的取消操作会写入不可变的操作回执,因为原生中止可能不会留下终态记录。不存在关闭操作:已完成或已取消的子级保持 active 状态。SendMessage 的 `to` 可以是子级 ID,也可以是创建时给出的 `name`。向运行中的子级发送消息和 remote 隔离会被拒绝,原生设置 `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS` 会移除后台工作。[Claude SDK adapter](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/packages/claude-sdk-adapter/README.md#subagents) 记录了详细信息。

**MiniMax Code.** 原生 ACP 委派(`task`、`task_append`、`task_stop`)会创建子级。Session 私有 SQLite 记录提供子级身份、已接受的输入、终态时间和自身消息。原生 task-create 事务会在启动前强制执行并发限制,并且原生准备过程必须在任何模型输入前确认该限制并确认后台 Bash 已关闭。不存在关闭或重新打开操作,原生工作器也不会委派嵌套工作。

Expand Down
6 changes: 3 additions & 3 deletions packages/claude-sdk-adapter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ The `none` profile accepts only text, explicit model/system instructions, manage

The internal SDK function-server helper uses the maintained MCP server's public request handlers and standard Tool/CallToolResult types. It snapshots definitions and forwards JSON Schema without a JSON Schema-to-Zod conversion; supplied tools are always loaded. Native call identity comes from the pinned harness's `claudecode/toolUseId` MCP metadata, independently of request IDs, names or arrival order. Missing identities and undeclared tools fail before invoking the host. Return content/error fields unchanged over MCP and forward its per-request abort signal. The private Go factory connects declared functions through this helper and reuses the daemon function-call/result interface and neutral observations. The native function-server registry must contain exactly those functions. SDK allowlisting admits only these host callbacks; the host still owns result decisions and any business permission checks. It grants no runtime-token business authority.

Function results remain pending after stdin/MCP delivery. A matching live, root native user tool_result confirms application only when its Session/call identity, error flag and ordered content match the submission. Text matches exactly; each submitted image position must remain a valid native base64 image. Native resizing or re-encoding may change image bytes. This acknowledges incorporation into native history, not byte/pixel fidelity or completed provider consumption. Public Items retain the original caller content; real image-dependent model responses separately qualify usability. Ignore replayed, synthetic and subagent messages. Native error text joins the submitted text parts with newlines; neutral observations retain their original order and separate failure status. Missing/mismatched receipts fail the execution; do not replay unknown delivery. Result submission waits at most ten seconds for a receipt and cancels uncertain execution on timeout. Invalid or unsupported image results fail before consuming a pending call. Function state belongs to one live Run and ends with it; the router owns receipt retry/conflict handling. This does not establish crash recovery or exactly-once effects.
Function results remain pending after stdin/MCP delivery. A matching live, root native user tool_result confirms application only when its Session/call identity, error flag and ordered content match the submission. Text matches exactly; each submitted image position must hold a native image, or the text note native substitutes for an image it cannot process. Native resizing or re-encoding may change image bytes. This acknowledges incorporation into native history, not byte/pixel fidelity or completed provider consumption. Public Items retain the original caller content; real image-dependent model responses separately qualify usability. Ignore replayed, synthetic and subagent messages. Native error text joins the submitted text parts with newlines; neutral observations retain their original order and separate failure status. Missing/mismatched receipts fail the execution; do not replay unknown delivery. Result submission waits at most ten seconds for a receipt and cancels uncertain execution on timeout. Invalid or unsupported image results fail before consuming a pending call. Function state belongs to one live Run and ends with it; the router owns receipt retry/conflict handling. This does not establish crash recovery or exactly-once effects.

### Usage

Expand All @@ -94,13 +94,13 @@ Active text uses the SDK's `AsyncIterable<SDKUserMessage>` input, with a fresh n

## Subagents

The adapter uses the pinned Claude Agent SDK's native Agent and SendMessage execution; it implements no model loop. An explicit Runtime request enables the `oac_worker` agent; ordinary requests keep their tools. The packaged `subagent_resources` readiness feature gates this request.
The adapter uses the pinned Claude Agent SDK's native Agent and SendMessage execution; it implements no model loop. An explicit Runtime request enables the native Agent and SendMessage tools with Claude Code's own agent types and prompts; ordinary requests keep their tools. The packaged `subagent_resources` readiness feature gates this request.

A child identity comes from native task admission and persisted child metadata. The metadata's `toolUseId` must identify the parent's original Agent call; `parentAgentId` identifies a nested parent, and root children require native `spawnDepth: 1`. The child history must belong to the same root Session and child ID. The SDK resolves its persisted conversation chain; the adapter reads the corresponding private original records for ownership and timestamps that the SDK's public TypeScript message shape omits.

The first own native user record has a null `parentUuid`. Its timestamp supplies `opened_at`, in seconds, and the first Turn's creation and start time, in milliseconds: the original input time, not discovery time or a copied parent record. The fixed native metadata has no separate creation timestamp. Reopening the Runtime preserves these values. Each later own input starts a child Turn; native end-turn assistant records supply completion time. Own messages, reasoning and native Bash receipts keep their native IDs and ordering. Completion leaves the Subagent active and idle: the adapter emits no closed state because this native profile has no qualified close operation.

The native query owns child execution and cleanup. PreToolUse admission reserves each native Agent or idle-child SendMessage call before execution. Native `task_started` associates the call and child ID; completion releases that reservation. The frozen limit applies across the child tree, excludes the root, and defaults to six. Unknown call associations fail closed. SendMessage to a running child is rejected; only idle continuation is qualified. Native background execution, alternate agent types, worktree isolation and per-call model overrides are rejected.
The native query owns child execution and cleanup. PreToolUse admission reserves each native Agent or idle-child SendMessage call before execution. Native `task_started` associates the call and child ID; completion releases that reservation. The frozen limit applies across the child tree, excludes the root, and defaults to six. Unknown call associations fail closed. SendMessage to a running child is rejected; only idle continuation is qualified. A SendMessage `to` is a child ID or the `name` given at spawn. Remote isolation, which needs an account credential Core does not hold, is rejected; native background execution is removed by `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS`. Agent types, per-call `model` and worktree isolation follow the native defaults, and the coordination fact reports the requested `model`.

Workspace children use native Bash with the same launching-user permissions as the parent, and the daemon and adapter add no filesystem, permission or network sandbox. A child's Agent and SendMessage calls are denied until native task admission has verified its identity. Workspace hooks keep their execution and event responsibilities but are not a private-file boundary: tools can access Runtime state that the host user can access. Isolation belongs to the outer Environment. Functions and MCP combined with Subagents are not qualified and are rejected explicitly ([subagents contract](../../contracts/agents-api/subagents.md)); functions and MCP without Subagents are unaffected. Claude on Windows requires Git Bash.

Expand Down
2 changes: 1 addition & 1 deletion packages/claude-sdk-adapter/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export async function execute(request: Start | Prepare | ExecutorPrepare, emit:
...(workspace?.options ?? {}),
disallowedTools: [...(workspace?.options.disallowedTools ?? []), ...(profile?.denied ?? []), ...unselected],
mcpServers,
...(subagents ? { ...subagents.options(!!workspace), hooks: {
...(subagents ? { forwardSubagentText: true, hooks: {
Stop: [{ hooks: [subagents.stopped] }], SubagentStart: [{ hooks: [subagents.childStart] }],
PostToolUseFailure: [{ hooks: [subagents.failedTool] }], PreToolUse: [{ hooks: [beforeTool] }],
} } : workspace || profile ? { hooks: { PreToolUse: [{ hooks: [beforeTool] }] } } : {}),
Expand Down
Loading
Loading