diff --git a/contracts/agents-api/index.md b/contracts/agents-api/index.md index f0ef5ea4b..bc93f43bc 100644 --- a/contracts/agents-api/index.md +++ b/contracts/agents-api/index.md @@ -89,7 +89,8 @@ Each item is Core's deliberate or native behavior where the official service beh - A function call the native Harness abandons, such as a Subagent call dropped when the root closes or interrupts the child or a MiniMax Code call past its 120 s timeout, is withdrawn: it stops being a required action and a later result for it returns 409 `conflict_error`. The official API has no withdrawal. - 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. +- 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`. - 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 9c5ccd37c..4b335cc2c 100644 --- a/contracts/agents-api/sessions-events.md +++ b/contracts/agents-api/sessions-events.md @@ -69,7 +69,7 @@ Checks run in this order: request validation, Session lookup, retry lookup, the | Input the Turn cannot accept in its state, such as a result after cancellation or after its Turn ended without a stored result | 409 `conflict_error` | "The Turn cannot accept this input in its current state." | | A result that differs from the call's stored result, before or after its Turn ends | 409 `conflict_error` | "The tool call already has a different result." | | The same `Idempotency-Key` with a different batch | 409 `idempotency_conflict` | "This idempotency key was used with different input." | -| A result whose `call_id` names no function call of this Session | 400 `invalid_request_error`, param null | "Unknown pending tool call." | +| A result whose `call_id` names no function call of this Session | 400 `invalid_request_error`, param null | "Unknown pending tool call: {call_id}" | | A result for a call of this Session whose `turn_id` names another Turn, an unknown Turn ID or a value that is not a Turn ID | 400 `invalid_request_error`, param null | "The tool call belongs to a different Turn." | | A missing, malformed or foreign Session | 404 `not_found_error` | "Resource not found." | | A reserved batch whose five-minute deadline passed before a Turn started | 409 `environment_input_expired` | "The environment input deadline elapsed before admission." | diff --git a/contracts/agents-api/zh/index.md b/contracts/agents-api/zh/index.md index 7cb9b2d1c..27fa0234e 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: 4431b17e74c51ece10dc606c8c2078aeb5e974259cf954868f79b797c91d63a7 +source_hash: fd489e7a0eb5b5847ba9b1a0ac8ad4bbc73b315e012adb9c9d7a8facbfa79c7f --- Core 实现下方固定版本的 OpenAI Agents API。本台账列出各项资源的覆盖情况、相对于官方服务的所有差异以及已知缺口。 @@ -92,7 +92,8 @@ Core 自身字段位于 `x_agents_core` 中([Core extensions](../../../docs/zh - 原生 Harness 放弃的函数调用(例如根关闭或中断子级时被丢弃的 Subagent 调用,或超过 120 秒超时的 MiniMax Code 调用)会被撤回:它不再是待执行操作,之后为它提交结果会返回 409 `conflict_error`。官方 API 没有撤回机制。 - 在 Turn 中途接入流时,不会发送补发 Item 快照。 - Items 列表会包含进行中和未完成的输出 Items,并保留失败函数结果中已提交的 `output`。 -- 错误消息会省略官方消息包含的 call 和 executor ID。 +- 错误消息会省略官方消息包含的 call 和 executor ID,但未知工具调用的消息会带上其 `call_id`。 +- 失败的 Turn 以官方名称报告模型失败类别,Runtime 或沙箱在执行环境中的失败报告为 `sandbox_error`。Core 的其他失败,例如缺少模型提供方、输入被拒绝或存储失败,一律报告 `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 c1314349a..78c27edba 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: 9e02f5fc7ace233f2a17cd7d8fa53447794b95430bf7bf219b66ece33f96ca35 +source_hash: ef11e7900a0dec4acceb06c60a0f1275c3e0231081a929c3f1a074e454fb785b --- 本契约涵盖 Session 内部发生的事情:发送输入、实时事件流,以及读取 Turn、Item 和使用量的持久化历史。Session 资源本身(创建配置、重试标识、更新、列出和删除)见 [API 行为](wire-semantics.md)。消息和函数结果内容见[消息内容](message-content.md)。[Agents API 指南](../../../docs/zh/api/public-agent-api.md)展示了使用 SDK 和 HTTP 的调用方式。 @@ -71,7 +71,7 @@ Turn 失败后 Session 仍可使用:新输入会启动一个新 Turn。后来 | Turn 在当前状态下无法接受的输入,例如取消后提交的结果,或 Turn 已结束且未存储结果后提交的结果 | 409 `conflict_error` | "The Turn cannot accept this input in its current state." | | 与该调用已存储结果不同的结果,无论其 Turn 结束之前还是之后 | 409 `conflict_error` | "The tool call already has a different result." | | 相同 `Idempotency-Key` 搭配不同批次 | 409 `idempotency_conflict` | "This idempotency key was used with different input." | -| `call_id` 未标识该 Session 的任何函数调用的结果 | 400 `invalid_request_error`, param null | "Unknown pending tool call." | +| `call_id` 未标识该 Session 的任何函数调用的结果 | 400 `invalid_request_error`, param null | "Unknown pending tool call: {call_id}" | | 为该 Session 的某个调用提交结果,但 `turn_id` 标识另一个 Turn、未知 Turn ID,或不是 Turn ID 的值 | 400 `invalid_request_error`, param null | "The tool call belongs to a different Turn." | | 缺失、格式错误或来自其他作用域的 Session | 404 `not_found_error` | "Resource not found." | | 预留的批次在 Turn 启动前超过五分钟截止时间 | 409 `environment_input_expired` | "The environment input deadline elapsed before admission." | diff --git a/packages/agents-client/src/client.test.ts b/packages/agents-client/src/client.test.ts index a14312838..acba7ac14 100644 --- a/packages/agents-client/src/client.test.ts +++ b/packages/agents-client/src/client.test.ts @@ -2271,7 +2271,7 @@ describe("OpenAIAgentsClient", () => { for (const [status, type, code, message] of [ [409, "conflict_error", "conflict_error", "The tool call already has a different result."], [409, "conflict_error", "conflict_error", "The Turn cannot accept this input in its current state."], - [400, "invalid_request_error", "invalid_request_error", "Unknown pending tool call."], + [400, "invalid_request_error", "invalid_request_error", "Unknown pending tool call: call"], [400, "invalid_request_error", "invalid_request_error", "The tool call belongs to a different Turn."], ] as const) { const calls: FetchCall[] = []; diff --git a/services/core/internal/api/errors_sessions.go b/services/core/internal/api/errors_sessions.go index 6cf86d9c1..e69ec141b 100644 --- a/services/core/internal/api/errors_sessions.go +++ b/services/core/internal/api/errors_sessions.go @@ -29,6 +29,7 @@ func writeInputError(w http.ResponseWriter, r *http.Request, err error) { // and Environment configuration checks they perform. func writeSessionsError(w http.ResponseWriter, r *http.Request, err error) { var cursor *sessions.CursorError + var unknownCall *sessions.UnknownFunctionCallError switch { case errors.Is(err, projects.ErrArchived): // Executor credential management checks the Project in its own @@ -61,8 +62,9 @@ func writeSessionsError(w http.ResponseWriter, r *http.Request, err error) { case errors.Is(err, sessions.ErrNotIdle): // Observed official status, type, code, null param and message. writeError(w, http.StatusConflict, "conflict_error", "session must be durably idle or failed without required actions before deletion") - case errors.Is(err, sessions.ErrUnknownFunctionCall): - writeError(w, http.StatusBadRequest, "invalid_request_error", "Unknown pending tool call.") + case errors.As(err, &unknownCall): + // The official SDK recognizes a lost race by this exact message. + writeError(w, http.StatusBadRequest, "invalid_request_error", "Unknown pending tool call: "+unknownCall.CallID) case errors.Is(err, sessions.ErrFunctionCallTurnMismatch): writeError(w, http.StatusBadRequest, "invalid_request_error", "The tool call belongs to a different Turn.") case errors.Is(err, sessions.ErrFunctionResultConflict): diff --git a/services/core/internal/api/function_inputs_test.go b/services/core/internal/api/function_inputs_test.go index 1f5bb7b5f..1905496e8 100644 --- a/services/core/internal/api/function_inputs_test.go +++ b/services/core/internal/api/function_inputs_test.go @@ -93,8 +93,8 @@ func TestPublicInputAdmissionErrorFields(t *testing.T) { `{"error":{"message":"Earlier input to this Session is still pending.","type":"conflict_error","code":"conflict_error","param":null}}`}, {"changed_result", fmt.Errorf("submit turn inputs: %w", sessions.ErrFunctionResultConflict), 409, `{"error":{"message":"The tool call already has a different result.","type":"conflict_error","code":"conflict_error","param":null}}`}, - {"unknown_call", fmt.Errorf("submit turn inputs: %w", sessions.ErrUnknownFunctionCall), 400, - `{"error":{"message":"Unknown pending tool call.","type":"invalid_request_error","code":"invalid_request_error","param":null}}`}, + {"unknown_call", fmt.Errorf("submit turn inputs: %w", &sessions.UnknownFunctionCallError{CallID: "call_1"}), 400, + `{"error":{"message":"Unknown pending tool call: call_1","type":"invalid_request_error","code":"invalid_request_error","param":null}}`}, {"other_turn", fmt.Errorf("submit turn inputs: %w", sessions.ErrFunctionCallTurnMismatch), 400, `{"error":{"message":"The tool call belongs to a different Turn.","type":"invalid_request_error","code":"invalid_request_error","param":null}}`}, {"missing_session", fmt.Errorf("submit turn inputs: %w", sessions.ErrNotFound), 404, diff --git a/services/core/internal/api/mcp_configuration_test.go b/services/core/internal/api/mcp_configuration_test.go index 9314cf0a0..0d4fe0ab5 100644 --- a/services/core/internal/api/mcp_configuration_test.go +++ b/services/core/internal/api/mcp_configuration_test.go @@ -8,6 +8,8 @@ import ( "reflect" "strings" "testing" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) const publicMCP = `{"type":"mcp","server_label":"records","connection_origin":"service","transport":{"type":"http","server_url":"https://mcp.example.test/tools"}}` @@ -195,6 +197,21 @@ func TestSessionMCPKeepsToolOrderAndStripsSavedHeaders(t *testing.T) { } } +func TestSessionReadOmitsMCPHeaders(t *testing.T) { + stored, err := resolveMCPTool(json.RawMessage(`{"type":"mcp","server_label":"records","transport":{"type":"http","server_url":"https://mcp.example.test","headers":{"X-Tenant":"a"}}}`), false) + if err != nil { + t.Fatal(err) + } + raw, _ := json.Marshal(configuration{Agent: v1.Agent{ID: "agent", Model: "fixture", Tools: []json.RawMessage{stored}}, Environment: v1.Environment{Type: "none"}}) + response, err := sessionResponse(sessions.Session{Configuration: raw}, "") + if err != nil || !strings.Contains(string(stored), "X-Tenant") { + t.Fatal(err) + } + if got := string(response.Agent.Tools[0]); strings.Contains(got, "headers") || !strings.Contains(got, `"transport":{"type":"http","server_url":"https://mcp.example.test"}`) { + t.Fatalf("Session read: %s", got) + } +} + func TestPublicEnvironmentMCPPreservesDeclaration(t *testing.T) { for _, saved := range []bool{false, true} { for _, allowed := range []string{"null", "[]", `["prove"]`} { diff --git a/services/core/internal/api/native_classification_integration_test.go b/services/core/internal/api/native_classification_integration_test.go index e9421dd60..6c7baae4c 100644 --- a/services/core/internal/api/native_classification_integration_test.go +++ b/services/core/internal/api/native_classification_integration_test.go @@ -75,11 +75,22 @@ func TestNativeClassificationPostgresRoundTripAndPublicPrivacy(t *testing.T) { if strings.Contains(string(eventBytes), "canary") || strings.Contains(string(eventBytes), "engine_error") || strings.Contains(string(eventBytes), "http_status") { t.Fatal("private metadata entered SSE") } - // Remove only additive private fields. All public response bytes must match. + // The Turn read alone publishes the category: official names stay, anything else is internal_error. + public := code + if code == "secret-canary" { + public = "internal_error" + } + if !strings.Contains(string(before[1]), `"error":{"code":"`+public+`"`) { + t.Fatal("Turn error code", before[1]) + } + // 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) } for i, path := range paths { + if i == 1 { + continue + } w := diagnosticRequest(h, path, "Bearer caller") if w.Code != 200 || !bytes.Equal(before[i], w.Body.Bytes()) { t.Fatal("private classification changed public response", path, w.Body) diff --git a/services/core/internal/api/session_response.go b/services/core/internal/api/session_response.go index 58e8f2911..5f244d58f 100644 --- a/services/core/internal/api/session_response.go +++ b/services/core/internal/api/session_response.go @@ -27,7 +27,7 @@ func sessionResponse(session sessions.Session, executorURL string) (v1.Session, return v1.Session{}, errors.New("unsupported stored tool configuration") } if tool.Type != "tool_search" { - tools = append(tools, projectedMCPCredential(raw, cfg)) + tools = append(tools, projectedMCPCredential(withoutMCPHeaders(raw), cfg)) } } cfg.Agent.Tools = tools @@ -106,6 +106,22 @@ func sessionResponse(session sessions.Session, executorURL string) (v1.Session, return response, nil } +// withoutMCPHeaders drops the stored HTTP headers of an MCP tool. The pinned +// Session read shape carries only type and server_url; dispatch keeps reading +// the headers from the stored configuration. +func withoutMCPHeaders(raw json.RawMessage) json.RawMessage { + var tool v1.MCPTool + if json.Unmarshal(raw, &tool) != nil || tool.Type != "mcp" || tool.Transport.Headers == nil { + return raw + } + tool.Transport.Headers = nil + projected, err := json.Marshal(tool) + if err != nil { + return raw + } + return projected +} + func sessionEnvironment(session sessions.Session, kind, executorURL string) (v1.SessionEnvironment, error) { if kind == "none" { return v1.SessionEnvironment{Type: "none"}, nil diff --git a/services/core/internal/api/turn_diagnostic_failure.go b/services/core/internal/api/turn_diagnostic_failure.go index 1f2081511..53805d0f7 100644 --- a/services/core/internal/api/turn_diagnostic_failure.go +++ b/services/core/internal/api/turn_diagnostic_failure.go @@ -3,6 +3,7 @@ package api import ( "encoding/json" + 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/sessions" ) @@ -56,3 +57,34 @@ func turnDiagnosticFailure(turn sessions.Turn) *DiagnosticFailure { } return &DiagnosticFailure{Code: code, Params: params, FailedAt: diagnosticTime(turn.CompletedAt)} } + +// 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. +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.", + "rate_limit_exceeded": "The request exceeds the available rate limit.", + "server_overloaded": "The model service is temporarily overloaded.", + "cyber_policy": "The request was rejected by a safety policy.", + "connection_failed": "The request could not connect to the model service.", + "server_error": "The model service encountered an unexpected error.", + "authentication_error": "The API credentials are invalid or lack the required access.", + "invalid_request": "The request contains invalid input or configuration.", + "resource_not_found": "The requested model or resource is unavailable.", + "request_timeout": "The request timed out before the model service responded.", + "sandbox_error": "The request could not complete in its execution environment.", +} + +func publicTurnError(diagnosticCode string) *v1.TurnError { + code := diagnosticCode + switch diagnosticCode { + case "runtime_unavailable", "runtime_disconnected", "runtime_preparation_failed": + code = "sandbox_error" + } + if message, ok := publicTurnErrors[code]; ok { + return &v1.TurnError{Code: code, Message: message} + } + return &v1.TurnError{Code: "internal_error", Message: "The execution could not complete."} +} diff --git a/services/core/internal/api/turn_diagnostic_failure_test.go b/services/core/internal/api/turn_diagnostic_failure_test.go index d5b3b20a7..9a46e8c1f 100644 --- a/services/core/internal/api/turn_diagnostic_failure_test.go +++ b/services/core/internal/api/turn_diagnostic_failure_test.go @@ -146,3 +146,21 @@ func TestDiagnosticNativeClassification(t *testing.T) { } } } + +func TestPublicTurnErrorFollowsDiagnosticClassification(t *testing.T) { + for outcome, want := range map[string]string{ + `{"error_code":"engine_failed","engine_error_code":"rate_limit_exceeded","error":"raw-secret-canary"}`: "rate_limit_exceeded", + `{"error_code":"engine_failed","engine_error_code":"connection_failed","engine_http_status":502}`: "connection_failed", + `{"error_code":"engine_failed","engine_error_code":"native-only"}`: "internal_error", + `{"error_code":"device_disconnected"}`: "sandbox_error", + `{"error_code":"preparation_start_failed"}`: "sandbox_error", + `{"error_code":"model_provider_required"}`: "internal_error", + `{"error_code":"event_persistence_failed"}`: "internal_error", + ``: "internal_error", + } { + got := publicTurnError(turnDiagnosticFailure(sessions.Turn{Status: sessions.TurnFailed, Outcome: json.RawMessage(outcome)}).Code) + if got.Code != want || got.Message == "" || strings.Contains(got.Message, "canary") { + t.Fatalf("%s: %+v", outcome, got) + } + } +} diff --git a/services/core/internal/api/turns.go b/services/core/internal/api/turns.go index 7aa48a01e..b07aa8dff 100644 --- a/services/core/internal/api/turns.go +++ b/services/core/internal/api/turns.go @@ -74,8 +74,7 @@ func turnResponse(session sessions.Session, turn sessions.Turn) (v1.Turn, error) // Session Turns are root Turns, so subagent_id is always null here. response := v1.Turn{Usage: tokenUsage(turn.Usage), ID: turn.ID, SessionID: turn.SessionID, AgentID: cfg.Agent.ID, Object: "agent.session.turn", Status: turn.Status, CreatedAt: turn.CreatedAt.Unix(), StartedAt: unixTime(turn.StartedAt), CompletedAt: unixTime(turn.CompletedAt)} if turn.Status == sessions.TurnFailed { - // Native errors can contain secrets; publish a stable category without raw diagnostics. - response.Error = &v1.TurnError{Code: "internal_error", Message: "The execution could not complete."} + response.Error = publicTurnError(turnDiagnosticFailure(turn).Code) } return response, nil } diff --git a/services/core/internal/sessions/errors.go b/services/core/internal/sessions/errors.go index 3b14d76d5..b938dfc57 100644 --- a/services/core/internal/sessions/errors.go +++ b/services/core/internal/sessions/errors.go @@ -78,3 +78,12 @@ var ( // Session Artifacts. ErrArtifactCursor = &CursorError{Message: "after is not a valid artifact ID"} ) + +// UnknownFunctionCallError is ErrUnknownFunctionCall with the rejected call_id. +type UnknownFunctionCallError struct{ CallID string } + +func (e *UnknownFunctionCallError) Error() string { + return ErrUnknownFunctionCall.Error() + ": " + e.CallID +} + +func (e *UnknownFunctionCallError) Unwrap() error { return ErrUnknownFunctionCall } diff --git a/services/core/internal/sessions/functions.go b/services/core/internal/sessions/functions.go index 7a666664b..5810ef9d4 100644 --- a/services/core/internal/sessions/functions.go +++ b/services/core/internal/sessions/functions.go @@ -131,7 +131,7 @@ func AdmitFunctionResult(ctx context.Context, tx FunctionResultTx, input Functio if elsewhere { return Turn{}, ErrFunctionCallTurnMismatch } - return Turn{}, ErrUnknownFunctionCall + return Turn{}, &UnknownFunctionCallError{CallID: input.CallID} } // PendingFunctionCallsTx reads a Turn's function calls that wait for a result diff --git a/services/core/tests/integration/input_conflicts_public_test.go b/services/core/tests/integration/input_conflicts_public_test.go index 7ec79a89b..8313628ee 100644 --- a/services/core/tests/integration/input_conflicts_public_test.go +++ b/services/core/tests/integration/input_conflicts_public_test.go @@ -21,7 +21,6 @@ const conflictAgent = `"agent":{"id":"agent_conflicts","model":"fixture","tools" // ERR-27). Every param is null and no message repeats caller input. const ( missingSessionBody = `{"error":{"message":"Resource not found.","type":"not_found_error","code":"not_found_error","param":null}}` + "\n" - unknownCallBody = `{"error":{"message":"Unknown pending tool call.","type":"invalid_request_error","code":"invalid_request_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" turnConflictBody = `{"error":{"message":"The Turn cannot accept this input in its current state.","type":"conflict_error","code":"conflict_error","param":null}}` + "\n" pendingInputBody = `{"error":{"message":"Earlier input to this Session is still pending.","type":"conflict_error","code":"conflict_error","param":null}}` + "\n" @@ -29,6 +28,10 @@ const ( eventKeyReuseBody = `{"error":{"message":"This idempotency key was used with different input.","type":"conflict_error","code":"idempotency_conflict","param":null}}` + "\n" ) +func unknownCallBody(call string) string { + return `{"error":{"message":"Unknown pending tool call: ` + call + `","type":"invalid_request_error","code":"invalid_request_error","param":null}}` + "\n" +} + // TestSessionInputConflictsAndResultTargetsPostgres replays the Session input // error rows CF2–CF4 and CF6–CF9 of the official semantics alignment over HTTP // and PostgreSQL. Inside an owned Session, an unknown call @@ -182,11 +185,11 @@ func TestSessionInputConflictsAndResultTargetsPostgres(t *testing.T) { // CF6–CF8: result targets in owned, other owned, foreign and missing Sessions. valid := result(current.TurnID, "pending-call", "value") reject([]rejection{ - {"unknown-call", owner, session, []string{result(current.TurnID, "absent-call", "x")}, 400, unknownCallBody, ""}, - {"unknown-call-any-turn", owner, session, []string{result(uuid.NewString(), "absent-call", "x")}, 400, unknownCallBody, ""}, - {"unknown-call-malformed-turn", owner, session, []string{result("turn_malformed", "absent-call", "x")}, 400, unknownCallBody, ""}, - {"call-of-other-session", owner, other, []string{valid}, 400, unknownCallBody, ""}, - {"rollback-batch", owner, session, []string{message, valid, result(current.TurnID, "absent-call", "x")}, 400, unknownCallBody, ""}, + {"unknown-call", owner, session, []string{result(current.TurnID, "absent-call", "x")}, 400, unknownCallBody("absent-call"), ""}, + {"unknown-call-any-turn", owner, session, []string{result(uuid.NewString(), "absent-call", "x")}, 400, unknownCallBody("absent-call"), ""}, + {"unknown-call-malformed-turn", owner, session, []string{result("turn_malformed", "absent-call", "x")}, 400, unknownCallBody("absent-call"), ""}, + {"call-of-other-session", owner, other, []string{valid}, 400, unknownCallBody("pending-call"), ""}, + {"rollback-batch", owner, session, []string{message, valid, result(current.TurnID, "absent-call", "x")}, 400, unknownCallBody("absent-call"), ""}, {"completed-turn", owner, session, []string{result(first.TurnID, "pending-call", "x")}, 400, otherTurnBody, ""}, {"call-of-completed-turn", owner, session, []string{result(current.TurnID, "first-call", "one")}, 400, otherTurnBody, ""}, {"unknown-turn", owner, session, []string{result(uuid.NewString(), "pending-call", "x")}, 400, otherTurnBody, ""}, diff --git a/services/core/tests/official_function_inputs.py b/services/core/tests/official_function_inputs.py index 0cb520648..16e5adef4 100644 --- a/services/core/tests/official_function_inputs.py +++ b/services/core/tests/official_function_inputs.py @@ -16,7 +16,8 @@ # Expected (type, code, message) by status; None skips the message check. MISSING = ("not_found_error", "not_found_error", "Resource not found.") -UNKNOWN_CALL = ("invalid_request_error", "invalid_request_error", "Unknown pending tool call.") +def unknown_call(call_id): + return ("invalid_request_error", "invalid_request_error", f"Unknown pending tool call: {call_id}") OTHER_TURN = ("invalid_request_error", "invalid_request_error", "The tool call belongs to a different Turn.") INVALID = ("invalid_request_error", "invalid_request", None) KEY_REUSE = ("conflict_error", "idempotency_conflict", None) @@ -47,10 +48,10 @@ def submit(api, events, key, expected=202, error=None, target=session): output = [{"type":"input_text","text":""}, {"type":"input_image","image_url":"data:image/png;base64,AA=="}, {"type":"input_text","text":"last"}] message = {"type":"agent.session.input.message","input":[{"role":"user","content":[{"type":"input_text","text":"follow up"}]}]} batch = [result("a", success=False, error="failure", output=output), result("b", success=True, output=None, error=None), result("c", success=True), message, {"type":"agent.session.input.cancel"}] - submit(api, [message, result("rollback", success=True), result("missing", success=True)], "rollback", 400, UNKNOWN_CALL) + submit(api, [message, result("rollback", success=True), result("missing", success=True)], "rollback", 400, unknown_call("missing")) submit(api, [result("a", str(uuid.uuid4()), success=True)], "other-turn", 400, OTHER_TURN) submit(api, [message, result("rollback", "turn_malformed", success=True)], "malformed-turn", 400, OTHER_TURN) - submit(api, [result("a", success=True)], "other-session", 400, UNKNOWN_CALL, other) + submit(api, [result("a", success=True)], "other-session", 400, unknown_call("a"), other) submit(api, [message, result("rollback", success=None)], "malformed", 400, INVALID) stranger = api.with_options(api_key=foreign) submit(stranger, [result("a", success=True)], "foreign", 404, MISSING) diff --git a/services/core/tests/official_self_hosted_cancel.py b/services/core/tests/official_self_hosted_cancel.py index cb12af1d4..abe5b1e51 100644 --- a/services/core/tests/official_self_hosted_cancel.py +++ b/services/core/tests/official_self_hosted_cancel.py @@ -132,7 +132,7 @@ def current(session_id): unknown = raw.post(endpoint + session_id + "/events", json={"events": missing_function}, headers={"Idempotency-Key": str(uuid.uuid4()), "Authorization": "Bearer " + token}) assert unknown.json() == {"error": {"type": "invalid_request_error", "code": "invalid_request_error", - "param": None, "message": "Unknown pending tool call."}} + "param": None, "message": "Unknown pending tool call: unknown-function"}} raw_submit(session_id, "foreign-cancel", expected=404, key_token=settings["foreign_token"]) for missing in (result["deleted_id"], str(uuid.uuid4())): sdk_submit(missing, "missing-cancel", expected=404)