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
10 changes: 7 additions & 3 deletions apps/daemon/internal/agent/claudesdk/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func (e *executor) read() {
break
}
if !ready {
if event.Type != "executor_ready" || event.Protocol != 3 || event.TurnID != "" {
if event.Type != "executor_ready" || event.Protocol != 4 || event.TurnID != "" {
if event.Type == "error" {
readyFailure = bridgeFailure(event.Code)
}
Expand Down Expand Up @@ -179,7 +179,7 @@ func (e *executor) read() {
close(e.done)
}

func (e *executor) StartTurn(ctx context.Context, run string, _ proto.TurnSettings, input proto.MessageInput, out chan<- proto.Envelope) (agent.Turn, error) {
func (e *executor) StartTurn(ctx context.Context, run string, turn proto.TurnSettings, input proto.MessageInput, out chan<- proto.Envelope) (agent.Turn, error) {
if ctx == nil {
ctx = context.Background()
}
Expand All @@ -199,7 +199,11 @@ func (e *executor) StartTurn(ctx context.Context, run string, _ proto.TurnSettin
go s.runTurn(start, out)
// Once the write is attempted, a lost receipt has an unknown input outcome.
stopWrite := context.AfterFunc(ctx, s.invalidate)
err := e.write(turnStartFrame{"turn_start", run, input})
settings := turnSettings{Model: turn.Model, Review: turn.Review}
if turn.ReasoningEffort != "" {
settings.Effort = &turn.ReasoningEffort
}
err := e.write(turnStartFrame{"turn_start", run, input, settings})
stopWrite()
if err != nil {
s.invalidate()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func startSingleTurn(ctx context.Context, config Config, req proto.PromptRequest
}

func helperTurn(scanner *bufio.Scanner, request *startRequest) (func(bridgeEvent), func()) {
_ = json.NewEncoder(os.Stdout).Encode(bridgeEvent{Type: "executor_ready", Protocol: 3})
_ = json.NewEncoder(os.Stdout).Encode(bridgeEvent{Type: "executor_ready", Protocol: 4})
if !scanner.Scan() {
os.Exit(4)
}
Expand Down
2 changes: 1 addition & 1 deletion apps/daemon/internal/agent/claudesdk/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func runPersistentExecutorHelper() {
}
_ = os.WriteFile(filepath.Join(root, "prepared"), []byte(strconv.Itoa(os.Getpid())), 0600)
encode := func(event bridgeEvent) { _ = json.NewEncoder(os.Stdout).Encode(event) }
encode(bridgeEvent{Type: "executor_ready", Protocol: 3})
encode(bridgeEvent{Type: "executor_ready", Protocol: 4})
if os.Getenv("SDK_EXECUTOR_MODE") == "block" {
time.Sleep(time.Hour)
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,14 @@ func runPreparationHelper() {
time.Sleep(time.Millisecond)
}
}
emit(bridgeEvent{Type: "executor_ready", Protocol: 3})
emit(bridgeEvent{Type: "executor_ready", Protocol: 4})
if !scanner.Scan() {
return
}
raw = append([]byte{}, scanner.Bytes()...)
_ = os.WriteFile(filepath.Join(state, "start.json"), raw, 0o600)
fields = nil
if json.Unmarshal(raw, &fields) != nil || len(fields) != 3 || string(fields["type"]) != `"turn_start"` || string(fields["input"]) != `[{"content":[{"type":"input_text","text":"hello"}]}]` {
if json.Unmarshal(raw, &fields) != nil || len(fields) != 4 || string(fields["type"]) != `"turn_start"` || string(fields["input"]) != `[{"content":[{"type":"input_text","text":"hello"}]}]` {
os.Exit(4)
}
var turnID string
Expand Down
6 changes: 5 additions & 1 deletion apps/daemon/internal/agent/claudesdk/preparation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,15 @@ func TestPreparationWaitsForReceiptAndRetainsConfiguration(t *testing.T) {
req.AgentSessionID = "changed"
out := make(chan proto.Envelope, 16)
operation, stopOperation := context.WithCancel(ctx)
turn, err := e.StartTurn(operation, "actual-run", proto.TurnSettings{}, proto.TextInput("hello"), out)
turn, err := e.StartTurn(operation, "actual-run", proto.TurnSettings{Model: "turn-model", Review: "auto_review"}, proto.TextInput("hello"), out)
stopOperation()
if err != nil {
t.Fatal(err)
}
var sent struct{ Settings json.RawMessage }
if json.Unmarshal(waitPreparationFile(t, filepath.Join(config.StateDir, "start.json")), &sent) != nil || string(sent.Settings) != `{"model":"turn-model","effort":null,"review":"auto_review"}` {
t.Fatalf("turn_start settings = %s", sent.Settings)
}
if turn.(*session).owner != e || turn.(*session).process.Cmd.Process.Pid != pid {
t.Fatal("StartTurn replaced the prepared native process")
}
Expand Down
9 changes: 9 additions & 0 deletions apps/daemon/internal/agent/claudesdk/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ type turnStartFrame struct {
Type string `json:"type"`
TurnID string `json:"turn_id"`
Input proto.MessageInput `json:"input"`
// Settings are the Turn's model, effort and review; the bridge applies what differs from the last Turn.
Settings turnSettings `json:"settings"`
}

// turnSettings carries a null Effort to return to the model's default effort.
type turnSettings struct {
Model string `json:"model"`
Effort *string `json:"effort"`
Review string `json:"review"`
}

type turnCancelFrame struct {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ case "$1" in
printf '%s\n' '{"type":"runtime_ready","protocol":3,"node":"fixture","sdk":"fixture","mcp":"fixture","native":"fixture","features":["workspace_tools","workspace_prepare","workspace_command_observations"]}' ;;
*)
IFS= read -r request
printf '%s\n' '{"type":"executor_ready","protocol":3}'
printf '%s\n' '{"type":"executor_ready","protocol":4}'
IFS= read -r request
printf '%s\n' '{"type":"result","turn_id":"run","session_id":"native","text":"completed"}'
printf '%s\n' '{"type":"turn_settled","turn_id":"run","confirmed":true,"reusable":true,"reason":""}' ;;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func TestWorkspaceStructuredPreparationQualificationAndFrozenSchema(t *testing.T
}
}
var started map[string]json.RawMessage
if err := json.Unmarshal(waitPreparationFile(t, filepath.Join(config.StateDir, "start.json")), &started); err != nil || len(started) != 3 || started["output_format"] != nil {
if err := json.Unmarshal(waitPreparationFile(t, filepath.Join(config.StateDir, "start.json")), &started); err != nil || len(started) != 4 || started["output_format"] != nil {
t.Fatal("Start replaced the prepared configuration", err)
}
})
Expand Down
2 changes: 1 addition & 1 deletion contracts/agents-api/harness-capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Placements are `none` (no Environment), hosted (`openai_hosted`) and self-hosted
| `reasoning.summary` | `concise`, `detailed`, `auto`: admitted | `auto`: admitted | Rejected |
| `service_tier` other than `auto` | `default`, `flex`, `priority`, `fast`: admitted; `ultrafast` rejected | Rejected | Rejected |
| `text.verbosity` other than `medium` | Admitted; the native model decides | Rejected | Rejected |
| Session update of `agent.model`, `reasoning.effort`, `service_tier` and `review: auto_review` | Applied on the next root Turn; `reasoning.effort` cannot be reset to null | Rejected | Rejected |
| Session update of `agent.model`, `reasoning.effort` (including null), `service_tier` and `review: auto_review` | Applied on the next root Turn; `reasoning.effort` cannot be reset to null | Applied on the next root Turn, except `service_tier`, which is rejected | Rejected |
| Session update of `agent.tools` | Rejected | Rejected | Rejected |
| `compaction.async: true`, `experimental.time_awareness: true` and the `automations` and `computer_use` tools | Rejected | Rejected | Rejected |
| [Public token usage](./sessions-events.md) | Measured counters | Null | Null |
Expand Down
2 changes: 1 addition & 1 deletion contracts/agents-api/harness-onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ The daemon's `clirunner` starts every native child in its own Unix process group

Owned output pipes stay readable after the leader exits. Consumers drain stdout and stderr before calling `Wait`, which joins the cached process result and closes the readers. `Done` reports leader reaping and group cleanup signals; it is not a native execution receipt or proof of persisted history. SDK adapters settle each Turn and drain its observations before publishing completion, and Executor close also closes the query and awaits the native child. Process groups are lifecycle supervision, not isolation or containment of descendants that leave the group.

Adapters run native tools unattended with the launching user's permissions, and a Harness never asks a human. Codex runs with approval policy `never`, under which it settles MCP elicitation itself without reaching the client, and with full access; under review `auto_review` every Turn instead sends `on-request` with the `auto_review` reviewer, whose denials stay inside Codex, and any request that still reaches the client gets JSON-RPC -32601, a denial; the adapter also disables its blocking `request_user_input` tool. Claude runs through the adapter's tool callback, which allows or denies without asking. MiniMax runs with bypassed permissions; the adapter disables `askUser`, advertises no elicitation and answers `session/request_permission` with the ACP `cancelled` outcome, which MiniMax treats as a denial. No native permission or question request reaches Core: a human in the loop goes through a function tool, the Session reads [`requires_action`](./sessions-events.md#session-status) and the application submits the function result. Do not add permission profiles, bubblewrap wrappers or native sandbox settings; there is one execution path for every Environment origin. Resource paths are operator configuration, not a permission boundary.
Adapters run native tools unattended with the launching user's permissions, and a Harness never asks a human. Codex runs with approval policy `never`, under which it settles MCP elicitation itself without reaching the client, and with full access; under review `auto_review` every Turn instead sends `on-request` with the `auto_review` reviewer, whose denials stay inside Codex, and any request that still reaches the client gets JSON-RPC -32601, a denial; the adapter also disables its blocking `request_user_input` tool. Claude runs through the adapter's tool callback, which allows or denies without asking; under `auto_review` it denies every request the native `auto` mode escalates to a person. MiniMax runs with bypassed permissions; the adapter disables `askUser`, advertises no elicitation and answers `session/request_permission` with the ACP `cancelled` outcome, which MiniMax treats as a denial. No native permission or question request reaches Core: a human in the loop goes through a function tool, the Session reads [`requires_action`](./sessions-events.md#session-status) and the application submits the function result. Do not add permission profiles, bubblewrap wrappers or native sandbox settings; there is one execution path for every Environment origin. Resource paths are operator configuration, not a permission boundary.

An unreachable model host must not hold a Turn open. The adapter keeps the Harness's own connection retries bounded through native settings, and the Harness's final connection error becomes `connection_failed` when the Harness reports it as structured data ([native failure classification](../../docs/runtime-protocol.md#native-failure-classification)). Core adds no model timeout of its own. Each adapter guide states its Harness's bound.

Expand Down
1 change: 1 addition & 0 deletions contracts/agents-api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ Each item is Core's deliberate or native behavior where the official service beh
- [Conflict and rejection cases](./automations.md#state-changes).
- [Target validation and saved references](./automations.md#targets).
- [Paging](./automations.md): `limit` clamping and unresolved `after`.
- Whether a Claude Code Subagent from an earlier Turn, resumed by `SendMessage`, runs in the current Turn's permission mode rather than the mode it started under: the native mode is session-wide, so the adapter assumes the current one.
- The order of errors when one request has several faults, and error, default and payload-limit parity in general.
- A Subagent's function call: Core makes it a required action of the root Turn that runs the child, with `turn_id` set to that Turn, and publishes the child's `function_call` Item once as `agent.session.turn.item.added` with the child Turn ID and a null `output_index`; a result may name either Turn. The Turn is `waiting` and the Session `requires_action` while root output may continue, and the `call_id` equals the `call_id` of the child's `function_call` Item. Claude Code publishes the child's Items when the root Turn settles, after the required action.
- Whether the official service emits Environment `pending` and when: Core emits it when a hosted Environment's initialization starts, and the one official sample showed only `ready`, about 29 s after creation.
Expand Down
4 changes: 2 additions & 2 deletions contracts/agents-api/zh/harness-capabilities.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Harness 能力"
source: contracts/agents-api/harness-capabilities.md
source_hash: aa70b282e3e6d03965bd92c82a2af983a4e3fed1e643cba7289eef6414e3a6ea
source_hash: c7b4dae594607c91790c8d7d5d346bff1f0f57cd75147ab520bcf016174b68d4
---

本页列出每个 Harness 在每种部署位置支持的能力。Core 根据 `internal/harnessconfig/<package>/declaration.go` 中 Harness 的声明决定准入,运行 Session 的 Runtime 也必须声明操作。所链接契约定义各操作;[Harness 接入](harness-onboarding.md#qualify-the-adapter)说明资格验证方法。
Expand Down Expand Up @@ -29,7 +29,7 @@ source_hash: aa70b282e3e6d03965bd92c82a2af983a4e3fed1e643cba7289eef6414e3a6ea
| `reasoning.summary` | `concise`、`detailed`、`auto`:已准入 | `auto`:已准入 | 已拒绝 |
| 非 `auto` 的 `service_tier` | `default`、`flex`、`priority`、`fast`:已准入;`ultrafast` 已拒绝 | 已拒绝 | 已拒绝 |
| 非 `medium` 的 `text.verbosity` | 已准入;由原生模型决定 | 已拒绝 | 已拒绝 |
| Session 更新 `agent.model`、`reasoning.effort`、`service_tier` 和 `review: auto_review` | 在下一个根 Turn 应用;`reasoning.effort` 不能重置为 null | 已拒绝 | 已拒绝 |
| Session 更新 `agent.model`、`reasoning.effort`(含 null)、`service_tier` 和 `review: auto_review` | 在下一个根 Turn 应用;`reasoning.effort` 不能重置为 null | 在下一个根 Turn 应用,但 `service_tier` 已拒绝 | 已拒绝 |
| Session 更新 `agent.tools` | 已拒绝 | 已拒绝 | 已拒绝 |
| `compaction.async: true`、`experimental.time_awareness: true`,以及 `automations` 和 `computer_use` 工具 | 已拒绝 | 已拒绝 | 已拒绝 |
| [公开 token 用量](sessions-events.md) | 测量计数器 | Null | Null |
Expand Down
4 changes: 2 additions & 2 deletions contracts/agents-api/zh/harness-onboarding.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Harness 协议"
source: contracts/agents-api/harness-onboarding.md
source_hash: bfbacab5d0653177ea0c440454f85e5ac68dbea374d58e5119c00010665620d6
source_hash: 27ab04dbb3aaf2792d54710372b370588d35db931af8f2e6b88a34d78030b7f9
---

**Harness** 是一种运行模型和工具循环的原生代理引擎(Codex、Claude Code、MiniMax Code);**Harness 适配器**将 Runtime 的 Executor 和 Turn 契约转换到该引擎的 SDK 或协议。Harness 协议是 Runtime 与适配器之间的契约:适配器接口及其生命周期义务、声明、注册和验收。本文面向添加或维护 Harness 的开发者。
Expand Down Expand Up @@ -216,7 +216,7 @@ daemon 的 `clirunner` 让每个原生子进程在自己的 Unix 进程组中启

所属输出管道在主进程退出后仍可读取。消费者在调用 `Wait` 之前耗尽 stdout 和 stderr;`Wait` 会汇合缓存的进程结果并关闭读取器。`Done` 报告主进程回收和进程组清理信号;它不是原生执行回执,也不是历史已持久化的证据。SDK 适配器会结算每个 Turn,并在发布完成状态前耗尽其观察结果;Executor 关闭还会关闭 Query 并等待原生子进程。进程组用于生命周期监管,而不是隔离或遏制离开进程组的后代进程。

适配器以启动用户的权限无人值守运行原生工具,Harness 从不询问人类。Codex 使用批准策略 `never` 和完全访问权限,在该策略下 Codex 自行处理 MCP elicitation,不会发给客户端;在 `auto_review` 下,每个 Turn 改为发送 `on-request` 和 `auto_review` 审查者,其拒绝留在 Codex 内部,仍到达客户端的请求会得到 JSON-RPC -32601,即拒绝;适配器还禁用其阻塞式 `request_user_input` 工具。Claude 通过适配器的工具回调运行,回调直接允许或拒绝、不会询问。MiniMax 绕过权限;适配器禁用 `askUser`、不声明 elicitation,并以 ACP `cancelled` 结果答复 `session/request_permission`,MiniMax 将其视为拒绝。原生权限或问题请求都不会到达 Core:需要人工介入时通过 function 工具完成,Session 读取 [`requires_action`](./sessions-events.md#session-status),由应用提交 function 结果。不要添加权限 profile、bubblewrap 包装器或原生 sandbox 设置;每种 Environment 来源都只有一条执行路径。资源路径属于操作员配置,而不是权限边界。
适配器以启动用户的权限无人值守运行原生工具,Harness 从不询问人类。Codex 使用批准策略 `never` 和完全访问权限,在该策略下 Codex 自行处理 MCP elicitation,不会发给客户端;在 `auto_review` 下,每个 Turn 改为发送 `on-request` 和 `auto_review` 审查者,其拒绝留在 Codex 内部,仍到达客户端的请求会得到 JSON-RPC -32601,即拒绝;适配器还禁用其阻塞式 `request_user_input` 工具。Claude 通过适配器的工具回调运行,回调直接允许或拒绝、不会询问;`auto_review` 下,凡是原生 `auto` 模式升级给人的请求,回调一律拒绝。MiniMax 绕过权限;适配器禁用 `askUser`、不声明 elicitation,并以 ACP `cancelled` 结果答复 `session/request_permission`,MiniMax 将其视为拒绝。原生权限或问题请求都不会到达 Core:需要人工介入时通过 function 工具完成,Session 读取 [`requires_action`](./sessions-events.md#session-status),由应用提交 function 结果。不要添加权限 profile、bubblewrap 包装器或原生 sandbox 设置;每种 Environment 来源都只有一条执行路径。资源路径属于操作员配置,而不是权限边界。

模型主机不可达时不能让 Turn 一直挂起。适配器通过原生设置让 Harness 自身的连接重试保持有界;Harness 以结构化数据报告最终连接错误时,该错误归为 `connection_failed`([原生失败分类](../../../docs/zh/runtime-protocol.md#native-failure-classification))。Core 不另设模型超时。各适配器指南写明其 Harness 的上限。

Expand Down
3 changes: 2 additions & 1 deletion contracts/agents-api/zh/index.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "OpenAI 兼容性"
source: contracts/agents-api/index.md
source_hash: 82331da66db2d0caf6f64c861a065948c1a1332a8306e99133dfc9af9a165e3d
source_hash: 9b3a9fc6f679b7ec35ca8a37978010c76d26d42e3822b4fb0ee42c3ccfdd5f79
---

Core 实现下方固定版本的 OpenAI Agents API。本台账列出各项资源的覆盖情况、相对于官方服务的所有差异以及已知缺口。
Expand Down Expand Up @@ -212,6 +212,7 @@ Core 自身字段位于 `x_agents_core` 中([Core extensions](../../../docs/zh
- [冲突与拒绝的情形](automations.md#state-changes)。
- [目标校验与保存的引用](automations.md#targets)。
- [分页](automations.md):`limit` 截断与无法解析的 `after`。
- Claude Code 中,由 `SendMessage` 恢复的早先 Turn 的 Subagent 是否按当前 Turn 的权限模式运行,而不是按其启动时的模式:原生模式作用于整个 Session,适配器假定其为当前模式。
- 当一个请求存在多项故障时的错误顺序,以及错误处理、默认值和载荷限制的总体一致性。
- Subagent 的函数调用:Core 将其作为运行该子级的根 Turn 的待执行操作,`turn_id` 为该 Turn,并把子级的 `function_call` Item 以 `agent.session.turn.item.added` 发布一次,`turn_id` 为子级 Turn ID,`output_index` 为 null;结果可以指定其中任一 Turn。该 Turn 为 `waiting`、Session 为 `requires_action`,根输出可继续,`call_id` 等于该子级 `function_call` Item 的 `call_id`。Claude Code 在根 Turn 收敛时才发布子级的 Item,晚于该待执行操作。
- 官方服务是否发出 Environment `pending` 以及何时发出:Core 在 hosted Environment 初始化开始时发出,而仅有的一个官方样本只显示了创建约 29 秒后的 `ready`。
Expand Down
Loading
Loading