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
2 changes: 1 addition & 1 deletion contracts/agents-api/execution-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The Worker fails previously claimed work without replay after execution loss; qu

Core carries the schema in `ExecutionControls.OutputFormat` and requires `structured_output` in the Harness's declaration and the Runtime's heartbeat, only for requests that use the option. Frozen schemas reach preparation before input and apply to initial and resumed execution; Start cannot replace them.

The Claude adapter passes `outputFormat` to the pinned SDK and allows its native `StructuredOutput` terminal tool, which is internal and never an extra caller function. A matching live root tool result and an attributed successful SDK result confirm the output. The adapter publishes the native `result.result` string unchanged as a completed `final_answer` message with the native tool-use ID; parent assistant prose keeps its own ID. Unvalidated retries and cancelled candidates never become the answer, and the adapter never serializes `structured_output` back to JSON. The stream follows the official message sequence with the whole text in one `output_text.delta`. The bridge advertises the operation only when it reports `structured_output`, and a workspace Runtime also needs `workspace_structured_output`.
The Claude adapter passes `outputFormat` to the pinned SDK and allows its native `StructuredOutput` terminal tool, which is internal and never an extra caller function. The adapter accumulates the original `input_json_delta` text by live root Session, native message ID, content-block index and tool-use ID. Only a completed matching assistant tool call, its successful receipt and a unique candidate consistent with the attributed successful SDK `structured_output` confirm that text. The adapter publishes it unchanged as a completed `final_answer` message with the native tool-use ID; parent assistant prose keeps its own ID. Native retry and fallback decisions stay in the SDK. Closed partial inputs without a completed tool snapshot and explicitly retracted candidates are discarded; replacements must establish the same confirmation chain. Missing or ambiguous identities and cancelled candidates never become the answer. The generic SDK `result` text is not the structured payload, and the adapter never serializes `structured_output` back to JSON. Raw numeric digits are preserved; exact-number schema validation and native fallbacks without raw tool-input events remain [known gaps](./index.md#known-gaps). The stream follows the official message sequence with the whole text in one `output_text.delta`. The bridge advertises the operation only when it reports `structured_output`, and a workspace Runtime also needs `workspace_structured_output`.

## Deferred function discovery

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 @@ -110,6 +110,7 @@ Each item is Core's deliberate or native behavior where the official service beh

- Explicit reasoning effort or summary, service tiers other than `auto`, enabled `web_search` and enabled programmatic tool calling are saved but rejected at Session admission.
- Harness support differs as each [declaration](./harness-onboarding.md#declare-support) states. Codex has no structured output or `tool_search`. Claude Code takes no whitespace-only text, `medium` verbosity only and function-result images only inline and in successful results; it rejects Subagents with MCP, including MCP servers that Environment Plugins install; structured output with Subagents, MCP, installed capabilities or `tool_search`; and `tool_search` with MCP or installed capabilities. MiniMax Code has no public functions, no service-origin MCP, no image input, no whitespace-only text, no required MCP and `medium` verbosity only, and takes `allowed_tools` null only. Each Harness reserves an MCP `server_label`: `codex_apps` for Codex, `functions` for Claude Code and `oac_workspace` for MiniMax Code. Claude Code also requires labels to match `^[a-zA-Z0-9_-]+$` and `allowed_tools` names to match `^[a-zA-Z0-9_.-]+$`.
- Claude structured output preserves the [acknowledged raw tool input](./execution-tools.md#structured-output), while native validation and parsed-value correlation use binary64. Distinct raw numbers can compare equal after parsing, so this does not establish exact-number schema conformance, even when every schema literal passes admission. Exact numeric output validation remains unqualified. A native streaming-to-nonstreaming fallback can emit an assistant tool snapshot without raw tool-input events. The adapter rejects such output because the original JSON text is unavailable; parsed input and generic result text cannot substitute for it.
- Model-derived reasoning defaults are not resolved.
- MCP tools support the `http` transport only; `stdio` is rejected, and so is an inline `authorization` on a Session MCP transport ([HTTP MCP](./execution-tools.md#http-mcp)).

Expand Down
4 changes: 2 additions & 2 deletions contracts/agents-api/zh/execution-tools.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "执行工具"
source: contracts/agents-api/execution-tools.md
source_hash: 21ba998ee2697491899ab001367ab7dd2dea7ef86f4bfc015b95cbbf5beba558
source_hash: 93a806df9242467c895429ebdec03e3daf28dd5f652f1f29bbda4dcfc18995f0
---

Agent 在 `tools` 中声明应用函数、控制项和 MCP 服务器,并可在 `text.format` 中声明输出 schema。本契约说明 Core 如何验证声明、哪些内容跨越 Runtime 边界,以及调用方如何恢复待执行操作。每个 Harness 的[声明](harness-onboarding.md#declare-support)说明它支持其中哪些内容。原生工作区工具和 Environment Plugin MCP 属于 [Environment](environments.md#skills-plugins-and-environment-mcp)。
Expand Down Expand Up @@ -43,7 +43,7 @@ SSE 仅提供实时事件。重启或流丢失后,读取 Session 的 `required

Core 在 `ExecutionControls.OutputFormat` 中携带 schema,仅对使用该选项的请求要求 Harness 的声明和 Runtime 的心跳都支持 `structured_output`。冻结的 schema 在输入之前送达准备阶段,适用于初次和恢复执行;Start 不能替换它。

Claude 适配器将 `outputFormat` 传给固定版本 SDK,并允许原生 `StructuredOutput` 终态工具;该工具属于内部,不是额外的调用方函数。匹配的实时根工具结果和已归属的成功 SDK 结果确认输出。适配器将原生 `result.result` 字符串原样发布为已完成的 `final_answer` 消息,使用原生 tool-use ID;父 assistant 文本保留自己的 ID。未验证重试和已取消候选不会成为答案,适配器不会把 `structured_output` 重新序列化为 JSON。流遵循官方消息顺序,将整段文本放入一个 `output_text.delta`。桥接层仅在报告 `structured_output` 时声明该操作,工作区 Runtime 还需要 `workspace_structured_output`。
Claude 适配器将 `outputFormat` 传给固定版本 SDK,并允许原生 `StructuredOutput` 终态工具;该工具属于内部,不是额外的调用方函数。适配器按实时根 Session、原生消息 ID、内容块索引和 tool-use ID 累积原始 `input_json_delta` 文本。只有匹配的完整 assistant 工具调用、成功回执,以及与已归属的成功 SDK `structured_output` 一致的唯一候选,才能确认该文本。适配器将其原样发布为已完成的 `final_answer` 消息,使用原生 tool-use ID;父 assistant 文本保留自己的 ID。原生重试和 fallback 决策仍由 SDK 负责。已关闭但没有完整工具 snapshot 的部分输入及显式撤回的候选会被弃置;替代候选必须建立相同的确认链。缺失或歧义身份以及已取消候选不会成为答案。通用 SDK `result` 文本不是结构化载荷,适配器不会把 `structured_output` 重新序列化为 JSON。原始数字文本会被保留;精确数值 schema 验证和不提供原始工具输入事件的原生 fallback 仍是[已知缺口](./index.md#known-gaps)。流遵循官方消息顺序,将整段文本放入一个 `output_text.delta`。桥接层仅在报告 `structured_output` 时声明该操作,工作区 Runtime 还需要 `workspace_structured_output`。

## 延迟函数发现 {#deferred-function-discovery}

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: "Agents API 覆盖台账"
source: contracts/agents-api/index.md
source_hash: 480884468c15159c84b123978c1d8d5fdac89bd3833556100be1ef4d31fb8829
source_hash: 4bbbd2097e76481ec50fc7bc41de7855e88fb8346e65f389ec0230f5654f15ce
---

Core 旨在以下方固定版本为准支持完整的 OpenAI Agents API([public API rule](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/AGENTS.md#public-api))。本台账记录 Core 对各项资源实现了哪些内容、哪些契约保存其详细信息,并列出相对于 OpenAI 服务的所有已知差异和所有未解决缺口。[API namespaces and credentials](../../../docs/zh/api/index.md) 说明谁调用哪些 API;[Agents API guide](../../../docs/zh/api/public-agent-api.md) 介绍使用方法。
Expand Down Expand Up @@ -112,6 +112,7 @@ Core 自身字段位于 `x_agents_core` 中([Core extensions](../../../docs/zh

- 显式指定推理强度或摘要、使用 `auto` 之外的服务层级、启用 `web_search` 或启用程序化工具调用,这些设置都会被保存,但在 Session 准入时会被拒绝。
- 各 Harness 的支持差异以其[声明](harness-onboarding.md#declare-support)为准。Codex 不支持结构化输出或 `tool_search`。Claude Code 不接受仅含空白的文本,只支持 `medium` 详细程度,函数结果图像只能内联且只能出现在成功结果中;它拒绝子智能体与 MCP(包括 Environment Plugins 安装的 MCP server)同时使用,拒绝结构化输出与子智能体、MCP、已安装能力或 `tool_search` 同时使用,也拒绝 `tool_search` 与 MCP 或已安装能力同时使用。MiniMax Code 不提供公共 functions,没有服务源 MCP,不支持图像输入、仅含空白的文本和必需 MCP,只支持 `medium` 详细程度,且只接受值为 null 的 `allowed_tools`。每个 Harness 都保留一个 MCP `server_label`:Codex 保留 `codex_apps`,Claude Code 保留 `functions`,MiniMax Code 保留 `oac_workspace`。Claude Code 还要求标签匹配 `^[a-zA-Z0-9_-]+$`,`allowed_tools` 中的名称匹配 `^[a-zA-Z0-9_.-]+$`。
- Claude 结构化输出保留[已确认的原始工具输入](./execution-tools.md#structured-output),但原生验证和解析值关联使用 binary64。不同的原始数字在解析后可能相等,因此即使 schema 的所有数值字面量都通过准入,也不能据此确认精确数值的 schema 符合性。精确数值输出验证仍未经资格验证。 原生从流式到非流式的 fallback 可能只产生 assistant 工具 snapshot,而没有原始工具输入事件。由于原始 JSON 文本不可用,适配器会拒绝此类输出;解析后的输入和通用结果文本不能替代它。
- 由模型推导出的推理默认值不会被解析确定。
- MCP 工具仅支持 `http` 传输,`stdio` 会被拒绝,Session MCP 传输中的内联 `authorization` 也会被拒绝([HTTP MCP](execution-tools.md#http-mcp))。

Expand Down
92 changes: 76 additions & 16 deletions packages/claude-sdk-adapter/src/structured_output.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,99 @@
import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
import { isDeepStrictEqual } from "node:util";
import type { MessageEvent } from "./messages.js";

type Candidate = { id: string; text: string; snapshot?: string; input?: unknown; receipt?: string; stopped: boolean; status?: "failed" | "accepted" | "settled" | "discarded" };

// StructuredOutput is the native terminal tool. Its acknowledged tool-use identity
// owns the final JSON message; the parent assistant may already own ordinary prose.
export class StructuredOutput {
private readonly calls = new Set<string>();
private accepted?: string;
private readonly calls = new Map<string, Candidate>();
private readonly events = new Set<string>();
private active?: { id: string; blocks: Map<number, Candidate> };
private session = "";

consume(message: SDKMessage, session: string): void {
if (!session || message.session_id !== session ||
!("parent_tool_use_id" in message) || message.parent_tool_use_id !== null ||
("isReplay" in message && message.isReplay) || ("isSynthetic" in message && message.isSynthetic)) return;
if (message.type === "assistant") {
const retracted = message.type === "assistant" && message.parent_tool_use_id === null ? message.supersedes :
message.type === "system" && message.subtype === "model_refusal_fallback" && message.scope !== "local" ? message.retracted_message_uuids : undefined;
if (retracted) for (const call of this.calls.values()) {
if (retracted.some(id => call.snapshot === id || call.receipt === id)) call.status = "discarded";
}
if (!("parent_tool_use_id" in message) || message.parent_tool_use_id !== null) return;
this.session = session;
if (message.type === "stream_event") {
if (!message.uuid || this.events.has(message.uuid)) throw new Error("invalid structured stream identity");
this.events.add(message.uuid);
const event = message.event;
if (event.type === "message_start") {
if (!event.message.id || this.active?.blocks.size) throw new Error("invalid structured message identity");
this.active = { id: event.message.id, blocks: new Map() };
} else if (event.type === "content_block_start" && event.content_block.type === "tool_use" && event.content_block.name === "StructuredOutput") {
const id = event.content_block.id;
// An abandoned prefix never acquired an executable native call identity.
const previous = this.calls.get(id);
if (!this.active || !id || this.active.blocks.has(event.index) ||
previous && (previous.status !== "discarded" || previous.snapshot)) throw new Error("invalid structured output identity");
const call: Candidate = { id, text: "", stopped: false };
this.calls.set(id, call);
this.active.blocks.set(event.index, call);
} else if (event.type === "content_block_delta" && event.delta.type === "input_json_delta") {
const call = this.active?.blocks.get(event.index);
if (call) {
if (call.snapshot || call.stopped) throw new Error("late structured output input");
call.text += event.delta.partial_json;
}
} else if (event.type === "content_block_stop") {
const call = this.active?.blocks.get(event.index);
if (call) {
if (call.stopped) throw new Error("duplicate structured output block stop");
call.stopped = true;
// Native connection retries close partial blocks without executing a tool.
if (!call.snapshot) call.status = "discarded";
}
} else if (event.type === "message_stop") {
if (this.active && [...this.active.blocks.values()].some(call => !call.stopped)) throw new Error("incomplete structured output message");
this.active = undefined;
}
} else if (message.type === "assistant") {
for (const block of message.message.content) {
if (block.type !== "tool_use" || block.name !== "StructuredOutput") continue;
if (!block.id || this.calls.has(block.id)) throw new Error("invalid structured output identity");
this.calls.add(block.id);
const call = this.calls.get(block.id);
if (!call || !this.active || message.message.id !== this.active.id ||
![...this.active.blocks.values()].includes(call) || call.snapshot || call.stopped || !message.uuid || message.error || message.aborted) throw new Error("unmatched structured output snapshot");
call.snapshot = message.uuid;
call.input = block.input;
}
} else if (message.type === "user" && Array.isArray(message.message.content)) {
for (const block of message.message.content) {
if (block.type !== "tool_result" || !this.calls.delete(block.tool_use_id)) continue;
if (!block.is_error) this.accepted = block.tool_use_id;
if (block.type !== "tool_result") continue;
const call = this.calls.get(block.tool_use_id);
if (!call) continue;
if (!call.snapshot || call.receipt || !message.uuid) throw new Error("invalid structured output receipt");
// Failed native attempts may contain malformed JSON and must reach the
// SDK retry loop. Successful inputs must match JSON transport's zero semantics.
if (call.status !== "discarded" && !block.is_error && !isDeepStrictEqual(JSON.parse(call.text, (_, value) => value === 0 ? 0 : value), call.input)) {
throw new Error("unmatched structured output input");
}
call.receipt = message.uuid;
if (call.status !== "discarded") call.status = block.is_error ? "failed" : "accepted";
}
}
}

complete(message: SDKMessage): MessageEvent {
if (message.type !== "result" || message.subtype !== "success" || message.is_error ||
message.structured_output === undefined || !this.accepted || this.calls.size ||
typeof message.result !== "string") throw new Error("unconfirmed structured output");
// Preserve the SDK's final string. Reserializing structured_output can round
// JSON numbers and would replace the native validated result.
JSON.parse(message.result);
const id = this.accepted;
this.accepted = undefined;
return { type: "output_message", message: { id, status: "completed", phase: "final_answer", text: message.result } };
message.session_id !== this.session || message.structured_output === undefined) throw new Error("unconfirmed structured output");
const calls = [...this.calls.values()];
const accepted = calls.filter(call => call.status === "accepted" && isDeepStrictEqual(call.input, message.structured_output));
if (calls.some(call => call.status !== "discarded" && (!call.stopped || !call.receipt)) || accepted.length !== 1) {
throw new Error("unconfirmed structured output");
}
// Native validation compares binary64 values. Publish only the attributed raw
// tool input: reserializing the validated object would lose original digits.
const { id, text } = accepted[0]!;
for (const call of calls) if (call.status === "accepted") call.status = "settled";
return { type: "output_message", message: { id, status: "completed", phase: "final_answer", text } };
}
}
Loading
Loading