From d25e31a4be9e24bd39cfdda06d415c5c7f68bb61 Mon Sep 17 00:00:00 2001 From: limityan Date: Thu, 18 Jun 2026 12:27:49 +0800 Subject: [PATCH] refactor(core): migrate tool context runtime owner --- docs/plans/core-decomposition-completed.md | 2 +- docs/plans/core-decomposition-plan.md | 4 +- .../rules/source/required-rules.mjs | 51 +++ scripts/core-boundaries/self-test.mjs | 3 + .../src/agentic/tools/tool_context_runtime.rs | 103 ++---- src/crates/execution/AGENTS.md | 2 +- src/crates/execution/tool-execution/AGENTS.md | 8 +- .../execution/tool-execution/Cargo.toml | 2 + .../execution/tool-execution/src/context.rs | 297 ++++++++++++++++++ .../execution/tool-execution/src/lib.rs | 1 + 10 files changed, 384 insertions(+), 89 deletions(-) create mode 100644 src/crates/execution/tool-execution/src/context.rs diff --git a/docs/plans/core-decomposition-completed.md b/docs/plans/core-decomposition-completed.md index de3c7c7d3f..fef9dbff08 100644 --- a/docs/plans/core-decomposition-completed.md +++ b/docs/plans/core-decomposition-completed.md @@ -21,7 +21,7 @@ - `bitfun-events` 已承接 backend event DTO、agentic event DTO 和 platform-neutral `EventEmitter` trait。 - `services-integrations` 已承接 remote-connect primitives、wire command routing / response assembly、IM bot provider-neutral config / persistence / file auto-push / locale / menu / state / command parsing、workspace search concrete owner、remote SSH/SFTP/PTY owner、DeepResearch report IO / display-map sidecar、MiniApp host dispatch / storage / worker / import IO。 - `tool-contracts` 已承接 provider-neutral tool DTO、manifest/catalog/admission/result presentation、Computer Use DTO/input parser/screenshot payload、confirmation facts、truncation recovery presentation、runtime restriction policy 和 provider-entry materialization;core 只保留 Computer Use 旧 public path re-export / compatibility shim 与产品执行入口。 -- `tool-execution` 已承接 local / remote IO helper、Bash shell helper、batching plan、retry policy、state counting、tool state event payload shaping / result redaction、cancellation-state/token-store policy、background exec output capture、部分 result rendering,以及 File tool 的 provider-neutral 结果展示、写入 mode/status/line-count 规则、Edit guardrail 分类和 Delete success 文本;core 只保留 ToolResult 包装、权限、checkpoint、read-state adapter、remote shell/FS 调用和旧工具入口。 +- `tool-execution` 已承接 local / remote IO helper、Bash shell helper、batching plan、retry policy、state counting、tool state event payload shaping / result redaction、cancellation-state/token-store policy、background exec output capture、prompt-safe tool context facts / custom-data materialization、部分 result rendering,以及 File tool 的 provider-neutral 结果展示、写入 mode/status/line-count 规则、Edit guardrail 分类和 Delete success 文本;core 只保留 ToolResult 包装、权限、checkpoint、runtime handles、read-state adapter、remote shell/FS 调用和旧工具入口。 - `agent-runtime` 已承接 scheduler/background delivery 纯决策、dialog lifecycle port contracts、runtime event queue/router、session management/cancellation port contracts、thread-goal facts、prompt markup / prompt / prompt-cache facts 与持久化写入决策、remote file delivery prompt facts、turn skill/agent snapshot DTO/diff/render/store、file-read session state / prior-read guardrail / freshness 决策、session evidence ledger 与 compression-contract projection、dialog-turn cancellation token store、tool confirmation / user-question wait channel state、custom agent / mode / subagent schema、默认值、discovery/loading、markdown IO、validation、review 工具过滤、skill catalog/root specs、mode policy、selection/shadow/mode-info 规则、assistant payload rendering、post-call hook routing、DeepReview provider-neutral policy/queue/retry/diagnostics shaping 与 queue event payload shaping、DeepResearch citation renumber 与 report post-process gate,并建立不暴露 `bitfun-core` / `product-full` / concrete manager 的内部 SDK facade。SDK facade 已支持注入 fake runtime services、tool registry、harness registry、hook registry 和 agent registry。 - `harness` 已建立 descriptor、route plan 和 legacy provider registry。 - `product-domains` 已承接 MiniApp state/workflow planning、compile / permission adaptation、import lifecycle、AI / Agent permission、rate-limit、model/message/session/workspace/turn-text bridge rules、AI / Agent 请求计划、stream / runtime event payload、worker restart / draft key / workspace input 规则、function-agent prompt/parser/response policy 和部分 Git snapshot/fallback 逻辑。 diff --git a/docs/plans/core-decomposition-plan.md b/docs/plans/core-decomposition-plan.md index 10998c133d..0a7dec06e8 100644 --- a/docs/plans/core-decomposition-plan.md +++ b/docs/plans/core-decomposition-plan.md @@ -21,7 +21,7 @@ - Desktop / CLI / ACP 仍通过 `bitfun-core/product-full` 获取完整能力;Server / Remote / Web / Mobile Web 不直接依赖 core。Product Assembly 已按入口矩阵裁剪能力计划:完整兼容入口保留 product-full 能力,无直接 core 入口不再 materialize product-full capability packs、feature groups、runtime services、tool groups 或 harness routes。 - Runtime Services、Agent Runtime、Tool Contracts、Tool Execution、Harness、Product Domains、Services Core、Services Integrations 等 owner crate 已建立;Agent Runtime SDK 内部 facade 已能注入 runtime services、tool registry、harness registry、hook registry、workspace-scoped agent registry 和 runtime event queue/router,部分 concrete 生命周期仍由 core concrete manager 或产品命令路径持有。 - 最新 custom agent / mode / skill 路径已纳入 `agent-runtime` owner:schema、默认值、skill catalog/root specs、mode policy、selection/shadow 规则、markdown parse/render、validation 与 review 工具过滤规则由 runtime 持有;core 和 desktop 只保留产品工具/模型查询、日志、registry/config 写入、文件路径选择、扫描加载 IO 和命令入口。 -- PR-B 已收口 Agent lifecycle 与 tool side-effect owner:turn skill/agent snapshot DTO / diff / render / store、file-read session state / prior-read guardrail / freshness 决策、session evidence ledger 与 compression-contract projection、dialog-turn cancellation token store、tool confirmation / user-question wait channel state 已迁入 `agent-runtime`;background exec output capture、tool cancellation token store 已迁入 `tool-execution`;core 保留 resolver、产品事件、具体工具执行、IO 编排和旧路径兼容 re-export。 +- PR-B 已收口 Agent lifecycle 与 tool side-effect owner:turn skill/agent snapshot DTO / diff / render / store、file-read session state / prior-read guardrail / freshness 决策、session evidence ledger 与 compression-contract projection、dialog-turn cancellation token store、tool confirmation / user-question wait channel state 已迁入 `agent-runtime`;background exec output capture、tool cancellation token store、prompt-safe tool context facts / custom-data materialization 已迁入 `tool-execution`;core 保留 resolver、产品事件、具体工具执行、IO 编排、runtime handles 和旧路径兼容 re-export。 - Computer Use 的 provider-neutral DTO、输入解析、截图结果 body/hint 组装已迁入 `tool-contracts`;core 保留 host trait、base64 attachment 生成、产品工具执行和旧 public path re-export / compatibility shim。 - File tool 的 provider-neutral 结果展示、写入 mode/status/line-count 规则、Edit guardrail 分类和 Delete success 文本已迁入 `tool-execution`;file-read state 的 provider-neutral guardrail / freshness 语义已迁入 `agent-runtime`;core 保留 ToolResult 包装、权限、checkpoint、read-state adapter、remote shell/FS 调用和旧工具入口。 - PR-C 已收口 Harness / product workflow 的低风险 owner:MiniApp AI / Agent permission、rate-limit、model/message/session/workspace/turn-text 规则迁入 `product-domains`;DeepResearch 后处理 gate 迁入 `agent-runtime`,report IO 继续由 `services-integrations` 持有;function-agent AI concrete acquisition 收拢为 core port adapter,旧 `runtime_services` 路径删除。 @@ -33,7 +33,7 @@ - `services-core` 已承接 session metadata store、session index rebuild、lineage / branch metadata shaping、JSON file store、session layout 和 legacy session-store merge。 - `runtime-services` 已承接 typed runtime service assembly、capability validation、provider registry、backend event delivery owner 和无副作用 capability marker ports。 - `agent-runtime` 已承接 provider-neutral scheduler decisions、dialog lifecycle port contracts、runtime event queue/router、background delivery decisions、thread-goal facts、prompt markup / prompt-cache facts 与持久化写入决策、remote file delivery prompt facts、turn skill/agent snapshot state、file-read session state / prior-read guardrail / freshness 决策、session evidence ledger、dialog-turn cancellation token store、tool confirmation / user-question wait channel state、DeepReview provider-neutral policy / queue / retry / diagnostics shaping 与 queue event payload shaping。 -- `tool-contracts` / `tool-execution` 已承接 tool manifest / catalog / admission、Computer Use contract/payload、batching plan、retry policy、state counting、tool state event payload shaping / result redaction、cancellation-state/token-store policy、background exec output capture、shell helper、部分 local / remote IO helper,以及 file tool provider-neutral result presentation / mode / status / guardrail facts。 +- `tool-contracts` / `tool-execution` 已承接 tool manifest / catalog / admission、Computer Use contract/payload、batching plan、retry policy、state counting、tool state event payload shaping / result redaction、cancellation-state/token-store policy、background exec output capture、prompt-safe tool context facts / custom-data materialization、shell helper、部分 local / remote IO helper,以及 file tool provider-neutral result presentation / mode / status / guardrail facts。 - `services-core` 已承接 managed runtime command resolution 和 PATH merge 规则;core 只保留产品 managed runtime root 适配。 - `services-integrations` 已承接 remote-connect primitives、workspace search concrete owner、remote SSH/SFTP/PTY owner、MiniApp host dispatch / storage / worker IO、DeepResearch report IO。 - `product-domains` 已承接 MiniApp workflow planning、compile / permission path adaptation、AI / Agent 请求计划、stream/event payload、worker restart / draft key / workspace input 规则、function-agent prompt / parser / response policy 和部分 Git snapshot/fallback 逻辑。 diff --git a/scripts/core-boundaries/rules/source/required-rules.mjs b/scripts/core-boundaries/rules/source/required-rules.mjs index 8aecdb6c77..3f836a2bf0 100644 --- a/scripts/core-boundaries/rules/source/required-rules.mjs +++ b/scripts/core-boundaries/rules/source/required-rules.mjs @@ -2968,6 +2968,45 @@ export const requiredContentRules = [ }, ], }, + { + path: 'src/crates/execution/tool-execution/src/context.rs', + reason: + 'tool-runtime must own provider-neutral tool custom-data materialization and context facts projection while core keeps runtime handles and concrete ToolUseContext', + patterns: [ + { + regex: /\bpub struct ToolRuntimeCustomDataInput\b/, + message: 'missing tool runtime custom-data input DTO', + }, + { + regex: /\bpub fn build_tool_runtime_custom_data\b/, + message: 'missing tool runtime custom-data owner', + }, + { + regex: /\bpub struct ToolRuntimeContextFactsInput\b/, + message: 'missing tool runtime context facts input DTO', + }, + { + regex: /\bpub fn project_tool_context_facts\b/, + message: 'missing tool runtime context facts projection owner', + }, + { + regex: /\bpub fn delegation_policy_from_custom_data\b/, + message: 'missing delegation policy parsing owner', + }, + { + regex: /\bpub fn primary_model_supports_image_understanding\b/, + message: 'missing model image-support policy owner', + }, + { + regex: /\bmaterializes_provider_neutral_tool_custom_data\b/, + message: 'missing tool runtime custom-data regression', + }, + { + regex: /\bprojects_prompt_safe_tool_context_facts_only\b/, + message: 'missing prompt-safe context facts regression', + }, + ], + }, { path: 'src/crates/execution/tool-execution/tests/tool_pipeline_planning.rs', reason: @@ -5895,6 +5934,18 @@ export const requiredContentRules = [ regex: /\bto_tool_context_facts\b/, message: 'missing portable ToolUseContext facts projection', }, + { + regex: /\bproject_tool_context_facts\b/, + message: 'missing tool-runtime context facts owner delegation', + }, + { + regex: /\bbuild_tool_runtime_custom_data\b/, + message: 'missing tool-runtime custom-data owner delegation', + }, + { + regex: /\bdelegation_policy_from_custom_data\b/, + message: 'missing tool-runtime delegation policy owner delegation', + }, { regex: /\bimpl PortableToolContextProvider for ToolUseContext\b/, message: 'missing portable ToolUseContext facts provider impl', diff --git a/scripts/core-boundaries/self-test.mjs b/scripts/core-boundaries/self-test.mjs index 1befadcd97..b616208541 100644 --- a/scripts/core-boundaries/self-test.mjs +++ b/scripts/core-boundaries/self-test.mjs @@ -2286,6 +2286,9 @@ export function runManifestParserSelfTest({ contracts: [ 'pub struct ToolUseContext', 'to_tool_context_facts', + 'project_tool_context_facts', + 'build_tool_runtime_custom_data', + 'delegation_policy_from_custom_data', 'impl PortableToolContextProvider for ToolUseContext', 'tool_context_facts_omit_runtime_owner_fields_even_when_context_is_populated', 'customData', diff --git a/src/crates/assembly/core/src/agentic/tools/tool_context_runtime.rs b/src/crates/assembly/core/src/agentic/tools/tool_context_runtime.rs index 5f507c38ab..42e42cdca4 100644 --- a/src/crates/assembly/core/src/agentic/tools/tool_context_runtime.rs +++ b/src/crates/assembly/core/src/agentic/tools/tool_context_runtime.rs @@ -2,8 +2,8 @@ //! //! This module intentionally keeps service handles, workspace runtime lookup, //! path enforcement, cancellation/post-call hooks, and checkpoint recording in -//! core. The portable facts projection uses `bitfun-agent-tools` DTOs while the -//! runtime owner type stays here. +//! core. Provider-neutral context projection and custom-data materialization are +//! owned by `tool-runtime` while the runtime owner type stays here. use crate::agentic::coordination::get_global_coordinator; use crate::agentic::deep_review::tool_context; @@ -45,6 +45,11 @@ use std::collections::HashMap; use std::future::Future; use std::path::{Path, PathBuf}; use tokio_util::sync::CancellationToken; +use tool_runtime::context::{ + build_tool_runtime_custom_data, delegation_policy_from_custom_data, + primary_model_supports_image_understanding as runtime_primary_model_supports_image_understanding, + project_tool_context_facts, ToolRuntimeContextFactsInput, ToolRuntimeCustomDataInput, +}; /// Core-owned tool use context. #[derive(Debug, Clone)] @@ -66,22 +71,7 @@ pub struct ToolUseContext { impl ToolUseContext { pub(crate) fn delegation_policy(&self) -> DelegationPolicy { - let allow_subagent_spawn = self - .custom_data - .get("delegation_allow_subagent_spawn") - .and_then(|value| value.as_bool()) - .unwrap_or(true); - let nesting_depth = self - .custom_data - .get("delegation_nesting_depth") - .and_then(|value| value.as_u64()) - .and_then(|value| u8::try_from(value).ok()) - .unwrap_or(0); - - DelegationPolicy { - allow_subagent_spawn, - nesting_depth, - } + delegation_policy_from_custom_data(&self.custom_data) } pub fn workspace_root(&self) -> Option<&Path> { @@ -104,7 +94,7 @@ impl ToolUseContext { } }); - ToolContextFacts { + project_tool_context_facts(ToolRuntimeContextFactsInput { tool_call_id: self.tool_call_id.clone(), agent_type: self.agent_type.clone(), session_id: self.session_id.clone(), @@ -117,16 +107,13 @@ impl ToolUseContext { .to_string() }), runtime_tool_restrictions: self.runtime_tool_restrictions.clone(), - } + }) } /// Whether the session primary model accepts image inputs (from tool-definition / pipeline context). /// Defaults to **true** when unset (e.g. API listings without model metadata). pub fn primary_model_supports_image_understanding(&self) -> bool { - self.custom_data - .get("primary_model_supports_image_understanding") - .and_then(|v| v.as_bool()) - .unwrap_or(true) + runtime_primary_model_supports_image_understanding(&self.custom_data) } pub fn cancellation_token(&self) -> Option<&CancellationToken> { @@ -264,60 +251,8 @@ pub(crate) fn build_tool_description_context( } fn build_tool_context_custom_data(context: &ToolExecutionContext) -> HashMap { - let mut map = HashMap::new(); - - map.insert( - "delegation_allow_subagent_spawn".to_string(), - serde_json::json!(context.delegation_policy.allow_subagent_spawn), - ); - map.insert( - "delegation_nesting_depth".to_string(), - serde_json::json!(context.delegation_policy.nesting_depth), - ); - - if let Some(turn_index) = context.context_vars.get("turn_index") { - if let Ok(n) = turn_index.parse::() { - map.insert("turn_index".to_string(), serde_json::json!(n)); - } - } - - if let Some(provider) = context.context_vars.get("primary_model_provider") { - if !provider.is_empty() { - map.insert( - "primary_model_provider".to_string(), - serde_json::json!(provider), - ); - } - } - if let Some(supports_images) = context - .context_vars - .get("primary_model_supports_image_understanding") - { - if let Ok(flag) = supports_images.parse::() { - map.insert( - "primary_model_supports_image_understanding".to_string(), - serde_json::json!(flag), - ); - } - } - if let Some(acp_transport) = context.context_vars.get("acp_transport") { - if let Ok(flag) = acp_transport.parse::() { - map.insert("acp_transport".to_string(), serde_json::json!(flag)); - } - } - if let Some(remote_file_delivery) = context - .context_vars - .get(TOOL_CONTEXT_REMOTE_FILE_DELIVERY_KEY) - { - if let Ok(flag) = remote_file_delivery.parse::() { - map.insert( - TOOL_CONTEXT_REMOTE_FILE_DELIVERY_KEY.to_string(), - serde_json::json!(flag), - ); - } - } - - let deep_review_parent_context = context.subagent_parent_info.as_ref().map(|parent_info| { + let mut extension_custom_data = HashMap::new(); + let deep_review_parent = context.subagent_parent_info.as_ref().map(|parent_info| { tool_context::DeepReviewToolParentContext { tool_call_id: parent_info.tool_call_id.as_str(), session_id: parent_info.session_id.as_str(), @@ -326,11 +261,15 @@ fn build_tool_context_custom_data(context: &ToolExecutionContext) -> HashMap { + pub context_vars: &'a HashMap, + pub delegation_policy: DelegationPolicy, + pub remote_file_delivery_key: &'a str, + pub extension_custom_data: Option<&'a HashMap>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ToolRuntimeContextFactsInput { + pub tool_call_id: Option, + pub agent_type: Option, + pub session_id: Option, + pub dialog_turn_id: Option, + pub workspace_kind: Option, + pub workspace_root: Option, + pub runtime_tool_restrictions: ToolRuntimeRestrictions, +} + +pub fn build_tool_runtime_custom_data( + input: ToolRuntimeCustomDataInput<'_>, +) -> HashMap { + let mut map = HashMap::new(); + + map.insert( + "delegation_allow_subagent_spawn".to_string(), + serde_json::json!(input.delegation_policy.allow_subagent_spawn), + ); + map.insert( + "delegation_nesting_depth".to_string(), + serde_json::json!(input.delegation_policy.nesting_depth), + ); + + insert_u64_context_var(input.context_vars, &mut map, "turn_index"); + insert_non_empty_string_context_var(input.context_vars, &mut map, "primary_model_provider"); + insert_bool_context_var( + input.context_vars, + &mut map, + "primary_model_supports_image_understanding", + ); + insert_bool_context_var(input.context_vars, &mut map, "acp_transport"); + insert_bool_context_var(input.context_vars, &mut map, input.remote_file_delivery_key); + if let Some(extension_custom_data) = input.extension_custom_data { + for (key, value) in extension_custom_data { + map.entry(key.clone()).or_insert_with(|| value.clone()); + } + } + + map +} + +pub fn project_tool_context_facts(input: ToolRuntimeContextFactsInput) -> ToolContextFacts { + ToolContextFacts { + tool_call_id: input.tool_call_id, + agent_type: input.agent_type, + session_id: input.session_id, + dialog_turn_id: input.dialog_turn_id, + workspace_kind: input.workspace_kind, + workspace_root: input.workspace_root, + runtime_tool_restrictions: input.runtime_tool_restrictions, + } +} + +pub fn delegation_policy_from_custom_data( + custom_data: &HashMap, +) -> DelegationPolicy { + let allow_subagent_spawn = custom_data + .get("delegation_allow_subagent_spawn") + .and_then(Value::as_bool) + .unwrap_or(true); + let nesting_depth = custom_data + .get("delegation_nesting_depth") + .and_then(Value::as_u64) + .and_then(|value| u8::try_from(value).ok()) + .unwrap_or(0); + + DelegationPolicy { + allow_subagent_spawn, + nesting_depth, + } +} + +/// Whether the session primary model accepts image inputs. +/// +/// Defaults to true when unset so API listings without model metadata keep the +/// historical behavior. +pub fn primary_model_supports_image_understanding(custom_data: &HashMap) -> bool { + custom_data + .get("primary_model_supports_image_understanding") + .and_then(Value::as_bool) + .unwrap_or(true) +} + +fn insert_u64_context_var( + context_vars: &HashMap, + map: &mut HashMap, + key: &str, +) { + if let Some(value) = context_vars.get(key) { + if let Ok(parsed) = value.parse::() { + map.insert(key.to_string(), serde_json::json!(parsed)); + } + } +} + +fn insert_bool_context_var( + context_vars: &HashMap, + map: &mut HashMap, + key: &str, +) { + if let Some(value) = context_vars.get(key) { + if let Ok(parsed) = value.parse::() { + map.insert(key.to_string(), serde_json::json!(parsed)); + } + } +} + +fn insert_non_empty_string_context_var( + context_vars: &HashMap, + map: &mut HashMap, + key: &str, +) { + if let Some(value) = context_vars + .get(key) + .map(String::as_str) + .filter(|value| !value.is_empty()) + { + map.insert(key.to_string(), serde_json::json!(value)); + } +} + +#[cfg(test)] +mod tests { + use super::{ + build_tool_runtime_custom_data, delegation_policy_from_custom_data, + primary_model_supports_image_understanding, project_tool_context_facts, + ToolRuntimeContextFactsInput, ToolRuntimeCustomDataInput, + }; + use bitfun_agent_tools::{ToolRuntimeRestrictions, ToolWorkspaceKind}; + use bitfun_runtime_ports::DelegationPolicy; + use serde_json::json; + use std::collections::{BTreeSet, HashMap}; + + #[test] + fn materializes_provider_neutral_tool_custom_data() { + let mut context_vars = HashMap::new(); + context_vars.insert("turn_index".to_string(), "7".to_string()); + context_vars.insert("primary_model_provider".to_string(), "openai".to_string()); + context_vars.insert( + "primary_model_supports_image_understanding".to_string(), + "false".to_string(), + ); + context_vars.insert("acp_transport".to_string(), "true".to_string()); + context_vars.insert("remote_file_delivery".to_string(), "true".to_string()); + let extension_custom_data = HashMap::from([("extension_key".to_string(), json!("kept"))]); + + let custom_data = build_tool_runtime_custom_data(ToolRuntimeCustomDataInput { + context_vars: &context_vars, + delegation_policy: DelegationPolicy::top_level().spawn_child(), + remote_file_delivery_key: "remote_file_delivery", + extension_custom_data: Some(&extension_custom_data), + }); + + assert_eq!(custom_data["delegation_allow_subagent_spawn"], json!(false)); + assert_eq!(custom_data["delegation_nesting_depth"], json!(1)); + assert_eq!(custom_data["turn_index"], json!(7)); + assert_eq!(custom_data["primary_model_provider"], json!("openai")); + assert_eq!( + custom_data["primary_model_supports_image_understanding"], + json!(false) + ); + assert_eq!(custom_data["acp_transport"], json!(true)); + assert_eq!(custom_data["remote_file_delivery"], json!(true)); + assert_eq!(custom_data["extension_key"], json!("kept")); + } + + #[test] + fn custom_data_ignores_invalid_or_empty_context_values() { + let mut context_vars = HashMap::new(); + context_vars.insert("turn_index".to_string(), "not-a-number".to_string()); + context_vars.insert("primary_model_provider".to_string(), "".to_string()); + context_vars.insert( + "primary_model_supports_image_understanding".to_string(), + "not-bool".to_string(), + ); + context_vars.insert("acp_transport".to_string(), "not-bool".to_string()); + context_vars.insert("remote_file_delivery".to_string(), "not-bool".to_string()); + + let custom_data = build_tool_runtime_custom_data(ToolRuntimeCustomDataInput { + context_vars: &context_vars, + delegation_policy: DelegationPolicy::top_level(), + remote_file_delivery_key: "remote_file_delivery", + extension_custom_data: None, + }); + + assert_eq!(custom_data["delegation_allow_subagent_spawn"], json!(true)); + assert_eq!(custom_data["delegation_nesting_depth"], json!(0)); + assert!(!custom_data.contains_key("turn_index")); + assert!(!custom_data.contains_key("primary_model_provider")); + assert!(!custom_data.contains_key("primary_model_supports_image_understanding")); + assert!(!custom_data.contains_key("acp_transport")); + assert!(!custom_data.contains_key("remote_file_delivery")); + } + + #[test] + fn extension_custom_data_cannot_override_runtime_owned_values() { + let mut context_vars = HashMap::new(); + context_vars.insert("turn_index".to_string(), "7".to_string()); + context_vars.insert( + "primary_model_supports_image_understanding".to_string(), + "true".to_string(), + ); + let extension_custom_data = HashMap::from([ + ("turn_index".to_string(), json!(99)), + ("delegation_allow_subagent_spawn".to_string(), json!(false)), + ( + "primary_model_supports_image_understanding".to_string(), + json!(false), + ), + ("extension_key".to_string(), json!("kept")), + ]); + + let custom_data = build_tool_runtime_custom_data(ToolRuntimeCustomDataInput { + context_vars: &context_vars, + delegation_policy: DelegationPolicy::top_level(), + remote_file_delivery_key: "remote_file_delivery", + extension_custom_data: Some(&extension_custom_data), + }); + + assert_eq!(custom_data["delegation_allow_subagent_spawn"], json!(true)); + assert_eq!(custom_data["turn_index"], json!(7)); + assert_eq!( + custom_data["primary_model_supports_image_understanding"], + json!(true) + ); + assert_eq!(custom_data["extension_key"], json!("kept")); + } + + #[test] + fn derives_runtime_policies_from_custom_data() { + let mut custom_data = HashMap::new(); + custom_data.insert("delegation_allow_subagent_spawn".to_string(), json!(false)); + custom_data.insert("delegation_nesting_depth".to_string(), json!(3)); + custom_data.insert( + "primary_model_supports_image_understanding".to_string(), + json!(false), + ); + + assert_eq!( + delegation_policy_from_custom_data(&custom_data), + DelegationPolicy { + allow_subagent_spawn: false, + nesting_depth: 3 + } + ); + assert!(!primary_model_supports_image_understanding(&custom_data)); + assert!(primary_model_supports_image_understanding(&HashMap::new())); + } + + #[test] + fn projects_prompt_safe_tool_context_facts_only() { + let facts = project_tool_context_facts(ToolRuntimeContextFactsInput { + tool_call_id: Some("tool-1".to_string()), + agent_type: Some("coding".to_string()), + session_id: Some("session-1".to_string()), + dialog_turn_id: Some("turn-1".to_string()), + workspace_kind: Some(ToolWorkspaceKind::Remote), + workspace_root: Some("/home/user/project".to_string()), + runtime_tool_restrictions: ToolRuntimeRestrictions { + allowed_tool_names: BTreeSet::from(["Read".to_string()]), + denied_tool_names: BTreeSet::from(["Bash".to_string()]), + denied_tool_messages: Default::default(), + path_policy: Default::default(), + }, + }); + + let value = serde_json::to_value(&facts).expect("serialize facts"); + assert_eq!(value["toolCallId"], "tool-1"); + assert_eq!(value["agentType"], "coding"); + assert_eq!(value["sessionId"], "session-1"); + assert_eq!(value["dialogTurnId"], "turn-1"); + assert_eq!(value["workspaceKind"], "remote"); + assert_eq!(value["workspaceRoot"], "/home/user/project"); + assert_eq!( + value["runtimeToolRestrictions"]["allowed_tool_names"][0], + "Read" + ); + assert!(value.get("customData").is_none()); + assert!(value.get("runtimeHandles").is_none()); + assert!(value.get("unlockedCollapsedTools").is_none()); + } +} diff --git a/src/crates/execution/tool-execution/src/lib.rs b/src/crates/execution/tool-execution/src/lib.rs index 5172a02411..7c1f72b15a 100644 --- a/src/crates/execution/tool-execution/src/lib.rs +++ b/src/crates/execution/tool-execution/src/lib.rs @@ -1,4 +1,5 @@ pub mod background_command_output; +pub mod context; pub mod fs; pub mod pipeline; pub mod search;