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 @@ -30,7 +30,7 @@ A function declaration requires `name`, `description` and a JSON Schema in `para

The native Harness can abandon a call before it applies a result, for example when the root Turn closes or interrupts the Subagent that made it, or when MiniMax Code's MCP client times the call out. The call then stops being a required action, the Turn continues and returns to `in_progress` when no required action is left, and a later `function_call_output` for it is rejected like a result after cancellation. An abandoned call has no application receipt.

A function call made by a Subagent is a required action of the root Turn that runs the child. Its `turn_id` is that root Turn, and its `call_id` equals the `call_id` of the child's `function_call` Item on the [Subagent routes](./subagents.md#public-reads). Answer it with `function_call_output` / `tool_result` under that `call_id` and the root Turn ID; the child's Turn ID is not a Session Turn and gets the 400 for a call of another Turn. A Subagent's HTTP MCP calls are `mcp_call` Items in the child's history.
A function call made by a Subagent is a required action of the root Turn that runs the child. Its `turn_id` is that root Turn, and its `call_id` equals the `call_id` of the child's `function_call` Item on the [Subagent routes](./subagents.md#public-reads). Answer it with `function_call_output` / `tool_result` under that `call_id` and either the root Turn ID or the child Turn ID that the call's `agent.session.turn.item.added` event names, which is what the official SDK sends; any other Turn ID gets the 400 for a call of another Turn. A Subagent's HTTP MCP calls are `mcp_call` Items in the child's history.

### Required actions and recovery

Expand Down
2 changes: 1 addition & 1 deletion contracts/agents-api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ Each item is Core's deliberate or native behavior where the official service beh
### Unverified against the official service

- The order of errors when one request has several faults, and error, default and payload-limit parity in general.
- A Subagent's function call: Core makes it a required action of the root Turn that runs the child, with `turn_id` set to that Turn. The Turn is `waiting` and the Session `requires_action` while root output may continue, and the `call_id` equals the `call_id` of the child's `function_call` Item. Claude Code publishes the child's Items when the root Turn settles, after the required action.
- A Subagent's function call: Core makes it a required action of the root Turn that runs the child, with `turn_id` set to that Turn, and publishes the child's `function_call` Item once as `agent.session.turn.item.added` with the child Turn ID and a null `output_index`; a result may name either Turn. The Turn is `waiting` and the Session `requires_action` while root output may continue, and the `call_id` equals the `call_id` of the child's `function_call` Item. Claude Code publishes the child's Items when the root Turn settles, after the required action.
- Whether Subagent child Turns and pending Environment file writes block Session deletion.
- The official order between the pending-input error and an unknown result target.
- Failure reasons for npm, initial-file and Skill installation have no official sample.
Expand Down
2 changes: 1 addition & 1 deletion contracts/agents-api/subagents.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Lists use `after`, `limit` (default 20) and `order` (default `desc`) and return
Child work appears only on the Subagent routes.

- Session `turns.list` and `turns.retrieve` return root Turns only. A child Turn ID, as a path or a list cursor, returns the same 404 as a missing Turn; Core's 404 message differs from the official one.
- The Session event stream and the creation stream carry root work only: child Turns and their Items publish no `agent.session.turn.*` or Item content events. `agent.session.subagent.*` events and root coordination Items stay. The creation stream still ends on the root's settled idle state.
- The Session event stream and the creation stream carry root work only: child Turns and their Items publish no `agent.session.turn.*` or Item content events, except that a Subagent's function call publishes its `function_call` Item once as `agent.session.turn.item.added`, with the child Turn ID as `turn_id`. `agent.session.subagent.*` events and root coordination Items stay. The creation stream still ends on the root's settled idle state.
- A child Turn's `agent_id` is the Session's Agent ID (a direct child's `parent_agent_id` and the `create_subagent_call` `agent_id`); `subagent_id` identifies the child. Nested children follow the same rule. Root Turns have `subagent_id: null`.
- Session Items stay root-owned; inherited native parent transcripts are not child work. Session usage sums root Turns only.

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: d88acdb94153f3974e66adbdb9443706ae47adef280cae71b03fdf896bdfe579
source_hash: 44b540ff6827f818d8bc4cbaa6dc5fc9929bbf82a15c66edeb604909ecb6b110
---

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 @@ -32,7 +32,7 @@ Agent 在 `tools` 中声明应用函数、控制项和 MCP 服务器,并可在

原生 Harness 可能在应用结果之前放弃调用,例如根 Turn 关闭或中断了发起该调用的 Subagent,或 MiniMax Code 的 MCP 客户端使该调用超时。该调用随即不再是待执行操作,Turn 继续运行,并在没有待执行操作时回到 `in_progress`;之后为它提交的 `function_call_output` 会像取消后的结果一样被拒绝。被放弃的调用没有应用回执。

Subagent 发起的函数调用是运行该子级的根 Turn 的待执行操作:`turn_id` 是该根 Turn,`call_id` 等于 [Subagent 路由](subagents.md#public-reads)上该子级 `function_call` Item 的 `call_id`。使用该 `call_id` 和根 Turn ID,以 `function_call_output` / `tool_result` 作答;子级的 Turn ID 不是 Session Turn,会得到“另一个 Turn 的调用”的 400。Subagent 的 HTTP MCP 调用是该子级历史中的 `mcp_call` Item。
Subagent 发起的函数调用是运行该子级的根 Turn 的待执行操作:`turn_id` 是该根 Turn,`call_id` 等于 [Subagent 路由](subagents.md#public-reads)上该子级 `function_call` Item 的 `call_id`。使用该 `call_id`,以及根 Turn ID 或该调用的 `agent.session.turn.item.added` 事件所标明的子级 Turn ID(官方 SDK 发送的即是后者),以 `function_call_output` / `tool_result` 作答;其他 Turn ID 会得到“另一个 Turn 的调用”的 400。Subagent 的 HTTP MCP 调用是该子级历史中的 `mcp_call` Item。

### 待执行操作与恢复 {#required-actions-and-recovery}

Expand Down
4 changes: 2 additions & 2 deletions contracts/agents-api/zh/index.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "OpenAI 兼容性"
source: contracts/agents-api/index.md
source_hash: 025c67e723ae2e055c4674f5c721cb51bd2b59899f907ccc05b27ce70360f142
source_hash: ae8f903f342adedb090dc95ff325796adef0c25f6ff1e67478b3c312901b28ee
---

Core 实现下方固定版本的 OpenAI Agents API。本台账列出各项资源的覆盖情况、相对于官方服务的所有差异以及已知缺口。
Expand Down Expand Up @@ -165,7 +165,7 @@ Core 自身字段位于 `x_agents_core` 中([Core extensions](../../../docs/zh
### 尚未与官方服务核实的内容 {#unverified-against-the-official-service}

- 当一个请求存在多项故障时的错误顺序,以及错误处理、默认值和载荷限制的总体一致性。
- Subagent 的函数调用:Core 将其作为运行该子级的根 Turn 的待执行操作,`turn_id` 为该 Turn。该 Turn 为 `waiting`、Session 为 `requires_action`,根输出可继续,`call_id` 等于该子级 `function_call` Item 的 `call_id`。Claude Code 在根 Turn 收敛时才发布子级的 Item,晚于该待执行操作。
- Subagent 的函数调用:Core 将其作为运行该子级的根 Turn 的待执行操作,`turn_id` 为该 Turn,并把子级的 `function_call` Item 以 `agent.session.turn.item.added` 发布一次,`turn_id` 为子级 Turn ID,`output_index` 为 null;结果可以指定其中任一 Turn。该 Turn 为 `waiting`、Session 为 `requires_action`,根输出可继续,`call_id` 等于该子级 `function_call` Item 的 `call_id`。Claude Code 在根 Turn 收敛时才发布子级的 Item,晚于该待执行操作。
- Subagent 子级 Turns 和待处理的 Environment file 写入是否会阻止 Session 删除。
- 官方服务在待处理输入错误与未知结果目标之间的先后顺序。
- npm、initial-file 和 Skill 安装的失败原因没有官方样本。
Expand Down
4 changes: 2 additions & 2 deletions contracts/agents-api/zh/subagents.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "子 Agent"
source: contracts/agents-api/subagents.md
source_hash: d8341bfd7747152dea23afc930cf5fe07ff1ac4a58f503cb0e2ec5fe86060cb9
source_hash: 11575ce73f7f00cb2fefd775d97899ac2439f04e0655a4980269bdb982740ad7
---

启用 `multi_agent.enabled` 后,Harness 可以启动原生子 Agent。Core 通过锁定版本的 SDK(见 [`upstream.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/contracts/agents-api/upstream.json))中的六项 Subagent 读取操作公开它们,并根据适配器观察结果记录它们。当 `multi_agent.enabled=false` 时,Runtime 会移除原生子 Agent 工具。[Harness capabilities](harness-capabilities.md) 列出各 Harness 在哪些组合中支持子 Agent。
Expand All @@ -26,7 +26,7 @@ source_hash: d8341bfd7747152dea23afc930cf5fe07ff1ac4a58f503cb0e2ec5fe86060cb9
子级工作仅出现在子 Agent 路由上。

- Session 的 `turns.list` 和 `turns.retrieve` 仅返回根级 Turns。将子级 Turn ID 作为路径参数或列表游标使用时,会返回与不存在的 Turn 相同的 404;Core 的 404 消息与官方消息不同。
- Session 事件流和创建流仅传输根级工作:子级 Turns 及其 Items 不会发布任何 `agent.session.turn.*` 或 Item 内容事件。`agent.session.subagent.*` 事件和根级协调 Items 仍会保留。创建流仍会在根级收敛到 idle 状态时结束。
- Session 事件流和创建流仅传输根级工作:子级 Turns 及其 Items 不会发布任何 `agent.session.turn.*` 或 Item 内容事件,但 Subagent 的函数调用会将其 `function_call` Item 作为 `agent.session.turn.item.added` 发布一次,`turn_id` 为子级 Turn ID。`agent.session.subagent.*` 事件和根级协调 Items 仍会保留。创建流仍会在根级收敛到 idle 状态时结束。
- 子级 Turn 的 `agent_id` 是 Session 的 Agent ID(即直接子级的 `parent_agent_id`,也是 `create_subagent_call` 的 `agent_id`);`subagent_id` 标识该子级。嵌套子级遵循相同规则。根级 Turns 带有 `subagent_id: null`。
- Session Items 始终归根级所有;继承的原生父级对话记录不属于子级工作。Session 用量仅汇总根级 Turns。

Expand Down
3 changes: 3 additions & 0 deletions services/core/internal/db/queries/functions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ SELECT EXISTS (
SELECT 1 FROM function_calls WHERE session_id = sqlc.arg(session_id) AND call_id = sqlc.arg(call_id)
)::boolean AS found;

-- name: FindFunctionCallTurn :one
SELECT turn_id, executor_call_id FROM function_calls WHERE session_id = $1 AND call_id = $2;

-- name: MatchFunctionResult :one
SELECT (result IS NOT NULL)::boolean AS submitted, COALESCE(result = sqlc.arg(result)::jsonb, false)::boolean AS matches,
withdrawn
Expand Down
21 changes: 21 additions & 0 deletions services/core/internal/db/sqlc/functions.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions services/core/internal/persistence/postgres/sessionpg/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ func (t *SessionTx) FindResultTurn(ctx context.Context, turn string) (sessions.T
return TurnFromRow(row), true, nil
}

func (t *SessionTx) FindCallTurn(ctx context.Context, call string) (sessions.Turn, string, bool, error) {
row, err := t.q.FindFunctionCallTurn(ctx, sqlc.FindFunctionCallTurnParams{SessionID: t.session, CallID: call})
if errors.Is(err, pgx.ErrNoRows) {
return sessions.Turn{}, "", false, nil
}
if err != nil {
return sessions.Turn{}, "", false, err
}
root, err := t.q.SessionEventTurn(ctx, sqlc.SessionEventTurnParams{SessionID: t.session, ID: row.TurnID})
if err != nil {
return sessions.Turn{}, "", false, err
}
return TurnFromRow(root), row.ExecutorCallID, true, nil
}

func (t *SessionTx) MatchFunctionResult(ctx context.Context, turn, call string, result json.RawMessage) (sessions.FunctionResultMatch, error) {
id, err := parseID(turn)
if err != nil {
Expand Down
35 changes: 23 additions & 12 deletions services/core/internal/sessions/execution_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"encoding/json"

v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1"

"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/items"
)

Expand Down Expand Up @@ -95,33 +97,35 @@ func decideFunctionWithdrawal(turn Turn, call FunctionCall) (bool, error) {
}

// resolveFunctionCall gives a call a Subagent made the identity of the child's
// function Item. A child the Session has not published, or a malformed native
// identity, is ErrInvalidInput.
func resolveFunctionCall(ctx context.Context, tx NativeSubagents, call FunctionCall) (FunctionCall, error) {
// function Item, and returns the child Turn's ID, empty for a root call. A
// child the Session has not published, or a malformed native identity, is
// ErrInvalidInput.
func resolveFunctionCall(ctx context.Context, tx NativeSubagents, call FunctionCall) (FunctionCall, string, error) {
if call.Subagent == "" && call.SubagentTurn == "" {
return call, nil
return call, "", nil
}
if !validNativeIdentity(call.Subagent) || !validNativeIdentity(call.SubagentTurn) {
return call, ErrInvalidInput
return call, "", ErrInvalidInput
}
child, err := visibleSubagent(ctx, tx, call.Subagent)
if err != nil {
return call, err
return call, "", err
}
call.CallID = items.Identity(items.Identity(child.ID, "turn:"+call.SubagentTurn), "tool:"+call.ExecutorCallID)
return call, nil
turn := items.Identity(child.ID, "turn:"+call.SubagentTurn)
call.CallID = items.Identity(turn, "tool:"+call.ExecutorCallID)
return call, turn, nil
}

// RecordFunctionCall records a call an executor reported for its Turn and
// journals the Turn's required actions with it. A call a Subagent made takes
// the identity of the child's function Item. A retry of the recorded call
// changes nothing.
// the identity of the child's function Item and is published as that Item. A
// retry of the recorded call changes nothing.
func (o *ExecutionOperations) RecordFunctionCall(ctx context.Context, tenant, session, turn string, call FunctionCall) error {
if err := validateFunctionCall(call); err != nil {
return err
}
return o.storage.WithFunctionTurn(ctx, tenant, session, turn, func(ctx context.Context, tx FunctionTx, current Turn) error {
call, err := resolveFunctionCall(ctx, tx, call)
call, childTurn, err := resolveFunctionCall(ctx, tx, call)
if err != nil {
return err
}
Expand All @@ -136,6 +140,13 @@ func (o *ExecutionOperations) RecordFunctionCall(ctx context.Context, tenant, se
if err := tx.CreateFunctionCall(ctx, current.ID, call); err != nil {
return err
}
if childTurn != "" {
// The official SDK answers calls from this event; child Items publish nothing else.
item := v1.Item{ID: call.CallID, TurnID: childTurn, Type: "function_call", Status: "in_progress", Name: call.Name, CallID: call.CallID, Arguments: call.Arguments}
if err := tx.AppendChanges(ctx, ItemChanges(items.Change{Item: item}, nil)...); err != nil {
return err
}
}
return settleFunctionState(ctx, tx, current)
})
}
Expand Down Expand Up @@ -189,7 +200,7 @@ func (o *ExecutionOperations) WithdrawFunctionCall(ctx context.Context, tenant,
return ErrInvalidInput
}
return o.storage.WithFunctionTurn(ctx, tenant, session, turn, func(ctx context.Context, tx FunctionTx, current Turn) error {
call, err := resolveFunctionCall(ctx, tx, call)
call, _, err := resolveFunctionCall(ctx, tx, call)
if err != nil {
return err
}
Expand Down
23 changes: 22 additions & 1 deletion services/core/internal/sessions/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import (
"fmt"
"strings"

"github.com/google/uuid"

v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/items"
"github.com/MiniMax-AI/OpenAgentCore/services/core/internal/jsonobject"
)

Expand Down Expand Up @@ -87,6 +90,9 @@ type FunctionResultTx interface {
// FindResultTurn reads the Session's Turn that a result names and reports
// whether the Session has it. A malformed ID names no Turn.
FindResultTurn(ctx context.Context, turn string) (Turn, bool, error)
// FindCallTurn reads the Turn that holds the Session's call, with the
// call's executor identity, and reports whether the Session has the call.
FindCallTurn(ctx context.Context, call string) (Turn, string, bool, error)
// MatchFunctionResult compares result with the Turn's call.
MatchFunctionResult(ctx context.Context, turn, call string, result json.RawMessage) (FunctionResultMatch, error)
// SubmitFunctionResult saves the result of the Turn's call.
Expand All @@ -99,13 +105,28 @@ type FunctionResultTx interface {
// and returns that Turn, which the result's input joins. It never creates a
// Turn. A result whose Turn does not have the call is
// ErrFunctionCallTurnMismatch when another Turn of the Session has it and
// ErrUnknownFunctionCall otherwise; the answer depends only on the Session's
// ErrUnknownFunctionCall otherwise; a Subagent Turn that made the call stands
// for the root Turn that holds it; the answer depends only on the Session's
// own calls.
func AdmitFunctionResult(ctx context.Context, tx FunctionResultTx, input FunctionResultInput) (Turn, error) {
turn, found, err := tx.FindResultTurn(ctx, input.TurnID)
if err != nil {
return Turn{}, err
}
if _, malformed := uuid.Parse(input.TurnID); !found && malformed == nil {
// A result may name the Subagent Turn that made the call, as the
// call's Session event does. The child's function Item identity is
// derived from its Turn and the call's executor identity, so a match
// proves the call was made there; the result joins the root Turn
// holding the call.
holder, executor, held, err := tx.FindCallTurn(ctx, input.CallID)
if err != nil {
return Turn{}, err
}
if held && items.Identity(input.TurnID, "tool:"+executor) == input.CallID {
turn, found = holder, true
}
}
if found {
match, err := tx.MatchFunctionResult(ctx, turn.ID, input.CallID, input.Result)
if err != nil {
Expand Down
Loading
Loading