diff --git a/docs/architecture/core-decomposition.md b/docs/architecture/core-decomposition.md index 7e12a42ae..03621004e 100644 --- a/docs/architecture/core-decomposition.md +++ b/docs/architecture/core-decomposition.md @@ -66,7 +66,7 @@ Rust 编译和链接面。 | `bitfun-agent-stream` | Stream 聚合和 stream-focused 测试 | done:stream 聚合已独立 | | `bitfun-runtime-ports` | 面向 service/agent 边界的轻量跨层 DTO 和 trait | partial:DTO/trait-only 边界已建立,包含 agent submission/transcript/cancel、remote state、runtime event 与 remote image attachment 契约;不拥有 runtime 实现 | | `bitfun-agent-runtime` | Sessions、execution、coordination、agent system | target:crate 尚不存在,agent runtime 仍在 core | -| `bitfun-agent-tools` | 轻量 tool DTO / contract、portable tool context facts / provider、runtime restriction、pure manifest/exposure contract、generic registry / static-provider / dynamic-provider container | partial:runtime manifest assembly / context filtering、`ToolUseContext`、`GetToolSpec` 执行和 concrete tools 仍在 core;core 当前仅把内置工具列表收敛为 core-owned static provider group,并只通过 `PortableToolContextProvider` 提供 `ToolContextFacts` 只读投影 | +| `bitfun-agent-tools` | 轻量 tool DTO / contract、portable tool context facts / provider、runtime restriction、pure manifest/exposure and GetToolSpec presentation/schema contract、generic registry / static-provider / dynamic-provider container | partial:runtime manifest assembly / context filtering、`ToolUseContext`、`GetToolSpec` 执行和 concrete tools 仍在 core;core 当前仅把内置工具列表收敛为 core-owned static provider group,并只通过 `PortableToolContextProvider` 提供 `ToolContextFacts` 只读投影 | | `bitfun-tool-packs` | 由 feature group 隔离的具体工具实现 | target/scaffold:仅提供 basic / git / mcp / browser-web / computer-use / image-analysis / miniapp / agent-control feature-group 元数据,不得声明 concrete tools 已迁移 | | `bitfun-services-core` | Config、session、workspace、storage、filesystem、system services | partial:部分 pure helper 已迁出;config/workspace/filesystem runtime 多数仍在 core | | `bitfun-services-integrations` | Git、MCP、remote SSH、remote connect、file watch integrations | partial:MCP runtime 已迁入;remote SSH 仍只迁移低风险 contracts/helpers;remote-connect 已拥有 wire DTO、request builder、tracker state / registry lifecycle 与 tracker event reduction,dispatcher/product execution 仍在 core | @@ -136,7 +136,8 @@ 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 等纯契约可由 `bitfun-agent-tools` 拥有;但 + collapsed stub、manifest ordering、GetToolSpec prompt / schema / assistant-detail + rendering 等纯契约可由 `bitfun-agent-tools` 拥有;但 `manifest_resolver`、产品 registry snapshot、collapsed-tool catalog、context-aware tool schema/description、`GetToolSpec` 执行和 `ToolUseContext.unlocked_collapsed_tools` 暂时仍属于 `bitfun-core` product tool runtime。继续迁移前必须证明 prompt-visible @@ -240,10 +241,12 @@ owner 边界,否则不要把一个 feature group 继续拆成更小的 crate - 已合入的 semantic baseline 已补 config failure、catalog replacement invalidation、沿用既有 list-changed helper baseline、dynamic manifest order/metadata、tool manifest / `GetToolSpec`、MiniApp storage layout adapter 等价和 remote search fallback gate;这些都是 behavior-locking tests,不移动 runtime owner。 -- 已合入的 tool manifest contract closure 只把 `ToolExposure`、`GetToolSpec` 名称、纯 manifest - policy、collapsed prompt stub 与 prompt-visible ordering 归入 `bitfun-agent-tools`,并让 core - runtime manifest resolver 委托这些纯 helper;不迁移 `ToolUseContext`、registry snapshot、 - context-aware schema/description、`GetToolSpecTool` 执行、unlock state 或 concrete tool implementation。 +- 本轮 tool manifest presentation contract closure 只把 `ToolExposure`、`GetToolSpec` 名称、纯 manifest + policy、collapsed prompt stub、prompt-visible ordering、GetToolSpec input schema / validation、 + prompt description、assistant-detail rendering 与 duplicate-load hint 归入 `bitfun-agent-tools`, + 并让 core runtime manifest resolver / `GetToolSpecTool` 委托这些纯 helper;不迁移 + `ToolUseContext`、registry snapshot、context-aware tool discovery、`GetToolSpecTool` 执行、 + unlock state 或 concrete tool implementation。 该阶段闭环后,后续不应再插入 baseline-only PR 才开始 runtime owner 迁移;下一组 PR 应直接以 单一 owner 为单位移动实际 runtime,并沿用本节等价测试和边界脚本。 - 已合入的 `Services/Product Runtime Owner Closure` 只收口已经有 port/contract 保护的低风险 owner: diff --git a/docs/plans/core-decomposition-plan.md b/docs/plans/core-decomposition-plan.md index bd2d749d5..093c5a549 100644 --- a/docs/plans/core-decomposition-plan.md +++ b/docs/plans/core-decomposition-plan.md @@ -1018,7 +1018,7 @@ cargo check -p bitfun-cli **任务:** - [x] 抽出 tool result、validation、dynamic metadata、runtime restriction、path resolution DTO,以及 generic registry / dynamic provider container 到 `agent-tools`。 -- [x] 抽出纯 manifest/exposure 契约到 `agent-tools`:`ToolExposure`、`GetToolSpec` 名称、纯 manifest policy、collapsed prompt stub 与 prompt-visible ordering;core 继续拥有 runtime assembly。 +- [x] 抽出纯 manifest/exposure / GetToolSpec presentation 契约到 `agent-tools`:`ToolExposure`、`GetToolSpec` 名称、纯 manifest policy、collapsed prompt stub、prompt-visible ordering、GetToolSpec prompt description / input schema / validation / assistant-detail rendering / duplicate-load hint;core 继续拥有 runtime assembly 和执行 owner。 - [x] 抽出 static tool provider 安装合约到 `agent-tools`,并将 core 内置工具列表收敛到 `static_providers.rs` 的 core-owned provider groups;不迁移 concrete tool implementation。 - [x] 抽出 `ToolContextFacts` / `ToolWorkspaceKind` 轻量上下文事实契约,并由 core `ToolUseContext` 提供只读投影;workspace root fact 使用 session identity 的 logical path,remote 场景输出 normalized remote root;不迁移 collapsed unlock state、runtime handles、workspace services 或 cancellation token。 - [x] 增加 `PortableToolContextProvider` 只读 facts provider 合约,并由 core `ToolUseContext` 兼容实现;该合约不暴露 workspace services、cancellation token、computer-use host 或 collapsed unlock state。 @@ -1056,18 +1056,19 @@ pub fn create_tool_registry() -> ToolRegistry { **当前安全迁移状态(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` 安装合约。dynamic tool provider / decorator contract 已通过 `agent-tools` 提供兼容 re-export,原 `runtime-ports` 路径保持可用;core 旧路径继续 re-export,并只保留 `BitFunError` 映射、路径 containment helper 与 `ToolUseContext` 到 facts 的只读投影。 -- `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 契约由 `bitfun-agent-tools` 拥有。 +- `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 和不依赖具体工具的 provider 安装合约,core product tool runtime 继续负责产品 registry snapshot、context-aware schema/description、unlock state 和执行路径。 +- 已通过 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 的纯契约已可由 `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 与 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 收尾中顺带执行。 **验证:** ```powershell cargo test -p bitfun-agent-tools +cargo test -p bitfun-agent-tools get_tool_spec_contract --test tool_contracts cargo test -p bitfun-tool-packs --features basic cargo check -p bitfun-tool-packs --features product-full cargo check -p bitfun-core --features product-full @@ -1627,7 +1628,7 @@ P2 后产品表面契约轨道(contract-only): - 文档校正:P2 后补充文档中的 MCP runtime step 已由本 PR2 闭环;后续 MCP 相关工作只保留 concrete tool implementation 迁移或 product registry / manifest assembly,不再重复迁移 config/process/transport lifecycle。 3. 已完成:remote-connect tracker / wire / pure policy owner slice:产品表面 DTO、remote command/response wire DTO、remote model catalog DTO、poll response assembly / model catalog poll delta、remote chat/image/tool/session wire DTO、relay/bot session/submission request builder、remote image attachment/request DTO、`AgentTurnCancellationPort`、`RemoteControlStatePort`、`RuntimeEventSink`、`RemoteSessionStateTracker`、`RemoteSessionTrackerRegistry`、`TrackerEvent`、legacy image context fallback / preference、restore target decision、cancel decision 与 remote file transfer size/chunk/name policy 已具备 owner/port 契约;core 仍保留 tracker host adapter、`ImageContextData` adapter、file IO/path resolution、dispatcher/product execution。 - 本轮收口:remote-connect 在当前批次以 tracker / wire / pure policy / registry lifecycle 归 owner crate、dispatcher / product execution 显式保留 core-owned 闭环;若未来继续迁移完整 dialog submission、terminal pre-warm、file IO/path resolution 或 `ImageContextData` adapter,必须另起 port/provider 设计与行为等价评审,不得混入 tool/provider owner 化。 -4. 已完成本轮可提交闭环:agent tools + `tool-packs` owner 化低风险部分。纯 tool contract/provider metadata、runtime restriction DTO、path resolution DTO、generic tool registry / static-provider / dynamic-provider container、`PortableToolContextProvider` 只读 facts provider,以及纯 manifest/exposure 契约已迁入 `bitfun-agent-tools`,并为 dynamic provider contract 提供 `agent-tools` 兼容 re-export;core tool runtime 保留 core-owned product provider groups、snapshot decorator、`dyn Tool` 适配、runtime manifest assembly / context filtering 和 `GetToolSpec` 执行。`tool-packs` 当前只提供计划内 feature-group 元数据,不注册或迁移具体工具。`ToolUseContext`、runtime manifest assembly / `GetToolSpec` 执行与 concrete tool implementation 按 feature group 外移需要新的 service port/provider 设计,必须保持 builtin/readonly/dynamic manifest、expanded/collapsed exposure、prompt stub、unlock state、snapshot wrapping、runtime restrictions、cancellation 与 Deep Review tool flow 等价,作为后续高风险迁移单独审视。 +4. 已完成本轮可提交闭环:agent tools + `tool-packs` owner 化低风险部分。纯 tool contract/provider metadata、runtime restriction DTO、path resolution DTO、generic tool registry / static-provider / dynamic-provider container、`PortableToolContextProvider` 只读 facts provider、纯 manifest/exposure 契约,以及 GetToolSpec presentation/schema helper 已迁入 `bitfun-agent-tools`,并为 dynamic provider contract 提供 `agent-tools` 兼容 re-export;core tool runtime 保留 core-owned product provider groups、snapshot decorator、`dyn Tool` 适配、runtime manifest assembly / context filtering 和 `GetToolSpec` 执行。`tool-packs` 当前只提供计划内 feature-group 元数据,不注册或迁移具体工具。`ToolUseContext`、runtime manifest assembly / `GetToolSpec` 执行与 concrete tool implementation 按 feature group 外移需要新的 service port/provider 设计,必须保持 builtin/readonly/dynamic manifest、expanded/collapsed exposure、prompt stub、unlock state、snapshot wrapping、runtime restrictions、cancellation 与 Deep Review tool flow 等价,作为后续高风险迁移单独审视。 5. `product-domains` runtime + core facade finalization:迁移 miniapp runtime/compiler/builtin 与 function-agent 运行逻辑,最后把 `bitfun-core` 收敛为 facade + product runtime assembly;不在本 PR 中修改 `bitfun-core default = []` 或 per-product feature matrix。 `bitfun-core default = []`、per-product feature set、构建矩阵和 release 能力调整仍作为重构完成后的独立评估,不计入上述 5 个 PR。 @@ -1658,7 +1659,7 @@ P2 后产品表面契约轨道(contract-only): **P3 进入条件与最新主干补充(2026-05-19):** -- P3 只能在 P2 剩余迁移闭环后启动:重 service 迁移、`ToolUseContext` / runtime manifest assembly / `GetToolSpec` 执行 / concrete tool implementation 迁移、product registry / provider assembly、miniapp/function-agent 运行逻辑迁移都必须先完成或显式保留为 core-owned runtime;generic registry / static-provider / dynamic-provider container 和纯 manifest/exposure 契约已在 agent-tools 低风险外移中完成。 +- P3 只能在 P2 剩余迁移闭环后启动:重 service 迁移、`ToolUseContext` / runtime manifest assembly / `GetToolSpec` 执行 / concrete tool implementation 迁移、product registry / provider assembly、miniapp/function-agent 运行逻辑迁移都必须先完成或显式保留为 core-owned runtime;generic registry / static-provider / dynamic-provider container、纯 manifest/exposure 契约和 GetToolSpec presentation/schema helper 已在 agent-tools 低风险外移中完成。 - 最近 `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 过滤语义。 diff --git a/scripts/check-core-boundaries.mjs b/scripts/check-core-boundaries.mjs index cef810f05..a247305b9 100644 --- a/scripts/check-core-boundaries.mjs +++ b/scripts/check-core-boundaries.mjs @@ -1145,6 +1145,22 @@ const requiredContentRules = [ regex: /\bpub fn build_collapsed_tool_stub_definition\b/, message: 'missing collapsed-tool prompt stub contract', }, + { + regex: /\bpub fn build_get_tool_spec_description\b/, + message: 'missing pure GetToolSpec prompt description contract', + }, + { + regex: /\bpub fn get_tool_spec_input_schema\b/, + message: 'missing pure GetToolSpec input schema contract', + }, + { + regex: /\bpub fn validate_get_tool_spec_input\b/, + message: 'missing pure GetToolSpec input validation contract', + }, + { + regex: /\bpub fn build_get_tool_spec_assistant_detail\b/, + message: 'missing pure GetToolSpec assistant detail rendering contract', + }, { regex: /\bpub fn sort_tool_manifest_definitions\b/, message: 'missing prompt-visible manifest ordering helper', @@ -1325,6 +1341,14 @@ const requiredContentRules = [ regex: /\balready_loaded\b/, message: 'missing duplicate-load assistant result contract', }, + { + regex: /\bbuild_get_tool_spec_assistant_detail\b/, + message: 'missing agent-tools GetToolSpec assistant detail helper delegation', + }, + { + regex: /\bvalidate_get_tool_spec_input\b/, + message: 'missing agent-tools GetToolSpec validation helper delegation', + }, ], }, { @@ -2872,6 +2896,10 @@ function runManifestParserSelfTest() { 'ToolManifestPolicyTool', 'resolve_tool_manifest_policy', 'build_collapsed_tool_stub_definition', + 'build_get_tool_spec_description', + 'get_tool_spec_input_schema', + 'validate_get_tool_spec_input', + 'build_get_tool_spec_assistant_detail', 'sort_tool_manifest_definitions', ], }, @@ -3000,7 +3028,13 @@ function runManifestParserSelfTest() { }, { path: 'src/crates/core/src/agentic/tools/implementations/get_tool_spec_tool.rs', - contracts: ['GetToolSpecTool', 'unlocked_collapsed_tools', 'already_loaded'], + contracts: [ + 'GetToolSpecTool', + 'unlocked_collapsed_tools', + 'already_loaded', + 'build_get_tool_spec_assistant_detail', + 'validate_get_tool_spec_input', + ], }, { path: 'src/crates/core/src/agentic/tools/framework.rs', diff --git a/src/crates/agent-tools/src/framework.rs b/src/crates/agent-tools/src/framework.rs index a2c04cf32..c46bc0728 100644 --- a/src/crates/agent-tools/src/framework.rs +++ b/src/crates/agent-tools/src/framework.rs @@ -178,6 +178,102 @@ pub fn build_collapsed_tool_stub_definition( ) } +pub fn build_get_tool_spec_collapsed_tool_entry( + tool_name: &str, + short_description: &str, +) -> String { + format!("- {}: {}", tool_name, short_description) +} + +pub fn build_get_tool_spec_description(collapsed_tools_list: &str) -> String { + format!( + r#"Read usage instructions for additional tools. + +You have access to the additional tools listed below. These tools are collapsed: +their names may appear in the tool list, but you must not call them directly +until you have loaded their definition with GetToolSpec. + + +{} + + +Before using one of these tools, first call GetToolSpec with its exact tool name +to read its full description and input schema. If a direct call to a collapsed +tool fails with a message like "Tool 'Git' is collapsed", make the next tool +call `GetToolSpec` with `{{"tool_name":"Git"}}`, then retry the real tool after +reading the returned schema. + +After reading the returned definition, call the real tool directly using its own name. + +Do not call GetToolSpec again for a tool whose definition is already loaded in the current conversation. + +Example: +- Suppose the catalog includes a tool named `GetWeather` and you need to use it. +- First call `GetToolSpec` with `{{"tool_name":"GetWeather"}}` +- Then read the returned schema and call `GetWeather` itself with the appropriate arguments +"#, + collapsed_tools_list + ) +} + +pub fn get_tool_spec_input_schema() -> Value { + serde_json::json!({ + "type": "object", + "additionalProperties": false, + "required": ["tool_name"], + "properties": { + "tool_name": { + "type": "string", + "description": "Exact collapsed tool name to load, using the tool's canonical casing from the catalog (for example, \"Git\"). Do not pass a command such as \"git status\" or an operation such as \"status\" here." + } + } + }) +} + +pub fn validate_get_tool_spec_input(input: &Value) -> ValidationResult { + let Some(tool_name) = input.get("tool_name").and_then(|value| value.as_str()) else { + return ValidationResult { + result: false, + message: Some("tool_name is required and cannot be empty".to_string()), + error_code: Some(400), + meta: None, + }; + }; + + if tool_name.is_empty() { + return ValidationResult { + result: false, + message: Some("tool_name is required and cannot be empty".to_string()), + error_code: Some(400), + meta: None, + }; + } + + ValidationResult::default() +} + +pub fn build_get_tool_spec_duplicate_load_hint(tool_name: &str) -> String { + format!( + "Tool '{}' is already loaded in the current conversation. Do not call GetToolSpec again for it. Use '{}' directly.", + tool_name, tool_name + ) +} + +pub fn build_get_tool_spec_assistant_detail(description: &str, input_schema: &Value) -> String { + format!( + "\n{}\n\n\n{}\n", + escape_get_tool_spec_xml_text(description), + escape_get_tool_spec_xml_text(&input_schema.to_string()) + ) +} + +fn escape_get_tool_spec_xml_text(value: &str) -> String { + value + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") +} + pub fn tool_manifest_sort_rank(tool_name: &str) -> usize { match tool_name { "Task" => 1, diff --git a/src/crates/agent-tools/src/lib.rs b/src/crates/agent-tools/src/lib.rs index 67eab1384..909f38d53 100644 --- a/src/crates/agent-tools/src/lib.rs +++ b/src/crates/agent-tools/src/lib.rs @@ -11,12 +11,15 @@ pub use bitfun_runtime_ports::{ DynamicToolDescriptor, DynamicToolProvider, PortError, PortErrorKind, PortResult, ToolDecorator, }; pub use framework::{ - build_collapsed_tool_stub_definition, resolve_tool_manifest_policy, - sort_tool_manifest_definitions, tool_manifest_sort_rank, DynamicMcpToolInfo, DynamicToolInfo, - PortableToolContextProvider, StaticToolProvider, ToolContextFacts, ToolExposure, - ToolManifestDefinition, ToolManifestPolicyResolution, ToolManifestPolicyTool, ToolPathBackend, - ToolPathOperation, ToolPathPolicy, ToolPathResolution, ToolRef, ToolRegistry, ToolRegistryItem, - ToolRenderOptions, ToolRestrictionError, ToolResult, ToolRuntimeRestrictions, - ToolWorkspaceKind, ValidationResult, GET_TOOL_SPEC_TOOL_NAME, + 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, get_tool_spec_input_schema, + resolve_tool_manifest_policy, sort_tool_manifest_definitions, tool_manifest_sort_rank, + validate_get_tool_spec_input, DynamicMcpToolInfo, DynamicToolInfo, PortableToolContextProvider, + StaticToolProvider, ToolContextFacts, ToolExposure, ToolManifestDefinition, + ToolManifestPolicyResolution, ToolManifestPolicyTool, ToolPathBackend, ToolPathOperation, + ToolPathPolicy, ToolPathResolution, ToolRef, ToolRegistry, ToolRegistryItem, ToolRenderOptions, + ToolRestrictionError, ToolResult, ToolRuntimeRestrictions, ToolWorkspaceKind, ValidationResult, + GET_TOOL_SPEC_TOOL_NAME, }; 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 1bb7b099d..959e32eec 100644 --- a/src/crates/agent-tools/tests/tool_contracts.rs +++ b/src/crates/agent-tools/tests/tool_contracts.rs @@ -1,9 +1,12 @@ use bitfun_agent_tools::{ - build_collapsed_tool_stub_definition, resolve_tool_manifest_policy, - sort_tool_manifest_definitions, DynamicMcpToolInfo, DynamicToolInfo, InputValidator, - ToolContextFacts, ToolExposure, ToolImageAttachment, ToolManifestDefinition, - ToolManifestPolicyTool, ToolPathBackend, ToolPathResolution, ToolRenderOptions, ToolResult, - ToolRuntimeRestrictions, ToolWorkspaceKind, ValidationResult, GET_TOOL_SPEC_TOOL_NAME, + 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, get_tool_spec_input_schema, + resolve_tool_manifest_policy, sort_tool_manifest_definitions, validate_get_tool_spec_input, + DynamicMcpToolInfo, DynamicToolInfo, InputValidator, ToolContextFacts, ToolExposure, + ToolImageAttachment, ToolManifestDefinition, ToolManifestPolicyTool, ToolPathBackend, + ToolPathResolution, ToolRenderOptions, ToolResult, ToolRuntimeRestrictions, ToolWorkspaceKind, + ValidationResult, GET_TOOL_SPEC_TOOL_NAME, }; use bitfun_agent_tools::{ DynamicToolDescriptor, DynamicToolProvider, PortResult, PortableToolContextProvider, @@ -464,6 +467,81 @@ fn tool_manifest_sorting_preserves_prompt_visible_order() { ); } +#[test] +fn get_tool_spec_contract_preserves_input_schema_and_validation() { + let schema = get_tool_spec_input_schema(); + + assert_eq!(schema["type"], "object"); + assert_eq!(schema["additionalProperties"], false); + assert_eq!(schema["required"], json!(["tool_name"])); + assert_eq!(schema["properties"]["tool_name"]["type"], "string"); + assert!(schema["properties"]["tool_name"]["description"] + .as_str() + .unwrap_or_default() + .contains("canonical casing")); + + let missing = validate_get_tool_spec_input(&json!({})); + assert!(!missing.result); + assert_eq!( + missing.message.as_deref(), + Some("tool_name is required and cannot be empty") + ); + assert_eq!(missing.error_code, Some(400)); + + let empty = validate_get_tool_spec_input(&json!({ "tool_name": "" })); + assert!(!empty.result); + assert_eq!( + empty.message.as_deref(), + Some("tool_name is required and cannot be empty") + ); + assert_eq!(empty.error_code, Some(400)); + + assert!(validate_get_tool_spec_input(&json!({ "tool_name": "Git" })).result); +} + +#[test] +fn get_tool_spec_contract_preserves_collapsed_prompt_description() { + let collapsed_tools_list = [ + build_get_tool_spec_collapsed_tool_entry("Git", "Inspect the repository."), + build_get_tool_spec_collapsed_tool_entry("WebFetch", "Fetch readable web content."), + ] + .join("\n"); + + let description = build_get_tool_spec_description(&collapsed_tools_list); + + assert!(description.contains("\n- Git: Inspect the repository.")); + assert!(description.contains("- WebFetch: Fetch readable web content.")); + assert!(description.contains("Do not call GetToolSpec again")); + assert!(description.contains("call `GetToolSpec` with `{\"tool_name\":\"Git\"}`")); +} + +#[test] +fn get_tool_spec_contract_escapes_assistant_detail_for_xml_sections() { + let detail = build_get_tool_spec_assistant_detail( + "Use & keep output valid.", + &json!({ + "type": "object", + "properties": { + "query": { + "description": "Match & symbols" + } + } + }), + ); + + assert!(detail.contains("\nUse <danger> & keep output valid.")); + assert!(detail.contains("\"description\":\"Match <tag> & symbols\"")); + assert!(!detail.contains("Use & keep output valid.")); +} + +#[test] +fn get_tool_spec_contract_preserves_duplicate_load_hint() { + assert_eq!( + build_get_tool_spec_duplicate_load_hint("WebFetch"), + "Tool 'WebFetch' is already loaded in the current conversation. Do not call GetToolSpec again for it. Use 'WebFetch' directly." + ); +} + #[derive(Clone)] struct RegistryMarkerTool { name: String, diff --git a/src/crates/core/AGENTS.md b/src/crates/core/AGENTS.md index 4001fb9a9..620148fc9 100644 --- a/src/crates/core/AGENTS.md +++ b/src/crates/core/AGENTS.md @@ -31,9 +31,10 @@ SessionManager → Session → DialogTurn → ModelRound - During core decomposition, `bitfun-core` is a compatibility facade and full product runtime assembly point. New modules should prefer the extracted owner crate listed in `docs/architecture/core-decomposition.md`. -- For tools, keep lightweight contracts, pure manifest/exposure contracts, and - portable tool context facts/provider plus generic registry / static-provider - / dynamic-provider container contracts in `bitfun-agent-tools`. Core tool +- For tools, keep lightweight contracts, pure manifest/exposure contracts, + GetToolSpec presentation/schema helpers, and portable tool context + facts/provider plus generic registry / static-provider / dynamic-provider + container contracts in `bitfun-agent-tools`. Core tool runtime should assemble product tool providers in `static_providers.rs`, adapt `dyn Tool`, apply snapshot decoration, and own runtime manifest assembly / context filtering plus on-demand spec discovery execution 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 ce2a91253..eccd5fcba 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 @@ -8,6 +8,11 @@ use crate::agentic::tools::registry::get_global_tool_registry; 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, +}; use log::debug; use serde_json::{json, Value}; use std::sync::Arc; @@ -19,44 +24,6 @@ impl GetToolSpecTool { Self } - fn escape_xml_text(value: &str) -> String { - value - .replace('&', "&") - .replace('<', "<") - .replace('>', ">") - } - - fn render_collapsed_tools_description(&self, collapsed_tools_list: String) -> String { - format!( - r#"Read usage instructions for additional tools. - -You have access to the additional tools listed below. These tools are collapsed: -their names may appear in the tool list, but you must not call them directly -until you have loaded their definition with GetToolSpec. - - -{} - - -Before using one of these tools, first call GetToolSpec with its exact tool name -to read its full description and input schema. If a direct call to a collapsed -tool fails with a message like "Tool 'Git' is collapsed", make the next tool -call `GetToolSpec` with `{{"tool_name":"Git"}}`, then retry the real tool after -reading the returned schema. - -After reading the returned definition, call the real tool directly using its own name. - -Do not call GetToolSpec again for a tool whose definition is already loaded in the current conversation. - -Example: -- Suppose the catalog includes a tool named `GetWeather` and you need to use it. -- First call `GetToolSpec` with `{{"tool_name":"GetWeather"}}` -- Then read the returned schema and call `GetWeather` itself with the appropriate arguments -"#, - collapsed_tools_list - ) - } - async fn get_contextual_collapsed_tools( &self, context: &ToolUseContext, @@ -80,7 +47,10 @@ Example: if let Some(context) = context { if let Ok(collapsed_tools) = self.get_contextual_collapsed_tools(context).await { for tool in collapsed_tools { - entries.push(format!("- {}: {}", tool.name(), tool.short_description())); + entries.push(build_get_tool_spec_collapsed_tool_entry( + tool.name(), + &tool.short_description(), + )); } } } else { @@ -99,7 +69,10 @@ Example: }; for (tool_name, short_description) in collapsed_tools { - entries.push(format!("- {}: {}", tool_name, short_description)); + entries.push(build_get_tool_spec_collapsed_tool_entry( + &tool_name, + &short_description, + )); } } @@ -109,7 +82,7 @@ Example: entries.join("\n") }; - self.render_collapsed_tools_description(collapsed_tools_list) + build_get_tool_spec_description(&collapsed_tools_list) } async fn build_tool_detail( @@ -163,7 +136,7 @@ impl Default for GetToolSpecTool { #[async_trait] impl Tool for GetToolSpecTool { fn name(&self) -> &str { - "GetToolSpec" + GET_TOOL_SPEC_TOOL_NAME } async fn description(&self) -> BitFunResult { @@ -182,17 +155,7 @@ impl Tool for GetToolSpecTool { } fn input_schema(&self) -> Value { - json!({ - "type": "object", - "additionalProperties": false, - "required": ["tool_name"], - "properties": { - "tool_name": { - "type": "string", - "description": "Exact collapsed tool name to load, using the tool's canonical casing from the catalog (for example, \"Git\"). Do not pass a command such as \"git status\" or an operation such as \"status\" here." - } - } - }) + get_tool_spec_input_schema() } fn is_readonly(&self) -> bool { @@ -220,25 +183,7 @@ impl Tool for GetToolSpecTool { input: &Value, _context: Option<&ToolUseContext>, ) -> ValidationResult { - let Some(tool_name) = input.get("tool_name").and_then(|v| v.as_str()) else { - return ValidationResult { - result: false, - message: Some("tool_name is required and cannot be empty".to_string()), - error_code: Some(400), - meta: None, - }; - }; - - if tool_name.is_empty() { - return ValidationResult { - result: false, - message: Some("tool_name is required and cannot be empty".to_string()), - error_code: Some(400), - meta: None, - }; - } - - ValidationResult::default() + validate_get_tool_spec_input(input) } async fn call_impl( @@ -261,10 +206,7 @@ impl Tool for GetToolSpecTool { "tool_name": tool_name, "already_loaded": true }), - result_for_assistant: Some(format!( - "Tool '{}' is already loaded in the current conversation. Do not call GetToolSpec again for it. Use '{}' directly.", - tool_name, tool_name - )), + result_for_assistant: Some(build_get_tool_spec_duplicate_load_hint(tool_name)), image_attachments: None, }]); } @@ -277,13 +219,9 @@ impl Tool for GetToolSpecTool { .unwrap_or(""); let input_schema = detail .get("input_schema") - .map(|value| value.to_string()) - .unwrap_or_else(|| "{}".to_string()); - let assistant_detail = format!( - "\n{}\n\n\n{}\n", - Self::escape_xml_text(description), - Self::escape_xml_text(&input_schema) - ); + .cloned() + .unwrap_or_else(|| json!({})); + let assistant_detail = build_get_tool_spec_assistant_detail(description, &input_schema); Ok(vec![ToolResult::Result { data: detail,