From f402b6cde04b82449b83c24e9869615c6b969842 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Fri, 23 Oct 2026 21:13:30 +0000 Subject: [PATCH 1/2] Report official Turn error codes on the stream Journaled Turn snapshots dropped the whole outcome, so every agent.session.turn.failed event reported internal_error while reads reported the classified code. Snapshots now keep Core's error code and the normalized model category, and nothing native. runtime_unavailable also covers Core failing to dispatch a Turn, so it reports internal_error; only a Runtime that disconnected or failed its preparation is sandbox_error. Docs that still described the generic Turn error and the old message rule are corrected. --- contracts/agents-api/core-errors.md | 2 +- contracts/agents-api/index.md | 2 +- contracts/agents-api/sessions-events.md | 4 +-- contracts/agents-api/zh/core-errors.md | 4 +-- contracts/agents-api/zh/index.md | 4 +-- contracts/agents-api/zh/sessions-events.md | 6 ++-- docs/api/public-agent-api.md | 2 +- docs/runtime-protocol.md | 4 +-- docs/zh/api/public-agent-api.md | 4 +-- docs/zh/runtime-protocol.md | 6 ++-- .../native_classification_integration_test.go | 17 +++++++++-- .../internal/api/turn_diagnostic_failure.go | 8 +++-- .../api/turn_diagnostic_failure_test.go | 1 + .../core/internal/sessions/turn_changes.go | 30 +++++++++++++++++-- .../internal/sessions/turn_changes_test.go | 9 ++++-- .../input_conflicts_public_test.go | 3 +- 16 files changed, 76 insertions(+), 30 deletions(-) diff --git a/contracts/agents-api/core-errors.md b/contracts/agents-api/core-errors.md index 3fe655a6d..430cefc5a 100644 --- a/contracts/agents-api/core-errors.md +++ b/contracts/agents-api/core-errors.md @@ -106,7 +106,7 @@ These codes have null `param` and no `details`. [Sandbox deployment](./sandbox-d ## Diagnostic failure categories -The [Session diagnostics](./session-diagnostics.md) routes return these categories inside a successful 200 snapshot, not as an error envelope. Public `/v1` Turn errors do not change. `params` is `{}` unless the table says otherwise. +The [Session diagnostics](./session-diagnostics.md) routes return these categories inside a successful 200 snapshot, not as an error envelope. A public `/v1` Turn error reports some of them under official codes, as [Differences from OpenAI](./index.md#differences-from-openai) describes. `params` is `{}` unless the table says otherwise. | Code | Stored cause or safe meaning | | --- | --- | diff --git a/contracts/agents-api/index.md b/contracts/agents-api/index.md index bc93f43bc..c0a4ee7cf 100644 --- a/contracts/agents-api/index.md +++ b/contracts/agents-api/index.md @@ -90,7 +90,7 @@ Each item is Core's deliberate or native behavior where the official service beh - Attaching to the stream mid-Turn sends no catch-up Item snapshots. - The Items list includes in-progress and incomplete output Items, and keeps a failed function result's submitted `output`. - Error messages omit the call and executor IDs that official messages include, except that the unknown tool call message names its `call_id`. -- A failed Turn reports the model failure categories under their official names and `sandbox_error` for Runtime or sandbox failures in the execution environment. Every other Core failure, such as a missing model provider, a rejected input or a storage failure, reports `internal_error`. The message is a fixed sentence per code and never carries native text. Subagent Turns always report `internal_error`. +- A failed Turn reports the model failure categories under their official names and `sandbox_error` for a Runtime that disconnected or failed its preparation. Every other Core failure, such as an unavailable Runtime, a missing model provider, a rejected input or a storage failure, reports `internal_error`. The message is a fixed sentence per code and never carries native text. Subagent Turns always report `internal_error`. - An empty text part beside other text is accepted and stored. - Empty input returns 400 `invalid_request` with a generic message and a null param; the official response is `invalid_request_error` with param `input`. - Session usage is available as soon as every root Turn has settled; official reads lag by seconds. diff --git a/contracts/agents-api/sessions-events.md b/contracts/agents-api/sessions-events.md index 4b335cc2c..b56910b2a 100644 --- a/contracts/agents-api/sessions-events.md +++ b/contracts/agents-api/sessions-events.md @@ -78,7 +78,7 @@ Checks run in this order: request validation, Session lookup, retry lookup, the | New input after a `self_hosted` Environment failed, input already waiting when the Environment failed, or an expired Environment | 409 `environment_unavailable` | "The environment is no longer available for new input." | | A message the Session's Harness cannot take, such as whitespace-only text on MiniMax Code | 400 `unsupported_or_invalid_configuration` | See [whitespace-only text](./message-content.md#whitespace-only-text) | -An empty `turn_id` or blank `call_id` is the generic 400 `invalid_request`. Error messages never repeat caller input or internal identifiers. +An empty `turn_id` or blank `call_id` is the generic 400 `invalid_request`. Apart from the unknown tool call message, which names its `call_id` as the official SDK expects, error messages never repeat caller input or internal identifiers. ## Initial input at Session creation @@ -127,7 +127,7 @@ A retry with the same `Idempotency-Key` and `stream: true` returns 201 with only Turn and Item lists take `after`, `limit` and `order` ([list rules](./wire-semantics.md)). Cursors are IDs within the same Session. -**Turns.** Session Turn routes hold root Turns only, ordered by creation time then ID; a child Turn ID returns 404 there. A failed Turn has `error: {code: "internal_error", message: "The execution could not complete."}` and never raw engine diagnostics. Administrators read the failure category through [Session diagnostics](./session-diagnostics.md). +**Turns.** Session Turn routes hold root Turns only, ordered by creation time then ID; a child Turn ID returns 404 there. A failed Turn's `error` has an official `code` and a fixed message for that code, never raw engine diagnostics; [Differences from OpenAI](./index.md#differences-from-openai) lists the codes Core reports. Stream snapshots report the same `error` as reads. Administrators read the failure category through [Session diagnostics](./session-diagnostics.md). **Items.** Items are ordered by the time they were first observed, then by their position in the Session, then by ID. Updates and retries never move an Item or change its `output_index`, a zero-based position among the Turn's output Items; input Items have none. Reads use the stored history index and never rebuild it from native journals. The Items list includes Items that are still in progress or incomplete. diff --git a/contracts/agents-api/zh/core-errors.md b/contracts/agents-api/zh/core-errors.md index 110a55188..a09140a3e 100644 --- a/contracts/agents-api/zh/core-errors.md +++ b/contracts/agents-api/zh/core-errors.md @@ -1,7 +1,7 @@ --- title: "Core 管理错误" source: contracts/agents-api/core-errors.md -source_hash: fbdf667a44da9eb95dae54ca7fc54129d7f8d1b50e1d7e864f00a8ce1794860b +source_hash: b2a6cd40a67bbe58fc78efa068a8ad0cf23cf62ad02e3c1456323fa66e9bb6ef --- `/core/v1` 上的错误使用此封装结构。`message` 是安全的英文文本;`code` 和 `param` 可以为 null。客户端依据稳定的 `code` 和可选的 `param` 进行处理,对未知代码显示 `message`,绝不解析消息,也绝不自动重试被拒绝的写操作。 @@ -108,7 +108,7 @@ Web 的控制台服务器在 `/core` 路径上发生自身故障时使用此封 ## 诊断失败类别 {#diagnostic-failure-categories} -[Session 诊断](session-diagnostics.md)接口会在成功的 200 快照内返回以下类别,而不是以错误封装的形式返回。公开 `/v1` 的 Turn 错误保持不变。除非表格另有说明,否则 `params` 为 `{}`。 +[Session 诊断](session-diagnostics.md)接口会在成功的 200 快照内返回以下类别,而不是以错误封装的形式返回。公开 `/v1` 的 Turn 错误会以官方 code 报告其中一部分类别,见[与 OpenAI 的差异](index.md#differences-from-openai)。除非表格另有说明,否则 `params` 为 `{}`。 | 代码 | 存储原因或安全含义 | | --- | --- | diff --git a/contracts/agents-api/zh/index.md b/contracts/agents-api/zh/index.md index 27fa0234e..053bd7ea6 100644 --- a/contracts/agents-api/zh/index.md +++ b/contracts/agents-api/zh/index.md @@ -1,7 +1,7 @@ --- title: "OpenAI 兼容性" source: contracts/agents-api/index.md -source_hash: fd489e7a0eb5b5847ba9b1a0ac8ad4bbc73b315e012adb9c9d7a8facbfa79c7f +source_hash: 025c67e723ae2e055c4674f5c721cb51bd2b59899f907ccc05b27ce70360f142 --- Core 实现下方固定版本的 OpenAI Agents API。本台账列出各项资源的覆盖情况、相对于官方服务的所有差异以及已知缺口。 @@ -93,7 +93,7 @@ Core 自身字段位于 `x_agents_core` 中([Core extensions](../../../docs/zh - 在 Turn 中途接入流时,不会发送补发 Item 快照。 - Items 列表会包含进行中和未完成的输出 Items,并保留失败函数结果中已提交的 `output`。 - 错误消息会省略官方消息包含的 call 和 executor ID,但未知工具调用的消息会带上其 `call_id`。 -- 失败的 Turn 以官方名称报告模型失败类别,Runtime 或沙箱在执行环境中的失败报告为 `sandbox_error`。Core 的其他失败,例如缺少模型提供方、输入被拒绝或存储失败,一律报告 `internal_error`。消息是每个 code 对应的固定句子,不含原生文本。Subagent Turn 始终报告 `internal_error`。 +- 失败的 Turn 以官方名称报告模型失败类别,Runtime 断开连接或准备失败报告为 `sandbox_error`。Core 的其他失败,例如 Runtime 不可用、缺少模型提供方、输入被拒绝或存储失败,一律报告 `internal_error`。消息是每个 code 对应的固定句子,不含原生文本。Subagent Turn 始终报告 `internal_error`。 - 与其他文本并存的空文本部分可被接受并存储。 - 空输入会返回 400 `invalid_request`,附带通用消息和值为 null 的 param;官方响应为 `invalid_request_error`,param 为 `input`。 - 一旦所有根级 Turn 均已进入终态,Session 用量即可用;官方读取会滞后数秒。 diff --git a/contracts/agents-api/zh/sessions-events.md b/contracts/agents-api/zh/sessions-events.md index 78c27edba..2ccb30548 100644 --- a/contracts/agents-api/zh/sessions-events.md +++ b/contracts/agents-api/zh/sessions-events.md @@ -1,7 +1,7 @@ --- title: "Session、事件和历史" source: contracts/agents-api/sessions-events.md -source_hash: ef11e7900a0dec4acceb06c60a0f1275c3e0231081a929c3f1a074e454fb785b +source_hash: 45b743284c8a03aa016c1b874f6dbeb578e7ec7afcbc565bdcafba333481c950 --- 本契约涵盖 Session 内部发生的事情:发送输入、实时事件流,以及读取 Turn、Item 和使用量的持久化历史。Session 资源本身(创建配置、重试标识、更新、列出和删除)见 [API 行为](wire-semantics.md)。消息和函数结果内容见[消息内容](message-content.md)。[Agents API 指南](../../../docs/zh/api/public-agent-api.md)展示了使用 SDK 和 HTTP 的调用方式。 @@ -80,7 +80,7 @@ Turn 失败后 Session 仍可使用:新输入会启动一个新 Turn。后来 | `self_hosted` Environment 失败后提交新输入、Environment 失败时输入已在等待,或 Environment 已过期 | 409 `environment_unavailable` | "The environment is no longer available for new input." | | Session Harness 无法处理的消息,例如 MiniMax Code 上仅含空白的文本 | 400 `unsupported_or_invalid_configuration` | 见[仅含空白的文本](message-content.md#whitespace-only-text) | -`turn_id` 为空或 `call_id` 为空白时,会返回通用的 400 `invalid_request`。错误消息绝不会重复调用方输入或内部标识符。 +`turn_id` 为空或 `call_id` 为空白时,会返回通用的 400 `invalid_request`。除未知工具调用的消息按官方 SDK 的预期给出其 `call_id` 外,错误消息绝不会重复调用方输入或内部标识符。 ## 创建 Session 时的初始输入 {#initial-input-at-session-creation} @@ -129,7 +129,7 @@ Turn 失败后 Session 仍可使用:新输入会启动一个新 Turn。后来 Turn 和 Item 列表接受 `after`、`limit` 和 `order`([列表规则](wire-semantics.md))。游标是同一 Session 内的 ID。 -**Turn。** Session Turn 路由只包含根 Turn,按创建时间、再按 ID 排序;在该路由中使用子 Turn ID 会返回 404。失败的 Turn 包含 `error: {code: "internal_error", message: "The execution could not complete."}`,绝不包含引擎原始诊断信息。管理员可通过[Session 诊断](session-diagnostics.md)读取失败类别。 +**Turn。** Session Turn 路由只包含根 Turn,按创建时间、再按 ID 排序;在该路由中使用子 Turn ID 会返回 404。失败 Turn 的 `error` 包含官方 `code` 和该 code 对应的固定消息,绝不包含引擎原始诊断信息;[与 OpenAI 的差异](index.md#differences-from-openai)列出 Core 报告的 code。流中的快照与读取返回相同的 `error`。管理员可通过[Session 诊断](session-diagnostics.md)读取失败类别。 **Item。** Item 按首次观察时间、再按其在 Session 中的位置、最后按 ID 排序。更新和重试绝不会移动 Item,也不会更改其 `output_index`;`output_index` 是 Item 在该 Turn 输出 Item 中从零开始的位置,输入 Item 没有此值。读取操作使用已存储的历史索引,绝不会根据原生日志重新构建该索引。Item 列表包含仍在进行中或尚未完成的 Item。 diff --git a/docs/api/public-agent-api.md b/docs/api/public-agent-api.md index 607dbb04d..eea9b7267 100644 --- a/docs/api/public-agent-api.md +++ b/docs/api/public-agent-api.md @@ -575,7 +575,7 @@ The HTTP path is `/vaults`, with the Beta header. A Session selects credentials ## Diagnose a failure -1. Read the Session's `status` and `error`, and the latest Turn's `error`. A failed Turn reports only a generic `internal_error`. +1. Read the Session's `status` and `error`, and the latest Turn's `error`. A failed Turn's `error.code` names the model failure category, `sandbox_error` or `internal_error`. 2. Check that the Environment is connected and its Harness is available. 3. Check the Harness, model and tool combination in [Harness capabilities](../../contracts/agents-api/harness-capabilities.md). 4. Ask the administrator for the [Session diagnostics](../../contracts/agents-api/session-diagnostics.md), which name the failure category, and to check [Troubleshooting](../getting-started/troubleshooting.md) for service logs, credentials and node readiness. diff --git a/docs/runtime-protocol.md b/docs/runtime-protocol.md index c8c1adb64..4b1fe1459 100644 --- a/docs/runtime-protocol.md +++ b/docs/runtime-protocol.md @@ -293,11 +293,11 @@ Retry only an operation whose own contract makes retry safe. A retired preparati ## Native failure classification -An adapter may add `code` and `http_status` to a Run's `error` frame. They are optional neutral metadata, not a terminal event or a public error contract; the frame's text, Usage, `done`, native Session identity and cancellation receipts keep their order and meaning. +An adapter may add `code` and `http_status` to a Run's `error` frame. They are optional neutral metadata, not a terminal event; the frame's text, Usage, `done`, native Session identity and cancellation receipts keep their order and meaning. The accepted codes are `authentication_error`, `rate_limit_exceeded`, `usage_limit_exceeded`, `server_overloaded`, `server_error`, `invalid_request`, `resource_not_found`, `request_timeout`, `context_length_exceeded`, `cyber_policy` and `connection_failed` ([`engine_failure.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/engine_failure.go)). Only `connection_failed` keeps `http_status`, and only an integer from 100 to 599; every other status is discarded. A missing, malformed or unknown value leaves the error unclassified without discarding Usage or `done`. -Core stores accepted values in the Turn outcome as `engine_error_code` and `engine_http_status`. A Run may carry more than one `error` frame, such as an adapter's classified failure followed by the Runtime's own settlement failure; a frame without an accepted code leaves the stored classification in place, and a later accepted code replaces it. The classification is subordinate to the terminal status and Core's `error_code`: it cannot turn a completed or cancelled Turn into a failure, hide an incomplete event stream, or override a persistence or cancellation-receipt failure. Normal delivery and terminal journal draining use the same extraction. [Session diagnostics](../contracts/agents-api/session-diagnostics.md) expose the category only for a failed Turn whose Core error is `engine_failed`. The [Codex](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/codex/README.md) and [Claude Code](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/claude/README.md) adapter guides give each Harness's mapping. An adapter classifies only from structured error data: the Harness's own error variant or status, or a model provider's JSON error body that the Harness passes through, whose own `error.code` or `error.type` may name an accepted code. It never classifies from error prose. +Core stores accepted values in the Turn outcome as `engine_error_code` and `engine_http_status`. A Run may carry more than one `error` frame, such as an adapter's classified failure followed by the Runtime's own settlement failure; a frame without an accepted code leaves the stored classification in place, and a later accepted code replaces it. The classification is subordinate to the terminal status and Core's `error_code`: it cannot turn a completed or cancelled Turn into a failure, hide an incomplete event stream, or override a persistence or cancellation-receipt failure. Normal delivery and terminal journal draining use the same extraction. Only a failed Turn whose Core error is `engine_failed` exposes the category: [Session diagnostics](../contracts/agents-api/session-diagnostics.md) name it, and the public Turn `error.code` reports it under its official name. The [Codex](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/codex/README.md) and [Claude Code](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/claude/README.md) adapter guides give each Harness's mapping. An adapter classifies only from structured error data: the Harness's own error variant or status, or a model provider's JSON error body that the Harness passes through, whose own `error.code` or `error.type` may name an accepted code. It never classifies from error prose. ## Workspace operations diff --git a/docs/zh/api/public-agent-api.md b/docs/zh/api/public-agent-api.md index 53dca8f76..8d459bd18 100644 --- a/docs/zh/api/public-agent-api.md +++ b/docs/zh/api/public-agent-api.md @@ -1,7 +1,7 @@ --- title: "Agents API 指南" source: docs/api/public-agent-api.md -source_hash: 83f4c5d7c0e5c748087e6baa51c792a7cd831e217b47980c65db7d04e9222140 +source_hash: bc22d7ef93e4284b122c25f39b15acb53eec314005b5273f9c9456638e727c67 --- Core 在 `/v1` 提供 OpenAI Agents API。本指南用官方 OpenAI SDK 和普通 HTTP 两种方式演示每项常见任务,并说明 Core 与 OpenAI 的差异。如果尚未启动过 Session,请先阅读[运行第一个 Session](../getting-started/quickstart.md)。 @@ -577,7 +577,7 @@ HTTP 路径为 `/vaults`,需要 Beta 请求头。Session 从其 `vault_ids` ## 诊断故障 {#diagnose-a-failure} -1. 读取 Session 的 `status` 和 `error`,以及最新 Turn 的 `error`。失败的 Turn 只会报告通用的 `internal_error`。 +1. 读取 Session 的 `status` 和 `error`,以及最新 Turn 的 `error`。失败 Turn 的 `error.code` 给出模型失败类别、`sandbox_error` 或 `internal_error`。 2. 检查 Environment 是否已连接,以及其 Harness 是否可用。 3. 在 [Harness 能力](../../../contracts/agents-api/zh/harness-capabilities.md) 中检查 Harness、模型和工具的组合。 4. 向管理员索取[Session 诊断](../../../contracts/agents-api/zh/session-diagnostics.md),其中会指出故障类别;同时请查看[故障排查](../getting-started/troubleshooting.md),了解服务日志、凭据和节点就绪状态。 diff --git a/docs/zh/runtime-protocol.md b/docs/zh/runtime-protocol.md index 63c06f23e..bb8bd145f 100644 --- a/docs/zh/runtime-protocol.md +++ b/docs/zh/runtime-protocol.md @@ -1,7 +1,7 @@ --- title: "Core–Runtime 协议" source: docs/runtime-protocol.md -source_hash: bf12f97d6420804e057223a02609bc4295d9169a834e2758bc0210ef70923efa +source_hash: 99b2b9674ea280bca00031bae7eb7d722198e472029a4773d8675e28742231f5 --- Core–Runtime 协议连接 Core 与 Runtime daemon,定义 daemon 连接上消息的含义和顺序。托管和自托管 Runtime 使用同一协议。 @@ -295,11 +295,11 @@ transport 与 execution 结果分开。Core 唯一 Run 订阅入口是 `Subscrib ## 原生故障分类 {#native-failure-classification} -adapter 可以给 Run 的 `error` frame 添加 `code` 和 `http_status`。它们是可选的中性元数据,不是终结事件或公开错误契约;frame 文本、Usage、`done`、原生 Session 身份和取消回执保持原顺序与含义。 +adapter 可以给 Run 的 `error` frame 添加 `code` 和 `http_status`。它们是可选的中性元数据,不是终结事件;frame 文本、Usage、`done`、原生 Session 身份和取消回执保持原顺序与含义。 接受的 code 为 `authentication_error`、`rate_limit_exceeded`、`usage_limit_exceeded`、`server_overloaded`、`server_error`、`invalid_request`、`resource_not_found`、`request_timeout`、`context_length_exceeded`、`cyber_policy` 和 `connection_failed`([`engine_failure.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/engine_failure.go))。仅 `connection_failed` 保留 `http_status`,且只能是 100 到 599 的整数;其他 status 都丢弃。值缺失、格式错误或未知时保持未分类错误,不丢弃 Usage 或 `done`。 -Core 在 Turn outcome 中将接受的值保存为 `engine_error_code` 和 `engine_http_status`。一个 Run 可以有多个 `error` frame,例如 adapter 的分类失败之后跟着 Runtime 自己的结算失败;没有接受 code 的 frame 不改变已保存的分类,后来接受的 code 会替换它。分类从属于终结状态和 Core 的 `error_code`:不能将已完成或已取消 Turn 变成失败、隐藏不完整事件流,或覆盖持久化或取消回执失败。正常 delivery 和 terminal journal draining 使用同一提取逻辑。[Session 诊断](../../contracts/agents-api/zh/session-diagnostics.md)仅为 Core error 为 `engine_failed` 的失败 Turn 暴露类别。[Codex](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/codex/README.md) 和 [Claude Code](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/claude/README.md) adapter 指南给出各 Harness 的映射。adapter 只依据结构化错误数据分类:Harness 自身的错误类型或状态,或 Harness 原样转交的模型提供商 JSON 错误 body,其 `error.code` 或 `error.type` 可以给出接受的 code。adapter 不依据错误文字分类。 +Core 在 Turn outcome 中将接受的值保存为 `engine_error_code` 和 `engine_http_status`。一个 Run 可以有多个 `error` frame,例如 adapter 的分类失败之后跟着 Runtime 自己的结算失败;没有接受 code 的 frame 不改变已保存的分类,后来接受的 code 会替换它。分类从属于终结状态和 Core 的 `error_code`:不能将已完成或已取消 Turn 变成失败、隐藏不完整事件流,或覆盖持久化或取消回执失败。正常 delivery 和 terminal journal draining 使用同一提取逻辑。只有 Core error 为 `engine_failed` 的失败 Turn 会暴露类别:[Session 诊断](../../contracts/agents-api/zh/session-diagnostics.md)给出该类别,公开 Turn 的 `error.code` 以官方名称报告它。[Codex](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/codex/README.md) 和 [Claude Code](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/claude/README.md) adapter 指南给出各 Harness 的映射。adapter 只依据结构化错误数据分类:Harness 自身的错误类型或状态,或 Harness 原样转交的模型提供商 JSON 错误 body,其 `error.code` 或 `error.type` 可以给出接受的 code。adapter 不依据错误文字分类。 ## 工作区操作 {#workspace-operations} diff --git a/services/core/internal/api/native_classification_integration_test.go b/services/core/internal/api/native_classification_integration_test.go index 6c7baae4c..be2bae85d 100644 --- a/services/core/internal/api/native_classification_integration_test.go +++ b/services/core/internal/api/native_classification_integration_test.go @@ -72,10 +72,10 @@ func TestNativeClassificationPostgresRoundTripAndPublicPrivacy(t *testing.T) { t.Fatal(err) } eventBytes, _ := json.Marshal(events) - if strings.Contains(string(eventBytes), "canary") || strings.Contains(string(eventBytes), "engine_error") || strings.Contains(string(eventBytes), "http_status") { + if strings.Contains(string(eventBytes), "canary") || strings.Contains(string(eventBytes), "private.example") || strings.Contains(string(eventBytes), "http_status") { t.Fatal("private metadata entered SSE") } - // The Turn read alone publishes the category: official names stay, anything else is internal_error. + // The Turn read and its stream event publish the same category: official names stay, anything else is internal_error. public := code if code == "secret-canary" { public = "internal_error" @@ -83,6 +83,19 @@ func TestNativeClassificationPostgresRoundTripAndPublicPrivacy(t *testing.T) { if !strings.Contains(string(before[1]), `"error":{"code":"`+public+`"`) { t.Fatal("Turn error code", before[1]) } + streamed := false + for _, change := range events { + if change.Event.Type == "agent.session.turn.failed" { + event, err := streamResponse(session, change, "") + if err != nil || event.Turn == nil || event.Turn.Error == nil || event.Turn.Error.Code != public { + t.Fatal("streamed Turn error", err, event.Turn) + } + streamed = true + } + } + if !streamed { + t.Fatal("no agent.session.turn.failed event") + } // Remove only the private fields; every other public response must keep its bytes. if _, err = pool.Exec(t.Context(), `UPDATE turns SET outcome=outcome-'engine_error_code'-'engine_http_status' WHERE id=$1`, receipt.TurnID); err != nil { t.Fatal(err) diff --git a/services/core/internal/api/turn_diagnostic_failure.go b/services/core/internal/api/turn_diagnostic_failure.go index 53805d0f7..e508c4e98 100644 --- a/services/core/internal/api/turn_diagnostic_failure.go +++ b/services/core/internal/api/turn_diagnostic_failure.go @@ -60,8 +60,10 @@ func turnDiagnosticFailure(turn sessions.Turn) *DiagnosticFailure { // publicTurnErrors maps the diagnostics code to the official // SessionTurnErrorCodeResource with a fixed customer-safe sentence: native -// model categories keep their names and Runtime or sandbox failures in the -// execution environment are sandbox_error. Native text never reaches it. +// model categories keep their names and a Runtime that disconnected or failed +// its preparation is sandbox_error. runtime_unavailable stays internal_error +// because it also covers Core failing to dispatch the Turn. Native text never +// reaches it. var publicTurnErrors = map[string]string{ "context_length_exceeded": "The request exceeds the model's context window.", "usage_limit_exceeded": "The organization has reached a usage, plan, or billing limit.", @@ -80,7 +82,7 @@ var publicTurnErrors = map[string]string{ func publicTurnError(diagnosticCode string) *v1.TurnError { code := diagnosticCode switch diagnosticCode { - case "runtime_unavailable", "runtime_disconnected", "runtime_preparation_failed": + case "runtime_disconnected", "runtime_preparation_failed": code = "sandbox_error" } if message, ok := publicTurnErrors[code]; ok { diff --git a/services/core/internal/api/turn_diagnostic_failure_test.go b/services/core/internal/api/turn_diagnostic_failure_test.go index 9a46e8c1f..e0cdd78cb 100644 --- a/services/core/internal/api/turn_diagnostic_failure_test.go +++ b/services/core/internal/api/turn_diagnostic_failure_test.go @@ -155,6 +155,7 @@ func TestPublicTurnErrorFollowsDiagnosticClassification(t *testing.T) { `{"error_code":"device_disconnected"}`: "sandbox_error", `{"error_code":"preparation_start_failed"}`: "sandbox_error", `{"error_code":"model_provider_required"}`: "internal_error", + `{"error_code":"execution_unavailable"}`: "internal_error", `{"error_code":"event_persistence_failed"}`: "internal_error", ``: "internal_error", } { diff --git a/services/core/internal/sessions/turn_changes.go b/services/core/internal/sessions/turn_changes.go index 789233f9f..b8876be72 100644 --- a/services/core/internal/sessions/turn_changes.go +++ b/services/core/internal/sessions/turn_changes.go @@ -7,6 +7,7 @@ import ( "time" v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/items" ) @@ -19,7 +20,7 @@ func TurnChanges(turn Turn, created bool) []SessionChange { if turn.Status == TurnWaiting { return nil } - turn.Outcome = nil + turn.Outcome = failureClassification(turn.Outcome) kind := turn.Status if created { kind = "created" @@ -33,7 +34,7 @@ func TurnChanges(turn Turn, created bool) []SessionChange { // usage. The snapshot of a Turn that ended is settled: it marks that Turn's // end, while an input reserved during its Artifact capture is newer work. func ActivityChange(turn Turn, usage json.RawMessage, actions []v1.FunctionCallAction) SessionChange { - turn.Outcome = nil + turn.Outcome = failureClassification(turn.Outcome) status := "in_progress" if TerminalStatus(turn.Status) { status = "idle" @@ -49,6 +50,31 @@ func ActivityChange(turn Turn, usage json.RawMessage, actions []v1.FunctionCallA } } +// failureClassification keeps only the categories a failed Turn's public error +// is classified from: Core's error code and the normalized model category. A +// journaled snapshot then reports the same error as a read, while native codes, +// text, HTTP status and the Runtime's result stay out of the journal. +func failureClassification(outcome json.RawMessage) json.RawMessage { + var fields map[string]json.RawMessage + if json.Unmarshal(outcome, &fields) != nil { + return nil + } + var failure struct { + ErrorCode string `json:"error_code"` + EngineErrorCode string `json:"engine_error_code,omitempty"` + } + var native string + // Malformed optional metadata cannot hide Core's error code. + _ = json.Unmarshal(fields["error_code"], &failure.ErrorCode) + _ = json.Unmarshal(fields["engine_error_code"], &native) + failure.EngineErrorCode, _ = proto.NormalizeEngineFailure(native, nil) + if failure.ErrorCode == "" { + return nil + } + value, _ := json.Marshal(failure) + return value +} + // ArtifactSettlement is what a Turn that ended does with the Artifacts it // staged. type ArtifactSettlement string diff --git a/services/core/internal/sessions/turn_changes_test.go b/services/core/internal/sessions/turn_changes_test.go index 3536e39e2..30eb4470f 100644 --- a/services/core/internal/sessions/turn_changes_test.go +++ b/services/core/internal/sessions/turn_changes_test.go @@ -15,9 +15,12 @@ const ( ) func turnWith(status string) Turn { - return Turn{ID: testTurn, SessionID: testSession, Status: status, CompletedAt: time.Unix(1700000000, 0), Outcome: json.RawMessage(`{"private":true}`)} + return Turn{ID: testTurn, SessionID: testSession, Status: status, CompletedAt: time.Unix(1700000000, 0), Outcome: json.RawMessage(`{"error":"native text","error_code":"engine_failed","engine_error_code":"rate_limit_exceeded","engine_http_status":429}`)} } +// classified is turnWith's outcome without its native text and HTTP status. +const classified = `{"error_code":"engine_failed","engine_error_code":"rate_limit_exceeded"}` + func types(changes []SessionChange) []string { var kinds []string for _, change := range changes { @@ -80,7 +83,7 @@ func TestTurnChangesReportTheNewStatus(t *testing.T) { t.Fatalf("%s: %q", test.status, types(changes)) } for _, change := range changes { - if change.Turn.Outcome != nil || change.Event.TurnID != testTurn || change.Turn.Status != test.status { + if string(change.Turn.Outcome) != classified || change.Event.TurnID != testTurn || change.Turn.Status != test.status { t.Fatalf("%s: snapshot %+v", test.status, change) } } @@ -108,7 +111,7 @@ func TestActivityChangeDerivesSessionActivity(t *testing.T) { {TurnFailed, nil, "agent.session.failed", true}, } { change := ActivityChange(turnWith(test.status), usage, test.actions) - if change.Event.Type != test.want || change.Settled != test.settled || change.Turn.Outcome != nil || + if change.Event.Type != test.want || change.Settled != test.settled || string(change.Turn.Outcome) != classified || string(change.SessionUsage) != string(usage) || !reflect.DeepEqual(change.RequiredActions, test.actions) { t.Fatalf("%s: %+v", test.status, change) } diff --git a/services/core/tests/integration/input_conflicts_public_test.go b/services/core/tests/integration/input_conflicts_public_test.go index 8313628ee..3506c6e34 100644 --- a/services/core/tests/integration/input_conflicts_public_test.go +++ b/services/core/tests/integration/input_conflicts_public_test.go @@ -18,7 +18,8 @@ import ( const conflictAgent = `"agent":{"id":"agent_conflicts","model":"fixture","tools":[],"multi_agent":{"enabled":false,"max_concurrent_subagents":null},"reasoning":{},"service_tier":"auto","text":{"format":{"type":"text"},"verbosity":"medium"}}` // Official error bodies for Session input rejections (EVT-11, EVT-12, ERR-22, -// ERR-27). Every param is null and no message repeats caller input. +// ERR-27). Every param is null and only the unknown tool call message names +// the caller's call_id. const ( missingSessionBody = `{"error":{"message":"Resource not found.","type":"not_found_error","code":"not_found_error","param":null}}` + "\n" otherTurnBody = `{"error":{"message":"The tool call belongs to a different Turn.","type":"invalid_request_error","code":"invalid_request_error","param":null}}` + "\n" From 95c6ca34656db6640f26997f1607dc1f20825870 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Fri, 23 Oct 2026 21:14:26 +0000 Subject: [PATCH 2/2] List command_output and not_directory in the Runtime protocol --- docs/runtime-protocol.md | 4 ++-- docs/zh/runtime-protocol.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/runtime-protocol.md b/docs/runtime-protocol.md index 4b1fe1459..387dcd763 100644 --- a/docs/runtime-protocol.md +++ b/docs/runtime-protocol.md @@ -108,7 +108,7 @@ The linked source files define the required fields, validators, limits and finit | --- | --- | --- | | `runtime_prepare` | `runtime_prepare_result` | [Initialization and capability transfer](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/runtime_prepare.go) | | `execution_prepare`, `execution_start`, `execution_release` | `preparation_status` | [Execution admission](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/preparation.go) | -| `prompt_cancel` | `delta`, `reasoning`, `output_message`, `tool_call`, `usage`, `error`, `done`, `heartbeat`, `interaction_decision_ack` | [Requests](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/outbound.go), [events and capabilities](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/inbound.go) | +| `prompt_cancel` | `delta`, `reasoning`, `output_message`, `tool_call`, `command_output`, `usage`, `error`, `done`, `heartbeat`, `interaction_decision_ack` | [Requests](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/outbound.go), [events and capabilities](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/inbound.go) | | `prompt_steer` | `prompt_steer_ack` | [Active input receipts](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/steering.go) | | `function_result` | `function_call`, `function_call_withdrawn`, `interaction_decision_ack` | [Function calls](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/functions.go) | | `workspace_read`, `workspace_write`, `workspace_export` | Matching `*_result` | [Read](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/workspace_read.go), [write](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/workspace_write.go), [export](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/workspace_export.go) | @@ -303,7 +303,7 @@ Core stores accepted values in the Turn outcome as `engine_error_code` and `engi A workspace read that needs no running Turn uses the read-only preparation profile: `execution_prepare` with `workspace_read_only`, which requires the `workspace_read_preparation` capability. It accepts only the bound Environment and resource identity; execution options, model and MCP credentials, native Session continuation and model or tool input are excluded, and the owner rejects `execution_start`. The Runtime serves it from its bound local workspace without starting a Harness process. The profile publishes `released` after the Runtime drops the preparation's ownership; a stale status snapshot never publishes success. A release request, HTTP disconnect or remote socket closure alone does not confirm the release. -`workspace_read` targets an existing preparation handle, or the Run it was transferred to, on the same authenticated device connection, with the exact frozen Environment identity; callers cannot supply sockets, credentials or workspace roots. `operation: directory` lists one workspace-relative directory (an empty path selects the root) with mutually exclusive byte and entry limits. A result carries a bounded number of single-component UTF-8 names, the entry kind, regular-file sizes and explicit truncation, and is returned only after directory access and handle cleanup settle. There is no snapshot, recursion or pagination at this layer. Byte and directory reads share target checks, correlation, capacity and retained operation waits. A rejected result carries one of `invalid_request`, `resource_unavailable`, `read_capacity`, `not_found` and `permission_denied`. +`workspace_read` targets an existing preparation handle, or the Run it was transferred to, on the same authenticated device connection, with the exact frozen Environment identity; callers cannot supply sockets, credentials or workspace roots. `operation: directory` lists one workspace-relative directory (an empty path selects the root) with mutually exclusive byte and entry limits. A result carries a bounded number of single-component UTF-8 names, the entry kind, regular-file sizes and explicit truncation, and is returned only after directory access and handle cleanup settle. There is no snapshot, recursion or pagination at this layer. Byte and directory reads share target checks, correlation, capacity and retained operation waits. A rejected result carries one of `invalid_request`, `resource_unavailable`, `read_capacity`, `not_found` and `permission_denied`, and a directory read whose path is missing, a regular file or a symbolic link carries `not_directory`. Request payloads and correlation IDs are bounded before admission; oversized IDs are not echoed, and oversized trace metadata is omitted from replies. Raw read results are bounded within the transport frame. These are private transport limits, not public Files parameters. A successful read requires complete bytes or explicit truncation and an acknowledged native close. A safe native rejection carries no bytes; an interrupted or ambiguous read stays unknown and stops further reads on that owner. A dispatched read keeps its bounded waiter across observer cancellation and resource transfer or release, and resource closure stops new admission. The gateway bounds subscriptions and never retries or replays a read on reconnect; a duplicate pending operation ID cannot start another read. diff --git a/docs/zh/runtime-protocol.md b/docs/zh/runtime-protocol.md index bb8bd145f..5cbe4e530 100644 --- a/docs/zh/runtime-protocol.md +++ b/docs/zh/runtime-protocol.md @@ -1,7 +1,7 @@ --- title: "Core–Runtime 协议" source: docs/runtime-protocol.md -source_hash: 99b2b9674ea280bca00031bae7eb7d722198e472029a4773d8675e28742231f5 +source_hash: 986ab6e6ef3979a672f2e49f7c75346e7bfff327d9de1a6138e7f5c75b55d720 --- Core–Runtime 协议连接 Core 与 Runtime daemon,定义 daemon 连接上消息的含义和顺序。托管和自托管 Runtime 使用同一协议。 @@ -110,7 +110,7 @@ Core 对 `usage` 和 `resume` 没有准入规则。 | --- | --- | --- | | `runtime_prepare` | `runtime_prepare_result` | [初始化与能力传输](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/runtime_prepare.go) | | `execution_prepare`, `execution_start`, `execution_release` | `preparation_status` | [执行准入](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/preparation.go) | -| `prompt_cancel` | `delta`, `reasoning`, `output_message`, `tool_call`, `usage`, `error`, `done`, `heartbeat`, `interaction_decision_ack` | [请求](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/outbound.go)、[事件与能力](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/inbound.go) | +| `prompt_cancel` | `delta`, `reasoning`, `output_message`, `tool_call`, `command_output`, `usage`, `error`, `done`, `heartbeat`, `interaction_decision_ack` | [请求](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/outbound.go)、[事件与能力](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/inbound.go) | | `prompt_steer` | `prompt_steer_ack` | [活动输入回执](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/steering.go) | | `function_result` | `function_call`, `function_call_withdrawn`, `interaction_decision_ack` | [Function 调用](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/functions.go) | | `workspace_read`, `workspace_write`, `workspace_export` | 对应的 `*_result` | [读取](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/workspace_read.go)、[写入](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/workspace_write.go)、[导出](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/workspace_export.go) | @@ -305,7 +305,7 @@ Core 在 Turn outcome 中将接受的值保存为 `engine_error_code` 和 `engin 无需运行 Turn 的工作区读取使用只读 preparation profile:带 `workspace_read_only` 的 `execution_prepare`,要求 `workspace_read_preparation` 能力。仅接受绑定的 Environment 和 resource 身份;不包含 execution option、model 与 MCP 凭据、原生 Session continuation、model 或 tool 输入,owner 拒绝 `execution_start`。Runtime 从绑定的本地工作区提供读取,不启动 Harness 进程。profile 在 Runtime 放弃该 preparation 的所有权后发布 `released`;旧 status snapshot 不发布成功。release 请求、HTTP 断连或远端 socket 关闭本身都不确认释放。 -`workspace_read` 在同一已认证设备连接上,针对现有 preparation handle 或它已转移给的 Run,使用精确冻结的 Environment 身份;调用方不能提供 socket、凭据或 workspace root。`operation: directory` 列出一个 workspace 相对目录(空路径选择根目录),字节与条目限制互斥。结果携带有限数量的单路径组件 UTF-8 名称,并包含 entry kind、普通文件大小和明确截断信息;仅在目录访问与 handle 清理结算后返回。此层没有快照、递归或分页。字节读取与目录读取共享目标检查、关联、容量和保留的操作等待。被拒绝的结果携带 `invalid_request`、`resource_unavailable`、`read_capacity`、`not_found` 和 `permission_denied` 之一。 +`workspace_read` 在同一已认证设备连接上,针对现有 preparation handle 或它已转移给的 Run,使用精确冻结的 Environment 身份;调用方不能提供 socket、凭据或 workspace root。`operation: directory` 列出一个 workspace 相对目录(空路径选择根目录),字节与条目限制互斥。结果携带有限数量的单路径组件 UTF-8 名称,并包含 entry kind、普通文件大小和明确截断信息;仅在目录访问与 handle 清理结算后返回。此层没有快照、递归或分页。字节读取与目录读取共享目标检查、关联、容量和保留的操作等待。被拒绝的结果携带 `invalid_request`、`resource_unavailable`、`read_capacity`、`not_found` 和 `permission_denied` 之一;路径不存在、是普通文件或符号链接的目录读取携带 `not_directory`。 准入前,请求 payload 与 correlation ID 受限;过长 ID 不回显,过大的 trace metadata 不进入回复。原始 read result 受限于 transport frame 之内。这些是私有 transport 限制,不是公开 Files 参数。成功读取要求完整字节或明确截断,并确认原生 close。安全的原生拒绝不携带字节;中断或有歧义的读取保持未知,并停止该 owner 的后续读取。已 dispatch 的读取在 observer 取消以及资源转移或释放后仍保留有限时等待方,资源关闭阻止新准入。gateway 限制订阅,不在重连后重试或重放读取;重复的 pending operation ID 不能启动另一次读取。