diff --git a/apps/daemon/internal/agent/codex/error_classification.go b/apps/daemon/internal/agent/codex/error_classification.go index 14670ec16..06585abe3 100644 --- a/apps/daemon/internal/agent/codex/error_classification.go +++ b/apps/daemon/internal/agent/codex/error_classification.go @@ -32,6 +32,8 @@ func classifyTurnError(failure *TurnError) proto.ErrorPayload { code = "invalid_request" case "cyberPolicy": code = "cyber_policy" + case "other": + code = providerRejectionCode(failure.Message) } return proto.ErrorPayload{Code: code} } @@ -58,3 +60,29 @@ func classifyTurnError(failure *TurnError) proto.ErrorPayload { } return proto.ErrorPayload{} } + +// Codex 0.153.4 reports a provider HTTP 400 as "other" and forwards the +// response body verbatim; its own "other" failures are prose. A JSON object with +// an object "error" member is the provider's rejection: a declared code in its +// own error.code or error.type wins, otherwise it is invalid_request. Prose, such +// as a plain-text 400 body, stays unclassified. +func providerRejectionCode(message string) string { + var body struct { + Error *struct { + Code json.RawMessage `json:"code"` + Type json.RawMessage `json:"type"` + } `json:"error"` + } + if json.Unmarshal([]byte(message), &body) != nil || body.Error == nil { + return "" + } + for _, raw := range []json.RawMessage{body.Error.Code, body.Error.Type} { + var name string + if json.Unmarshal(raw, &name) == nil { + if code, _ := proto.NormalizeEngineFailure(name, nil); code != "" { + return code + } + } + } + return "invalid_request" +} diff --git a/apps/daemon/internal/agent/codex/error_classification_test.go b/apps/daemon/internal/agent/codex/error_classification_test.go index 6a5a04014..024a09873 100644 --- a/apps/daemon/internal/agent/codex/error_classification_test.go +++ b/apps/daemon/internal/agent/codex/error_classification_test.go @@ -93,3 +93,38 @@ func TestClassificationOnlyFromRootTerminalError(t *testing.T) { }) } } + +// Codex 0.153.4 reports a provider HTTP 400 as "other" with the verbatim body and +// repeats it on the error notification stream. +func TestProviderRejectionClassifiedOnce(t *testing.T) { + body := `{"error":{"message":"invalid params, unknown model","code":"invalid_prompt"}}` + for message, want := range map[string]string{ + body: "invalid_request", + `{"error":{"message":"too long","code":"context_length_exceeded"}}`: "context_length_exceeded", + `{"error":{"message":"bad key","type":"authentication_error"}}`: "authentication_error", + "plain text bad request": "", "unexpected status 404 Not Found: The model bogus does not exist": "", `{"error":"bad"}`: "", + } { + if got := classifyTurnError(&TurnError{Message: message, CodexErrorInfo: json.RawMessage(`"other"`)}); got.Code != want { + t.Fatal(message, got) + } + } + out := make(chan proto.Envelope, 16) + s := &Session{runID: "run", out: out, cancelCtx: context.Background(), cfg: defaultSessionConfig(testStateRoot(t)), rpc: NewJSONRPCClient(JSONRPCConfig{})} + s.registerHandlers() + s.setThreadID("root") + turnError, _ := json.Marshal(TurnError{Message: body, CodexErrorInfo: json.RawMessage(`"other"`)}) + scopeNotification(t, s, "turn/started", `{"threadId":"root","turn":{"id":"turn"}}`) + scopeNotification(t, s, "error", `{"threadId":"root","turnId":"turn","error":`+string(turnError)+`,"willRetry":false}`) + scopeNotification(t, s, "turn/completed", `{"threadId":"root","turn":{"id":"turn","status":"failed","error":`+string(turnError)+`}}`) + var failures []proto.ErrorPayload + for _, env := range collectObserved(t, out) { + if env.Type == proto.TypeError { + var p proto.ErrorPayload + _ = env.DecodePayload(&p) + failures = append(failures, p) + } + } + if len(failures) != 1 || failures[0].Code != "invalid_request" || failures[0].Error != "invalid_prompt: invalid params, unknown model" { + t.Fatalf("%+v", failures) + } +} diff --git a/apps/daemon/internal/agent/codex/session.go b/apps/daemon/internal/agent/codex/session.go index 044c66ae0..71f6a1f7d 100644 --- a/apps/daemon/internal/agent/codex/session.go +++ b/apps/daemon/internal/agent/codex/session.go @@ -210,12 +210,13 @@ func (s *Session) onTurnCompleted(raw json.RawMessage) { // operators can't see why a key was rejected. // 3. buffered text from the "error" notification stream // (sandbox warnings, late stream packets) — last because - // it's noisier than turn.error. + // it's noisier than turn.error. Codex also reports the + // terminal error on that stream, so identical text is + // kept once. body := finalText - if turnErrMsg := turnErrorMessage(p.Turn.Error); turnErrMsg != "" { - body = appendOnNewline(body, turnErrMsg) - } - if errText != "" { + turnErrMsg := turnErrorMessage(p.Turn.Error) + body = appendOnNewline(body, turnErrMsg) + if errText != turnErrMsg { body = appendOnNewline(body, errText) } s.emitTerminalFailure(body, true, classifyTurnError(p.Turn.Error)) diff --git a/docs/runtime-protocol.md b/docs/runtime-protocol.md index d87df0a68..728658980 100644 --- a/docs/runtime-protocol.md +++ b/docs/runtime-protocol.md @@ -182,7 +182,7 @@ An adapter may add `code` and `http_status` to a Run's `error` frame. They are o 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`. 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 never classifies error prose. +Core stores accepted values in the Turn outcome as `engine_error_code` and `engine_http_status`. 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. ## Workspace operations diff --git a/docs/zh/runtime-protocol.md b/docs/zh/runtime-protocol.md index 3500b87df..2071b0741 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: 8349aca995e66669b7941b6aad5f28e6ae3259516d923a0494938a61fb3e711e +source_hash: 47121442f49eb3b7d6590fa0769d1531fc49fc397169cdc219f9bf9eb05f80f2 --- 此协议在 Runtime daemon 获取机器凭据后连接 Core 与 daemon,定义 daemon 连接上消息的含义和顺序。wire 类型、限制和验证器仅在 [`internal/agentdaemon/proto`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/internal/agentdaemon/proto) 中定义一次;Core 的 [gateway](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/services/core/internal/runtimegateway) 与参考 Runtime 的 [dispatcher](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/apps/daemon/internal/dispatch) 都使用它们,因此无需同步第二套 payload schema。签发凭据和打开连接的 HTTP 路由见[机器连接 API](../../contracts/agents-api/zh/machine-api.md)。 @@ -185,7 +185,7 @@ adapter 可以给 Run 的 `error` frame 添加 `code` 和 `http_status`。它们 接受的 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`。分类从属于终结状态和 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 不依据错误文字分类。 +Core 在 Turn outcome 中将接受的值保存为 `engine_error_code` 和 `engine_http_status`。分类从属于终结状态和 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 不依据错误文字分类。 ## 工作区操作 {#workspace-operations} diff --git a/services/core/deploy/codex/README.md b/services/core/deploy/codex/README.md index 645908565..aeaddef93 100644 --- a/services/core/deploy/codex/README.md +++ b/services/core/deploy/codex/README.md @@ -52,10 +52,11 @@ The adapter classifies a failed Turn only from the exact root terminal Turn's `c | `internalServerError` | `server_error` | | `badRequest` | `invalid_request` | | `cyberPolicy` | `cyber_policy` | +| `other` whose message is a JSON object with an object `error` member | that member's `code` or `type` when it is an accepted code, otherwise `invalid_request` | | `httpConnectionFailed`, `responseStreamConnectionFailed`, `responseStreamDisconnected` | `connection_failed`, with the upstream `httpStatusCode` | | `responseTooManyFailedAttempts` | `rate_limit_exceeded` when `httpStatusCode` is 429, otherwise `connection_failed` | -Every other variant stays unclassified. +Codex `0.153.4` reports a provider HTTP 400 as `other` and puts the provider's error body in the message, such as the rejection of an unknown model name. Every other variant stays unclassified, and so does `other` with any other message, including a plain-text 400 body and the `unexpected status` text Codex writes for provider 401 and 404 responses. ## Runtime image diff --git a/services/core/deploy/mcode/README.md b/services/core/deploy/mcode/README.md index 9272bd8a3..f993eb85d 100644 --- a/services/core/deploy/mcode/README.md +++ b/services/core/deploy/mcode/README.md @@ -36,7 +36,7 @@ In the workspace profile, the frozen installation's Skills are linked into the S - Continuation requires the exact recorded native Session ID and the same working directory. Recovery without a recorded ID is rejected. - MiniMax Code reports no usage: ACP context occupancy and cumulative cost are not per-Turn usage. - Workspace `workspace_bash` calls become command observations with the command text, output text and status; exit code and duration stay unknown. Native task and Skill utilities produce no public Items. -- The adapter attaches no [native failure classification](../../../../docs/runtime-protocol.md#native-failure-classification) to its errors. +- The adapter attaches no [native failure classification](../../../../docs/runtime-protocol.md#native-failure-classification) to its errors: MiniMax Code `0.4.12` reports a model provider rejection over ACP as JSON-RPC internal error `-32603` whose only detail is prose, so these failures stay unclassified. ## Runtime image