Skip to content

MultiAgent工具规格

从 MultiAgent 版本选择、V1/V2 schema、namespace 与暴露条件,到 spawn/wait/message 工具的真实运行边界。

基于rust-v0.150.0
CodexRustToolsMulti Agent

MultiAgent工具规格 ​

MultiAgent 工具不是一个统一的“agent 工具”。当前源码同时维护 V1 和 V2 两套工具族:V1 兼容旧的 multi_agent_v1 namespace 与 send_input/resume_agent/close_agent;V2 使用 spawn_agent、send_message、followup_task、wait_agent、interrupt_agent、list_agents,并允许 provider namespace、Code Mode-only 与模型覆盖字段。规格选择发生在 tool plan,真正的 spawn、消息投递和 mailbox 等待由各 handler 再交给 AgentControl/Session 执行。

本文面向已经读过ToolRouter解析与分派、ThreadManager管理、DynamicTool处理器和ToolSearch工具的读者。本文只研究协作工具如何选择版本、生成 schema、决定 namespace/exposure,并追踪代表性的 spawn、message、wait 运行路径;不重复 ThreadManager 的线程创建实现,也不把 V1/V2 的名字差异写成纯 API 文档。读完后,读者应能解释一个模型为什么看到 V1 namespace、V2 顶层函数或完全看不到协作工具,以及 wait_agent 的 timeout 与 mailbox activity 为什么属于当前 turn 状态。

1. 版本选择 ​

1.1 三种状态 ​

collab_tools_enabled 先看 TurnContext.multi_agent_version:Disabled 直接不注册;V1 需要当前 session source 没有超过 spawn depth limit;V2 允许 root thread,子 agent 只有在自身模型也支持 V2 时才继续拥有 V2 工具。

源码位置:codex-rs/core/src/tools/spec_plan.rs :: collab_tools_enabled

rust
fn collab_tools_enabled(turn_context: &TurnContext) -> bool {
    match turn_context.multi_agent_version {
        MultiAgentVersion::Disabled => false,
        MultiAgentVersion::V1 => !exceeds_thread_spawn_depth_limit(
            next_thread_spawn_depth(&turn_context.session_source),
            turn_context.config.agent_max_depth,
        ),
        MultiAgentVersion::V2 => {
            turn_context.session_source.get_agent_path().is_none()
                || turn_context.model_info.multi_agent_version == Some(MultiAgentVersion::V2)
        }
    }
}

这段函数只回答“是否进入协作工具装配”,不决定具体工具数量。V2 后面还会检查 wait_agent_enabled、namespace capability 和 exposure;V1 则会统一注册五个旧 handler。

1.2 Model能力 ​

V2 的 worker 还要经过 model_supports_multi_agent_backend。如果一个 V2 spawn 请求选择了明确模型,而该模型声明 multi_agent_version=Disabled,common helper 会拒绝 override;这不是 schema 层能表达的约束。

源码位置:codex-rs/core/src/tools/handlers/multi_agents_common.rs :: model_supports_multi_agent_backend

rust
pub(crate) fn model_supports_multi_agent_backend(
    model: &ModelPreset,
    multi_agent_version: MultiAgentVersion,
) -> bool {
    multi_agent_version != MultiAgentVersion::V2
        || model.multi_agent_version != Some(MultiAgentVersion::Disabled)
}

1.3 装配分叉 ​

V2 分支注册 spawn_agent、send_message、followup_task、可选 wait_agent、interrupt_agent 和 list_agents;V1 分支注册 spawn_agent、send_input、resume_agent、wait_agent 和 close_agent。V1/V2 不能同时作为同一组 canonical runtime 出现。

源码位置:codex-rs/core/src/tools/spec_plan.rs :: add_collaboration_tools

rust
if collab_tools_enabled(turn_context) {
    if multi_agent_v2_enabled(turn_context) {
        let exposure = if turn_context.config.multi_agent_v2.non_code_mode_only {
            ToolExposure::DirectModelOnly
        } else {
            ToolExposure::Direct
        };
        registry.register_trusted_with_exposure(
            multi_agent_v2_handler(
                SpawnAgentHandlerV2::new(SpawnAgentToolOptions {
                    available_models: turn_context.available_models.clone(),
                    agent_type_description,
                    expose_agent_type: !turn_context.config.agent_roles.is_empty(),
                    hide_agent_type_model_reasoning: hide_spawn_agent_metadata,
                    expose_spawn_agent_model_overrides: turn_context
                        .config
                        .multi_agent_v2
                        .expose_spawn_agent_model_overrides,
                    multi_agent_version: turn_context.multi_agent_version,
                    usage_hint_text: turn_context.config.multi_agent_v2.usage_hint_text.clone(),
                }),
                tool_namespace,
            ),
            exposure,
        );
        registry.register_trusted_with_exposure(
            multi_agent_v2_handler(SendMessageHandlerV2, tool_namespace),
            exposure,
        );
        registry.register_trusted_with_exposure(
            multi_agent_v2_handler(FollowupTaskHandlerV2, tool_namespace),
            exposure,
        );
        // wait, interrupt and list handlers follow
    } else {
        let exposure = if search_tool_enabled(turn_context) {
            ToolExposure::Deferred
        } else {
            ToolExposure::Direct
        };
        registry.add_with_exposure(
            SpawnAgentHandler::new(SpawnAgentToolOptions {
                available_models: turn_context.available_models.clone(),
                agent_type_description,
                expose_agent_type: !turn_context.config.agent_roles.is_empty(),
                hide_agent_type_model_reasoning: false,
                expose_spawn_agent_model_overrides: true,
                multi_agent_version: turn_context.multi_agent_version,
                usage_hint_text: turn_context.config.multi_agent_v2.usage_hint_text.clone(),
            }),
            exposure,
        );
        registry.add_with_exposure(SendInputHandler, exposure);
        registry.add_with_exposure(ResumeAgentHandler, exposure);
        registry.add_with_exposure(WaitAgentHandler::new(context.wait_agent_timeouts), exposure);
        registry.add_with_exposure(CloseAgentHandler, exposure);
    }
}

2. Spawn规格 ​

2.1 V1结构 ​

V1 的 spawn_agent 是 multi_agent_v1.spawn_agent namespace function,保留 fork_context 旧字段;V2 则把 fork_turns、task_name 和 message 设为新的契约。V1 schema 仍然重要,因为 resumed legacy thread 可以固定 MultiAgentVersion::V1。

源码位置:codex-rs/core/src/tools/handlers/multi_agents_spec.rs :: create_spawn_agent_tool_v1

rust
pub fn create_spawn_agent_tool_v1(options: SpawnAgentToolOptions) -> ToolSpec {
    let mut properties = spawn_agent_common_properties_v1(&options.agent_type_description);
    if !options.expose_agent_type {
        properties.remove("agent_type");
    }
    if options.hide_agent_type_model_reasoning {
        hide_spawn_agent_metadata_options(&mut properties);
    }

    ToolSpec::Namespace(ResponsesApiNamespace {
        name: MULTI_AGENT_V1_NAMESPACE.to_string(),
        description: "Tools for spawning and managing sub-agents.".to_string(),
        tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
            name: "spawn_agent".to_string(),
            strict: false,
            defer_loading: None,
            parameters: JsonSchema::object(properties, None, Some(false.into())),
            output_schema: Some(spawn_agent_output_schema_v1()),
            description: spawn_agent_tool_description(
                available_models_description.as_deref(),
                inherited_model_guidance,
                return_value_description,
                options.usage_hint_text,
            ),
        })],
    })
}

2.2 V2结构 ​

V2 spawn_agent 是顶层 function,task_name 和 message 必填;只有 expose_spawn_agent_model_overrides 打开时才暴露 model、reasoning_effort,service tier 由 metadata hide 设置单独控制。agent_type 适用于任意 fork depth,包括 full history;hide_spawn_agent_metadata 不会取消 task_name/message 这两个核心输入。

源码位置:codex-rs/core/src/tools/handlers/multi_agents_spec.rs :: create_spawn_agent_tool_v2

rust
pub fn create_spawn_agent_tool_v2(options: SpawnAgentToolOptions) -> ToolSpec {
    let mut properties = spawn_agent_common_properties_v2(&options.agent_type_description);
    if !options.expose_agent_type {
        properties.remove("agent_type");
    }
    if options.hide_agent_type_model_reasoning {
        properties.remove("service_tier");
    }
    if !options.expose_spawn_agent_model_overrides {
        properties.remove("model");
        properties.remove("reasoning_effort");
    }
    properties.insert(
        "task_name".to_string(),
        JsonSchema::string(Some(
            "Task name for the new agent. Use lowercase letters, digits, and underscores."
                .to_string(),
        )),
    );

    ToolSpec::Function(ResponsesApiTool {
        name: "spawn_agent".to_string(),
        description: spawn_agent_tool_description_v2(
            available_models_description.as_deref(),
            inherited_model_guidance,
            options.usage_hint_text,
        ),
        strict: false,
        defer_loading: None,
        parameters: JsonSchema::object(
            properties,
            Some(vec!["task_name".to_string(), "message".to_string()]),
            Some(false.into()),
        ),
        output_schema: Some(spawn_agent_output_schema_v2(
            options.hide_agent_type_model_reasoning,
        )),
    })
}

测试还限制可见 model summary 数量为 MAX_SPAWN_AGENT_MODEL_OVERRIDES,并截断 reasoning effort 描述,防止动态模型目录把 spawn schema 无限扩大。

2.3 Namespace包装 ​

V2 可以配置 multi_agent_v2.tool_namespace,但只有 provider 支持 namespace tools 时才使用;没有 namespace capability 时,V2 保持顶层 function。MultiAgentV2NamespaceOverride 只包装 ToolSpec::Function,不改变底层 handler 的 tool name 与执行逻辑。

源码位置:codex-rs/core/src/tools/spec_plan.rs :: MultiAgentV2NamespaceOverride

rust
impl ToolExecutor<ToolInvocation> for MultiAgentV2NamespaceOverride {
    fn tool_name(&self) -> ToolName {
        ToolName::namespaced(self.namespace.clone(), self.handler.tool_name().name)
    }

    fn spec(&self) -> ToolSpec {
        match self.handler.spec() {
            ToolSpec::Function(tool) => ToolSpec::Namespace(ResponsesApiNamespace {
                name: self.namespace.clone(),
                description: MULTI_AGENT_V2_NAMESPACE_DESCRIPTION.to_string(),
                tools: vec![ResponsesApiNamespaceTool::Function(tool)],
            }),
            spec => spec,
        }
    }

    fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_> {
        self.handler.handle(invocation)
    }
}

3. 其他工具 ​

3.1 消息差异 ​

V1 send_input 支持 message/items 和 interrupt;V2 拆成 send_message 与 followup_task。两者共享 target/message 校验,但 delivery mode 不同:send_message 只入队,followup_task 会在目标 idle 时触发新 turn。

源码位置:codex-rs/core/src/tools/handlers/multi_agents_v2/send_message.rs :: Handler::handle_call

rust
let arguments = function_arguments(invocation.payload.clone())?;
let args: SendMessageArgs = parse_arguments(&arguments)?;
handle_message_string_tool(
    invocation,
    MessageDeliveryMode::QueueOnly,
    args.target,
    args.message,
)
.await
.map(boxed_tool_output)

共享路径在发送前用当前 TurnContext 构造 resume config,并确保 unloaded V2 agent 被重新加载;真正提交 communication 时同时携带 parent_turn_id 和 root_turn_id。QueueOnly 的 parent turn 为空,TriggerTurn 才把当前 turn 作为直接 parent,但两者都保留 root lineage。

源码位置:codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs :: handle_message_string_tool

rust
let resume_config = build_agent_resume_config(turn.as_ref())?;
session
    .services
    .agent_control
    .ensure_v2_agent_loaded(resume_config, receiver_thread_id, /*parent*/ None)
    .await
    .map_err(|err| collab_agent_error(receiver_thread_id, err))?;

let parent_turn_id =
    matches!(mode, MessageDeliveryMode::TriggerTurn).then(|| turn.sub_id.clone());
session
    .services
    .agent_control
    .send_inter_agent_communication(
        receiver_thread_id,
        communication,
        context,
        parent_turn_id,
        turn.turn_metadata_state.root_turn_id(),
    )
    .await?;

源码位置:codex-rs/core/src/tools/handlers/multi_agents_v2/followup_task.rs :: Handler::handle_call

rust
let arguments = function_arguments(invocation.payload.clone())?;
let args: FollowupTaskArgs = parse_arguments(&arguments)?;
handle_message_string_tool(
    invocation,
    MessageDeliveryMode::TriggerTurn,
    args.target,
    args.message,
)
.await
.map(boxed_tool_output)

message 字段 schema 使用 encrypted 标记;日志与 wire transport 的保护由共享工具层负责,handler 只选择 delivery mode。

3.2 Wait语义 ​

V2 wait_agent 等待的是当前 turn 的 mailbox activity,而不是直接轮询某一个 agent 的最终消息。activity 可能来自 mailbox 或 steer;超时由 multi_agent_v2.min/max/default_wait_timeout_ms 决定。超过 max 仍报错,低于 min 则 clamp 到 min,不再拒绝调用。

源码位置:codex-rs/core/src/tools/handlers/multi_agents_v2/wait.rs :: Handler::handle_call

rust
let requested_timeout_ms = args.timeout_ms;
let timeout_ms = match requested_timeout_ms {
    Some(ms) if ms > max_timeout_ms => {
        return Err(FunctionCallError::RespondToModel(format!(
            "timeout_ms must be at most {max_timeout_ms}"
        )));
    }
    Some(ms) => ms.max(min_timeout_ms),
    None => default_timeout_ms,
};

let (mut activity_rx, pending_activity) = session
    .input_queue
    .subscribe_activity(turn_state.as_deref())
    .await;
let deadline = Instant::now() + Duration::from_millis(timeout_ms as u64);
let outcome = wait_for_activity(&mut activity_rx, pending_activity, deadline).await;
let result = WaitAgentResult::from_outcome(
    outcome,
    requested_timeout_ms,
    timeout_ms,
);

WaitAgentResult 只返回 Wait completed、Wait interrupted by new input 或 Wait timed out 的摘要,不把 mailbox 内容直接作为工具 output 返回;内容会通过后续上下文/通知进入模型。如果发生 clamp,message 还会同时写出 requested timeout 与实际 minimum,output schema 因此明确包含 timeout adjustment。

3.3 List与Interrupt ​

V2 list_agents 读取当前 root thread tree,可用 path prefix 过滤;interrupt_agent 通过 resolver 接受 agent id 或 canonical task name,并拒绝 root/self 等非法目标。两者都只在 V2 装配分支中注册。

源码位置:codex-rs/core/src/tools/handlers/multi_agents_v2/list_agents.rs :: Handler::handle_call

rust
let agents = session
    .services
    .agent_control
    .list_agents(&turn.session_source, args.path_prefix.as_deref())
    .await
    .map_err(collab_spawn_error)?;

Ok(boxed_tool_output(ListAgentsResult { agents }))

4. Spawn运行 ​

4.1 参数解析 ​

V2 spawn handler 先解析 fork mode、加密 message、role 和 model overrides,再从当前 turn 构造 child config。fork_turns 允许 none、all 或正整数;fork_context 在 V2 明确拒绝,避免把 V1 字段悄悄解释成另一种历史 fork。

源码位置:codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs :: SpawnAgentArgs::fork_mode

rust
fn fork_mode(&self) -> Result<Option<SpawnAgentForkMode>, FunctionCallError> {
    if self.fork_context.is_some() {
        return Err(FunctionCallError::RespondToModel(
            "fork_context is not supported in MultiAgentV2; use fork_turns instead".to_string(),
        ));
    }
    let fork_turns = self
        .fork_turns
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .unwrap_or("all");
    if fork_turns.eq_ignore_ascii_case("none") {
        return Ok(None);
    }
    if fork_turns.eq_ignore_ascii_case("all") {
        return Ok(Some(SpawnAgentForkMode::FullHistory));
    }
    let last_n_turns = fork_turns.parse::<usize>().map_err(|_| {
        FunctionCallError::RespondToModel(
            "fork_turns must be `none`, `all`, or a positive integer string".to_string(),
        )
    })?;
    if last_n_turns == 0 {
        return Err(FunctionCallError::RespondToModel(
            "fork_turns must be `none`, `all`, or a positive integer string".to_string(),
        ));
    }
    Ok(Some(SpawnAgentForkMode::LastNTurns(last_n_turns)))
}

4.2 Child配置 ​

handler 使用 build_agent_spawn_config 从当前 TurnContext 的 effective config 重建 child config,再叠加 role、model、reasoning 和 service tier override。full-history fork 现在也允许显式 role override;如果 role 没提供 developer instructions,则回填当前 turn instructions。runtime permissions 使用 Session snapshot,避免 role config 把当前 sandbox 权限覆盖回旧值。

源码位置:codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs :: handle_spawn_agent

rust
let mut config = build_agent_spawn_config(
    &session.get_base_instructions().await,
    turn.as_ref(),
)?;
apply_requested_spawn_agent_model_overrides(
    &session,
    turn.as_ref(),
    &mut config,
    args.model.as_deref(),
    args.reasoning_effort.clone(),
)
.await?;
if !is_full_history_fork || role_name.is_some() {
    apply_spawn_agent_role(&session, &mut config, role_name).await?;
    if is_full_history_fork && config.developer_instructions.is_none() {
        config
            .developer_instructions
            .clone_from(&turn.developer_instructions);
    }
}
apply_spawn_agent_runtime_overrides(&mut config, turn.as_ref())?;

没有显式 role 的非-full fork 还可能把配置文件提供的 default role 记录到 SessionSource,使 cold reload 能重新应用同一组限制。role 是否写入 lineage 与 role 是否改变模型是两个不同问题。

4.3 AgentControl ​

spawn handler 通过 AgentControl::spawn_agent_with_communication 创建 child,并传入 parent thread/turn、fork mode、environment selections 和 communication context。成功后发 SubAgentActivity::Started,返回 task name 与可选 nickname;失败由 collab_spawn_error 转成模型可见错误。

源码位置:codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs :: handle_spawn_agent

rust
let spawned_agent = session
    .services
    .agent_control
    .spawn_agent_with_communication(
        config,
        communication,
        context,
        Some(spawn_source),
        SpawnAgentOptions {
            fork_parent_spawn_call_id: fork_mode.as_ref().map(|_| call_id.clone()),
            fork_mode,
            parent_thread_id: Some(session.thread_id),
            parent_turn_id: Some(turn.sub_id.clone()),
            root_turn_id: turn.turn_metadata_state.root_turn_id(),
            environments: Some(step_context.environments.to_selections()),
            multi_agent_v2_usage_hints,
        },
    )
    .await
.map_err(collab_spawn_error)?;

root_turn_id 让后续 follow-up、审查和统计保留整棵协作树的根 turn;full-history V2 fork 还根据 child model catalog 解析 usage hints,避免把只适用于 parent model 的协作提示直接复制给 child。环境仍以当前 StepContext selections 快照传入。

5. 暴露边界 ​

5.1 DeferredV1 ​

V1 在 ToolSearch 可用时整体 Deferred;ToolSearch 返回的是 multi_agent_v1 namespace 子工具,后续 function call 仍由原 V1 runtime dispatch。V2 默认 Direct 或 DirectModelOnly,不走同一 Deferred 默认。

源码位置:codex-rs/core/tests/suite/search_tool.rs :: tool_search_returns_deferred_v1_multi_agent_tools

rust
assert!(first_request_tools.iter().any(|name| name == "tool_search"));
assert!(
    !first_request_tools
        .iter()
        .any(|name| name == "spawn_agent")
);
let tools = tool_search_output_tools(&requests[1], search_call_id);
let spawn_agent = namespace_child_tool(
    &json!({ "tools": tools }),
    "multi_agent_v1",
    "spawn_agent",
)
.expect("tool_search should return V1 spawn_agent");
assert_eq!(spawn_agent["defer_loading"], true);

5.2 CodeMode-only ​

V2 的 non_code_mode_only=true 设置 DirectModelOnly:普通模型请求不直接暴露,Code Mode 仍可使用相应 surface。该 exposure 与 V2 namespace 可以同时存在;前者决定 surface,后者只决定 wire 包装。

5.3 Namespace能力 ​

如果 provider 不支持 namespace tools,tool_namespace 配置不会强行包装 V2。Bedrock 测试验证:配置 namespace 后,支持 V2 的模型注册 agents.spawn_agent,不支持时则不注册协作 runtime;namespace capability 不是纯配置字段。

6. 等待与消息 ​

6.1 Mailbox等待 ​

wait_agent 将当前 turn 的 InputQueueActivity 订阅到 watch receiver。已有 pending activity 会立即返回;否则等待 mailbox、steer 或 deadline。它不读取完成消息正文,正文由其他 inter-agent context/notification 路径传递。

源码位置:codex-rs/core/src/tools/handlers/multi_agents_v2/wait.rs :: wait_for_activity

rust
async fn wait_for_activity(
    activity_rx: &mut tokio::sync::watch::Receiver<InputQueueActivity>,
    pending_activity: Option<InputQueueActivity>,
    deadline: Instant,
) -> WaitOutcome {
    if let Some(activity) = pending_activity {
        return match activity {
            InputQueueActivity::Mailbox => WaitOutcome::MailboxActivity,
            InputQueueActivity::Steer => WaitOutcome::Steered,
        };
    }
    match timeout_at(deadline, activity_rx.changed()).await {
        Ok(Ok(())) => match *activity_rx.borrow_and_update() {
            InputQueueActivity::Mailbox => WaitOutcome::MailboxActivity,
            InputQueueActivity::Steer => WaitOutcome::Steered,
        },
        Ok(Err(_)) | Err(_) => WaitOutcome::TimedOut,
    }
}

6.2 消息模式 ​

V2 message handlers 共用 handle_message_string_tool,差别只在 MessageDeliveryMode:QueueOnly 不触发目标新 turn,TriggerTurn 会在目标 idle 时触发 follow-up。目标解析支持 relative/canonical task name,错误由 common helper 转成模型可见文本。

源码位置:codex-rs/core/src/tools/handlers/multi_agents_v2/followup_task.rs :: Handler::handle_call

rust
let arguments = function_arguments(invocation.payload.clone())?;
let args: FollowupTaskArgs = parse_arguments(&arguments)?;
handle_message_string_tool(
    invocation,
    MessageDeliveryMode::TriggerTurn,
    args.target,
    args.message,
)
.await
.map(boxed_tool_output)

6.3 状态结果 ​

V2 wait_agent 的 output 是摘要;list_agents 返回 live agent tree;spawn 返回 task name/nickname;interrupt 返回目标先前状态。这些 output schema 在 multi_agents_spec.rs,不是 handler 随意拼接字符串。

6.4 公开Activity ​

V2 的公开时间线以 SubAgentActivity 为主:spawn 发 Started,send/followup 发 Interacted,interrupt 发 Interrupted;child turn 正常完成后,child Session 再向 initiating thread 发 Completed。完成通知使用独立的 subagent-completed-<turn> id,不复用 spawn call id。

源码位置:codex-rs/core/src/session/mod.rs :: Session::forward_child_completion_to_parent

rust
if matches!(status, AgentStatus::Completed(_))
    && let Some(parent_turn_id) = turn_context.turn_metadata_state.parent_turn_id()
{
    if let Some(initiating_thread_id) = initiating_thread_id
        && let Err(err) = self
            .services
            .agent_control
            .emit_sub_agent_activity(
                initiating_thread_id,
                parent_turn_id,
                SubAgentActivityItem {
                    id: format!("subagent-completed-{}", turn_context.sub_id),
                    kind: SubAgentActivityKind::Completed,
                    agent_thread_id: self.thread_id,
                    agent_path: child_agent_path.clone(),
                },
            )
            .await
    {
        debug!(
            "failed to emit completed activity to initiating thread {initiating_thread_id}: {err}"
        );
    }
}

Completed 只对应正常完成;interrupted turn 不发送 completed activity。App Server protocol 现在公开映射 V2 的 SendMessage/FollowupTask/InterruptAgent/ListAgents、CollabAgentToolCallStatus::Interrupted 和 SubAgentActivityKind::Completed,旧客户端不能再假定枚举只包含 V1 工具。

6.5 私有统计 ​

V2 message、followup、interrupt 和 list handler 使用 ToolCallAnalytics;spawn 记录额外的 child model、reasoning 和 status。guard 初始状态是 Interrupted,正常返回后改成 Completed 或 Failed,因此 future 被取消或 panic/drop 时不会被误记为成功。

源码位置:codex-rs/core/src/tools/handlers/multi_agents_v2/analytics.rs :: ToolCallAnalytics

rust
pub(super) fn new(invocation: &ToolInvocation, tool: CollabAgentTool) -> Self {
    Self {
        item: CollabAgentToolCallItem {
            id: invocation.call_id.clone(),
            tool,
            status: CollabAgentToolCallStatus::Interrupted,
            sender_thread_id: invocation.session.thread_id,
            /* private analytics fields */
        },
        /* timing and client */
    }
}

pub(super) fn finish<T, E>(mut self, result: &Result<T, E>) {
    self.item.status = if result.is_ok() {
        CollabAgentToolCallStatus::Completed
    } else {
        CollabAgentToolCallStatus::Failed
    };
}

这条统计不会再发送公开 tool item。App Server 集成测试断言 thread items 中保留 activity,却没有 V2 CollabAgentToolCall;analytics 事件只携带工具、sender/receiver、状态和耗时,不能包含 message/prompt 私文。wait_agent 不使用这个 guard,而是由 handler 自己发送公开 wait item。

当前运行没有完成这项 App Server 断言:multi_agent_v2_tools_emit_collaborator_analytics 连续两次都在 initialize 阶段超时,尚未创建 thread 或执行协作工具。因此“私有统计不生成公开 item”由 handler 与测试源码共同说明,但这次集成运行不能作为通过结果。

7. 错误与恢复 ​

7.1 Spawn失败 ​

spawn 失败可能来自 thread manager dropped、depth limit、invalid target model、role override 或 AgentControl 内部错误;collab_spawn_error 将内部错误压缩为模型可理解的文本。成功创建 child 后,activity item 已发送,后续 child 执行不由 spawn handler 等待。

7.2 Target错误 ​

send/followup/list/interrupt/wait 都通过 AgentControl/agent resolver 处理 not found、closed、root/self 等目标边界。目标错误不会把 parent turn 标记成 fatal,通常作为该工具的 RespondToModel。

7.3 Wait取消 ​

wait 的 deadline 或 watch receiver 关闭会形成 TimedOut,handler 随后发送 CollabAgentToolCallStatus::Completed,output 用 timed_out=true 表达结果;steer/mailbox 也正常完成。外层 turn cancellation 则可能在 handler 发 completed item 前直接 drop future,不能与普通 timeout 合并理解。不要把 timeout 当成 agent failed,也不要假定被取消的 wait 一定有 completed item。

8. 源码练习 ​

先复述这条主线:MultiAgentVersion/model/depth gate → V1/V2 spec factory → namespace/exposure → spawn/message/wait handler → AgentControl、InterAgentCommunication 或 InputQueue → ToolOutput 与 SubAgentActivity。

再做三个只读验证:

  • 找到 spawn_agent_tool_v2_requires_task_name_and_lists_visible_models,解释为什么模型目录只影响 description,却不能让 task_name 变成可选;
  • 找到 multi_agent_v2_spawn_fork_turns_all_applies_agent_type_override,说明 full-history role 怎样影响 developer instructions 与 persisted role lineage;
  • 找到 multi_agent_v2_wait_agent_clamps_timeout_below_configured_min 与 multi_agent_v2_wait_agent_does_not_return_completed_content,说明 timeout adjustment 与 mailbox content 为什么属于两个不同输出通道。

在 Codex 源码仓库的 codex-rs/ 目录运行:

bash
rg -n "add_collaboration_tools|create_spawn_agent_tool_v[12]|ToolCallAnalytics|wait_for_activity|root_turn_id" core
cargo test -p codex-core --lib 'tools::handlers::multi_agents_spec::tests::' -- --test-threads=1
RUST_MIN_STACK=8388608 cargo test -p codex-core --lib 'tools::handlers::multi_agents::tests::multi_agent_v2_' -- --test-threads=1
RUST_MIN_STACK=8388608 cargo test -p codex-core --lib 'agent::control::tests::multi_agent_v2_completion_' -- --test-threads=1

第一组测试覆盖 V1/V2 schema;后两组覆盖 V2 spawn/message/wait/interrupt/list 与 completion routing。它们不能替代 App Server analytics、child session 和跨进程通知的完整部署验证。