Skip to content

ToolLifecycle事件

追踪工具 start、finish 与 aborted 事件的发出门槛、outcome 映射、来源投影和取消终态竞争。

基于rust-v0.150.0
CodexRustToolsLifecycle

ToolLifecycle事件 ​

工具 lifecycle 事件不是“收到模型调用就必然 start,结束时必然 success”的简单二元回调。当前实现只有在 Registry 找到 runtime 且 payload kind 匹配后才发 start;PreToolUse block、输入改写失败、handler 错误、业务 success:false 和取消分别对应不同 outcome。取消还可能在 dispatch admission 前获胜,因此 Aborted 明确允许 没有匹配 start。

本文面向已经读过ToolOrchestrator执行流程和 ToolRouter解析与分派的读者。本文只研究 extension-facing tool lifecycle, 不展开 Turn/Thread lifecycle,也不把 telemetry trace 当成同一种事件。读完后,读者应能从一个失败现象判断是否 应该出现 start、finish 的 outcome 是什么,以及为什么 PostToolUse feedback 不会回滚已经完成的 handler。

1. 事件契约 ​

1.1 Outcome枚举 ​

extension API 将终态分成四类。Completed.success 来自 ToolOutput 自己的 logging success;Failed 额外记录是否 已经进入 handler;Blocked 专指 handler 前的 host policy block;Aborted 表示 host 取消正常执行。

源码位置:codex-rs/ext/extension-api/src/contributors/tool_lifecycle.rs :: ToolCallOutcome

rust
pub enum ToolCallOutcome {
    Completed {
        success: bool,
    },
    Blocked,
    Failed {
        handler_executed: bool,
    },
    Aborted,
}

这四类不是 Rust Result 的直接翻译。handler 可以正常返回一个 success:false 的 ToolOutput,此时 lifecycle 仍是 Completed { success:false };只有 handler 没有产生正常 output 才是 Failed。

1.2 事件输入 ​

start 和 finish 都携带 session/thread/turn 三层 extension store、turn id、call id、canonical tool name 和 source; finish 额外携带 outcome。Contributor 因而可以按调用身份维护自己的状态,而不必从日志文本反推。

相关源码:

  • codex-rs/ext/extension-api/src/contributors/tool_lifecycle.rs :: ToolStartInput
  • codex-rs/ext/extension-api/src/contributors/tool_lifecycle.rs :: ToolFinishInput
rust
pub struct ToolStartInput<'a> {
    pub session_store: &'a ExtensionData,
    pub thread_store: &'a ExtensionData,
    pub turn_store: &'a ExtensionData,
    pub turn_id: &'a str,
    pub call_id: &'a str,
    pub tool_name: &'a ToolName,
    pub source: ToolCallSource,
}

pub struct ToolFinishInput<'a> {
    pub session_store: &'a ExtensionData,
    pub thread_store: &'a ExtensionData,
    pub turn_store: &'a ExtensionData,
    pub turn_id: &'a str,
    pub call_id: &'a str,
    pub tool_name: &'a ToolName,
    pub source: ToolCallSource,
    pub outcome: ToolCallOutcome,
}

2. Start门槛 ​

Registry 先增加调用计数,然后查 runtime、检查 payload kind。未知工具返回 RespondToModel,kind mismatch 返回 Fatal;这两个分支都位于 notify_tool_start 之前,因此 lifecycle contributor 不会收到 start/finish。

源码位置:codex-rs/core/src/tools/registry.rs :: dispatch_any_with_terminal_outcome

rust
let tool = match self.tool(&tool_name) {
    Some(tool) => tool,
    None => {
        let message = unsupported_tool_call_message(&invocation.payload, &tool_name);
        return Err(FunctionCallError::RespondToModel(message));
    }
};

if !tool.matches_kind(&invocation.payload) {
    let message = format!("tool {tool_name} invoked with incompatible payload");
    return Err(FunctionCallError::Fatal(message));
}

notify_tool_start(&invocation).await;

这条门槛让 start 表示“Registry 已接受该调用并准备进入 host policy/handler 流程”,而不是“模型曾经发出某个名字”。

3. Outcome映射 ​

3.1 PreToolUse阻止 ​

PreToolUse 返回 Blocked 时,handler 没有执行,Registry 记录 trace failure,并通过唯一终态 helper 发出 ToolCallOutcome::Blocked。

源码位置:codex-rs/core/src/tools/registry.rs :: dispatch_any_with_terminal_outcome

rust
PreToolUseHookResult::Blocked(message) => {
    let err = FunctionCallError::RespondToModel(message);
    dispatch_trace.record_failed(&err);
    notify_tool_finish_if_unclaimed(
        &invocation,
        terminal_outcome_reached.as_deref(),
        ToolCallOutcome::Blocked,
    )
    .await;
    return Err(err);
}

3.2 输入改写失败 ​

Hook 允许继续但返回的 updated_input 无法还原成 ToolInvocation 时,handler 同样没有执行,但这不是 policy block, 所以 outcome 是 Failed { handler_executed:false }。

源码位置:codex-rs/core/src/tools/registry.rs :: dispatch_any_with_terminal_outcome

rust
Err(err) => {
    dispatch_trace.record_failed(&err);
    notify_tool_finish_if_unclaimed(
        &invocation,
        terminal_outcome_reached.as_deref(),
        ToolCallOutcome::Failed {
            handler_executed: false,
        },
    )
    .await;
    return Err(err);
}

3.3 Handler结果 ​

handler 调用完成后,Registry 根据 result 和 output 的 success_for_logging() 计算 lifecycle outcome:正常 output 始终是 Completed,success 字段可以为 false;handler 返回 Err 或内部没有存入 output,则是 Failed { handler_executed:true }。

源码位置:codex-rs/core/src/tools/registry.rs :: dispatch_any_with_terminal_outcome

rust
let lifecycle_outcome = match &result {
    Ok(_) => {
        let guard = response_cell.lock().await;
        match guard.as_ref() {
            Some(result) => ToolCallOutcome::Completed {
                success: result.result.success_for_logging(),
            },
            None => ToolCallOutcome::Failed {
                handler_executed: true,
            },
        }
    }
    Err(_) => ToolCallOutcome::Failed {
        handler_executed: true,
    },
};

4. 事件投影 ​

4.1 Contributor调用 ​

notify_tool_start 和 notify_tool_finish_parts 顺序遍历 session extension registry 中的 lifecycle contributors,并 await 每个 callback。事件不是 fire-and-forget;慢 contributor 会延长对应 start/finish 阶段。

相关源码:

  • codex-rs/core/src/tools/lifecycle.rs :: notify_tool_start
  • codex-rs/core/src/tools/lifecycle.rs :: notify_tool_finish_parts
rust
for contributor in invocation
    .session
    .services
    .extensions
    .tool_lifecycle_contributors()
{
    contributor
        .on_tool_start(ToolStartInput {
            session_store: &invocation.session.services.session_extension_data,
            thread_store: &invocation.session.services.thread_extension_data,
            turn_store: invocation.turn.extension_data.as_ref(),
            turn_id: invocation.turn.sub_id.as_str(),
            call_id: invocation.call_id.as_str(),
            tool_name: &invocation.tool_name,
            source: extension_tool_call_source(invocation.source.clone()),
        })
        .await;
}

4.2 Source转换 ​

Core 的 DirectPlaintextMessage 在 extension API 中投影为 Direct,因为明文标记只服务 Core 日志脱敏;Code Mode 则保留 cell id 与 runtime tool call id。

源码位置:codex-rs/core/src/tools/lifecycle.rs :: extension_tool_call_source

rust
match source {
    ToolCallSource::Direct | ToolCallSource::DirectPlaintextMessage => {
        ExtensionToolCallSource::Direct
    }
    ToolCallSource::CodeMode {
        cell_id,
        runtime_tool_call_id,
    } => ExtensionToolCallSource::CodeMode {
        cell_id,
        runtime_tool_call_id,
    },
}

5. PostToolUse边界 ​

Registry 在 handler 结果返回后运行 PostToolUse,但 lifecycle outcome 在应用 hook feedback/block 之前计算和通知。 因此:

  • PostToolUse feedback 可以替换模型可见结果;
  • PostToolUse should_block 可以让最终 dispatch 返回 RespondToModel;
  • lifecycle 仍描述 handler 的实际完成/失败,不会改写成 Blocked;
  • 已发生的外部副作用不会因为 feedback 被回滚。

源码位置:codex-rs/core/src/tools/registry.rs :: dispatch_any_with_terminal_outcome

rust
notify_tool_finish_if_unclaimed(
    &invocation,
    terminal_outcome_reached.as_deref(),
    lifecycle_outcome,
)
.await;

match result {
    Ok(_) => {
        let mut result = response_cell
            .lock()
            .await
            .take()
            .ok_or_else(|| FunctionCallError::Fatal("tool produced no output".to_string()))?;

        if let Some(outcome) = post_tool_use_outcome {
            if outcome.should_block {
                return Err(FunctionCallError::RespondToModel(
                    outcome.feedback_message.unwrap_or_else(|| {
                        "PostToolUse hook blocked the tool result".to_string()
                    }),
                ));
            }
        }
        Ok(result)
    }
    Err(err) => Err(err),
}

6. 取消配对 ​

6.1 无Start的Aborted ​

extension API 注释明确指出:取消可能在 dispatch 接受调用前获胜,contributors 不能假设 Aborted 一定有匹配 start。 例如工具仍在 readiness 或 RwLock admission 等待时,取消分支可以 abort task 并直接调用 notify_tool_aborted。

源码位置:codex-rs/ext/extension-api/src/contributors/tool_lifecycle.rs :: ToolCallOutcome::Aborted

rust
/// The host cancelled the tool before normal completion. Cancellation can
/// win before the dispatch path accepts the call, so contributors should not
/// assume a matching start callback exists.
Aborted,

6.2 唯一终态 ​

Registry finish 与取消分支共享 terminal_outcome_reached。notify_tool_finish_if_unclaimed 使用 atomic swap 抢占 终态;取消分支也先 load/swap。只有 winner 发送 finish 或 aborted,避免同一 call id 收到两个终态。

源码位置:codex-rs/core/src/tools/registry.rs :: notify_tool_finish_if_unclaimed

rust
if terminal_outcome_reached.is_some_and(|reached| reached.swap(true, Ordering::AcqRel)) {
    return false;
}

notify_tool_finish(invocation, outcome).await;
true

7. 测试路径 ​

7.1 完成与失败 ​

dispatch_uses_canonical_tool_names_for_lifecycle_contributors 构造两个 runtime:一个正常返回 success:false output,另一个返回 handler error。测试断言前者是 Completed { success:false },后者是 Failed { handler_executed:true },并验证 default namespace 使用 canonical name。

7.2 取消竞争 ​

cancellation_after_handler_finishes_preserves_completed_lifecycle 阻塞 finish callback,在 handler 已完成后触发 取消,最终只记录 Completed { success:true }。cleanup-aware runtime 的 teardown 分支由 handle_tool_call_with_source 中的 wait_for_runtime_cancellation 条件实现,当前没有独立同名测试。

相关测试:

  • codex-rs/core/src/tools/registry_tests.rs :: dispatch_uses_canonical_tool_names_for_lifecycle_contributors
  • codex-rs/core/src/tools/parallel.rs :: cancellation_after_handler_finishes_preserves_completed_lifecycle

这些测试证明 canonical identity、Completed/Failed 区分和唯一取消终态;当前没有独立单元测试直接覆盖 Blocked 与 handler_executed:false 两个分支,它们的结论来自显式源码分支。

8. 阅读练习 ​

在 Codex 源码 workspace 中运行:

bash
cargo test -p codex-core dispatch_uses_canonical_tool_names_for_lifecycle_contributors
cargo test -p codex-core cancellation_after_handler_finishes_preserves_completed_lifecycle

然后尝试回答:

  1. 未知工具为什么不会产生 start?payload kind mismatch 又为什么相同?
  2. Completed { success:false } 与 Failed { handler_executed:true } 分别表示什么?
  3. PreToolUse block 与输入 rewrite 失败为什么使用不同 outcome?
  4. PostToolUse block 为什么不映射成 lifecycle Blocked?
  5. 为什么 lifecycle consumer 必须接受“Aborted 但没有 start”的序列?

9. 边界 ​

Tool lifecycle 事件描述 host 观察到的调用阶段,不负责:

  • ToolOutput 如何写入模型历史或 Code Mode;
  • telemetry/rollout trace 的完整字段和持久化;
  • Turn/Thread lifecycle、进程 spawn lifecycle 或审批事件;
  • handler 外部事务是否真正回滚。

这些事件不证明 provider、远端 MCP server 或 OS 进程已经完成自己的资源清理,也不能证明业务副作用具备事务性; 它们只表达 Codex host 在当前调用链中观察到的阶段和终态。

实现 contributor 时,应以 (turn_id, call_id, canonical tool_name, source) 建立关联,并把 Aborted 视为允许无 start 的 终态。不要把 success:false 自动等同于 Failed,也不要依据最终模型 feedback 倒推 handler 是否执行。