Files
codex/codex-rs/tools/src/tool_call.rs
T
Won Park 61f5a84930 Let image generation extension hosts control output persistence (#29711)
## Why

Some extension hosts need generated images returned without writing them
to the local filesystem or giving the model a local path.

## What changed

**tl;dr**: we now conduct all extension operations in the image gen
extension

- Let hosts provide an optional image save root when installing the
extension.
- Save images and return path hints only when a save root is configured.
- Return image data without saving or adding a path hint when no save
root is configured.
- Preserve the extension-provided `saved_path` instead of persisting
extension images again in core.
- Leave built-in image generation unchanged.

## Validation

- `just test -p codex-image-generation-extension`
- `just test -p codex-app-server
standalone_image_generation_returns_saved_path_hint_to_model`
- `just test -p codex-core
extension_tool_uses_granted_turn_permissions_without_local_persistence`
- `just test -p codex-core tools::handlers::extension_tools::tests`
- tested on CODEX CLI on both save_root: CODEX_HOME and None 
- tested on CODEX APP on both as well
2026-06-23 18:51:49 -07:00

122 lines
4.1 KiB
Rust

use crate::FunctionCallError;
use crate::ToolName;
use crate::ToolPayload;
use codex_file_system::ExecutorFileSystem;
use codex_file_system::FileSystemSandboxContext;
use codex_protocol::items::ImageGenerationItem;
use codex_protocol::items::WebSearchItem;
use codex_protocol::models::ResponseItem;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_output_truncation::TruncationPolicy;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
/// Raw response history snapshot available when an extension tool is invoked.
#[derive(Clone, Debug, Default)]
pub struct ConversationHistory {
items: Arc<[ResponseItem]>,
}
impl ConversationHistory {
pub fn new(items: Vec<ResponseItem>) -> Self {
Self {
items: items.into(),
}
}
pub fn items(&self) -> &[ResponseItem] {
&self.items
}
}
/// Future returned when an extension tool emits a visible turn-item lifecycle event.
pub type TurnItemEmissionFuture<'a> = Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
/// Visible turn items that an extension may publish into the host lifecycle.
#[derive(Clone, Debug, PartialEq)]
pub enum ExtensionTurnItem {
WebSearch(WebSearchItem),
ImageGeneration(ImageGenerationItem),
}
/// Host-provided capability for extension tools to emit visible turn items.
///
/// Implementations route lifecycle events through the host's normal item event
/// pipeline and client delivery.
pub trait TurnItemEmitter: Send + Sync {
/// Emits the beginning of one visible turn item.
fn emit_started<'a>(&'a self, item: ExtensionTurnItem) -> TurnItemEmissionFuture<'a>;
/// Emits one completed visible turn item.
fn emit_completed<'a>(&'a self, item: ExtensionTurnItem) -> TurnItemEmissionFuture<'a>;
}
/// Host-owned turn environment summary visible to extension tools.
#[derive(Clone)]
pub struct ToolEnvironment {
/// Stable host environment id used to route executor-scoped capabilities.
pub environment_id: String,
/// Effective working directory for this turn in the environment.
pub cwd: AbsolutePathBuf,
/// Filesystem implementation for this environment.
pub file_system: Arc<dyn ExecutorFileSystem>,
/// Sandbox context to use for filesystem operations.
pub file_system_sandbox_context: FileSystemSandboxContext,
}
/// Turn-item emitter used when a caller does not expose visible item emission.
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopTurnItemEmitter;
impl TurnItemEmitter for NoopTurnItemEmitter {
fn emit_started<'a>(&'a self, _item: ExtensionTurnItem) -> TurnItemEmissionFuture<'a> {
Box::pin(std::future::ready(()))
}
fn emit_completed<'a>(&'a self, _item: ExtensionTurnItem) -> TurnItemEmissionFuture<'a> {
Box::pin(std::future::ready(()))
}
}
#[derive(Clone)]
pub struct ToolCall {
pub turn_id: String,
pub call_id: String,
pub tool_name: ToolName,
pub model: String,
pub truncation_policy: TruncationPolicy,
pub conversation_history: ConversationHistory,
pub turn_item_emitter: Arc<dyn TurnItemEmitter>,
pub environments: Vec<ToolEnvironment>,
pub payload: ToolPayload,
}
impl std::fmt::Debug for ToolCall {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ToolCall")
.field("turn_id", &self.turn_id)
.field("call_id", &self.call_id)
.field("tool_name", &self.tool_name)
.field("model", &self.model)
.field("truncation_policy", &self.truncation_policy)
.field("conversation_history", &self.conversation_history)
.field("turn_item_emitter", &"<host turn item emitter>")
.field("environment_count", &self.environments.len())
.field("payload", &self.payload)
.finish()
}
}
impl ToolCall {
pub fn function_arguments(&self) -> Result<&str, FunctionCallError> {
match &self.payload {
ToolPayload::Function { arguments } => Ok(arguments),
_ => Err(FunctionCallError::Fatal(format!(
"tool {} invoked with incompatible payload",
self.tool_name
))),
}
}
}