Pass TurnContext around instead of sub_id (#5421)

Today `sub_id` is an ID of a single incoming Codex Op submition. We then
associate all events triggered by this operation using the same
`sub_id`.

At the same time we are also creating a TurnContext per submission and
we'd like to start associating some events (item added/item completed)
with an entire turn instead of just the operation that started it.

Using turn context when sending events give us flexibility to change
notification scheme.
This commit is contained in:
pakrym-oai
2025-10-21 08:04:16 -07:00
committed by GitHub
parent 42d5c35020
commit 789e65b9d2
28 changed files with 293 additions and 401 deletions

View File

@@ -24,7 +24,6 @@ pub struct ToolInvocation {
pub session: Arc<Session>,
pub turn: Arc<TurnContext>,
pub tracker: SharedTurnDiffTracker,
pub sub_id: String,
pub call_id: String,
pub tool_name: String,
pub payload: ToolPayload,
@@ -234,7 +233,7 @@ mod tests {
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub(crate) struct ExecCommandContext {
pub(crate) sub_id: String,
pub(crate) turn: Arc<TurnContext>,
pub(crate) call_id: String,
pub(crate) command_for_display: Vec<String>,
pub(crate) cwd: PathBuf,

View File

@@ -1,7 +1,7 @@
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::exec::ExecToolCallOutput;
use crate::parse_command::parse_command;
use crate::protocol::Event;
use crate::protocol::EventMsg;
use crate::protocol::ExecCommandBeginEvent;
use crate::protocol::ExecCommandEndEvent;
@@ -20,7 +20,7 @@ use super::format_exec_output_str;
#[derive(Clone, Copy)]
pub(crate) struct ToolEventCtx<'a> {
pub session: &'a Session,
pub sub_id: &'a str,
pub turn: &'a TurnContext,
pub call_id: &'a str,
pub turn_diff_tracker: Option<&'a SharedTurnDiffTracker>,
}
@@ -28,13 +28,13 @@ pub(crate) struct ToolEventCtx<'a> {
impl<'a> ToolEventCtx<'a> {
pub fn new(
session: &'a Session,
sub_id: &'a str,
turn: &'a TurnContext,
call_id: &'a str,
turn_diff_tracker: Option<&'a SharedTurnDiffTracker>,
) -> Self {
Self {
session,
sub_id,
turn,
call_id,
turn_diff_tracker,
}
@@ -79,15 +79,15 @@ impl ToolEmitter {
match (self, stage) {
(Self::Shell { command, cwd }, ToolEventStage::Begin) => {
ctx.session
.send_event(Event {
id: ctx.sub_id.to_string(),
msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
.send_event(
ctx.turn,
EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
call_id: ctx.call_id.to_string(),
command: command.clone(),
cwd: cwd.clone(),
parsed_cmd: parse_command(command),
}),
})
)
.await;
}
(Self::Shell { .. }, ToolEventStage::Success(output)) => {
@@ -139,14 +139,14 @@ impl ToolEmitter {
guard.on_patch_begin(changes);
}
ctx.session
.send_event(Event {
id: ctx.sub_id.to_string(),
msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
.send_event(
ctx.turn,
EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
call_id: ctx.call_id.to_string(),
auto_approved: *auto_approved,
changes: changes.clone(),
}),
})
)
.await;
}
(Self::ApplyPatch { .. }, ToolEventStage::Success(output)) => {
@@ -190,9 +190,9 @@ async fn emit_exec_end(
formatted_output: String,
) {
ctx.session
.send_event(Event {
id: ctx.sub_id.to_string(),
msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent {
.send_event(
ctx.turn,
EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id: ctx.call_id.to_string(),
stdout,
stderr,
@@ -201,21 +201,21 @@ async fn emit_exec_end(
duration,
formatted_output,
}),
})
)
.await;
}
async fn emit_patch_end(ctx: ToolEventCtx<'_>, stdout: String, stderr: String, success: bool) {
ctx.session
.send_event(Event {
id: ctx.sub_id.to_string(),
msg: EventMsg::PatchApplyEnd(PatchApplyEndEvent {
.send_event(
ctx.turn,
EventMsg::PatchApplyEnd(PatchApplyEndEvent {
call_id: ctx.call_id.to_string(),
stdout,
stderr,
success,
}),
})
)
.await;
if let Some(tracker) = ctx.turn_diff_tracker {
@@ -225,10 +225,7 @@ async fn emit_patch_end(ctx: ToolEventCtx<'_>, stdout: String, stderr: String, s
};
if let Ok(Some(unified_diff)) = unified_diff {
ctx.session
.send_event(Event {
id: ctx.sub_id.to_string(),
msg: EventMsg::TurnDiff(TurnDiffEvent { unified_diff }),
})
.send_event(ctx.turn, EventMsg::TurnDiff(TurnDiffEvent { unified_diff }))
.await;
}
}

View File

@@ -42,7 +42,6 @@ impl ToolHandler for ApplyPatchHandler {
session,
turn,
tracker,
sub_id,
call_id,
tool_name,
payload,
@@ -81,7 +80,6 @@ impl ToolHandler for ApplyPatchHandler {
Arc::clone(&session),
Arc::clone(&turn),
Arc::clone(&tracker),
sub_id.clone(),
call_id.clone(),
)
.await?;

View File

@@ -19,7 +19,7 @@ impl ToolHandler for McpHandler {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
let ToolInvocation {
session,
sub_id,
turn,
call_id,
payload,
..
@@ -43,7 +43,7 @@ impl ToolHandler for McpHandler {
let response = handle_mcp_tool_call(
session.as_ref(),
&sub_id,
turn.as_ref(),
call_id.clone(),
server,
tool,

View File

@@ -21,8 +21,8 @@ use serde::de::DeserializeOwned;
use serde_json::Value;
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::function_tool::FunctionCallError;
use crate::protocol::Event;
use crate::protocol::EventMsg;
use crate::protocol::McpInvocation;
use crate::protocol::McpToolCallBeginEvent;
@@ -189,7 +189,7 @@ impl ToolHandler for McpResourceHandler {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
let ToolInvocation {
session,
sub_id,
turn,
call_id,
tool_name,
payload,
@@ -211,7 +211,7 @@ impl ToolHandler for McpResourceHandler {
"list_mcp_resources" => {
handle_list_resources(
Arc::clone(&session),
sub_id.clone(),
Arc::clone(&turn),
call_id.clone(),
arguments_value.clone(),
)
@@ -220,14 +220,20 @@ impl ToolHandler for McpResourceHandler {
"list_mcp_resource_templates" => {
handle_list_resource_templates(
Arc::clone(&session),
sub_id.clone(),
Arc::clone(&turn),
call_id.clone(),
arguments_value.clone(),
)
.await
}
"read_mcp_resource" => {
handle_read_resource(Arc::clone(&session), sub_id, call_id, arguments_value).await
handle_read_resource(
Arc::clone(&session),
Arc::clone(&turn),
call_id,
arguments_value,
)
.await
}
other => Err(FunctionCallError::RespondToModel(format!(
"unsupported MCP resource tool: {other}"
@@ -238,7 +244,7 @@ impl ToolHandler for McpResourceHandler {
async fn handle_list_resources(
session: Arc<Session>,
sub_id: String,
turn: Arc<TurnContext>,
call_id: String,
arguments: Option<Value>,
) -> Result<ToolOutput, FunctionCallError> {
@@ -253,7 +259,7 @@ async fn handle_list_resources(
arguments: arguments.clone(),
};
emit_tool_call_begin(&session, &sub_id, &call_id, invocation.clone()).await;
emit_tool_call_begin(&session, turn.as_ref(), &call_id, invocation.clone()).await;
let start = Instant::now();
let payload_result: Result<ListResourcesPayload, FunctionCallError> = async {
@@ -297,7 +303,7 @@ async fn handle_list_resources(
let duration = start.elapsed();
emit_tool_call_end(
&session,
&sub_id,
turn.as_ref(),
&call_id,
invocation,
duration,
@@ -311,7 +317,7 @@ async fn handle_list_resources(
let message = err.to_string();
emit_tool_call_end(
&session,
&sub_id,
turn.as_ref(),
&call_id,
invocation,
duration,
@@ -326,7 +332,7 @@ async fn handle_list_resources(
let message = err.to_string();
emit_tool_call_end(
&session,
&sub_id,
turn.as_ref(),
&call_id,
invocation,
duration,
@@ -340,7 +346,7 @@ async fn handle_list_resources(
async fn handle_list_resource_templates(
session: Arc<Session>,
sub_id: String,
turn: Arc<TurnContext>,
call_id: String,
arguments: Option<Value>,
) -> Result<ToolOutput, FunctionCallError> {
@@ -355,7 +361,7 @@ async fn handle_list_resource_templates(
arguments: arguments.clone(),
};
emit_tool_call_begin(&session, &sub_id, &call_id, invocation.clone()).await;
emit_tool_call_begin(&session, turn.as_ref(), &call_id, invocation.clone()).await;
let start = Instant::now();
let payload_result: Result<ListResourceTemplatesPayload, FunctionCallError> = async {
@@ -403,7 +409,7 @@ async fn handle_list_resource_templates(
let duration = start.elapsed();
emit_tool_call_end(
&session,
&sub_id,
turn.as_ref(),
&call_id,
invocation,
duration,
@@ -417,7 +423,7 @@ async fn handle_list_resource_templates(
let message = err.to_string();
emit_tool_call_end(
&session,
&sub_id,
turn.as_ref(),
&call_id,
invocation,
duration,
@@ -432,7 +438,7 @@ async fn handle_list_resource_templates(
let message = err.to_string();
emit_tool_call_end(
&session,
&sub_id,
turn.as_ref(),
&call_id,
invocation,
duration,
@@ -446,7 +452,7 @@ async fn handle_list_resource_templates(
async fn handle_read_resource(
session: Arc<Session>,
sub_id: String,
turn: Arc<TurnContext>,
call_id: String,
arguments: Option<Value>,
) -> Result<ToolOutput, FunctionCallError> {
@@ -461,7 +467,7 @@ async fn handle_read_resource(
arguments: arguments.clone(),
};
emit_tool_call_begin(&session, &sub_id, &call_id, invocation.clone()).await;
emit_tool_call_begin(&session, turn.as_ref(), &call_id, invocation.clone()).await;
let start = Instant::now();
let payload_result: Result<ReadResourcePayload, FunctionCallError> = async {
@@ -489,7 +495,7 @@ async fn handle_read_resource(
let duration = start.elapsed();
emit_tool_call_end(
&session,
&sub_id,
turn.as_ref(),
&call_id,
invocation,
duration,
@@ -503,7 +509,7 @@ async fn handle_read_resource(
let message = err.to_string();
emit_tool_call_end(
&session,
&sub_id,
turn.as_ref(),
&call_id,
invocation,
duration,
@@ -518,7 +524,7 @@ async fn handle_read_resource(
let message = err.to_string();
emit_tool_call_end(
&session,
&sub_id,
turn.as_ref(),
&call_id,
invocation,
duration,
@@ -544,39 +550,39 @@ fn call_tool_result_from_content(content: &str, success: Option<bool>) -> CallTo
async fn emit_tool_call_begin(
session: &Arc<Session>,
sub_id: &str,
turn: &TurnContext,
call_id: &str,
invocation: McpInvocation,
) {
session
.send_event(Event {
id: sub_id.to_string(),
msg: EventMsg::McpToolCallBegin(McpToolCallBeginEvent {
.send_event(
turn,
EventMsg::McpToolCallBegin(McpToolCallBeginEvent {
call_id: call_id.to_string(),
invocation,
}),
})
)
.await;
}
async fn emit_tool_call_end(
session: &Arc<Session>,
sub_id: &str,
turn: &TurnContext,
call_id: &str,
invocation: McpInvocation,
duration: Duration,
result: Result<CallToolResult, String>,
) {
session
.send_event(Event {
id: sub_id.to_string(),
msg: EventMsg::McpToolCallEnd(McpToolCallEndEvent {
.send_event(
turn,
EventMsg::McpToolCallEnd(McpToolCallEndEvent {
call_id: call_id.to_string(),
invocation,
duration,
result,
}),
})
)
.await;
}

View File

@@ -1,6 +1,7 @@
use crate::client_common::tools::ResponsesApiTool;
use crate::client_common::tools::ToolSpec;
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::function_tool::FunctionCallError;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
@@ -10,7 +11,6 @@ use crate::tools::registry::ToolKind;
use crate::tools::spec::JsonSchema;
use async_trait::async_trait;
use codex_protocol::plan_tool::UpdatePlanArgs;
use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use std::collections::BTreeMap;
use std::sync::LazyLock;
@@ -68,7 +68,7 @@ impl ToolHandler for PlanHandler {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
let ToolInvocation {
session,
sub_id,
turn,
call_id,
payload,
..
@@ -84,7 +84,7 @@ impl ToolHandler for PlanHandler {
};
let content =
handle_update_plan(session.as_ref(), arguments, sub_id.clone(), call_id).await?;
handle_update_plan(session.as_ref(), turn.as_ref(), arguments, call_id).await?;
Ok(ToolOutput::Function {
content,
@@ -98,16 +98,13 @@ impl ToolHandler for PlanHandler {
/// than forcing it to come up and document a plan (TBD how that affects performance).
pub(crate) async fn handle_update_plan(
session: &Session,
turn_context: &TurnContext,
arguments: String,
sub_id: String,
_call_id: String,
) -> Result<String, FunctionCallError> {
let args = parse_update_plan_arguments(&arguments)?;
session
.send_event(Event {
id: sub_id.to_string(),
msg: EventMsg::PlanUpdate(args),
})
.send_event(turn_context, EventMsg::PlanUpdate(args))
.await;
Ok("Plan updated".to_string())
}

View File

@@ -47,7 +47,6 @@ impl ToolHandler for ShellHandler {
session,
turn,
tracker,
sub_id,
call_id,
tool_name,
payload,
@@ -68,7 +67,6 @@ impl ToolHandler for ShellHandler {
Arc::clone(&session),
Arc::clone(&turn),
Arc::clone(&tracker),
sub_id.clone(),
call_id.clone(),
)
.await?;
@@ -85,7 +83,6 @@ impl ToolHandler for ShellHandler {
Arc::clone(&session),
Arc::clone(&turn),
Arc::clone(&tracker),
sub_id.clone(),
call_id.clone(),
)
.await?;

View File

@@ -37,7 +37,6 @@ impl ToolHandler for UnifiedExecHandler {
let ToolInvocation {
session,
turn,
sub_id,
call_id,
tool_name: _tool_name,
payload,
@@ -91,7 +90,6 @@ impl ToolHandler for UnifiedExecHandler {
crate::unified_exec::UnifiedExecContext {
session: &session,
turn: turn.as_ref(),
sub_id: &sub_id,
call_id: &call_id,
session_id: parsed_session_id,
},

View File

@@ -3,7 +3,6 @@ use serde::Deserialize;
use tokio::fs;
use crate::function_tool::FunctionCallError;
use crate::protocol::Event;
use crate::protocol::EventMsg;
use crate::protocol::ViewImageToolCallEvent;
use crate::tools::context::ToolInvocation;
@@ -31,7 +30,6 @@ impl ToolHandler for ViewImageHandler {
session,
turn,
payload,
sub_id,
call_id,
..
} = invocation;
@@ -76,13 +74,13 @@ impl ToolHandler for ViewImageHandler {
})?;
session
.send_event(Event {
id: sub_id.to_string(),
msg: EventMsg::ViewImageToolCall(ViewImageToolCallEvent {
.send_event(
turn.as_ref(),
EventMsg::ViewImageToolCall(ViewImageToolCallEvent {
call_id,
path: event_path,
}),
})
)
.await;
Ok(ToolOutput::Function {

View File

@@ -61,7 +61,6 @@ pub(crate) async fn handle_container_exec_with_params(
sess: Arc<Session>,
turn_context: Arc<TurnContext>,
turn_diff_tracker: SharedTurnDiffTracker,
sub_id: String,
call_id: String,
) -> Result<String, FunctionCallError> {
let _otel_event_manager = turn_context.client.get_otel_event_manager();
@@ -78,14 +77,8 @@ pub(crate) async fn handle_container_exec_with_params(
// check if this was a patch, and apply it if so
let apply_patch_exec = match maybe_parse_apply_patch_verified(&params.command, &params.cwd) {
MaybeApplyPatchVerified::Body(changes) => {
match apply_patch::apply_patch(
sess.as_ref(),
turn_context.as_ref(),
&sub_id,
&call_id,
changes,
)
.await
match apply_patch::apply_patch(sess.as_ref(), turn_context.as_ref(), &call_id, changes)
.await
{
InternalApplyPatchInvocation::Output(item) => return item,
InternalApplyPatchInvocation::DelegateToExec(apply_patch_exec) => {
@@ -122,7 +115,7 @@ pub(crate) async fn handle_container_exec_with_params(
),
};
let event_ctx = ToolEventCtx::new(sess.as_ref(), &sub_id, &call_id, diff_opt);
let event_ctx = ToolEventCtx::new(sess.as_ref(), turn_context.as_ref(), &call_id, diff_opt);
event_emitter.emit(event_ctx, ToolEventStage::Begin).await;
// Build runtime contexts only when needed (shell/apply_patch below).
@@ -141,7 +134,7 @@ pub(crate) async fn handle_container_exec_with_params(
let mut runtime = ApplyPatchRuntime::new();
let tool_ctx = ToolCtx {
session: sess.as_ref(),
sub_id: sub_id.clone(),
turn: turn_context.as_ref(),
call_id: call_id.clone(),
tool_name: tool_name.to_string(),
};
@@ -172,7 +165,7 @@ pub(crate) async fn handle_container_exec_with_params(
let mut runtime = ShellRuntime::new();
let tool_ctx = ToolCtx {
session: sess.as_ref(),
sub_id: sub_id.clone(),
turn: turn_context.as_ref(),
call_id: call_id.clone(),
tool_name: tool_name.to_string(),
};

View File

@@ -53,7 +53,7 @@ impl ToolOrchestrator {
if needs_initial_approval {
let approval_ctx = ApprovalCtx {
session: tool_ctx.session,
sub_id: &tool_ctx.sub_id,
turn: turn_ctx,
call_id: &tool_ctx.call_id,
retry_reason: None,
};
@@ -110,7 +110,7 @@ impl ToolOrchestrator {
let reason_msg = build_denial_reason_from_output(output.as_ref());
let approval_ctx = ApprovalCtx {
session: tool_ctx.session,
sub_id: &tool_ctx.sub_id,
turn: turn_ctx,
call_id: &tool_ctx.call_id,
retry_reason: Some(reason_msg),
};

View File

@@ -18,7 +18,6 @@ pub(crate) struct ToolCallRuntime {
session: Arc<Session>,
turn_context: Arc<TurnContext>,
tracker: SharedTurnDiffTracker,
sub_id: String,
parallel_execution: Arc<RwLock<()>>,
}
@@ -28,14 +27,12 @@ impl ToolCallRuntime {
session: Arc<Session>,
turn_context: Arc<TurnContext>,
tracker: SharedTurnDiffTracker,
sub_id: String,
) -> Self {
Self {
router,
session,
turn_context,
tracker,
sub_id,
parallel_execution: Arc::new(RwLock::new(())),
}
}
@@ -50,7 +47,6 @@ impl ToolCallRuntime {
let session = Arc::clone(&self.session);
let turn = Arc::clone(&self.turn_context);
let tracker = Arc::clone(&self.tracker);
let sub_id = self.sub_id.clone();
let lock = Arc::clone(&self.parallel_execution);
let handle: AbortOnDropHandle<Result<ResponseInputItem, FunctionCallError>> =
@@ -62,7 +58,7 @@ impl ToolCallRuntime {
};
router
.dispatch_tool_call(session, turn, tracker, sub_id, call)
.dispatch_tool_call(session, turn, tracker, call)
.await
}));

View File

@@ -134,7 +134,6 @@ impl ToolRouter {
session: Arc<Session>,
turn: Arc<TurnContext>,
tracker: SharedTurnDiffTracker,
sub_id: String,
call: ToolCall,
) -> Result<ResponseInputItem, FunctionCallError> {
let ToolCall {
@@ -149,7 +148,6 @@ impl ToolRouter {
session,
turn,
tracker,
sub_id,
call_id,
tool_name,
payload,

View File

@@ -68,7 +68,7 @@ impl ApplyPatchRuntime {
fn stdout_stream(ctx: &ToolCtx<'_>) -> Option<crate::exec::StdoutStream> {
Some(crate::exec::StdoutStream {
sub_id: ctx.sub_id.clone(),
sub_id: ctx.turn.sub_id.clone(),
call_id: ctx.call_id.clone(),
tx_event: ctx.session.get_tx_event(),
})
@@ -101,7 +101,7 @@ impl Approvable<ApplyPatchRequest> for ApplyPatchRuntime {
) -> BoxFuture<'a, ReviewDecision> {
let key = self.approval_key(req);
let session = ctx.session;
let sub_id = ctx.sub_id.to_string();
let turn = ctx.turn;
let call_id = ctx.call_id.to_string();
let cwd = req.cwd.clone();
let retry_reason = ctx.retry_reason.clone();
@@ -111,7 +111,7 @@ impl Approvable<ApplyPatchRequest> for ApplyPatchRuntime {
if let Some(reason) = retry_reason {
session
.request_command_approval(
sub_id,
turn,
call_id,
vec!["apply_patch".to_string()],
cwd,

View File

@@ -51,7 +51,7 @@ impl ShellRuntime {
fn stdout_stream(ctx: &ToolCtx<'_>) -> Option<crate::exec::StdoutStream> {
Some(crate::exec::StdoutStream {
sub_id: ctx.sub_id.clone(),
sub_id: ctx.turn.sub_id.clone(),
call_id: ctx.call_id.clone(),
tx_event: ctx.session.get_tx_event(),
})
@@ -91,12 +91,12 @@ impl Approvable<ShellRequest> for ShellRuntime {
.clone()
.or_else(|| req.justification.clone());
let session = ctx.session;
let sub_id = ctx.sub_id.to_string();
let turn = ctx.turn;
let call_id = ctx.call_id.to_string();
Box::pin(async move {
with_cached_approval(&session.services, key, || async move {
session
.request_command_approval(sub_id, call_id, command, cwd, reason)
.request_command_approval(turn, call_id, command, cwd, reason)
.await
})
.await

View File

@@ -80,7 +80,7 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
) -> BoxFuture<'b, ReviewDecision> {
let key = self.approval_key(req);
let session = ctx.session;
let sub_id = ctx.sub_id.to_string();
let turn = ctx.turn;
let call_id = ctx.call_id.to_string();
let command = req.command.clone();
let cwd = req.cwd.clone();
@@ -88,7 +88,7 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
Box::pin(async move {
with_cached_approval(&session.services, key, || async move {
session
.request_command_approval(sub_id, call_id, command, cwd, reason)
.request_command_approval(turn, call_id, command, cwd, reason)
.await
})
.await

View File

@@ -5,6 +5,7 @@
//! and helpers (`Sandboxable`, `ToolRuntime`, `SandboxAttempt`, etc.).
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::error::CodexErr;
use crate::protocol::SandboxPolicy;
use crate::sandboxing::CommandSpec;
@@ -77,7 +78,7 @@ where
#[derive(Clone)]
pub(crate) struct ApprovalCtx<'a> {
pub session: &'a Session,
pub sub_id: &'a str,
pub turn: &'a TurnContext,
pub call_id: &'a str,
pub retry_reason: Option<String>,
}
@@ -145,7 +146,7 @@ pub(crate) trait Sandboxable {
pub(crate) struct ToolCtx<'a> {
pub session: &'a Session,
pub sub_id: String,
pub turn: &'a TurnContext,
pub call_id: String,
pub tool_name: String,
}