diff --git a/docs/architecture/core-decomposition.md b/docs/architecture/core-decomposition.md index f5f243dbd..37a7dc439 100644 --- a/docs/architecture/core-decomposition.md +++ b/docs/architecture/core-decomposition.md @@ -136,9 +136,9 @@ owner 边界,否则不要把一个 feature group 继续拆成更小的 crate 处理前,必须保留 `report.md` / `citations.md` / `display_map.json` 的 deterministic post-processing 行为; 在此之前该 hook 仍属于 `bitfun-core` agent runtime assembly。 - 最新主干新增 on-demand tool spec discovery。`ToolExposure`、`GetToolSpec` 名称、 - collapsed stub、manifest ordering、GetToolSpec prompt / schema / assistant-detail - rendering 等纯契约可由 `bitfun-agent-tools` 拥有;但 - `manifest_resolver`、产品 registry snapshot、collapsed-tool catalog、context-aware + collapsed stub、manifest ordering、generic collapsed exposure query、GetToolSpec + prompt / schema / assistant-detail rendering 等纯契约可由 `bitfun-agent-tools` + 拥有;但 `manifest_resolver`、产品 registry snapshot、product collapsed-tool catalog、context-aware tool schema/description、`GetToolSpec` 执行和 `ToolUseContext.unlocked_collapsed_tools` 暂时仍属于 `bitfun-core` product tool runtime。继续迁移前必须证明 prompt-visible manifest、expanded/collapsed exposure、unlock state 与 desktop/MCP/ACP tool catalog 等价。 diff --git a/docs/plans/core-decomposition-plan.md b/docs/plans/core-decomposition-plan.md index 418dc531f..03da18c98 100644 --- a/docs/plans/core-decomposition-plan.md +++ b/docs/plans/core-decomposition-plan.md @@ -1079,29 +1079,35 @@ pub fn create_tool_registry() -> ToolRegistry { - [x] H1 解锁契约切片只抽出 `GetToolSpec` 结果到 collapsed 工具名集合的纯收集规则; `ToolUseContext.unlocked_collapsed_tools`、执行消息解析、runtime manifest assembly 和 `GetToolSpecTool` 执行仍由 core 拥有。 +- [x] H1 manifest builder 切片只抽出 prompt-visible manifest definition 的纯组装规则: + expanded 工具的 description/schema 仍由 core 按 `ToolUseContext` 获取,collapsed stub + 渲染和排序由 `bitfun-agent-tools` 统一;runtime manifest owner 仍未迁移。 +- [x] H1 catalog/exposure 切片继续抽出 registry snapshot 到 manifest policy input、 + generic collapsed exposure 查询和 `GetToolSpec` catalog description 的纯规则;core 仍负责 + tool availability、context-aware detail、snapshot decorator、runtime unlock state 和工具执行。 **当前安全迁移状态(2026-05-18):** -- 已迁移到 `bitfun-agent-tools`:`ToolResult`、`ValidationResult`、`InputValidator`、dynamic tool metadata、tool render options、runtime restriction DTO、path resolution DTO、`ToolContextFacts` / `ToolWorkspaceKind` 轻量上下文事实、`PortableToolContextProvider` 只读 facts provider、不依赖 core service 的 `ToolRegistry` / `ToolRegistryItem` generic registry container、`StaticToolProvider` / `install_static_provider` 安装合约,以及 `GetToolSpec` load observation 到 collapsed 工具名集合的纯收集 helper。dynamic tool provider / decorator contract 已通过 `agent-tools` 提供兼容 re-export,原 `runtime-ports` 路径保持可用;core 旧路径继续 re-export,并只保留 `BitFunError` 映射、路径 containment helper、`ToolUseContext` 到 facts 的只读投影和 runtime unlock state。 +- 已迁移到 `bitfun-agent-tools`:`ToolResult`、`ValidationResult`、`InputValidator`、dynamic tool metadata、tool render options、runtime restriction DTO、path resolution DTO、`ToolContextFacts` / `ToolWorkspaceKind` 轻量上下文事实、`PortableToolContextProvider` 只读 facts provider、不依赖 core service 的 `ToolRegistry` / `ToolRegistryItem` generic registry container、`StaticToolProvider` / `install_static_provider` 安装合约、registry snapshot 到 manifest policy input 的纯 helper、generic collapsed exposure 查询、`GetToolSpec` load observation 到 collapsed 工具名集合的纯收集 helper、prompt-visible manifest definition 的纯组装 helper,以及 `GetToolSpec` catalog description 的纯组装 helper。dynamic tool provider / decorator contract 已通过 `agent-tools` 提供兼容 re-export,原 `runtime-ports` 路径保持可用;core 旧路径继续 re-export,并只保留 `BitFunError` 映射、路径 containment helper、`ToolUseContext` 到 facts 的只读投影和 runtime unlock state。 - `bitfun-core::agentic::tools` 现在保留 core-owned product provider groups、snapshot decorator 组装、旧构造函数、`dyn Tool` 到 generic registry 的适配、`ToolUseContext` runtime handle / service owner,以及最新主干新增的 runtime manifest assembly / context filtering / `GetToolSpec` 执行;dynamic metadata map、tool map、dynamic descriptor assembly、static provider 安装合约、portable context facts、纯 manifest/exposure 契约和 GetToolSpec presentation/schema 纯 helper 由 `bitfun-agent-tools` 拥有。 - 已新增 `bitfun-tool-packs` feature scaffold,默认 feature 为空,`product-full` 只聚合 feature;当前只提供 `ToolPackFeatureGroup` / `all_feature_groups` / `enabled_feature_groups` 元数据,不注册或迁移任何工具实现。 - 已通过 boundary check 锁定 `agent-tools` / `tool-packs` 暂不拥有 product tool runtime assembly、`GetToolSpecTool` 执行或 collapsed-tool unlock state;`tool-packs` 也不得拥有 manifest/exposure 契约。`agent-tools` 只允许拥有纯 manifest/exposure helper、GetToolSpec presentation/schema helper 和不依赖具体工具的 provider 安装合约,core product tool runtime 继续负责产品 registry snapshot、context-aware discovery、unlock state 和执行路径。 - boundary check 也已补充 core owner anchor:要求产品工具注册、expanded/collapsed manifest、`GetToolSpec` duplicate-load guard、`ToolUseContext.unlocked_collapsed_tools`、执行管线 gating 与 execution unlock collector 仍保留在 core。后续若迁移这些 owner,必须先更新 port/provider 设计、等价测试与该脚本,而不能只删除 core 侧实现。 - `Tool` trait、`ToolUseContext` 和具体工具实现仍在 core;它们直接连接 workspace service、snapshot wrapper、computer-use host、cancellation token 与 Deep Review checkpoint hook。`ToolContextFacts` / `PortableToolContextProvider` 只能作为只读事实投影,继续迁移前必须先确认 service port 方案,并补工具清单等价性测试。 - 最新主干新增的 Deep Review shared-context / evidence-ledger checkpoint hook 仍保留在 core 的 `ToolUseContext` 中;在设计独立 tool context / event port 前,不应把 `ToolUseContext` 或 concrete tool implementation 继续外移。 -- 最新主干新增 on-demand tool spec discovery:`ToolExposure`、`GetToolSpec` 名称、collapsed prompt stub、manifest ordering 与 GetToolSpec presentation/schema 的纯契约已可由 `bitfun-agent-tools` 承载;`manifest_resolver`、collapsed-tool catalog、context-aware `description_with_context` / `input_schema_for_model_with_context`、`GetToolSpecTool` 执行以及 `ToolUseContext.unlocked_collapsed_tools` 仍会影响模型可见工具集合。该变化不推翻 PR4 的低风险结论,但把后续 tool/provider 迁移提升为高风险项,不能在 product-domain runtime 收尾中顺带执行。 +- 最新主干新增 on-demand tool spec discovery:`ToolExposure`、`GetToolSpec` 名称、collapsed prompt stub、manifest ordering、generic collapsed exposure 查询与 GetToolSpec presentation/schema 的纯契约已可由 `bitfun-agent-tools` 承载;`manifest_resolver`、product collapsed-tool catalog、context-aware `description_with_context` / `input_schema_for_model_with_context`、`GetToolSpecTool` 执行以及 `ToolUseContext.unlocked_collapsed_tools` 仍会影响模型可见工具集合。该变化不推翻 PR4 的低风险结论,但把后续 tool/provider 迁移提升为高风险项,不能在 product-domain runtime 收尾中顺带执行。 - H1 start(2026-05-19):`StaticToolProviderGroup` 通用容器已迁入 `bitfun-agent-tools`,core 的 `static_providers.rs` 只负责实例化 concrete tools 并按既有 provider group 顺序装配。该切片不移动 concrete tool implementation、`ToolUseContext`、 runtime manifest assembly 或 `GetToolSpec` 执行;provider id、工具顺序与 manifest 快照由 `bitfun-agent-tools` contract test、core registry snapshot 和 boundary check 共同保护。 -- H1 follow-up(2026-05-19):下一步先补迁移前 baseline,而不是直接移动 runtime owner。 - 新增保护应覆盖完整 collapsed 工具清单、`ToolExecutionContext` 到 core-owned - `ToolUseContext` 的运行时状态传递,以及 `ToolContextFacts` / `PortableToolContextProvider` - 不携带 `unlocked_collapsed_tools`、custom data、cancellation token 或 workspace services 的边界; - core `resolve_tool_manifest` 还要保留当前显式允许 `GetToolSpec` 时的 runtime insertion 快照。 - 这仍不声明 `ToolUseContext`、runtime manifest assembly、`GetToolSpecTool` 执行或 concrete - tools 已经外移。 +- H1 follow-up(2026-05-19):已补迁移前 baseline 和纯 catalog/manifest helper 外移, + 覆盖完整 collapsed 工具清单、`ToolExecutionContext` 到 core-owned `ToolUseContext` + 的运行时状态传递、`ToolContextFacts` / `PortableToolContextProvider` 不携带 + `unlocked_collapsed_tools`、custom data、cancellation token 或 workspace services 的边界, + 以及显式允许 `GetToolSpec` 时的 runtime insertion 快照。下一步若继续迁移, + 才进入 `ToolUseContext`、runtime manifest assembly、`GetToolSpecTool` 执行或 concrete + tools 的单一 owner 设计与等价性证明。 **验证:** @@ -1751,7 +1757,7 @@ P2 后产品表面契约轨道(contract-only): - 最近 `origin/main` 的 Deep Review 变更增加了 context profile、evidence ledger、capacity/cost/queue 控制、`deep_review_run_manifest` / `deep_review_cache`、以及 review-team UI orchestration;最新主干还补充了 agent-stream tool-call dedupe、search remote/fallback、session rollback persistence、remote workspace compatibility guard、ACP startup timeout / operation diff fallback 和 companion typewriter。P3 facade 收敛前必须确认这些行为要么仍由 core product runtime assembly 或对应 product surface 拥有,要么已有对应 owner crate + port/provider 合约和等价测试。 - 最新主干的 mode-scoped subagent visibility 将 `agentic::agents` 重组为 definitions / registry / visibility 边界,并扩展了 desktop subagent API、CLI `/subagents` mode-aware list/config 与 Review Team 可见性测试;后续又加入 `Multitask` mode、内置 `GeneralPurpose` subagent 和后台 subagent result delivery。后续若迁移 agent registry / subagent definitions / scheduler,不能只做路径 re-export,必须保留 mode 可见性过滤、hidden/custom/review 分组语义、CLI availability override 路径、前后端 API contract、`Task.run_in_background` 的 parent metadata / workspace routing、running-turn injection 与 idle-session follow-up turn 语义。 - 最新主干的 DeepResearch citation renumber hook 是 deterministic post-turn runtime 行为,不是普通 prompt 文案;后续若迁移 agent runtime / report finalization,必须保留 `report.md`、`citations.md`、`display_map.json` 与 REJECTED citation 过滤语义。 -- 最新主干的 on-demand tool spec discovery 将 `manifest_resolver`、`GetToolSpecTool` 执行、collapsed-tool catalog 和 `ToolUseContext.unlocked_collapsed_tools` 接入 agent prompt / execution pipeline / desktop-MCP-ACP catalog。P3 facade 收敛前必须把这些显式保留在 core product tool runtime,或先完成等价快照与 port/provider 设计后再迁移;`ToolExposure`、`GetToolSpec` 名称、collapsed-tool prompt stub 和 manifest ordering 仅作为纯契约保留在 `bitfun-agent-tools`。 +- 最新主干的 on-demand tool spec discovery 将 `manifest_resolver`、`GetToolSpecTool` 执行、product collapsed-tool catalog 和 `ToolUseContext.unlocked_collapsed_tools` 接入 agent prompt / execution pipeline / desktop-MCP-ACP catalog。P3 facade 收敛前必须把这些显式保留在 core product tool runtime,或先完成等价快照与 port/provider 设计后再迁移;`ToolExposure`、`GetToolSpec` 名称、collapsed-tool prompt stub、generic collapsed exposure 查询和 manifest ordering 仅作为纯契约保留在 `bitfun-agent-tools`。 - 最新主干的 search result rendering / context handling 与 remote workspace compatibility guard 要求后续 `service::search`、`workspace` 或 remote runtime 迁移保留 startup restored workspace guard、remote runtime ensure、remote flashgrep FilesWithMatches fallback、preview split 和 local/remote fallback contract。 - ACP startup timeout 和 Web file-operation diff fallback 属于 product surface 行为:可以在后续 contract 中记录 operation/diff facts,但不能把 ACP timeout policy 或 Web diff rendering 迁入 core contract crate。 - 最新主干的 ACP agents config 继续把 remote workspace config reuse 放在 ACP/app surface:remote workspaces 复用 local ACP config,ACP client manager / remote shell / remote capability store / workspace menu 共同决定可用 agent。后续只能抽取 environment/capability facts;ACP config persistence、remote probing 和 workspace surface selection 不进入 core contract crate。 @@ -1832,7 +1838,7 @@ git diff -- package.json scripts/dev.cjs scripts/desktop-tauri-build.mjs scripts 16. 已完成:关键语义回归 baseline,不移动 runtime owner。覆盖 MCP config failure / catalog invalidation / 既有 list-changed helper / dynamic manifest、tool manifest / `GetToolSpec`、product-domains adapter equivalence、remote workspace search fallback 的 focused tests 或 snapshots。 17. 已完成:remote-connect runtime 当前批次收口。已基于当前 port baseline 记录 remote command/response、remote model catalog、poll response、model catalog delta、session restore、active turn、cancel、image context、tracker event、queue/event fanout 的输入输出和验证命令;tracker state / registry lifecycle、legacy image context fallback / preference、restore target decision、cancel decision 与 remote file transfer size/chunk/name policy 已迁入 `bitfun-services-integrations`。dispatcher / product execution、`ImageContextData` adapter、file IO/path resolution、terminal pre-warm 与 workspace/session restore 执行显式保留在 core-owned runtime;后续只有在另起 port/provider 设计且 focused regression 继续通过时才允许继续移动这些 runtime owner,不能把 generic attachment guard 当作已接入多模态行为。 18. 已完成:`product-domains` runtime port/facade closure。已迁入 MiniApp storage-backed runtime-state facade、built-in seed plan / marker wire helper 与 function-agent Git/AI port-backed runtime facade,并补充 focused contract tests;core 只对 MiniApp deps/restart/recompile/sync/rollback 的状态持久化委托 facade,仍保留 `PathManager` 注入、filesystem IO、worker process execution、host dispatch 执行、built-in asset seeding/source-hash lookup、prompt template、JSON extraction 和 error mapping adapter。Git commit-message 与 Startchat work-state 产品路径已通过 core-owned Git/AI adapter 接入 function-agent facade;extracted JSON string 可委托 product-domain parser,但 JSON 提取、日志与错误映射仍在 core。Startchat 接线已用 no-HEAD diff fallback、非 Git 目录空状态和 `analyze_git=false` time-info 保护旧行为,`analyzed_at` 仍由 core 在 AI 分析完成后赋值。 -19. 当前阶段:H1 tool runtime migration baseline。已开始把纯 helper 从 runtime owner 中剥离:`StaticToolProviderGroup` 和 `GetToolSpec` collapsed-load 纯收集规则可由 `bitfun-agent-tools` 拥有;完整 collapsed 工具清单、runtime context 传递和 portable facts 边界回归仍需继续补齐,再评估单一 owner 外移。不得把 `ToolUseContext`、`GetToolSpecTool` 执行、runtime manifest assembly、snapshot decorator 或 concrete tools 的迁移混成隐式结构调整。 +19. 当前阶段:H1 tool runtime migration baseline。已把纯 helper 从 runtime owner 中剥离:`StaticToolProviderGroup`、registry snapshot 到 manifest policy input、generic collapsed exposure 查询、`GetToolSpec` collapsed-load 纯收集规则、prompt-visible manifest definition 组装规则和 `GetToolSpec` catalog description 组装规则可由 `bitfun-agent-tools` 拥有;完整 collapsed 工具清单、runtime context 传递和 portable facts 边界回归已作为迁移前基线。后续若继续外移,必须进入单一 owner 设计,不得把 `ToolUseContext`、`GetToolSpecTool` 执行、runtime manifest assembly、snapshot decorator 或 concrete tools 的迁移混成隐式结构调整。 20. 后续独立评估:`bitfun-core default = []`、per-product feature set、依赖版本收敛或构建收益优化;任何收益声明都需要记录 `cargo check -p bitfun-core`、workspace check 和目标 crate check 的前后数据。 冗余清理 PR 不进入上述主线序号。只有在满足 `0A.6` 的绝对等价要求时,才可以插入到相邻里程碑之间,并且不得与主线拆分 PR 混合。 diff --git a/scripts/check-core-boundaries.mjs b/scripts/check-core-boundaries.mjs index 376795063..fcc746c4e 100644 --- a/scripts/check-core-boundaries.mjs +++ b/scripts/check-core-boundaries.mjs @@ -1141,14 +1141,38 @@ const requiredContentRules = [ regex: /\bpub fn resolve_tool_manifest_policy\b/, message: 'missing pure tool manifest policy resolver', }, + { + regex: /\bfn default_exposure\b/, + message: 'missing generic tool exposure contract', + }, + { + regex: /\bpub fn build_tool_manifest_policy_tools\b/, + message: 'missing registry snapshot to manifest policy input helper', + }, { regex: /\bpub fn build_collapsed_tool_stub_definition\b/, message: 'missing collapsed-tool prompt stub contract', }, + { + regex: /\bpub enum PromptVisibleToolManifestItem\b/, + message: 'missing prompt-visible manifest item contract', + }, + { + regex: /\bpub fn build_prompt_visible_tool_manifest_definitions\b/, + message: 'missing prompt-visible manifest definition builder', + }, { regex: /\bpub fn build_get_tool_spec_description\b/, message: 'missing pure GetToolSpec prompt description contract', }, + { + regex: /\bpub struct GetToolSpecCollapsedToolSummary\b/, + message: 'missing pure GetToolSpec collapsed catalog summary', + }, + { + regex: /\bpub fn build_get_tool_spec_catalog_description\b/, + message: 'missing pure GetToolSpec catalog description builder', + }, { regex: /\bpub fn get_tool_spec_input_schema\b/, message: 'missing pure GetToolSpec input schema contract', @@ -1177,6 +1201,14 @@ const requiredContentRules = [ regex: /\bpub struct StaticToolProviderGroup\b/, message: 'missing generic static provider group container', }, + { + regex: /\bpub fn is_tool_collapsed\b/, + message: 'missing generic collapsed-tool registry query', + }, + { + regex: /\bpub fn get_collapsed_tool_names\b/, + message: 'missing generic collapsed-tool registry catalog query', + }, ], }, { @@ -1228,8 +1260,8 @@ const requiredContentRules = [ message: 'missing collapsed-tool manifest migration baseline', }, { - regex: /\bToolExposure::Collapsed\b/, - message: 'missing collapsed exposure lookup', + regex: /\binner\.is_tool_collapsed\b/, + message: 'missing collapsed exposure lookup delegation', }, ], }, @@ -1330,13 +1362,17 @@ const requiredContentRules = [ regex: /\bresolve_tool_manifest_policy\b/, message: 'missing agent-tools manifest policy contract use', }, + { + regex: /\bbuild_tool_manifest_policy_tools\b/, + message: 'missing agent-tools manifest policy input builder delegation', + }, { regex: /\bcollapsed_tool_names\b/, message: 'missing collapsed-tool name tracking', }, { - regex: /\bbuild_collapsed_tool_stub_definition\b/, - message: 'missing collapsed-tool prompt stub contract use', + regex: /\bbuild_prompt_visible_tool_manifest_definitions\b/, + message: 'missing agent-tools prompt-visible manifest builder delegation', }, { regex: /\bmanifest_preserves_explicit_get_tool_spec_runtime_contract\b/, @@ -1365,6 +1401,10 @@ const requiredContentRules = [ regex: /\bbuild_get_tool_spec_assistant_detail\b/, message: 'missing agent-tools GetToolSpec assistant detail helper delegation', }, + { + regex: /\bbuild_get_tool_spec_catalog_description\b/, + message: 'missing agent-tools GetToolSpec catalog description helper delegation', + }, { regex: /\bvalidate_get_tool_spec_input\b/, message: 'missing agent-tools GetToolSpec validation helper delegation', @@ -3006,14 +3046,22 @@ function runManifestParserSelfTest() { 'ToolExposure', 'ToolManifestPolicyTool', 'resolve_tool_manifest_policy', + 'default_exposure', + 'build_tool_manifest_policy_tools', 'build_collapsed_tool_stub_definition', + 'PromptVisibleToolManifestItem', + 'build_prompt_visible_tool_manifest_definitions', 'build_get_tool_spec_description', + 'GetToolSpecCollapsedToolSummary', + 'build_get_tool_spec_catalog_description', 'get_tool_spec_input_schema', 'validate_get_tool_spec_input', 'build_get_tool_spec_assistant_detail', 'GetToolSpecLoadObservation', 'collect_loaded_collapsed_tool_names', 'sort_tool_manifest_definitions', + 'is_tool_collapsed', + 'get_collapsed_tool_names', ], }, { @@ -3136,7 +3184,8 @@ function runManifestParserSelfTest() { 'resolve_tool_manifest', 'GET_TOOL_SPEC_TOOL_NAME', 'resolve_tool_manifest_policy', - 'build_collapsed_tool_stub_definition', + 'build_tool_manifest_policy_tools', + 'build_prompt_visible_tool_manifest_definitions', 'collapsed_tool_names', ], }, @@ -3147,6 +3196,7 @@ function runManifestParserSelfTest() { 'unlocked_collapsed_tools', 'already_loaded', 'build_get_tool_spec_assistant_detail', + 'build_get_tool_spec_catalog_description', 'validate_get_tool_spec_input', ], }, diff --git a/src/crates/agent-tools/src/framework.rs b/src/crates/agent-tools/src/framework.rs index ffc5bdda4..fa95ac4b8 100644 --- a/src/crates/agent-tools/src/framework.rs +++ b/src/crates/agent-tools/src/framework.rs @@ -86,6 +86,15 @@ impl ToolManifestDefinition { } } +#[derive(Debug, Clone, PartialEq)] +pub enum PromptVisibleToolManifestItem { + Expanded(ToolManifestDefinition), + Collapsed { + name: String, + short_description: String, + }, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct ToolManifestPolicyTool { pub name: String, @@ -151,6 +160,23 @@ pub fn resolve_tool_manifest_policy( } } +pub fn build_tool_manifest_policy_tools( + tool_snapshot: &[ToolRef], + available_tool_names: &HashSet, +) -> Vec { + tool_snapshot + .iter() + .map(|tool| { + let name = tool.name().to_string(); + ToolManifestPolicyTool { + available: available_tool_names.contains(&name), + default_exposure: tool.default_exposure(), + name, + } + }) + .collect() +} + pub fn build_collapsed_tool_stub_definition( tool_name: &str, short_description: &str, @@ -185,6 +211,30 @@ pub fn build_get_tool_spec_collapsed_tool_entry( format!("- {}: {}", tool_name, short_description) } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GetToolSpecCollapsedToolSummary { + pub name: String, + pub short_description: String, +} + +pub fn build_get_tool_spec_catalog_description( + collapsed_tools: &[GetToolSpecCollapsedToolSummary], +) -> String { + let collapsed_tools_list = if collapsed_tools.is_empty() { + "No additional tools are available.".to_string() + } else { + collapsed_tools + .iter() + .map(|tool| { + build_get_tool_spec_collapsed_tool_entry(&tool.name, &tool.short_description) + }) + .collect::>() + .join("\n") + }; + + build_get_tool_spec_description(&collapsed_tools_list) +} + pub fn build_get_tool_spec_description(collapsed_tools_list: &str) -> String { format!( r#"Read usage instructions for additional tools. @@ -332,6 +382,23 @@ pub fn sort_tool_manifest_definitions(tool_definitions: &mut [ToolManifestDefini tool_definitions.sort_by_key(|tool| tool_manifest_sort_rank(&tool.name)); } +pub fn build_prompt_visible_tool_manifest_definitions( + items: &[PromptVisibleToolManifestItem], +) -> Vec { + let mut definitions = items + .iter() + .map(|item| match item { + PromptVisibleToolManifestItem::Expanded(definition) => definition.clone(), + PromptVisibleToolManifestItem::Collapsed { + name, + short_description, + } => build_collapsed_tool_stub_definition(name, short_description), + }) + .collect::>(); + sort_tool_manifest_definitions(&mut definitions); + definitions +} + #[async_trait] pub trait ToolRegistryItem: Send + Sync { fn name(&self) -> &str; @@ -340,6 +407,10 @@ pub trait ToolRegistryItem: Send + Sync { fn input_schema(&self) -> Value; + fn default_exposure(&self) -> ToolExposure { + ToolExposure::Expanded + } + async fn input_schema_for_model(&self) -> Value { self.input_schema() } @@ -508,6 +579,21 @@ impl ToolRegistry { .map(|metadata| metadata.info.clone()) } + pub fn is_tool_collapsed(&self, name: &str) -> bool { + self.tools + .get(name) + .is_some_and(|tool| tool.default_exposure() == ToolExposure::Collapsed) + } + + pub fn get_collapsed_tool_names(&self) -> Vec { + self.tools + .iter() + .filter_map(|(name, tool)| { + (tool.default_exposure() == ToolExposure::Collapsed).then(|| name.clone()) + }) + .collect() + } + pub fn get_tool_names(&self) -> Vec { self.tools.keys().cloned().collect() } diff --git a/src/crates/agent-tools/src/lib.rs b/src/crates/agent-tools/src/lib.rs index 2e4ebc3f5..73f558306 100644 --- a/src/crates/agent-tools/src/lib.rs +++ b/src/crates/agent-tools/src/lib.rs @@ -11,15 +11,18 @@ pub use bitfun_runtime_ports::{ DynamicToolDescriptor, DynamicToolProvider, PortError, PortErrorKind, PortResult, ToolDecorator, }; pub use framework::{ - DynamicMcpToolInfo, DynamicToolInfo, GET_TOOL_SPEC_TOOL_NAME, GetToolSpecLoadObservation, - PortableToolContextProvider, StaticToolProvider, StaticToolProviderGroup, ToolContextFacts, - ToolExposure, ToolManifestDefinition, ToolManifestPolicyResolution, ToolManifestPolicyTool, - ToolPathBackend, ToolPathOperation, ToolPathPolicy, ToolPathResolution, ToolRef, ToolRegistry, - ToolRegistryItem, ToolRenderOptions, ToolRestrictionError, ToolResult, ToolRuntimeRestrictions, + DynamicMcpToolInfo, DynamicToolInfo, GET_TOOL_SPEC_TOOL_NAME, GetToolSpecCollapsedToolSummary, + GetToolSpecLoadObservation, PortableToolContextProvider, PromptVisibleToolManifestItem, + StaticToolProvider, StaticToolProviderGroup, ToolContextFacts, ToolExposure, + ToolManifestDefinition, ToolManifestPolicyResolution, ToolManifestPolicyTool, ToolPathBackend, + ToolPathOperation, ToolPathPolicy, ToolPathResolution, ToolRef, ToolRegistry, ToolRegistryItem, + ToolRenderOptions, ToolRestrictionError, ToolResult, ToolRuntimeRestrictions, ToolWorkspaceKind, ValidationResult, build_collapsed_tool_stub_definition, - build_get_tool_spec_assistant_detail, build_get_tool_spec_collapsed_tool_entry, - build_get_tool_spec_description, build_get_tool_spec_duplicate_load_hint, - collect_loaded_collapsed_tool_names, get_tool_spec_input_schema, resolve_tool_manifest_policy, - sort_tool_manifest_definitions, tool_manifest_sort_rank, validate_get_tool_spec_input, + build_get_tool_spec_assistant_detail, build_get_tool_spec_catalog_description, + build_get_tool_spec_collapsed_tool_entry, build_get_tool_spec_description, + build_get_tool_spec_duplicate_load_hint, build_prompt_visible_tool_manifest_definitions, + build_tool_manifest_policy_tools, collect_loaded_collapsed_tool_names, + get_tool_spec_input_schema, resolve_tool_manifest_policy, sort_tool_manifest_definitions, + tool_manifest_sort_rank, validate_get_tool_spec_input, }; pub use input_validator::InputValidator; diff --git a/src/crates/agent-tools/tests/tool_contracts.rs b/src/crates/agent-tools/tests/tool_contracts.rs index e82145eda..b8c8e6ad4 100644 --- a/src/crates/agent-tools/tests/tool_contracts.rs +++ b/src/crates/agent-tools/tests/tool_contracts.rs @@ -1,13 +1,14 @@ use bitfun_agent_tools::{ - DynamicMcpToolInfo, DynamicToolInfo, GET_TOOL_SPEC_TOOL_NAME, GetToolSpecLoadObservation, - InputValidator, ToolContextFacts, ToolExposure, ToolImageAttachment, ToolManifestDefinition, - ToolManifestPolicyTool, ToolPathBackend, ToolPathResolution, ToolRenderOptions, ToolResult, - ToolRuntimeRestrictions, ToolWorkspaceKind, ValidationResult, - build_collapsed_tool_stub_definition, build_get_tool_spec_assistant_detail, + DynamicMcpToolInfo, DynamicToolInfo, GET_TOOL_SPEC_TOOL_NAME, GetToolSpecCollapsedToolSummary, + GetToolSpecLoadObservation, InputValidator, PromptVisibleToolManifestItem, ToolContextFacts, + ToolExposure, ToolImageAttachment, ToolManifestDefinition, ToolManifestPolicyTool, + ToolPathBackend, ToolPathResolution, ToolRenderOptions, ToolResult, ToolRuntimeRestrictions, + ToolWorkspaceKind, ValidationResult, build_collapsed_tool_stub_definition, + build_get_tool_spec_assistant_detail, build_get_tool_spec_catalog_description, build_get_tool_spec_collapsed_tool_entry, build_get_tool_spec_description, - build_get_tool_spec_duplicate_load_hint, collect_loaded_collapsed_tool_names, - get_tool_spec_input_schema, resolve_tool_manifest_policy, sort_tool_manifest_definitions, - validate_get_tool_spec_input, + build_get_tool_spec_duplicate_load_hint, build_prompt_visible_tool_manifest_definitions, + collect_loaded_collapsed_tool_names, get_tool_spec_input_schema, resolve_tool_manifest_policy, + sort_tool_manifest_definitions, validate_get_tool_spec_input, }; use bitfun_agent_tools::{ DynamicToolDescriptor, DynamicToolProvider, PortResult, PortableToolContextProvider, @@ -509,6 +510,44 @@ fn tool_manifest_sorting_preserves_prompt_visible_order() { ); } +#[test] +fn prompt_visible_manifest_builder_preserves_expanded_and_collapsed_contract() { + let definitions = build_prompt_visible_tool_manifest_definitions(&[ + PromptVisibleToolManifestItem::Collapsed { + name: "WebFetch".to_string(), + short_description: "Fetch readable web content.".to_string(), + }, + PromptVisibleToolManifestItem::Expanded(ToolManifestDefinition::new( + "Read", + "Read files from the workspace.", + json!({ "type": "object", "properties": { "path": { "type": "string" } } }), + )), + PromptVisibleToolManifestItem::Expanded(ToolManifestDefinition::new( + "Bash", + "Run shell commands.", + json!({ "type": "object", "properties": { "command": { "type": "string" } } }), + )), + ]); + + assert_eq!( + definitions + .iter() + .map(|definition| definition.name.as_str()) + .collect::>(), + vec!["Bash", "Read", "WebFetch"] + ); + assert_eq!(definitions[0].description, "Run shell commands."); + assert_eq!( + definitions[0].parameters["properties"]["command"]["type"], + json!("string") + ); + assert!( + definitions[2] + .description + .contains("First call `GetToolSpec` with {\"tool_name\":\"WebFetch\"}") + ); +} + #[test] fn get_tool_spec_contract_preserves_input_schema_and_validation() { let schema = get_tool_spec_input_schema(); @@ -559,6 +598,26 @@ fn get_tool_spec_contract_preserves_collapsed_prompt_description() { assert!(description.contains("call `GetToolSpec` with `{\"tool_name\":\"Git\"}`")); } +#[test] +fn get_tool_spec_catalog_description_uses_summary_entries_and_empty_fallback() { + let description = build_get_tool_spec_catalog_description(&[ + GetToolSpecCollapsedToolSummary { + name: "Git".to_string(), + short_description: "Inspect the repository.".to_string(), + }, + GetToolSpecCollapsedToolSummary { + name: "WebFetch".to_string(), + short_description: "Fetch readable web content.".to_string(), + }, + ]); + + assert!(description.contains("- Git: Inspect the repository.")); + assert!(description.contains("- WebFetch: Fetch readable web content.")); + + let empty = build_get_tool_spec_catalog_description(&[]); + assert!(empty.contains("No additional tools are available.")); +} + #[test] fn get_tool_spec_contract_escapes_assistant_detail_for_xml_sections() { let detail = build_get_tool_spec_assistant_detail( @@ -590,6 +649,7 @@ fn get_tool_spec_contract_preserves_duplicate_load_hint() { struct RegistryMarkerTool { name: String, provider_id: Option, + exposure: ToolExposure, } #[async_trait::async_trait] @@ -606,6 +666,10 @@ impl ToolRegistryItem for RegistryMarkerTool { json!({ "type": "object" }) } + fn default_exposure(&self) -> ToolExposure { + self.exposure + } + async fn input_schema_for_model(&self) -> serde_json::Value { self.input_schema() } @@ -622,9 +686,18 @@ impl ToolRegistryItem for RegistryMarkerTool { } fn registry_marker_tool(name: &str, provider_id: Option<&str>) -> Arc { + registry_marker_tool_with_exposure(name, provider_id, ToolExposure::Expanded) +} + +fn registry_marker_tool_with_exposure( + name: &str, + provider_id: Option<&str>, + exposure: ToolExposure, +) -> Arc { Arc::new(RegistryMarkerTool { name: name.to_string(), provider_id: provider_id.map(str::to_string), + exposure, }) } @@ -671,6 +744,7 @@ impl ToolDecorator> for RegistryMarkerDecorator { Arc::new(RegistryMarkerTool { name: format!("decorated_{}", tool.name), provider_id: tool.provider_id.clone(), + exposure: tool.exposure, }) } } @@ -711,6 +785,65 @@ fn generic_tool_registry_applies_decorator_to_static_provider_tools() { ); } +#[test] +fn generic_tool_registry_preserves_exposure_catalog_contract() { + let mut registry = ToolRegistry::new(); + registry.register_tool(registry_marker_tool("Read", None)); + registry.register_tool(registry_marker_tool_with_exposure( + "WebFetch", + None, + ToolExposure::Collapsed, + )); + registry.register_tool(registry_marker_tool_with_exposure( + "Git", + None, + ToolExposure::Collapsed, + )); + + assert!(!registry.is_tool_collapsed("Read")); + assert!(registry.is_tool_collapsed("WebFetch")); + assert_eq!( + registry.get_collapsed_tool_names(), + vec!["WebFetch".to_string(), "Git".to_string()] + ); +} + +#[test] +fn manifest_policy_tools_from_registry_snapshot_preserve_exposure_and_availability() { + let tools = vec![ + registry_marker_tool("Read", None), + registry_marker_tool_with_exposure("WebFetch", None, ToolExposure::Collapsed), + registry_marker_tool_with_exposure("Git", None, ToolExposure::Collapsed), + ]; + let available_tool_names = ["Read".to_string(), "Git".to_string()] + .into_iter() + .collect(); + + let policy_tools = + bitfun_agent_tools::build_tool_manifest_policy_tools(&tools, &available_tool_names); + + assert_eq!( + policy_tools, + vec![ + ToolManifestPolicyTool { + name: "Read".to_string(), + default_exposure: ToolExposure::Expanded, + available: true, + }, + ToolManifestPolicyTool { + name: "WebFetch".to_string(), + default_exposure: ToolExposure::Collapsed, + available: false, + }, + ToolManifestPolicyTool { + name: "Git".to_string(), + default_exposure: ToolExposure::Collapsed, + available: true, + }, + ] + ); +} + #[tokio::test] async fn generic_tool_registry_preserves_dynamic_descriptor_contract() { let mut registry = ToolRegistry::new(); diff --git a/src/crates/core/src/agentic/tools/implementations/get_tool_spec_tool.rs b/src/crates/core/src/agentic/tools/implementations/get_tool_spec_tool.rs index eccd5fcba..e286ddcd7 100644 --- a/src/crates/core/src/agentic/tools/implementations/get_tool_spec_tool.rs +++ b/src/crates/core/src/agentic/tools/implementations/get_tool_spec_tool.rs @@ -9,12 +9,12 @@ use crate::agentic::tools::resolve_visible_tools; use crate::util::errors::{BitFunError, BitFunResult}; use async_trait::async_trait; use bitfun_agent_tools::{ - build_get_tool_spec_assistant_detail, build_get_tool_spec_collapsed_tool_entry, - build_get_tool_spec_description, build_get_tool_spec_duplicate_load_hint, - get_tool_spec_input_schema, validate_get_tool_spec_input, GET_TOOL_SPEC_TOOL_NAME, + GET_TOOL_SPEC_TOOL_NAME, GetToolSpecCollapsedToolSummary, build_get_tool_spec_assistant_detail, + build_get_tool_spec_catalog_description, build_get_tool_spec_duplicate_load_hint, + get_tool_spec_input_schema, validate_get_tool_spec_input, }; use log::debug; -use serde_json::{json, Value}; +use serde_json::{Value, json}; use std::sync::Arc; pub struct GetToolSpecTool; @@ -42,20 +42,20 @@ impl GetToolSpecTool { } async fn build_collapsed_tools_description(&self, context: Option<&ToolUseContext>) -> String { - let mut entries = Vec::new(); + let mut summaries = Vec::new(); if let Some(context) = context { if let Ok(collapsed_tools) = self.get_contextual_collapsed_tools(context).await { for tool in collapsed_tools { - entries.push(build_get_tool_spec_collapsed_tool_entry( - tool.name(), - &tool.short_description(), - )); + summaries.push(GetToolSpecCollapsedToolSummary { + name: tool.name().to_string(), + short_description: tool.short_description(), + }); } } } else { let registry = get_global_tool_registry(); - let collapsed_tools = { + summaries = { let registry = registry.read().await; registry .get_all_tools() @@ -64,25 +64,15 @@ impl GetToolSpecTool { tool.default_exposure() == crate::agentic::tools::framework::ToolExposure::Collapsed }) - .map(|tool| (tool.name().to_string(), tool.short_description())) + .map(|tool| GetToolSpecCollapsedToolSummary { + name: tool.name().to_string(), + short_description: tool.short_description(), + }) .collect::>() }; - - for (tool_name, short_description) in collapsed_tools { - entries.push(build_get_tool_spec_collapsed_tool_entry( - &tool_name, - &short_description, - )); - } } - let collapsed_tools_list = if entries.is_empty() { - "No additional tools are available.".to_string() - } else { - entries.join("\n") - }; - - build_get_tool_spec_description(&collapsed_tools_list) + build_get_tool_spec_catalog_description(&summaries) } async fn build_tool_detail( @@ -234,14 +224,14 @@ impl Tool for GetToolSpecTool { #[cfg(test)] mod tests { use super::GetToolSpecTool; + use crate::agentic::tools::ToolRuntimeRestrictions; use crate::agentic::tools::framework::{ Tool, ToolExposure, ToolResult, ToolUseContext, ValidationResult, }; use crate::agentic::tools::registry::get_global_tool_registry; - use crate::agentic::tools::ToolRuntimeRestrictions; use crate::util::errors::BitFunResult; use async_trait::async_trait; - use serde_json::{json, Value}; + use serde_json::{Value, json}; use std::collections::HashMap; use std::sync::Arc; @@ -304,7 +294,9 @@ mod tests { .await; assert!(description.contains(&format!("- {}: Concise catalog entry.", tool_name))); - assert!(!description.contains(&format!("- {}: Verbose description first line.", tool_name))); + assert!( + !description.contains(&format!("- {}: Verbose description first line.", tool_name)) + ); } #[tokio::test] @@ -340,9 +332,11 @@ mod tests { assert_eq!(data["tool_name"], "WebFetch"); assert_eq!(data["already_loaded"], true); - assert!(result_for_assistant - .as_deref() - .unwrap_or_default() - .contains("already loaded in the current conversation")); + assert!( + result_for_assistant + .as_deref() + .unwrap_or_default() + .contains("already loaded in the current conversation") + ); } } diff --git a/src/crates/core/src/agentic/tools/manifest_resolver.rs b/src/crates/core/src/agentic/tools/manifest_resolver.rs index 1a9014163..3ec9eab69 100644 --- a/src/crates/core/src/agentic/tools/manifest_resolver.rs +++ b/src/crates/core/src/agentic/tools/manifest_resolver.rs @@ -3,8 +3,9 @@ use crate::agentic::tools::framework::{Tool, ToolUseContext}; use crate::agentic::tools::registry::{GET_TOOL_SPEC_TOOL_NAME, get_global_tool_registry}; use crate::util::types::ToolDefinition; use bitfun_agent_tools::{ - ToolManifestDefinition, ToolManifestPolicyTool, build_collapsed_tool_stub_definition, - resolve_tool_manifest_policy, sort_tool_manifest_definitions, + PromptVisibleToolManifestItem, ToolManifestDefinition, + build_prompt_visible_tool_manifest_definitions, build_tool_manifest_policy_tools, + resolve_tool_manifest_policy, }; use std::collections::HashSet; use std::sync::Arc; @@ -32,17 +33,7 @@ fn build_visible_tools( exposure_overrides: &AgentToolPolicyOverrides, available_tool_names: &HashSet, ) -> ResolvedVisibleTools { - let policy_tools = tool_snapshot - .iter() - .map(|tool| { - let name = tool.name().to_string(); - ToolManifestPolicyTool { - available: available_tool_names.contains(&name), - default_exposure: tool.default_exposure(), - name, - } - }) - .collect::>(); + let policy_tools = build_tool_manifest_policy_tools(tool_snapshot, available_tool_names); let policy = resolve_tool_manifest_policy( &policy_tools, allowed_tools, @@ -113,7 +104,7 @@ pub async fn resolve_tool_manifest( ) -> ResolvedToolManifest { let visible_tools = resolve_visible_tools(allowed_tools, exposure_overrides, context).await; - let mut tool_definitions = Vec::with_capacity( + let mut manifest_items = Vec::with_capacity( visible_tools.expanded_tools.len() + visible_tools.collapsed_tools.len(), ); for tool in &visible_tools.expanded_tools { @@ -125,21 +116,19 @@ pub async fn resolve_tool_manifest( .input_schema_for_model_with_context(Some(context)) .await; - tool_definitions.push(ToolManifestDefinition::new( - tool.name().to_string(), - description, - parameters, + manifest_items.push(PromptVisibleToolManifestItem::Expanded( + ToolManifestDefinition::new(tool.name().to_string(), description, parameters), )); } for tool in &visible_tools.collapsed_tools { - tool_definitions.push(build_collapsed_tool_stub_definition( - tool.name(), - &tool.short_description(), - )); + manifest_items.push(PromptVisibleToolManifestItem::Collapsed { + name: tool.name().to_string(), + short_description: tool.short_description(), + }); } - sort_tool_manifest_definitions(&mut tool_definitions); + let tool_definitions = build_prompt_visible_tool_manifest_definitions(&manifest_items); ResolvedToolManifest { allowed_tool_names: visible_tools.allowed_tool_names, diff --git a/src/crates/core/src/agentic/tools/registry.rs b/src/crates/core/src/agentic/tools/registry.rs index c3b7e041d..c43979447 100644 --- a/src/crates/core/src/agentic/tools/registry.rs +++ b/src/crates/core/src/agentic/tools/registry.rs @@ -152,16 +152,11 @@ impl ToolRegistry { } pub fn is_tool_collapsed(&self, name: &str) -> bool { - self.inner - .get_tool(name) - .is_some_and(|tool| tool.default_exposure() == ToolExposure::Collapsed) + self.inner.is_tool_collapsed(name) } pub fn get_collapsed_tool_names(&self) -> Vec { - self.get_tool_names() - .into_iter() - .filter(|name| self.is_tool_collapsed(name)) - .collect() + self.inner.get_collapsed_tool_names() } /// Get all tool names @@ -202,6 +197,10 @@ impl ToolRegistryItem for dyn Tool { Tool::input_schema(self) } + fn default_exposure(&self) -> ToolExposure { + Tool::default_exposure(self) + } + async fn input_schema_for_model(&self) -> Value { Tool::input_schema_for_model(self).await }