Skip to main content

codex_tools/
tool_call.rs

1use crate::FunctionCallError;
2use crate::ToolName;
3use crate::ToolPayload;
4use codex_extension_items::ExtensionItem;
5use codex_file_system::ExecutorFileSystem;
6use codex_file_system::FileSystemSandboxContext;
7use codex_protocol::models::ResponseItem;
8use codex_protocol::protocol::EventMsg;
9use codex_utils_absolute_path::AbsolutePathBuf;
10use codex_utils_output_truncation::TruncationPolicy;
11use std::future::Future;
12use std::pin::Pin;
13use std::sync::Arc;
14
15/// Raw response history snapshot available when an extension tool is invoked.
16#[derive(Clone, Debug, Default)]
17pub struct ConversationHistory {
18    items: Arc<[ResponseItem]>,
19}
20
21impl ConversationHistory {
22    pub fn new(items: Vec<ResponseItem>) -> Self {
23        Self {
24            items: items.into(),
25        }
26    }
27
28    pub fn items(&self) -> &[ResponseItem] {
29        &self.items
30    }
31}
32
33/// Future returned when an extension tool emits a visible turn-item lifecycle event.
34pub type TurnItemEmissionFuture<'a> = Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
35
36/// Visible turn items that an extension may publish into the host lifecycle.
37#[derive(Clone, Debug)]
38pub struct ExtensionTurnItem {
39    /// Canonical extension item plus compatibility events derived by its owner.
40    ///
41    /// Core intentionally does not inspect extension-owned payloads, so it
42    /// cannot derive their legacy fanout. It emits the canonical lifecycle
43    /// event first, then these extension-provided events. Core also skips
44    /// global turn-item contributors here so extensions cannot mutate items
45    /// owned by other extensions.
46    pub item: ExtensionItem,
47    pub legacy_events: Vec<EventMsg>,
48}
49
50/// Host-provided capability for extension tools to emit visible turn items.
51///
52/// Implementations route lifecycle events through the host's normal item event
53/// pipeline and client delivery.
54pub trait TurnItemEmitter: Send + Sync {
55    /// Emits the beginning of one visible turn item.
56    fn emit_started<'a>(&'a self, item: ExtensionTurnItem) -> TurnItemEmissionFuture<'a>;
57
58    /// Emits one completed visible turn item.
59    fn emit_completed<'a>(&'a self, item: ExtensionTurnItem) -> TurnItemEmissionFuture<'a>;
60}
61
62/// Host-owned turn environment summary visible to extension tools.
63#[derive(Clone)]
64pub struct ToolEnvironment {
65    /// Stable host environment id used to route executor-scoped capabilities.
66    pub environment_id: String,
67    /// Effective working directory for this turn in the environment.
68    pub cwd: AbsolutePathBuf,
69    /// Filesystem implementation for this environment.
70    pub file_system: Arc<dyn ExecutorFileSystem>,
71    /// Sandbox context to use for filesystem operations.
72    pub file_system_sandbox_context: FileSystemSandboxContext,
73}
74
75/// Turn-item emitter used when a caller does not expose visible item emission.
76#[derive(Debug, Default, Clone, Copy)]
77pub struct NoopTurnItemEmitter;
78
79impl TurnItemEmitter for NoopTurnItemEmitter {
80    fn emit_started<'a>(&'a self, _item: ExtensionTurnItem) -> TurnItemEmissionFuture<'a> {
81        Box::pin(std::future::ready(()))
82    }
83
84    fn emit_completed<'a>(&'a self, _item: ExtensionTurnItem) -> TurnItemEmissionFuture<'a> {
85        Box::pin(std::future::ready(()))
86    }
87}
88
89#[derive(Clone)]
90pub struct ToolCall {
91    pub turn_id: String,
92    pub call_id: String,
93    pub tool_name: ToolName,
94    pub model: String,
95    pub codex_turn_metadata: Option<String>,
96    pub truncation_policy: TruncationPolicy,
97    pub conversation_history: ConversationHistory,
98    pub turn_item_emitter: Arc<dyn TurnItemEmitter>,
99    pub environments: Vec<ToolEnvironment>,
100    pub payload: ToolPayload,
101}
102
103impl std::fmt::Debug for ToolCall {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        f.debug_struct("ToolCall")
106            .field("turn_id", &self.turn_id)
107            .field("call_id", &self.call_id)
108            .field("tool_name", &self.tool_name)
109            .field("model", &self.model)
110            .field(
111                "has_codex_turn_metadata",
112                &self.codex_turn_metadata.is_some(),
113            )
114            .field("truncation_policy", &self.truncation_policy)
115            .field("conversation_history", &self.conversation_history)
116            .field("turn_item_emitter", &"<host turn item emitter>")
117            .field("environment_count", &self.environments.len())
118            .field("payload", &self.payload)
119            .finish()
120    }
121}
122
123impl ToolCall {
124    pub fn function_arguments(&self) -> Result<&str, FunctionCallError> {
125        match &self.payload {
126            ToolPayload::Function { arguments } => Ok(arguments),
127            _ => Err(FunctionCallError::Fatal(format!(
128                "tool {} invoked with incompatible payload",
129                self.tool_name
130            ))),
131        }
132    }
133}