mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +00:00
Add code-mode trace relationships
This commit is contained in:
@@ -24,6 +24,7 @@ pub use runtime::ExecuteRequest;
|
||||
pub use runtime::RuntimeResponse;
|
||||
pub use runtime::WaitRequest;
|
||||
pub use service::CodeModeService;
|
||||
pub use service::CodeModeToolInvocation;
|
||||
pub use service::CodeModeTurnHost;
|
||||
pub use service::CodeModeTurnWorker;
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ const EXIT_SENTINEL: &str = "__codex_code_mode_exit__";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ExecuteRequest {
|
||||
pub cell_id: Option<String>,
|
||||
pub tool_call_id: String,
|
||||
pub enabled_tools: Vec<ToolDefinition>,
|
||||
pub source: String,
|
||||
@@ -331,6 +332,7 @@ mod tests {
|
||||
|
||||
fn execute_request(source: &str) -> ExecuteRequest {
|
||||
ExecuteRequest {
|
||||
cell_id: None,
|
||||
tool_call_id: "call_1".to_string(),
|
||||
enabled_tools: Vec::new(),
|
||||
source: source.to_string(),
|
||||
|
||||
@@ -23,12 +23,26 @@ use crate::runtime::TurnMessage;
|
||||
use crate::runtime::WaitRequest;
|
||||
use crate::runtime::spawn_runtime;
|
||||
|
||||
/// A nested tool request made by one running code-mode cell.
|
||||
///
|
||||
/// The model only sees the outer `exec` call. Trace reduction needs the runtime
|
||||
/// cell id and runtime tool id so it can attach this nested tool to the right
|
||||
/// code cell without guessing from event ordering.
|
||||
#[derive(Debug)]
|
||||
pub struct CodeModeToolInvocation {
|
||||
/// Code-mode runtime cell that issued the request.
|
||||
pub cell_id: String,
|
||||
/// Per-cell tool invocation id generated by the JavaScript runtime.
|
||||
pub runtime_tool_call_id: String,
|
||||
pub tool_name: ToolName,
|
||||
pub input: Option<JsonValue>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait CodeModeTurnHost: Send + Sync {
|
||||
async fn invoke_tool(
|
||||
&self,
|
||||
tool_name: ToolName,
|
||||
input: Option<JsonValue>,
|
||||
invocation: CodeModeToolInvocation,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Result<JsonValue, String>;
|
||||
|
||||
@@ -76,24 +90,40 @@ impl CodeModeService {
|
||||
*self.inner.stored_values.lock().await = values;
|
||||
}
|
||||
|
||||
pub async fn execute(&self, request: ExecuteRequest) -> Result<RuntimeResponse, String> {
|
||||
let cell_id = self
|
||||
.inner
|
||||
pub fn allocate_cell_id(&self) -> String {
|
||||
self.inner
|
||||
.next_cell_id
|
||||
.fetch_add(1, Ordering::Relaxed)
|
||||
.to_string();
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub async fn execute(&self, request: ExecuteRequest) -> Result<RuntimeResponse, String> {
|
||||
let cell_id = request
|
||||
.cell_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.allocate_cell_id());
|
||||
if self.inner.sessions.lock().await.contains_key(&cell_id) {
|
||||
return Err(format!("exec cell {cell_id} already exists"));
|
||||
}
|
||||
|
||||
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
||||
let (runtime_tx, runtime_terminate_handle) = spawn_runtime(request.clone(), event_tx)?;
|
||||
let (control_tx, control_rx) = mpsc::unbounded_channel();
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
|
||||
self.inner.sessions.lock().await.insert(
|
||||
let mut sessions = self.inner.sessions.lock().await;
|
||||
if sessions.contains_key(&cell_id) {
|
||||
let _ = runtime_terminate_handle.terminate_execution();
|
||||
return Err(format!("exec cell {cell_id} already exists"));
|
||||
}
|
||||
sessions.insert(
|
||||
cell_id.clone(),
|
||||
SessionHandle {
|
||||
control_tx: control_tx.clone(),
|
||||
runtime_tx: runtime_tx.clone(),
|
||||
},
|
||||
);
|
||||
drop(sessions);
|
||||
|
||||
tokio::spawn(run_session_control(
|
||||
Arc::clone(&self.inner),
|
||||
@@ -181,9 +211,14 @@ impl CodeModeService {
|
||||
let host = Arc::clone(&host);
|
||||
let inner = Arc::clone(&inner);
|
||||
tokio::spawn(async move {
|
||||
let response = host
|
||||
.invoke_tool(name, input, CancellationToken::new())
|
||||
.await;
|
||||
let invocation = CodeModeToolInvocation {
|
||||
cell_id: cell_id.clone(),
|
||||
runtime_tool_call_id: id.clone(),
|
||||
tool_name: name,
|
||||
input,
|
||||
};
|
||||
let response =
|
||||
host.invoke_tool(invocation, CancellationToken::new()).await;
|
||||
let runtime_tx = inner
|
||||
.sessions
|
||||
.lock()
|
||||
@@ -490,6 +525,7 @@ mod tests {
|
||||
|
||||
fn execute_request(source: &str) -> ExecuteRequest {
|
||||
ExecuteRequest {
|
||||
cell_id: None,
|
||||
tool_call_id: "call_1".to_string(),
|
||||
enabled_tools: Vec::new(),
|
||||
source: source.to_string(),
|
||||
|
||||
@@ -6027,6 +6027,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
|
||||
session: Arc::clone(&session),
|
||||
turn: Arc::clone(&turn_context),
|
||||
tracker: Arc::clone(&turn_diff_tracker),
|
||||
source: crate::tools::context::ToolCallSource::Direct,
|
||||
call_id,
|
||||
tool_name: codex_tools::ToolName::plain(tool_name),
|
||||
payload: ToolPayload::Function {
|
||||
@@ -6105,6 +6106,7 @@ async fn unified_exec_rejects_escalated_permissions_when_policy_not_on_request()
|
||||
session: Arc::clone(&session),
|
||||
turn: Arc::clone(&turn_context),
|
||||
tracker: Arc::clone(&tracker),
|
||||
source: crate::tools::context::ToolCallSource::Direct,
|
||||
call_id: "exec-call".to_string(),
|
||||
tool_name: codex_tools::ToolName::plain("exec_command"),
|
||||
payload: ToolPayload::Function {
|
||||
|
||||
@@ -143,6 +143,7 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid
|
||||
session: Arc::clone(&session),
|
||||
turn: Arc::clone(&turn_context),
|
||||
tracker: Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())),
|
||||
source: crate::tools::context::ToolCallSource::Direct,
|
||||
call_id: "test-call".to_string(),
|
||||
tool_name: codex_tools::ToolName::plain("shell"),
|
||||
payload: ToolPayload::Function {
|
||||
@@ -209,6 +210,7 @@ async fn guardian_allows_unified_exec_additional_permissions_requests_past_polic
|
||||
session: Arc::clone(&session),
|
||||
turn: Arc::clone(&turn_context),
|
||||
tracker: Arc::clone(&tracker),
|
||||
source: crate::tools::context::ToolCallSource::Direct,
|
||||
call_id: "exec-call".to_string(),
|
||||
tool_name: codex_tools::ToolName::plain("exec_command"),
|
||||
payload: ToolPayload::Function {
|
||||
@@ -322,6 +324,7 @@ async fn shell_handler_allows_sticky_turn_permissions_without_inline_request_per
|
||||
session: Arc::clone(&session),
|
||||
turn: Arc::clone(&turn_context),
|
||||
tracker: Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())),
|
||||
source: crate::tools::context::ToolCallSource::Direct,
|
||||
call_id: "sticky-turn-grant".to_string(),
|
||||
tool_name: codex_tools::ToolName::plain("shell"),
|
||||
payload: ToolPayload::Function {
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use serde_json::json;
|
||||
|
||||
use super::ExecContext;
|
||||
use super::PUBLIC_TOOL_NAME;
|
||||
@@ -30,12 +31,34 @@ impl CodeModeExecuteHandler {
|
||||
.code_mode_service
|
||||
.stored_values()
|
||||
.await;
|
||||
// Allocate the runtime cell id before starting V8. Nested tools can be
|
||||
// requested before the model-visible `exec` item has been reduced, so
|
||||
// the trace needs this explicit bridge up front.
|
||||
let runtime_cell_id = exec.session.services.code_mode_service.allocate_cell_id();
|
||||
let invocation_payload = codex_trace::write_payload(
|
||||
"code_cell_invocation",
|
||||
&json!({
|
||||
"cell_id": runtime_cell_id,
|
||||
"model_visible_call_id": call_id,
|
||||
"source_js": args.code,
|
||||
"yield_time_ms": args.yield_time_ms,
|
||||
"max_output_tokens": args.max_output_tokens,
|
||||
}),
|
||||
);
|
||||
emit_code_cell_started(
|
||||
&exec,
|
||||
&runtime_cell_id,
|
||||
&call_id,
|
||||
&args.code,
|
||||
invocation_payload.as_ref(),
|
||||
);
|
||||
let started_at = std::time::Instant::now();
|
||||
let response = exec
|
||||
let response = match exec
|
||||
.session
|
||||
.services
|
||||
.code_mode_service
|
||||
.execute(codex_code_mode::ExecuteRequest {
|
||||
cell_id: Some(runtime_cell_id.clone()),
|
||||
tool_call_id: call_id,
|
||||
enabled_tools,
|
||||
source: args.code,
|
||||
@@ -44,13 +67,146 @@ impl CodeModeExecuteHandler {
|
||||
max_output_tokens: args.max_output_tokens,
|
||||
})
|
||||
.await
|
||||
.map_err(FunctionCallError::RespondToModel)?;
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
let result_payload = codex_trace::write_payload(
|
||||
"code_cell_result",
|
||||
&json!({ "cell_id": runtime_cell_id, "error_text": err }),
|
||||
);
|
||||
emit_code_cell_failed(&exec, &runtime_cell_id, result_payload.as_ref());
|
||||
return Err(FunctionCallError::RespondToModel(err));
|
||||
}
|
||||
};
|
||||
if !matches!(&response, codex_code_mode::RuntimeResponse::Yielded { .. }) {
|
||||
let result_payload = codex_trace::write_payload(
|
||||
"code_cell_result",
|
||||
&code_cell_response_payload(&response),
|
||||
);
|
||||
emit_code_cell_ended(
|
||||
&exec,
|
||||
&runtime_cell_id,
|
||||
&response,
|
||||
/*model_visible_wait_call_id*/ None,
|
||||
result_payload.as_ref(),
|
||||
);
|
||||
}
|
||||
handle_runtime_response(&exec, response, args.max_output_tokens, started_at)
|
||||
.await
|
||||
.map_err(FunctionCallError::RespondToModel)
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_code_cell_started(
|
||||
exec: &ExecContext,
|
||||
runtime_cell_id: &str,
|
||||
model_visible_call_id: &str,
|
||||
source_js: &str,
|
||||
invocation_payload: Option<&codex_trace::RawPayloadRef>,
|
||||
) {
|
||||
tracing::event!(
|
||||
target: codex_otel::OTEL_TRACE_SAFE_TARGET,
|
||||
tracing::Level::INFO,
|
||||
event.name = %"codex.code_cell.started",
|
||||
thread.id = %exec.session.conversation_id,
|
||||
turn.id = %exec.turn.sub_id,
|
||||
code_cell.runtime_id = %runtime_cell_id,
|
||||
model_visible_call.id = %model_visible_call_id,
|
||||
code_cell.source_js = %source_js,
|
||||
raw_payload.invocation.id = %invocation_payload.map(|payload| payload.raw_payload_id.as_str()).unwrap_or(""),
|
||||
raw_payload.invocation.path = %invocation_payload.map(|payload| payload.path.as_str()).unwrap_or(""),
|
||||
raw_payload.invocation.kind = %invocation_payload.map(|payload| payload.kind.as_str()).unwrap_or(""),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn emit_code_cell_ended(
|
||||
exec: &ExecContext,
|
||||
runtime_cell_id: &str,
|
||||
response: &codex_code_mode::RuntimeResponse,
|
||||
model_visible_wait_call_id: Option<&str>,
|
||||
result_payload: Option<&codex_trace::RawPayloadRef>,
|
||||
) {
|
||||
tracing::event!(
|
||||
target: codex_otel::OTEL_TRACE_SAFE_TARGET,
|
||||
tracing::Level::INFO,
|
||||
event.name = %"codex.code_cell.ended",
|
||||
thread.id = %exec.session.conversation_id,
|
||||
turn.id = %exec.turn.sub_id,
|
||||
code_cell.runtime_id = %runtime_cell_id,
|
||||
status = %code_cell_status(response),
|
||||
model_visible_wait_call.id = %model_visible_wait_call_id.unwrap_or(""),
|
||||
raw_payload.result.id = %result_payload.map(|payload| payload.raw_payload_id.as_str()).unwrap_or(""),
|
||||
raw_payload.result.path = %result_payload.map(|payload| payload.path.as_str()).unwrap_or(""),
|
||||
raw_payload.result.kind = %result_payload.map(|payload| payload.kind.as_str()).unwrap_or(""),
|
||||
);
|
||||
}
|
||||
|
||||
fn emit_code_cell_failed(
|
||||
exec: &ExecContext,
|
||||
runtime_cell_id: &str,
|
||||
result_payload: Option<&codex_trace::RawPayloadRef>,
|
||||
) {
|
||||
tracing::event!(
|
||||
target: codex_otel::OTEL_TRACE_SAFE_TARGET,
|
||||
tracing::Level::INFO,
|
||||
event.name = %"codex.code_cell.ended",
|
||||
thread.id = %exec.session.conversation_id,
|
||||
turn.id = %exec.turn.sub_id,
|
||||
code_cell.runtime_id = %runtime_cell_id,
|
||||
status = %"failed",
|
||||
raw_payload.result.id = %result_payload.map(|payload| payload.raw_payload_id.as_str()).unwrap_or(""),
|
||||
raw_payload.result.path = %result_payload.map(|payload| payload.path.as_str()).unwrap_or(""),
|
||||
raw_payload.result.kind = %result_payload.map(|payload| payload.kind.as_str()).unwrap_or(""),
|
||||
);
|
||||
}
|
||||
|
||||
fn code_cell_status(response: &codex_code_mode::RuntimeResponse) -> &'static str {
|
||||
match response {
|
||||
codex_code_mode::RuntimeResponse::Yielded { .. } => "yielded",
|
||||
codex_code_mode::RuntimeResponse::Terminated { .. } => "terminated",
|
||||
codex_code_mode::RuntimeResponse::Result { error_text, .. } => {
|
||||
if error_text.is_some() {
|
||||
"failed"
|
||||
} else {
|
||||
"completed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn code_cell_response_payload(response: &codex_code_mode::RuntimeResponse) -> serde_json::Value {
|
||||
match response {
|
||||
codex_code_mode::RuntimeResponse::Yielded {
|
||||
cell_id,
|
||||
content_items,
|
||||
} => json!({
|
||||
"cell_id": cell_id,
|
||||
"status": "yielded",
|
||||
"content_items": content_items,
|
||||
}),
|
||||
codex_code_mode::RuntimeResponse::Terminated {
|
||||
cell_id,
|
||||
content_items,
|
||||
} => json!({
|
||||
"cell_id": cell_id,
|
||||
"status": "terminated",
|
||||
"content_items": content_items,
|
||||
}),
|
||||
codex_code_mode::RuntimeResponse::Result {
|
||||
cell_id,
|
||||
content_items,
|
||||
stored_values,
|
||||
error_text,
|
||||
} => json!({
|
||||
"cell_id": cell_id,
|
||||
"status": if error_text.is_some() { "failed" } else { "completed" },
|
||||
"content_items": content_items,
|
||||
"stored_values": stored_values,
|
||||
"error_text": error_text,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolHandler for CodeModeExecuteHandler {
|
||||
type Output = FunctionToolOutput;
|
||||
|
||||
@@ -62,6 +218,23 @@ impl ToolHandler for CodeModeExecuteHandler {
|
||||
matches!(payload, ToolPayload::Custom { .. })
|
||||
}
|
||||
|
||||
fn uses_first_class_trace_object(&self, invocation: &ToolInvocation) -> bool {
|
||||
// Code-mode `exec` is represented by the first-class CodeCell runtime
|
||||
// lifecycle. Emitting the generic dispatch ToolCall as well would give
|
||||
// viewers two top-level objects for the same model-visible JS cell.
|
||||
//
|
||||
// Only suppress the generic ToolCall once parsing is known to succeed.
|
||||
// Parse errors return before a CodeCell id exists, so those failures
|
||||
// still need the ordinary tool trace object.
|
||||
invocation.tool_name.namespace.is_none()
|
||||
&& invocation.tool_name.name.as_str() == PUBLIC_TOOL_NAME
|
||||
&& matches!(
|
||||
&invocation.payload,
|
||||
ToolPayload::Custom { input }
|
||||
if codex_code_mode::parse_exec_source(input).is_ok()
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
|
||||
let ToolInvocation {
|
||||
session,
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_code_mode::CodeModeToolInvocation;
|
||||
use codex_code_mode::CodeModeTurnHost;
|
||||
use codex_code_mode::RuntimeResponse;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
@@ -73,6 +74,10 @@ impl CodeModeService {
|
||||
self.inner.replace_stored_values(values).await;
|
||||
}
|
||||
|
||||
pub(crate) fn allocate_cell_id(&self) -> String {
|
||||
self.inner.allocate_cell_id()
|
||||
}
|
||||
|
||||
pub(crate) async fn execute(
|
||||
&self,
|
||||
request: codex_code_mode::ExecuteRequest,
|
||||
@@ -118,15 +123,13 @@ struct CoreTurnHost {
|
||||
impl CodeModeTurnHost for CoreTurnHost {
|
||||
async fn invoke_tool(
|
||||
&self,
|
||||
tool_name: ToolName,
|
||||
input: Option<JsonValue>,
|
||||
invocation: CodeModeToolInvocation,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Result<JsonValue, String> {
|
||||
call_nested_tool(
|
||||
self.exec.clone(),
|
||||
self.tool_runtime.clone(),
|
||||
tool_name,
|
||||
input,
|
||||
invocation,
|
||||
cancellation_token,
|
||||
)
|
||||
.await
|
||||
@@ -298,10 +301,15 @@ async fn build_nested_router(exec: &ExecContext) -> ToolRouter {
|
||||
async fn call_nested_tool(
|
||||
exec: ExecContext,
|
||||
tool_runtime: ToolCallRuntime,
|
||||
tool_name: ToolName,
|
||||
input: Option<JsonValue>,
|
||||
invocation: CodeModeToolInvocation,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Result<JsonValue, FunctionCallError> {
|
||||
let CodeModeToolInvocation {
|
||||
cell_id,
|
||||
runtime_tool_call_id,
|
||||
tool_name,
|
||||
input,
|
||||
} = invocation;
|
||||
if tool_name.namespace.is_none() && tool_name.name == PUBLIC_TOOL_NAME {
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"{PUBLIC_TOOL_NAME} cannot invoke itself"
|
||||
@@ -335,7 +343,14 @@ async fn call_nested_tool(
|
||||
payload,
|
||||
};
|
||||
let result = tool_runtime
|
||||
.handle_tool_call_with_source(call, ToolCallSource::CodeMode, cancellation_token)
|
||||
.handle_tool_call_with_source(
|
||||
call,
|
||||
ToolCallSource::CodeMode {
|
||||
cell_id,
|
||||
runtime_tool_call_id,
|
||||
},
|
||||
cancellation_token,
|
||||
)
|
||||
.await?;
|
||||
Ok(result.code_mode_result())
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::tools::registry::ToolKind;
|
||||
use super::DEFAULT_WAIT_YIELD_TIME_MS;
|
||||
use super::ExecContext;
|
||||
use super::WAIT_TOOL_NAME;
|
||||
use super::execute_handler::emit_code_cell_ended;
|
||||
use super::handle_runtime_response;
|
||||
|
||||
pub struct CodeModeWaitHandler;
|
||||
@@ -49,6 +50,7 @@ impl ToolHandler for CodeModeWaitHandler {
|
||||
let ToolInvocation {
|
||||
session,
|
||||
turn,
|
||||
call_id,
|
||||
tool_name,
|
||||
payload,
|
||||
..
|
||||
@@ -66,12 +68,28 @@ impl ToolHandler for CodeModeWaitHandler {
|
||||
.services
|
||||
.code_mode_service
|
||||
.wait(codex_code_mode::WaitRequest {
|
||||
cell_id: args.cell_id,
|
||||
cell_id: args.cell_id.clone(),
|
||||
yield_time_ms: args.yield_time_ms,
|
||||
terminate: args.terminate,
|
||||
})
|
||||
.await
|
||||
.map_err(FunctionCallError::RespondToModel)?;
|
||||
if !matches!(&response, codex_code_mode::RuntimeResponse::Yielded { .. })
|
||||
&& !is_missing_cell_response(&response, &args.cell_id)
|
||||
{
|
||||
// The initial `exec` call only records a terminal end when
|
||||
// it finishes immediately. Yielded cells close later via
|
||||
// `wait`, so the runtime object stays live until this point.
|
||||
// Duplicate waits for a removed cell return a model-visible
|
||||
// error but must not rewrite an already-finished CodeCell.
|
||||
emit_code_cell_ended(
|
||||
&exec,
|
||||
&args.cell_id,
|
||||
&response,
|
||||
Some(&call_id),
|
||||
/*result_payload*/ None,
|
||||
);
|
||||
}
|
||||
handle_runtime_response(&exec, response, args.max_tokens, started_at)
|
||||
.await
|
||||
.map_err(FunctionCallError::RespondToModel)
|
||||
@@ -82,3 +100,14 @@ impl ToolHandler for CodeModeWaitHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_missing_cell_response(response: &codex_code_mode::RuntimeResponse, cell_id: &str) -> bool {
|
||||
let codex_code_mode::RuntimeResponse::Result {
|
||||
error_text: Some(error_text),
|
||||
..
|
||||
} = response
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
error_text == &format!("exec cell {cell_id} not found")
|
||||
}
|
||||
|
||||
@@ -28,11 +28,16 @@ use tokio::sync::Mutex;
|
||||
|
||||
pub type SharedTurnDiffTracker = Arc<Mutex<TurnDiffTracker>>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ToolCallSource {
|
||||
Direct,
|
||||
JsRepl,
|
||||
CodeMode,
|
||||
CodeMode {
|
||||
/// Runtime cell that issued the nested tool request.
|
||||
cell_id: String,
|
||||
/// Code-mode's per-cell tool invocation id.
|
||||
runtime_tool_call_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -40,6 +45,7 @@ pub struct ToolInvocation {
|
||||
pub session: Arc<Session>,
|
||||
pub turn: Arc<TurnContext>,
|
||||
pub tracker: SharedTurnDiffTracker,
|
||||
pub source: ToolCallSource,
|
||||
pub call_id: String,
|
||||
pub tool_name: ToolName,
|
||||
pub payload: ToolPayload,
|
||||
|
||||
@@ -68,6 +68,7 @@ fn invocation(
|
||||
session,
|
||||
turn,
|
||||
tracker: Arc::new(Mutex::new(TurnDiffTracker::default())),
|
||||
source: crate::tools::context::ToolCallSource::Direct,
|
||||
call_id: "call-1".to_string(),
|
||||
tool_name: codex_tools::ToolName::plain(tool_name),
|
||||
payload,
|
||||
|
||||
@@ -28,6 +28,7 @@ async fn multi_agent_v2_request_user_input_rejects_subagent_threads() {
|
||||
session: Arc::new(session),
|
||||
turn: Arc::new(turn),
|
||||
tracker: Arc::new(Mutex::new(TurnDiffTracker::default())),
|
||||
source: crate::tools::context::ToolCallSource::Direct,
|
||||
call_id: "call-1".to_string(),
|
||||
tool_name: codex_tools::ToolName::plain(REQUEST_USER_INPUT_TOOL_NAME),
|
||||
payload: ToolPayload::Function {
|
||||
|
||||
@@ -226,6 +226,7 @@ async fn shell_pre_tool_use_payload_uses_joined_command() {
|
||||
session: session.into(),
|
||||
turn: turn.into(),
|
||||
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
|
||||
source: crate::tools::context::ToolCallSource::Direct,
|
||||
call_id: "call-41".to_string(),
|
||||
tool_name: codex_tools::ToolName::plain("shell"),
|
||||
payload,
|
||||
@@ -251,6 +252,7 @@ async fn shell_command_pre_tool_use_payload_uses_raw_command() {
|
||||
session: session.into(),
|
||||
turn: turn.into(),
|
||||
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
|
||||
source: crate::tools::context::ToolCallSource::Direct,
|
||||
call_id: "call-42".to_string(),
|
||||
tool_name: codex_tools::ToolName::plain("shell_command"),
|
||||
payload,
|
||||
|
||||
@@ -211,6 +211,7 @@ async fn exec_command_pre_tool_use_payload_uses_raw_command() {
|
||||
session: session.into(),
|
||||
turn: turn.into(),
|
||||
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
|
||||
source: crate::tools::context::ToolCallSource::Direct,
|
||||
call_id: "call-43".to_string(),
|
||||
tool_name: codex_tools::ToolName::plain("exec_command"),
|
||||
payload,
|
||||
@@ -234,6 +235,7 @@ async fn exec_command_pre_tool_use_payload_skips_write_stdin() {
|
||||
session: session.into(),
|
||||
turn: turn.into(),
|
||||
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
|
||||
source: crate::tools::context::ToolCallSource::Direct,
|
||||
call_id: "call-44".to_string(),
|
||||
tool_name: codex_tools::ToolName::plain("write_stdin"),
|
||||
payload,
|
||||
|
||||
@@ -75,6 +75,10 @@ pub trait ToolHandler: Send + Sync {
|
||||
None
|
||||
}
|
||||
|
||||
fn uses_first_class_trace_object(&self, _invocation: &ToolInvocation) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Perform the actual [ToolInvocation] and returns a [ToolOutput] containing
|
||||
/// the final output to return to the model.
|
||||
fn handle(
|
||||
@@ -133,6 +137,8 @@ trait AnyToolHandler: Send + Sync {
|
||||
result: &dyn ToolOutput,
|
||||
) -> Option<PostToolUsePayload>;
|
||||
|
||||
fn uses_first_class_trace_object(&self, invocation: &ToolInvocation) -> bool;
|
||||
|
||||
fn handle_any<'a>(
|
||||
&'a self,
|
||||
invocation: ToolInvocation,
|
||||
@@ -164,6 +170,10 @@ where
|
||||
ToolHandler::post_tool_use_payload(self, call_id, payload, result)
|
||||
}
|
||||
|
||||
fn uses_first_class_trace_object(&self, invocation: &ToolInvocation) -> bool {
|
||||
ToolHandler::uses_first_class_trace_object(self, invocation)
|
||||
}
|
||||
|
||||
fn handle_any<'a>(
|
||||
&'a self,
|
||||
invocation: ToolInvocation,
|
||||
@@ -224,16 +234,23 @@ impl ToolRegistry {
|
||||
) -> Result<AnyToolResult, FunctionCallError> {
|
||||
let call_id_owned = invocation.call_id.clone();
|
||||
let trace_tool_call_id = format!("tool:{call_id_owned}");
|
||||
let invocation_payload =
|
||||
codex_trace::write_payload("tool_invocation", &trace_tool_invocation(&invocation));
|
||||
emit_tool_started(
|
||||
&invocation,
|
||||
&trace_tool_call_id,
|
||||
invocation_payload.as_ref(),
|
||||
);
|
||||
let uses_first_class_trace_object = self
|
||||
.handler(&invocation.tool_name)
|
||||
.is_some_and(|handler| handler.uses_first_class_trace_object(&invocation));
|
||||
if !uses_first_class_trace_object {
|
||||
let invocation_payload =
|
||||
codex_trace::write_payload("tool_invocation", &trace_tool_invocation(&invocation));
|
||||
emit_tool_started(
|
||||
&invocation,
|
||||
&trace_tool_call_id,
|
||||
invocation_payload.as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
let result = self.dispatch_any_inner(invocation).await;
|
||||
emit_tool_ended(&trace_tool_call_id, &result);
|
||||
if !uses_first_class_trace_object {
|
||||
emit_tool_ended(&trace_tool_call_id, &result);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
@@ -466,6 +483,8 @@ fn emit_tool_started(
|
||||
tool_call_id: &str,
|
||||
invocation_payload: Option<&codex_trace::RawPayloadRef>,
|
||||
) {
|
||||
let (requester_type, model_visible_call_id, runtime_cell_id, runtime_tool_call_id) =
|
||||
trace_tool_source_fields(invocation);
|
||||
tracing::event!(
|
||||
target: codex_otel::OTEL_TRACE_SAFE_TARGET,
|
||||
tracing::Level::INFO,
|
||||
@@ -474,13 +493,34 @@ fn emit_tool_started(
|
||||
turn.id = %invocation.turn.sub_id,
|
||||
tool.call_id = %tool_call_id,
|
||||
tool.name = %invocation.tool_name.display(),
|
||||
model_visible_call.id = %invocation.call_id,
|
||||
tool.requester.type = %requester_type,
|
||||
model_visible_call.id = %model_visible_call_id,
|
||||
code_cell.runtime_id = %runtime_cell_id,
|
||||
code_mode_runtime_tool.id = %runtime_tool_call_id,
|
||||
raw_payload.invocation.id = %invocation_payload.map(|payload| payload.raw_payload_id.as_str()).unwrap_or(""),
|
||||
raw_payload.invocation.path = %invocation_payload.map(|payload| payload.path.as_str()).unwrap_or(""),
|
||||
raw_payload.invocation.kind = %invocation_payload.map(|payload| payload.kind.as_str()).unwrap_or(""),
|
||||
);
|
||||
}
|
||||
|
||||
fn trace_tool_source_fields(invocation: &ToolInvocation) -> (&str, &str, &str, &str) {
|
||||
match &invocation.source {
|
||||
crate::tools::context::ToolCallSource::Direct => {
|
||||
("model", invocation.call_id.as_str(), "", "")
|
||||
}
|
||||
crate::tools::context::ToolCallSource::CodeMode {
|
||||
cell_id,
|
||||
runtime_tool_call_id,
|
||||
} => (
|
||||
"code_cell",
|
||||
"",
|
||||
cell_id.as_str(),
|
||||
runtime_tool_call_id.as_str(),
|
||||
),
|
||||
crate::tools::context::ToolCallSource::JsRepl => ("runtime", "", "", ""),
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_tool_ended(tool_call_id: &str, result: &Result<AnyToolResult, FunctionCallError>) {
|
||||
let (status, output_preview) = match result {
|
||||
Ok(result) => {
|
||||
@@ -541,10 +581,26 @@ fn trace_tool_invocation(invocation: &ToolInvocation) -> Value {
|
||||
"call_id": &invocation.call_id,
|
||||
"tool_name": invocation.tool_name.display(),
|
||||
"turn_id": &invocation.turn.sub_id,
|
||||
"source": trace_tool_invocation_source(invocation),
|
||||
"payload": payload,
|
||||
})
|
||||
}
|
||||
|
||||
fn trace_tool_invocation_source(invocation: &ToolInvocation) -> Value {
|
||||
match &invocation.source {
|
||||
crate::tools::context::ToolCallSource::Direct => json!({ "type": "model" }),
|
||||
crate::tools::context::ToolCallSource::CodeMode {
|
||||
cell_id,
|
||||
runtime_tool_call_id,
|
||||
} => json!({
|
||||
"type": "code_cell",
|
||||
"runtime_cell_id": cell_id,
|
||||
"runtime_tool_call_id": runtime_tool_call_id,
|
||||
}),
|
||||
crate::tools::context::ToolCallSource::JsRepl => json!({ "type": "runtime" }),
|
||||
}
|
||||
}
|
||||
|
||||
fn trace_tool_result(
|
||||
result: &Result<AnyToolResult, FunctionCallError>,
|
||||
status: &str,
|
||||
|
||||
@@ -285,6 +285,7 @@ impl ToolRouter {
|
||||
session,
|
||||
turn,
|
||||
tracker,
|
||||
source,
|
||||
call_id,
|
||||
tool_name,
|
||||
payload,
|
||||
|
||||
+1400
-27
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user