mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat: external artifacts builder (#13485)
This PR reverts the built-in artifact render while a decision is being reached. No impact expected on any features
This commit is contained in:
@@ -55,12 +55,6 @@ use async_channel::Receiver;
|
||||
use async_channel::Sender;
|
||||
use chrono::Local;
|
||||
use chrono::Utc;
|
||||
use codex_artifact_presentation::PresentationArtifactError;
|
||||
use codex_artifact_presentation::PresentationArtifactExecutionRequest;
|
||||
use codex_artifact_presentation::PresentationArtifactResponse;
|
||||
use codex_artifact_spreadsheet::SpreadsheetArtifactError;
|
||||
use codex_artifact_spreadsheet::SpreadsheetArtifactRequest;
|
||||
use codex_artifact_spreadsheet::SpreadsheetArtifactResponse;
|
||||
use codex_hooks::HookEvent;
|
||||
use codex_hooks::HookEventAfterAgent;
|
||||
use codex_hooks::HookPayload;
|
||||
@@ -1798,24 +1792,6 @@ impl Session {
|
||||
state.clear_connector_selection();
|
||||
}
|
||||
|
||||
pub(crate) async fn execute_presentation_artifact(
|
||||
&self,
|
||||
request: PresentationArtifactExecutionRequest,
|
||||
cwd: &Path,
|
||||
) -> Result<PresentationArtifactResponse, PresentationArtifactError> {
|
||||
let mut state = self.state.lock().await;
|
||||
state.artifacts.presentation.execute_requests(request, cwd)
|
||||
}
|
||||
|
||||
pub(crate) async fn execute_spreadsheet_artifact(
|
||||
&self,
|
||||
request: SpreadsheetArtifactRequest,
|
||||
cwd: &Path,
|
||||
) -> Result<SpreadsheetArtifactResponse, SpreadsheetArtifactError> {
|
||||
let mut state = self.state.lock().await;
|
||||
state.artifacts.spreadsheet.execute(request, cwd)
|
||||
}
|
||||
|
||||
async fn record_initial_history(&self, conversation_history: InitialHistory) {
|
||||
let turn_context = self.new_default_turn().await;
|
||||
self.clear_mcp_tool_selection().await;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
//! Session-wide mutable state.
|
||||
|
||||
use codex_artifact_presentation::PresentationArtifactManager;
|
||||
use codex_artifact_spreadsheet::SpreadsheetArtifactManager;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
@@ -18,12 +16,6 @@ use crate::tasks::RegularTask;
|
||||
use crate::truncate::TruncationPolicy;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct SessionArtifacts {
|
||||
pub(crate) presentation: PresentationArtifactManager,
|
||||
pub(crate) spreadsheet: SpreadsheetArtifactManager,
|
||||
}
|
||||
|
||||
/// Persistent, session-scoped state previously stored directly on `Session`.
|
||||
pub(crate) struct SessionState {
|
||||
pub(crate) session_configuration: SessionConfiguration,
|
||||
@@ -40,7 +32,6 @@ pub(crate) struct SessionState {
|
||||
pub(crate) startup_regular_task: Option<JoinHandle<CodexResult<RegularTask>>>,
|
||||
pub(crate) active_mcp_tool_selection: Option<Vec<String>>,
|
||||
pub(crate) active_connector_selection: HashSet<String>,
|
||||
pub(crate) artifacts: SessionArtifacts,
|
||||
}
|
||||
|
||||
impl SessionState {
|
||||
@@ -58,7 +49,6 @@ impl SessionState {
|
||||
startup_regular_task: None,
|
||||
active_mcp_tool_selection: None,
|
||||
active_connector_selection: HashSet::new(),
|
||||
artifacts: SessionArtifacts::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,412 @@
|
||||
use async_trait::async_trait;
|
||||
use codex_artifacts::ArtifactBuildRequest;
|
||||
use codex_artifacts::ArtifactCommandOutput;
|
||||
use codex_artifacts::ArtifactRuntimeError;
|
||||
use codex_artifacts::ArtifactRuntimePlatform;
|
||||
use codex_artifacts::ArtifactsClient;
|
||||
use codex_artifacts::ArtifactsError;
|
||||
use codex_artifacts::InstalledArtifactRuntime;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::exec::ExecToolCallOutput;
|
||||
use crate::exec::StreamOutput;
|
||||
use crate::features::Feature;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::protocol::ExecCommandSource;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolOutput;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::events::ToolEmitter;
|
||||
use crate::tools::events::ToolEventCtx;
|
||||
use crate::tools::events::ToolEventFailure;
|
||||
use crate::tools::events::ToolEventStage;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
|
||||
const ARTIFACTS_TOOL_NAME: &str = "artifacts";
|
||||
const ARTIFACTS_PRAGMA_PREFIXES: [&str; 2] = ["// codex-artifacts:", "// codex-artifact-tool:"];
|
||||
const PINNED_ARTIFACT_RUNTIME_VERSION: &str = "2.4.0";
|
||||
const DEFAULT_EXECUTION_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
pub struct ArtifactsHandler;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct ArtifactsToolArgs {
|
||||
source: String,
|
||||
timeout_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ToolHandler for ArtifactsHandler {
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
fn matches_kind(&self, payload: &ToolPayload) -> bool {
|
||||
matches!(payload, ToolPayload::Custom { .. })
|
||||
}
|
||||
|
||||
async fn is_mutating(&self, _invocation: &ToolInvocation) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
|
||||
let ToolInvocation {
|
||||
session,
|
||||
turn,
|
||||
payload,
|
||||
call_id,
|
||||
..
|
||||
} = invocation;
|
||||
|
||||
if !session.enabled(Feature::Artifact) {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"artifacts is disabled by feature flag".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let args = match payload {
|
||||
ToolPayload::Custom { input } => parse_freeform_args(&input)?,
|
||||
_ => {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"artifacts expects freeform JavaScript input authored against the preloaded @oai/artifact-tool surface".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let runtime = resolve_preinstalled_runtime(&turn.config.codex_home)
|
||||
.await
|
||||
.map_err(artifacts_error)?;
|
||||
let client = ArtifactsClient::from_installed_runtime(runtime);
|
||||
|
||||
let started_at = Instant::now();
|
||||
emit_exec_begin(session.as_ref(), turn.as_ref(), &call_id).await;
|
||||
|
||||
let result = client
|
||||
.execute_build(ArtifactBuildRequest {
|
||||
source: args.source,
|
||||
cwd: turn.cwd.clone(),
|
||||
timeout: Some(Duration::from_millis(
|
||||
args.timeout_ms
|
||||
.unwrap_or(DEFAULT_EXECUTION_TIMEOUT.as_millis() as u64),
|
||||
)),
|
||||
env: Default::default(),
|
||||
})
|
||||
.await;
|
||||
|
||||
let (success, output) = match result {
|
||||
Ok(output) => (output.success(), output),
|
||||
Err(error) => (false, error_output(&error)),
|
||||
};
|
||||
|
||||
emit_exec_end(
|
||||
session.as_ref(),
|
||||
turn.as_ref(),
|
||||
&call_id,
|
||||
&output,
|
||||
started_at.elapsed(),
|
||||
success,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(ToolOutput::Function {
|
||||
body: FunctionCallOutputBody::Text(format_artifact_output(&output)),
|
||||
success: Some(success),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_preinstalled_runtime(
|
||||
codex_home: &Path,
|
||||
) -> Result<InstalledArtifactRuntime, ArtifactsError> {
|
||||
let platform = ArtifactRuntimePlatform::detect_current()
|
||||
.map_err(ArtifactRuntimeError::from)
|
||||
.map_err(ArtifactsError::Runtime)?;
|
||||
let install_dir = codex_home
|
||||
.join("packages")
|
||||
.join("artifacts")
|
||||
.join(PINNED_ARTIFACT_RUNTIME_VERSION)
|
||||
.join(platform.as_str());
|
||||
if !install_dir.exists() {
|
||||
return Err(ArtifactsError::Io {
|
||||
context: format!(
|
||||
"artifact runtime {} is not installed at {}",
|
||||
PINNED_ARTIFACT_RUNTIME_VERSION,
|
||||
install_dir.display()
|
||||
),
|
||||
source: std::io::Error::new(std::io::ErrorKind::NotFound, "missing artifact runtime"),
|
||||
});
|
||||
}
|
||||
|
||||
InstalledArtifactRuntime::load(install_dir, platform).map_err(ArtifactsError::Runtime)
|
||||
}
|
||||
|
||||
fn parse_freeform_args(input: &str) -> Result<ArtifactsToolArgs, FunctionCallError> {
|
||||
if input.trim().is_empty() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"artifacts expects raw JavaScript source text (non-empty) authored against the preloaded @oai/artifact-tool surface. Provide JS only, optionally with first-line `// codex-artifacts: timeout_ms=15000` or `// codex-artifact-tool: timeout_ms=15000`."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut args = ArtifactsToolArgs {
|
||||
source: input.to_string(),
|
||||
timeout_ms: None,
|
||||
};
|
||||
|
||||
let mut lines = input.splitn(2, '\n');
|
||||
let first_line = lines.next().unwrap_or_default();
|
||||
let rest = lines.next().unwrap_or_default();
|
||||
let trimmed = first_line.trim_start();
|
||||
let Some(pragma) = parse_pragma_prefix(trimmed) else {
|
||||
reject_json_or_quoted_source(&args.source)?;
|
||||
return Ok(args);
|
||||
};
|
||||
|
||||
let mut timeout_ms = None;
|
||||
let directive = pragma.trim();
|
||||
if !directive.is_empty() {
|
||||
for token in directive.split_whitespace() {
|
||||
let (key, value) = token.split_once('=').ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"artifacts pragma expects space-separated key=value pairs (supported keys: timeout_ms); got `{token}`"
|
||||
))
|
||||
})?;
|
||||
match key {
|
||||
"timeout_ms" => {
|
||||
if timeout_ms.is_some() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"artifacts pragma specifies timeout_ms more than once".to_string(),
|
||||
));
|
||||
}
|
||||
let parsed = value.parse::<u64>().map_err(|_| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"artifacts pragma timeout_ms must be an integer; got `{value}`"
|
||||
))
|
||||
})?;
|
||||
timeout_ms = Some(parsed);
|
||||
}
|
||||
_ => {
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"artifacts pragma only supports timeout_ms; got `{key}`"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if rest.trim().is_empty() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"artifacts pragma must be followed by JavaScript source on subsequent lines"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
reject_json_or_quoted_source(rest)?;
|
||||
args.source = rest.to_string();
|
||||
args.timeout_ms = timeout_ms;
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
fn reject_json_or_quoted_source(code: &str) -> Result<(), FunctionCallError> {
|
||||
let trimmed = code.trim();
|
||||
if trimmed.starts_with("```") {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"artifacts expects raw JavaScript source, not markdown code fences. Resend plain JS only (optional first line `// codex-artifacts: ...` or `// codex-artifact-tool: ...`)."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
let Ok(value) = serde_json::from_str::<JsonValue>(trimmed) else {
|
||||
return Ok(());
|
||||
};
|
||||
match value {
|
||||
JsonValue::Object(_) | JsonValue::String(_) => Err(FunctionCallError::RespondToModel(
|
||||
"artifacts is a freeform tool and expects raw JavaScript source authored against the preloaded @oai/artifact-tool surface. Resend plain JS only (optional first line `// codex-artifacts: ...` or `// codex-artifact-tool: ...`); do not send JSON (`{\"code\":...}`), quoted code, or markdown fences."
|
||||
.to_string(),
|
||||
)),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_pragma_prefix(line: &str) -> Option<&str> {
|
||||
ARTIFACTS_PRAGMA_PREFIXES
|
||||
.iter()
|
||||
.find_map(|prefix| line.strip_prefix(prefix))
|
||||
}
|
||||
|
||||
fn artifacts_error(error: ArtifactsError) -> FunctionCallError {
|
||||
FunctionCallError::RespondToModel(error.to_string())
|
||||
}
|
||||
|
||||
async fn emit_exec_begin(session: &Session, turn: &TurnContext, call_id: &str) {
|
||||
let emitter = ToolEmitter::shell(
|
||||
vec![ARTIFACTS_TOOL_NAME.to_string()],
|
||||
turn.cwd.clone(),
|
||||
ExecCommandSource::Agent,
|
||||
true,
|
||||
);
|
||||
let ctx = ToolEventCtx::new(session, turn, call_id, None);
|
||||
emitter.emit(ctx, ToolEventStage::Begin).await;
|
||||
}
|
||||
|
||||
async fn emit_exec_end(
|
||||
session: &Session,
|
||||
turn: &TurnContext,
|
||||
call_id: &str,
|
||||
output: &ArtifactCommandOutput,
|
||||
duration: Duration,
|
||||
success: bool,
|
||||
) {
|
||||
let exec_output = ExecToolCallOutput {
|
||||
exit_code: output.exit_code.unwrap_or(1),
|
||||
stdout: StreamOutput::new(output.stdout.clone()),
|
||||
stderr: StreamOutput::new(output.stderr.clone()),
|
||||
aggregated_output: StreamOutput::new(format_artifact_output(output)),
|
||||
duration,
|
||||
timed_out: false,
|
||||
};
|
||||
let emitter = ToolEmitter::shell(
|
||||
vec![ARTIFACTS_TOOL_NAME.to_string()],
|
||||
turn.cwd.clone(),
|
||||
ExecCommandSource::Agent,
|
||||
true,
|
||||
);
|
||||
let ctx = ToolEventCtx::new(session, turn, call_id, None);
|
||||
let stage = if success {
|
||||
ToolEventStage::Success(exec_output)
|
||||
} else {
|
||||
ToolEventStage::Failure(ToolEventFailure::Output(exec_output))
|
||||
};
|
||||
emitter.emit(ctx, stage).await;
|
||||
}
|
||||
|
||||
fn format_artifact_output(output: &ArtifactCommandOutput) -> String {
|
||||
let stdout = output.stdout.trim();
|
||||
let stderr = output.stderr.trim();
|
||||
let mut sections = vec![format!(
|
||||
"exit_code: {}",
|
||||
output
|
||||
.exit_code
|
||||
.map(|code| code.to_string())
|
||||
.unwrap_or_else(|| "null".to_string())
|
||||
)];
|
||||
if !stdout.is_empty() {
|
||||
sections.push(format!("stdout:\n{stdout}"));
|
||||
}
|
||||
if !stderr.is_empty() {
|
||||
sections.push(format!("stderr:\n{stderr}"));
|
||||
}
|
||||
if stdout.is_empty() && stderr.is_empty() && output.success() {
|
||||
sections.push("artifact JS completed successfully.".to_string());
|
||||
}
|
||||
sections.join("\n\n")
|
||||
}
|
||||
|
||||
fn error_output(error: &ArtifactsError) -> ArtifactCommandOutput {
|
||||
ArtifactCommandOutput {
|
||||
exit_code: Some(1),
|
||||
stdout: String::new(),
|
||||
stderr: error.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_artifacts::RuntimeEntrypoints;
|
||||
use codex_artifacts::RuntimePathEntry;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn parse_freeform_args_without_pragma() {
|
||||
let args = parse_freeform_args("console.log('ok');").expect("parse args");
|
||||
assert_eq!(args.source, "console.log('ok');");
|
||||
assert_eq!(args.timeout_ms, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_freeform_args_with_pragma() {
|
||||
let args = parse_freeform_args("// codex-artifacts: timeout_ms=45000\nconsole.log('ok');")
|
||||
.expect("parse args");
|
||||
assert_eq!(args.source, "console.log('ok');");
|
||||
assert_eq!(args.timeout_ms, Some(45_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_freeform_args_with_artifact_tool_pragma() {
|
||||
let args =
|
||||
parse_freeform_args("// codex-artifact-tool: timeout_ms=45000\nconsole.log('ok');")
|
||||
.expect("parse args");
|
||||
assert_eq!(args.source, "console.log('ok');");
|
||||
assert_eq!(args.timeout_ms, Some(45_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_freeform_args_rejects_json_wrapped_code() {
|
||||
let err =
|
||||
parse_freeform_args("{\"code\":\"console.log('ok')\"}").expect_err("expected error");
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("artifacts is a freeform tool and expects raw JavaScript source")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_preinstalled_runtime_reads_pinned_cache_path() {
|
||||
let codex_home = TempDir::new().expect("create temp codex home");
|
||||
let platform = ArtifactRuntimePlatform::detect_current().expect("detect platform");
|
||||
let install_dir = codex_home
|
||||
.path()
|
||||
.join("packages")
|
||||
.join("artifacts")
|
||||
.join(PINNED_ARTIFACT_RUNTIME_VERSION)
|
||||
.join(platform.as_str());
|
||||
std::fs::create_dir_all(&install_dir).expect("create install dir");
|
||||
std::fs::write(
|
||||
install_dir.join("manifest.json"),
|
||||
serde_json::json!({
|
||||
"schema_version": 1,
|
||||
"runtime_version": PINNED_ARTIFACT_RUNTIME_VERSION,
|
||||
"node": { "relative_path": "node/bin/node" },
|
||||
"entrypoints": {
|
||||
"build_js": { "relative_path": "artifact-tool/dist/artifact_tool.mjs" },
|
||||
"render_cli": { "relative_path": "granola-render/dist/render_cli.mjs" }
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.expect("write manifest");
|
||||
|
||||
let runtime = resolve_preinstalled_runtime(codex_home.path())
|
||||
.await
|
||||
.expect("resolve runtime");
|
||||
assert_eq!(runtime.runtime_version(), PINNED_ARTIFACT_RUNTIME_VERSION);
|
||||
assert_eq!(
|
||||
runtime.manifest().entrypoints,
|
||||
RuntimeEntrypoints {
|
||||
build_js: RuntimePathEntry {
|
||||
relative_path: "artifact-tool/dist/artifact_tool.mjs".to_string(),
|
||||
},
|
||||
render_cli: RuntimePathEntry {
|
||||
relative_path: "granola-render/dist/render_cli.mjs".to_string(),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_artifact_output_includes_success_message_when_silent() {
|
||||
let formatted = format_artifact_output(&ArtifactCommandOutput {
|
||||
exit_code: Some(0),
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
});
|
||||
assert!(formatted.contains("artifact JS completed successfully."));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub(crate) mod agent_jobs;
|
||||
pub mod apply_patch;
|
||||
mod artifacts;
|
||||
mod dynamic;
|
||||
mod grep_files;
|
||||
mod js_repl;
|
||||
@@ -8,12 +9,10 @@ mod mcp;
|
||||
mod mcp_resource;
|
||||
pub(crate) mod multi_agents;
|
||||
mod plan;
|
||||
mod presentation_artifact;
|
||||
mod read_file;
|
||||
mod request_user_input;
|
||||
mod search_tool_bm25;
|
||||
mod shell;
|
||||
mod spreadsheet_artifact;
|
||||
mod test_sync;
|
||||
pub(crate) mod unified_exec;
|
||||
mod view_image;
|
||||
@@ -29,6 +28,7 @@ use crate::function_tool::FunctionCallError;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::sandboxing::normalize_additional_permissions;
|
||||
pub use apply_patch::ApplyPatchHandler;
|
||||
pub use artifacts::ArtifactsHandler;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
pub use dynamic::DynamicToolHandler;
|
||||
@@ -40,7 +40,6 @@ pub use mcp::McpHandler;
|
||||
pub use mcp_resource::McpResourceHandler;
|
||||
pub use multi_agents::MultiAgentHandler;
|
||||
pub use plan::PlanHandler;
|
||||
pub use presentation_artifact::PresentationArtifactHandler;
|
||||
pub use read_file::ReadFileHandler;
|
||||
pub use request_user_input::RequestUserInputHandler;
|
||||
pub(crate) use request_user_input::request_user_input_tool_description;
|
||||
@@ -49,7 +48,6 @@ pub(crate) use search_tool_bm25::SEARCH_TOOL_BM25_TOOL_NAME;
|
||||
pub use search_tool_bm25::SearchToolBm25Handler;
|
||||
pub use shell::ShellCommandHandler;
|
||||
pub use shell::ShellHandler;
|
||||
pub use spreadsheet_artifact::SpreadsheetArtifactHandler;
|
||||
pub use test_sync::TestSyncHandler;
|
||||
pub use unified_exec::UnifiedExecHandler;
|
||||
pub use view_image::ViewImageHandler;
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use codex_artifact_presentation::PathAccessKind;
|
||||
use codex_artifact_presentation::PathAccessRequirement;
|
||||
use codex_artifact_presentation::PresentationArtifactError;
|
||||
use codex_artifact_presentation::PresentationArtifactToolRequest;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use serde_json::to_string;
|
||||
use std::path::Component;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::features::Feature;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::path_utils::normalize_for_path_comparison;
|
||||
use crate::path_utils::resolve_symlink_write_paths;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolOutput;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use crate::tools::sandboxing::with_cached_approval;
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
|
||||
pub struct PresentationArtifactHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl ToolHandler for PresentationArtifactHandler {
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
async fn is_mutating(&self, invocation: &ToolInvocation) -> bool {
|
||||
let ToolPayload::Function { arguments } = &invocation.payload else {
|
||||
return true;
|
||||
};
|
||||
let Ok(request) = parse_arguments::<PresentationArtifactToolRequest>(arguments) else {
|
||||
return true;
|
||||
};
|
||||
request.is_mutating().unwrap_or(true)
|
||||
}
|
||||
|
||||
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
|
||||
let ToolInvocation {
|
||||
session,
|
||||
turn,
|
||||
payload,
|
||||
call_id,
|
||||
..
|
||||
} = invocation;
|
||||
|
||||
if !session.enabled(Feature::Artifact) {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"presentation_artifact is disabled by feature flag".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let arguments = match payload {
|
||||
ToolPayload::Function { arguments } => arguments,
|
||||
_ => {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"presentation_artifact handler received unsupported payload".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let request: PresentationArtifactToolRequest = parse_arguments(&arguments)?;
|
||||
for access in request
|
||||
.required_path_accesses(&turn.cwd)
|
||||
.map_err(presentation_error)?
|
||||
{
|
||||
authorize_path_access(session.as_ref(), turn.as_ref(), &call_id, &access).await?;
|
||||
}
|
||||
|
||||
let response = session
|
||||
.execute_presentation_artifact(
|
||||
request
|
||||
.into_execution_request()
|
||||
.map_err(presentation_error)?,
|
||||
&turn.cwd,
|
||||
)
|
||||
.await
|
||||
.map_err(presentation_error)?;
|
||||
|
||||
Ok(ToolOutput::Function {
|
||||
body: FunctionCallOutputBody::Text(to_string(&response).map_err(|error| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"failed to serialize presentation_artifact response: {error}"
|
||||
))
|
||||
})?),
|
||||
success: Some(true),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn presentation_error(error: PresentationArtifactError) -> FunctionCallError {
|
||||
FunctionCallError::RespondToModel(error.to_string())
|
||||
}
|
||||
|
||||
async fn authorize_path_access(
|
||||
session: &Session,
|
||||
turn: &TurnContext,
|
||||
call_id: &str,
|
||||
access: &PathAccessRequirement,
|
||||
) -> Result<(), FunctionCallError> {
|
||||
let effective_path = match access.kind {
|
||||
PathAccessKind::Read => effective_read_path(&access.path),
|
||||
PathAccessKind::Write => effective_write_path(&access.path),
|
||||
};
|
||||
let allowed = match access.kind {
|
||||
PathAccessKind::Read => path_is_readable(turn, &effective_path),
|
||||
PathAccessKind::Write => path_is_writable(turn, &effective_path),
|
||||
};
|
||||
if allowed {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let approval_policy = turn.approval_policy.value();
|
||||
if !matches!(
|
||||
approval_policy,
|
||||
AskForApproval::OnRequest | AskForApproval::UnlessTrusted
|
||||
) {
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"{} path `{}` is outside the current sandbox policy",
|
||||
access_kind_label(access.kind),
|
||||
access.path.display()
|
||||
)));
|
||||
}
|
||||
|
||||
let key = format!(
|
||||
"presentation_artifact:{:?}:{}",
|
||||
access.kind,
|
||||
effective_path.display()
|
||||
);
|
||||
let path = access.path.clone();
|
||||
let action = access.action.clone();
|
||||
let decision = with_cached_approval(
|
||||
&session.services,
|
||||
"presentation_artifact",
|
||||
vec![key],
|
||||
|| {
|
||||
let path = path.clone();
|
||||
let action = action.clone();
|
||||
async move {
|
||||
session
|
||||
.request_command_approval(
|
||||
turn,
|
||||
call_id.to_string(),
|
||||
None,
|
||||
vec![
|
||||
"presentation_artifact".to_string(),
|
||||
action,
|
||||
path.display().to_string(),
|
||||
],
|
||||
turn.cwd.clone(),
|
||||
Some(format!(
|
||||
"Allow presentation_artifact to {} `{}`?",
|
||||
access_kind_verb(access.kind),
|
||||
path.display()
|
||||
)),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
if matches!(
|
||||
decision,
|
||||
ReviewDecision::Approved
|
||||
| ReviewDecision::ApprovedForSession
|
||||
| ReviewDecision::ApprovedExecpolicyAmendment { .. }
|
||||
) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(FunctionCallError::RespondToModel(format!(
|
||||
"{} path `{}` was not approved",
|
||||
access_kind_label(access.kind),
|
||||
access.path.display()
|
||||
)))
|
||||
}
|
||||
|
||||
fn path_is_readable(turn: &TurnContext, path: &Path) -> bool {
|
||||
if turn.sandbox_policy.has_full_disk_read_access() {
|
||||
return true;
|
||||
}
|
||||
|
||||
turn.sandbox_policy
|
||||
.get_readable_roots_with_cwd(&turn.cwd)
|
||||
.iter()
|
||||
.any(|root| path.starts_with(root.as_path()))
|
||||
}
|
||||
|
||||
fn path_is_writable(turn: &TurnContext, path: &Path) -> bool {
|
||||
if turn.sandbox_policy.has_full_disk_write_access() {
|
||||
return true;
|
||||
}
|
||||
|
||||
turn.sandbox_policy
|
||||
.get_writable_roots_with_cwd(&turn.cwd)
|
||||
.iter()
|
||||
.any(|root| root.is_path_writable(path))
|
||||
}
|
||||
|
||||
fn effective_read_path(path: &Path) -> PathBuf {
|
||||
normalize_for_path_comparison(path).unwrap_or_else(|_| normalize_without_fs(path))
|
||||
}
|
||||
|
||||
fn effective_write_path(path: &Path) -> PathBuf {
|
||||
let write_path = resolve_symlink_write_paths(path)
|
||||
.map(|paths| paths.write_path)
|
||||
.unwrap_or_else(|_| path.to_path_buf());
|
||||
normalize_for_path_comparison(&write_path).unwrap_or_else(|_| normalize_without_fs(&write_path))
|
||||
}
|
||||
|
||||
fn normalize_without_fs(path: &Path) -> PathBuf {
|
||||
let mut normalized = PathBuf::new();
|
||||
for component in path.components() {
|
||||
match component {
|
||||
Component::ParentDir => {
|
||||
normalized.pop();
|
||||
}
|
||||
Component::CurDir => {}
|
||||
other => normalized.push(other.as_os_str()),
|
||||
}
|
||||
}
|
||||
normalized
|
||||
}
|
||||
|
||||
fn access_kind_label(kind: PathAccessKind) -> &'static str {
|
||||
match kind {
|
||||
PathAccessKind::Read => "read",
|
||||
PathAccessKind::Write => "write",
|
||||
}
|
||||
}
|
||||
|
||||
fn access_kind_verb(kind: PathAccessKind) -> &'static str {
|
||||
match kind {
|
||||
PathAccessKind::Read => "read from",
|
||||
PathAccessKind::Write => "write to",
|
||||
}
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use codex_artifact_spreadsheet::PathAccessKind;
|
||||
use codex_artifact_spreadsheet::PathAccessRequirement;
|
||||
use codex_artifact_spreadsheet::SpreadsheetArtifactError;
|
||||
use codex_artifact_spreadsheet::SpreadsheetArtifactRequest;
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use serde_json::to_string;
|
||||
use std::path::Component;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::features::Feature;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::path_utils::normalize_for_path_comparison;
|
||||
use crate::path_utils::resolve_symlink_write_paths;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolOutput;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use crate::tools::sandboxing::with_cached_approval;
|
||||
|
||||
pub struct SpreadsheetArtifactHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl ToolHandler for SpreadsheetArtifactHandler {
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
async fn is_mutating(&self, _invocation: &ToolInvocation) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
|
||||
let ToolInvocation {
|
||||
session,
|
||||
turn,
|
||||
payload,
|
||||
call_id,
|
||||
..
|
||||
} = invocation;
|
||||
|
||||
if !session.enabled(Feature::Artifact) {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"spreadsheet_artifact is disabled by feature flag".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let arguments = match payload {
|
||||
ToolPayload::Function { arguments } => arguments,
|
||||
_ => {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"spreadsheet_artifact handler received unsupported payload".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let request: SpreadsheetArtifactRequest = parse_arguments(&arguments)?;
|
||||
for access in request
|
||||
.required_path_accesses(&turn.cwd)
|
||||
.map_err(spreadsheet_error)?
|
||||
{
|
||||
authorize_path_access(session.as_ref(), turn.as_ref(), &call_id, &access).await?;
|
||||
}
|
||||
|
||||
let response = session
|
||||
.execute_spreadsheet_artifact(request, &turn.cwd)
|
||||
.await
|
||||
.map_err(spreadsheet_error)?;
|
||||
|
||||
Ok(ToolOutput::Function {
|
||||
body: FunctionCallOutputBody::Text(to_string(&response).map_err(|error| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"failed to serialize spreadsheet_artifact response: {error}"
|
||||
))
|
||||
})?),
|
||||
success: Some(true),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn spreadsheet_error(error: SpreadsheetArtifactError) -> FunctionCallError {
|
||||
FunctionCallError::RespondToModel(error.to_string())
|
||||
}
|
||||
|
||||
async fn authorize_path_access(
|
||||
session: &Session,
|
||||
turn: &TurnContext,
|
||||
call_id: &str,
|
||||
access: &PathAccessRequirement,
|
||||
) -> Result<(), FunctionCallError> {
|
||||
let effective_path = match access.kind {
|
||||
PathAccessKind::Read => effective_read_path(&access.path),
|
||||
PathAccessKind::Write => effective_write_path(&access.path),
|
||||
};
|
||||
let allowed = match access.kind {
|
||||
PathAccessKind::Read => path_is_readable(turn, &effective_path),
|
||||
PathAccessKind::Write => path_is_writable(turn, &effective_path),
|
||||
};
|
||||
if allowed {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let approval_policy = turn.approval_policy.value();
|
||||
if !matches!(
|
||||
approval_policy,
|
||||
AskForApproval::OnRequest | AskForApproval::UnlessTrusted
|
||||
) {
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"{} path `{}` is outside the current sandbox policy",
|
||||
access_kind_label(access.kind),
|
||||
access.path.display()
|
||||
)));
|
||||
}
|
||||
|
||||
let key = format!(
|
||||
"spreadsheet_artifact:{:?}:{}",
|
||||
access.kind,
|
||||
effective_path.display()
|
||||
);
|
||||
let path = access.path.clone();
|
||||
let action = access.action.clone();
|
||||
let decision =
|
||||
with_cached_approval(&session.services, "spreadsheet_artifact", vec![key], || {
|
||||
let path = path.clone();
|
||||
let action = action.clone();
|
||||
async move {
|
||||
session
|
||||
.request_command_approval(
|
||||
turn,
|
||||
call_id.to_string(),
|
||||
None,
|
||||
vec![
|
||||
"spreadsheet_artifact".to_string(),
|
||||
action,
|
||||
path.display().to_string(),
|
||||
],
|
||||
turn.cwd.clone(),
|
||||
Some(format!(
|
||||
"Allow spreadsheet_artifact to {} `{}`?",
|
||||
access_kind_verb(access.kind),
|
||||
path.display()
|
||||
)),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
if matches!(
|
||||
decision,
|
||||
ReviewDecision::Approved
|
||||
| ReviewDecision::ApprovedForSession
|
||||
| ReviewDecision::ApprovedExecpolicyAmendment { .. }
|
||||
) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(FunctionCallError::RespondToModel(format!(
|
||||
"{} path `{}` was not approved",
|
||||
access_kind_label(access.kind),
|
||||
access.path.display()
|
||||
)))
|
||||
}
|
||||
|
||||
fn path_is_readable(turn: &TurnContext, path: &Path) -> bool {
|
||||
if turn.sandbox_policy.has_full_disk_read_access() {
|
||||
return true;
|
||||
}
|
||||
|
||||
turn.sandbox_policy
|
||||
.get_readable_roots_with_cwd(&turn.cwd)
|
||||
.iter()
|
||||
.any(|root| path.starts_with(root.as_path()))
|
||||
}
|
||||
|
||||
fn path_is_writable(turn: &TurnContext, path: &Path) -> bool {
|
||||
if turn.sandbox_policy.has_full_disk_write_access() {
|
||||
return true;
|
||||
}
|
||||
|
||||
turn.sandbox_policy
|
||||
.get_writable_roots_with_cwd(&turn.cwd)
|
||||
.iter()
|
||||
.any(|root| root.is_path_writable(path))
|
||||
}
|
||||
|
||||
fn effective_read_path(path: &Path) -> PathBuf {
|
||||
normalize_for_path_comparison(path).unwrap_or_else(|_| normalize_without_fs(path))
|
||||
}
|
||||
|
||||
fn effective_write_path(path: &Path) -> PathBuf {
|
||||
let write_path = resolve_symlink_write_paths(path)
|
||||
.map(|paths| paths.write_path)
|
||||
.unwrap_or_else(|_| path.to_path_buf());
|
||||
normalize_for_path_comparison(&write_path).unwrap_or_else(|_| normalize_without_fs(&write_path))
|
||||
}
|
||||
|
||||
fn normalize_without_fs(path: &Path) -> PathBuf {
|
||||
let mut normalized = PathBuf::new();
|
||||
for component in path.components() {
|
||||
match component {
|
||||
Component::ParentDir => {
|
||||
normalized.pop();
|
||||
}
|
||||
Component::CurDir => {}
|
||||
other => normalized.push(other.as_os_str()),
|
||||
}
|
||||
}
|
||||
normalized
|
||||
}
|
||||
|
||||
fn access_kind_label(kind: PathAccessKind) -> &'static str {
|
||||
match kind {
|
||||
PathAccessKind::Read => "read",
|
||||
PathAccessKind::Write => "write",
|
||||
}
|
||||
}
|
||||
|
||||
fn access_kind_verb(kind: PathAccessKind) -> &'static str {
|
||||
match kind {
|
||||
PathAccessKind::Read => "read from",
|
||||
PathAccessKind::Write => "write to",
|
||||
}
|
||||
}
|
||||
+32
-100
@@ -575,97 +575,6 @@ fn create_view_image_tool() -> ToolSpec {
|
||||
})
|
||||
}
|
||||
|
||||
fn create_presentation_artifact_tool() -> ToolSpec {
|
||||
let action_step_schema = JsonSchema::Object {
|
||||
properties: BTreeMap::from([
|
||||
(
|
||||
"action".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some("Action name to run for this step.".to_string()),
|
||||
},
|
||||
),
|
||||
(
|
||||
"args".to_string(),
|
||||
JsonSchema::Object {
|
||||
properties: BTreeMap::new(),
|
||||
required: None,
|
||||
additional_properties: Some(true.into()),
|
||||
},
|
||||
),
|
||||
]),
|
||||
required: Some(vec!["action".to_string(), "args".to_string()]),
|
||||
additional_properties: Some(false.into()),
|
||||
};
|
||||
let properties = BTreeMap::from([
|
||||
(
|
||||
"artifact_id".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"Artifact id returned by an earlier presentation_artifact call.".to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"actions".to_string(),
|
||||
JsonSchema::Array {
|
||||
items: Box::new(action_step_schema),
|
||||
description: Some(
|
||||
"Array of `(action, args)` steps to execute sequentially.".to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "presentation_artifact".to_string(),
|
||||
description: "Create or edit a presentation artifact for the current thread.".to_string(),
|
||||
strict: false,
|
||||
parameters: JsonSchema::Object {
|
||||
properties,
|
||||
required: Some(vec!["actions".to_string()]),
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn create_spreadsheet_artifact_tool() -> ToolSpec {
|
||||
let properties = BTreeMap::from([
|
||||
(
|
||||
"artifact_id".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"Artifact id returned by an earlier spreadsheet_artifact call.".to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"action".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some("Action name to run for this request.".to_string()),
|
||||
},
|
||||
),
|
||||
(
|
||||
"args".to_string(),
|
||||
JsonSchema::Object {
|
||||
properties: BTreeMap::new(),
|
||||
required: None,
|
||||
additional_properties: Some(true.into()),
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "spreadsheet_artifact".to_string(),
|
||||
description: "Create or edit a spreadsheet artifact for the current thread.".to_string(),
|
||||
strict: false,
|
||||
parameters: JsonSchema::Object {
|
||||
properties,
|
||||
required: Some(vec!["action".to_string(), "args".to_string()]),
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn create_collab_input_items_schema() -> JsonSchema {
|
||||
let properties = BTreeMap::from([
|
||||
(
|
||||
@@ -1461,6 +1370,33 @@ JS_SOURCE: /(?:\s*)(?:[^\s{\"`]|`[^`]|``[^`])[\s\S]*/
|
||||
})
|
||||
}
|
||||
|
||||
fn create_artifacts_tool() -> ToolSpec {
|
||||
const ARTIFACTS_FREEFORM_GRAMMAR: &str = r#"
|
||||
start: pragma_source | plain_source
|
||||
|
||||
pragma_source: PRAGMA_LINE NEWLINE js_source
|
||||
plain_source: PLAIN_JS_SOURCE
|
||||
|
||||
js_source: JS_SOURCE
|
||||
|
||||
PRAGMA_LINE: /[ \t]*\/\/ codex-artifacts:[^\r\n]*/ | /[ \t]*\/\/ codex-artifact-tool:[^\r\n]*/
|
||||
NEWLINE: /\r?\n/
|
||||
PLAIN_JS_SOURCE: /(?:\s*)(?:[^\s{\"`]|`[^`]|``[^`])[\s\S]*/
|
||||
JS_SOURCE: /(?:\s*)(?:[^\s{\"`]|`[^`]|``[^`])[\s\S]*/
|
||||
"#;
|
||||
|
||||
ToolSpec::Freeform(FreeformTool {
|
||||
name: "artifacts".to_string(),
|
||||
description: "Runs raw JavaScript against the preinstalled Codex @oai/artifact-tool runtime for creating presentations or spreadsheets. This is plain JavaScript executed by Node with top-level await, not TypeScript: do not use type annotations, `interface`, `type`, or `import type`. Author code the same way you would for `import { Presentation, Workbook, PresentationFile, SpreadsheetFile, FileBlob, ... } from \"@oai/artifact-tool\"`, but omit that import line because the package surface is already preloaded. Named exports are available directly on `globalThis`, and the full module is available as `globalThis.artifactTool` (also aliased as `globalThis.artifacts` and `globalThis.codexArtifacts`). Node built-ins such as `node:fs/promises` may still be imported when needed for saving preview bytes. This is a freeform tool: send raw JavaScript source text, optionally with a first-line pragma like `// codex-artifacts: timeout_ms=15000` or `// codex-artifact-tool: timeout_ms=15000`; do not send JSON/quotes/markdown fences."
|
||||
.to_string(),
|
||||
format: FreeformToolFormat {
|
||||
r#type: "grammar".to_string(),
|
||||
syntax: "lark".to_string(),
|
||||
definition: ARTIFACTS_FREEFORM_GRAMMAR.to_string(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn create_js_repl_reset_tool() -> ToolSpec {
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "js_repl_reset".to_string(),
|
||||
@@ -1781,6 +1717,7 @@ pub(crate) fn build_specs(
|
||||
dynamic_tools: &[DynamicToolSpec],
|
||||
) -> ToolRegistryBuilder {
|
||||
use crate::tools::handlers::ApplyPatchHandler;
|
||||
use crate::tools::handlers::ArtifactsHandler;
|
||||
use crate::tools::handlers::DynamicToolHandler;
|
||||
use crate::tools::handlers::GrepFilesHandler;
|
||||
use crate::tools::handlers::JsReplHandler;
|
||||
@@ -1790,13 +1727,11 @@ pub(crate) fn build_specs(
|
||||
use crate::tools::handlers::McpResourceHandler;
|
||||
use crate::tools::handlers::MultiAgentHandler;
|
||||
use crate::tools::handlers::PlanHandler;
|
||||
use crate::tools::handlers::PresentationArtifactHandler;
|
||||
use crate::tools::handlers::ReadFileHandler;
|
||||
use crate::tools::handlers::RequestUserInputHandler;
|
||||
use crate::tools::handlers::SearchToolBm25Handler;
|
||||
use crate::tools::handlers::ShellCommandHandler;
|
||||
use crate::tools::handlers::ShellHandler;
|
||||
use crate::tools::handlers::SpreadsheetArtifactHandler;
|
||||
use crate::tools::handlers::TestSyncHandler;
|
||||
use crate::tools::handlers::UnifiedExecHandler;
|
||||
use crate::tools::handlers::ViewImageHandler;
|
||||
@@ -1819,8 +1754,7 @@ pub(crate) fn build_specs(
|
||||
let search_tool_handler = Arc::new(SearchToolBm25Handler);
|
||||
let js_repl_handler = Arc::new(JsReplHandler);
|
||||
let js_repl_reset_handler = Arc::new(JsReplResetHandler);
|
||||
let presentation_artifact_handler = Arc::new(PresentationArtifactHandler);
|
||||
let spreadsheet_artifact_handler = Arc::new(SpreadsheetArtifactHandler);
|
||||
let artifacts_handler = Arc::new(ArtifactsHandler);
|
||||
let request_permission_enabled = config.request_permission_enabled;
|
||||
|
||||
match &config.shell_type {
|
||||
@@ -1965,10 +1899,8 @@ pub(crate) fn build_specs(
|
||||
builder.register_handler("view_image", view_image_handler);
|
||||
|
||||
if config.artifact_tools {
|
||||
builder.push_spec(create_presentation_artifact_tool());
|
||||
builder.push_spec(create_spreadsheet_artifact_tool());
|
||||
builder.register_handler("presentation_artifact", presentation_artifact_handler);
|
||||
builder.register_handler("spreadsheet_artifact", spreadsheet_artifact_handler);
|
||||
builder.push_spec(create_artifacts_tool());
|
||||
builder.register_handler("artifacts", artifacts_handler);
|
||||
}
|
||||
|
||||
if config.collab_tools {
|
||||
@@ -2301,7 +2233,7 @@ mod tests {
|
||||
session_source: SessionSource::Cli,
|
||||
});
|
||||
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
|
||||
assert_contains_tool_names(&tools, &["presentation_artifact", "spreadsheet_artifact"]);
|
||||
assert_contains_tool_names(&tools, &["artifacts"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user