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
49 changes: 49 additions & 0 deletions apps/daemon/internal/agent/codex/harness_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,52 @@ func TestReasoningEffortReachesEveryNativeTurn(t *testing.T) {
})
}
}

func TestOutputSchemaReachesEveryNativeTurn(t *testing.T) {
for _, resumeID := range []string{"", "fixture-native-thread"} {
t.Run("resume="+resumeID, func(t *testing.T) {
req, cfg, root := preparationFixture(t)
t.Setenv("OAC_TEST_EXECUTOR_MODE", "complete")
req.AgentSessionID = resumeID
schema := json.RawMessage(`{"type":"object","properties":{"answer":{"type":"string"}},"required":["answer"],"additionalProperties":false}`)
req.ExecutionControls = &proto.ExecutionControls{OutputFormat: &proto.OutputFormat{Type: "json_schema", Schema: schema}}
ownerCtx, cancelOwner := context.WithCancel(context.Background())
t.Cleanup(cancelOwner)
e, err := newExecutor(ownerCtx, req, cfg)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := e.Close(ctx); err != nil {
t.Error(err)
}
})
for _, id := range []string{"one", "two"} {
out := make(chan proto.Envelope, 20)
turn, err := e.StartTurn(t.Context(), id, proto.TextInput("answer"), out)
if err != nil {
t.Fatal(err)
}
awaitExecutorTurn(t, turn, out)
}
turns := 0
for _, frame := range preparationFrames(t, root) {
if frame.Method != "turn/start" {
continue
}
turns++
var wire struct {
OutputSchema json.RawMessage `json:"outputSchema"`
}
if json.Unmarshal(frame.Params, &wire) != nil || string(wire.OutputSchema) != string(schema) {
t.Fatal("Turn lost the frozen output schema", string(frame.Params))
}
}
if turns != 2 {
t.Fatal("expected two Turns", turns)
}
})
}
}
7 changes: 7 additions & 0 deletions apps/daemon/internal/agent/codex/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ type SessionPlan struct {
// ModelReasoningEffort is frozen for launch and every native Turn.
ModelReasoningEffort string

// OutputSchema is the frozen public JSON Schema sent as outputSchema on every
// native root Turn.
OutputSchema json.RawMessage

// Cleanup is the deferred housekeeping the session must run after the child exits.
Cleanup func()
}
Expand Down Expand Up @@ -161,6 +165,9 @@ func applyExecutionControls(plan *SessionPlan, controls *proto.ExecutionControls
}
}
}
if format := controls.OutputFormat; format != nil {
plan.OutputSchema = format.Schema
}
if controls.ReasoningEffort != "" {
plan.ModelReasoningEffort = controls.ReasoningEffort
set("model_reasoning_effort", strconv(controls.ReasoningEffort))
Expand Down
3 changes: 0 additions & 3 deletions apps/daemon/internal/agent/codex/preparation.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ import (
)

func newPreparation(parent context.Context, req proto.PromptRequestPayload, cfg sessionConfig) (*Prepared, error) {
if req.ExecutionControls != nil && req.ExecutionControls.OutputFormat != nil {
return nil, errors.New("codex: structured output is not qualified")
}
if req.WorkspaceReadOnly {
return nil, errors.New("codex: workspace reads use the local Runtime interface")
}
Expand Down
2 changes: 2 additions & 0 deletions apps/daemon/internal/agent/codex/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ type TurnStartParams struct {
ThreadID string `json:"threadId"`
Input []UserInput `json:"input"`
CollaborationMode *CollaborationMode `json:"collaborationMode,omitempty"`
// OutputSchema constrains this Turn's final assistant message natively.
OutputSchema json.RawMessage `json:"outputSchema,omitempty"`
}

type CollaborationModeKind string
Expand Down
5 changes: 3 additions & 2 deletions apps/daemon/internal/agent/codex/session_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ func (s *Session) startNative(ctx context.Context, plan SessionPlan, req proto.P
return err
}
turnParams := TurnStartParams{
ThreadID: s.currentThreadID(),
Input: input,
ThreadID: s.currentThreadID(),
Input: input,
OutputSchema: plan.OutputSchema,
}
model := strings.TrimSpace(s.resolvedModel)
if model == "" {
Expand Down
2 changes: 2 additions & 0 deletions contracts/agents-api/execution-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ Core carries the schema in `ExecutionControls.OutputFormat` and requires structu

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 Codex adapter sends the schema unchanged as the native `outputSchema` on every root Turn, including Turns after resume or recovery, and publishes the native final agent message text unchanged. Codex reads JSON numbers losslessly, so Session admission applies no number or root-type limit beyond the common rules. A steered input joins the running Turn under its schema, and Subagent threads do not receive the schema. Codex passes the schema to the model provider as a strict Responses `text.format`; a provider that cannot run it returns its own error unchanged, and a provider that ignores it can return text that does not match the schema.

## Deferred function discovery

A `tool_search` tool has only `type`; Responses-only execution fields are rejected. Function `defer_loading` marks which definitions load lazily. Discovery requires both: `tool_search` without a deferred function, or deferred functions without `tool_search`, is rejected. The saved-Agent tool union keeps `tool_search`; the pinned Session response union omits it, so Session and SSE resources project it out while the frozen configuration keeps it. The pinned Items union has no tool-search Item, and Core invents none.
Expand Down
2 changes: 1 addition & 1 deletion contracts/agents-api/harness-capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Native model parameters and provider protocols per Harness are in [model executi
| --- | --- | --- | --- |
| [Public functions](./execution-tools.md#functions) with text results | Verified: `none`, hosted; admitted: self-hosted | Verified on all placements; object-root schemas only | Rejected |
| [Function results with images](./message-content.md#function-results) | Verified: `none`, hosted; admitted: self-hosted | Verified on all placements; successful inline PNG or JPEG results only | Rejected |
| [Structured output](./execution-tools.md#structured-output) | Rejected | Verified on all placements | Rejected |
| [Structured output](./execution-tools.md#structured-output) | Admitted on all placements | Verified on all placements | Rejected |
| [Deferred function discovery](./execution-tools.md#deferred-function-discovery) | Rejected | Verified: `none`, self-hosted; admitted: hosted | Rejected |
| [Disabled web search and programmatic tool calling](./execution-tools.md#web-search-and-programmatic-tool-calling) | Verified: `none`; admitted: hosted, self-hosted | Verified: `none`; admitted: hosted, self-hosted | Verified: `none`; admitted: hosted, self-hosted |
| Web search `cached` or `live` and programmatic tool calling `enabled` | Admitted on all placements | `live` admitted on hosted and self-hosted, rejected on `none`; `cached` and `enabled` rejected | Rejected |
Expand Down
4 changes: 3 additions & 1 deletion 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: 6b0ae99f9482a8082d20369ebe1facbc262718426dfc5d697ae63bc47ae39853
source_hash: 7608c08f707c1970f1365d9b5d5f0979a71af35c4b9ca82d64ec6d2d95344a2a
---

Agent 在 `tools` 中声明应用函数、控制项和 MCP 服务器,并可在 `text.format` 中声明输出 schema。本契约说明 Core 如何验证声明、哪些内容跨越 Runtime 边界,以及调用方如何恢复待执行操作。[Harness 能力](harness-capabilities.md)列出各 Harness 在不同部署位置支持的操作。原生工作区工具和 Environment Plugin MCP 属于 [Environment](environments.md#skills-plugins-and-environment-mcp)。
Expand Down Expand Up @@ -45,6 +45,8 @@ Core 在 `ExecutionControls.OutputFormat` 中携带 schema,仅对使用该选

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`。

Codex 适配器在每个根 Turn(包括 resume 或恢复后的 Turn)上将 schema 原样作为原生 `outputSchema` 发送,并原样发布原生最终 agent 消息文本。Codex 无损读取 JSON 数字,因此 Session 准入除通用规则外不附加数字或根类型限制。引导(steer)输入并入正在运行的 Turn 并沿用其 schema,Subagent 线程不接收该 schema。Codex 将 schema 作为 strict 的 Responses `text.format` 传给模型提供商;无法运行它的提供商原样返回自己的错误,忽略它的提供商可能返回与 schema 不符的文本。

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

`tool_search` 工具仅包含 `type`;仅适用于 Responses 的执行字段被拒绝。函数 `defer_loading` 标记延迟加载的定义。发现要求两者同时存在:没有延迟函数的 `tool_search`,或没有 `tool_search` 的延迟函数均被拒绝。已保存 Agent 的工具联合类型保留 `tool_search`;固定版本 Session 响应联合类型省略它,因此 Session 与 SSE 资源投影移除它,冻结配置仍保留它。固定版本 Item 联合类型没有 tool-search Item,Core 不自行创建。
Expand Down
4 changes: 2 additions & 2 deletions contracts/agents-api/zh/harness-capabilities.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Harness 能力"
source: contracts/agents-api/harness-capabilities.md
source_hash: c471671c3e4518b2968fc36fd5310edf449de5684fc976ebf0c015996aca3b6c
source_hash: 818fecdaf4bca14f5d37b6135af4eed6c13b48c801e4dd1034317a86ef5a247c
---

本页列出每个 Harness 在每种部署位置支持的能力。Core 根据 `internal/harnessconfig/<package>/declaration.go` 中 Harness 的声明决定准入,运行 Session 的 Runtime 也必须声明操作。所链接契约定义各操作;[Harness 接入](harness-onboarding.md#qualify-the-adapter)说明资格验证方法。
Expand Down Expand Up @@ -39,7 +39,7 @@ source_hash: c471671c3e4518b2968fc36fd5310edf449de5684fc976ebf0c015996aca3b6c
| --- | --- | --- | --- |
| 文本结果的[公开函数](execution-tools.md#functions) | 已验证:`none`、托管;已准入:自托管 | 所有位置已验证;仅对象根 schema | 已拒绝 |
| [含图像函数结果](message-content.md#function-results) | 已验证:`none`、托管;已准入:自托管 | 所有位置已验证;仅成功结果中的内联 PNG 或 JPEG | 已拒绝 |
| [结构化输出](execution-tools.md#structured-output) | 已拒绝 | 所有位置已验证 | 已拒绝 |
| [结构化输出](execution-tools.md#structured-output) | 所有位置已准入 | 所有位置已验证 | 已拒绝 |
| [延迟函数发现](execution-tools.md#deferred-function-discovery) | 已拒绝 | 已验证:`none`、自托管;已准入:托管 | 已拒绝 |
| [禁用 Web 搜索与程序化工具调用](execution-tools.md#web-search-and-programmatic-tool-calling) | 已验证:`none`;已准入:托管、自托管 | 已验证:`none`;已准入:托管、自托管 | 已验证:`none`;已准入:托管、自托管 |
| Web 搜索 `cached` 或 `live`,以及程序化工具调用 `enabled` | 所有位置已准入 | `live` 在托管和自托管已准入,在 `none` 已拒绝;`cached` 和 `enabled` 已拒绝 | 已拒绝 |
Expand Down
2 changes: 1 addition & 1 deletion internal/harnessconfig/codex/declaration.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ var Declaration = harnessconfig.Declaration{
WorkspaceReadPreparation: proto.CapabilitySupported,
WorkspaceOutputExport: proto.CapabilitySupported,
ExecutionControls: proto.CapabilitySupported,
StructuredOutput: proto.CapabilityUnsupported,
StructuredOutput: proto.CapabilitySupported,
ToolSearch: proto.CapabilityUnsupported,
MessageImages: proto.CapabilitySupported,
FunctionResultImages: proto.CapabilitySupported,
Expand Down
20 changes: 20 additions & 0 deletions internal/harnessconfig/codex/declaration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package codex

import (
"testing"

"github.com/MiniMax-AI/OpenAgentCore/internal/harnessconfig"
)

func TestDeclarationAdmitsStructuredOutput(t *testing.T) {
if !Declaration.Capabilities.StructuredOutput.IsSupported() {
t.Fatal("structured output is not declared")
}
err := Declaration.ValidateConfiguration(harnessconfig.Admission{
Model: "model", TextFormat: "json_schema", TextSchema: []byte(`{"type":"object"}`),
Environment: &harnessconfig.AdmissionEnvironment{Type: "none"},
})
if err != nil {
t.Fatalf("json_schema rejected: %v", err)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,6 @@ func TestAgentConfigurationLocalLimitsKeepCodes(t *testing.T) {
session := configurationOperations()[2]
for _, tc := range []struct{ fields, message string }{
{`"tools":[{"type":"programmatic_tool_calling","enabled":false},{"type":"programmatic_tool_calling","enabled":false}]`, "Execution requires distinct tool controls."},
{`"text":{"format":{"type":"json_schema","schema":{"type":"object"}}}`, "Harness codex does not support the requested Agent/environment configuration: Structured output is not supported by this engine."},
{`"multi_agent":{"enabled":true},"tools":[{"type":"programmatic_tool_calling","enabled":true}]`, "Harness codex does not support the requested Agent/environment configuration: programmatic_tool_calling.enabled=true is not supported with multi_agent by this Harness."},
{`"tools":[{"type":"function","name":"","description":"","parameters":{}}]`, "Function names must be nonempty, unique and at most 512 bytes."},
} {
Expand Down
2 changes: 1 addition & 1 deletion services/core/internal/api/text_configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func TestTextConfigurationHTTP(t *testing.T) {
}
})
}
for _, invalid := range []string{`{"verbosity":""}`, `{"verbosity":"verbose"}`, `{"verbosity":4}`, `{"format":{}}`, `{"format":{"type":"json_schema","schema":{}}}`, `{"format":{"type":"text","extra":true}}`, `{"unknown":true}`} {
for _, invalid := range []string{`{"verbosity":""}`, `{"verbosity":"verbose"}`, `{"verbosity":4}`, `{"format":{}}`, `{"format":{"type":"json_schema","schema":{"type":"string"}}}`, `{"format":{"type":"text","extra":true}}`, `{"unknown":true}`} {
h, s, _ := testHandler(t)
req := httptest.NewRequest(http.MethodPost, "/v1/agents/sessions", strings.NewReader(`{"agent":{"model":"example","text":`+invalid+`},"environment":{"type":"none"},"input":"Describe the configured response format."}`))
req.Header.Set("Authorization", "Bearer test-api-key")
Expand Down
2 changes: 1 addition & 1 deletion services/core/internal/execution/structured_output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
func TestStructuredOutputNeedsOperationQualification(t *testing.T) {
raw := json.RawMessage(`{"agent":{"model":"model","text":{"format":{"type":"json_schema","schema":{"type":"object"}}}},"environment":{"type":"none"}}`)
for _, kind := range []string{"codex", "claude_sdk", "mcode"} {
if err := (Policy{}).ValidateSessionConfiguration(kind, raw); (err == nil) != (kind == "claude_sdk") {
if err := (Policy{}).ValidateSessionConfiguration(kind, raw); (err == nil) != (kind != "mcode") {
t.Fatalf("%s: %v", kind, err)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ func TestAgentConfigurationValidationRejectsWithoutWritesPostgres(t *testing.T)
// K2 keeps the local configuration code; saved enabled web_search (TV-05) is
// covered by TestSavedWebSearchPostgres.
for _, request := range []struct{ path, body, message string }{
{"/v1/agents/sessions", `{"agent":{"model":"m","text":{"format":{"type":"json_schema","schema":{"type":"object"}}}},"environment":{"type":"none"},"input":"hi"}`, "Harness codex does not support the requested Agent/environment configuration: Structured output is not supported by this engine."},
{"/v1/agents/sessions", `{"agent":{"model":"m","multi_agent":{"enabled":true},"tools":[{"type":"programmatic_tool_calling","enabled":true}]},"environment":{"type":"none"},"input":"hi"}`, "Harness codex does not support the requested Agent/environment configuration: programmatic_tool_calling.enabled=true is not supported with multi_agent by this Harness."},
} {
encoded, _ := json.Marshal(request.message)
want := `{"error":{"message":` + string(encoded) + `,"type":"invalid_request_error","code":"unsupported_or_invalid_configuration","param":null}}` + "\n"
Expand Down
2 changes: 1 addition & 1 deletion services/core/tests/official_agent_reference_retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def mutate(**values):
fresh = sessions.create(**spec)
assert fresh.agent.model == "changed-model" and fresh.agent.instructions == "changed"
assert first.agent.model == "original-model" and first.agent.instructions == "frozen"
mutate(patch={"text":{"format":{"type":"json_schema","schema":{"type":"object"}},"verbosity":"medium"}})
mutate(patch={"tools":[{"type":"tool_search"}]})
expect(BadRequestError, lambda:sessions.create(**spec))
assert sessions.create(**spec, extra_headers=headers) == first
for changed in ({"input":"different"}, {"metadata":{"changed":"yes"}}, {"agent":{"instructions":"frozen"}}, {"agent_id":fresh.id}):
Expand Down
2 changes: 1 addition & 1 deletion services/core/tests/official_agent_references.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ def verify_agent_references(client, other, expect_error):
# Configuration storage is broader than execution. Never silently drop an
# unsupported saved option, but admit a supported whole-field replacement.
for field, value, replacement in (
("text", {"format": {"type": "json_schema", "schema": {"type": "object"}}}, {"verbosity": "medium"}),
("tools", [dict(tool, defer_loading=True)], None),
("tools", [{"type": "tool_search"}], []),
):
Expand All @@ -78,6 +77,7 @@ def verify_agent_references(client, other, expect_error):
("reasoning", {"effort": "high"}, {"effort": "low"}),
("reasoning", {"summary": "auto"}, {"summary": "concise"}),
("service_tier", "fast", "auto"),
("text", {"format": {"type": "json_schema", "schema": {"type": "object"}}, "verbosity": "medium"}, {"format": {"type": "text"}, "verbosity": "medium"}),
):
supported = agents.create(model="model", **{field: value})
reference = {"input": "Verify agent references fixture admission.", "agent_id": supported.id, "environment": {"type": "none"}}
Expand Down
Loading