@steerable/agent-shell 0.6.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +91 -0
- package/contracts/tool-contract.json +326 -0
- package/dist/attachments.d.ts +41 -0
- package/dist/attachments.js +147 -0
- package/dist/brand.d.ts +24 -0
- package/dist/brand.js +92 -0
- package/dist/host/http-routes.d.ts +21 -0
- package/dist/host/http-routes.js +55 -0
- package/dist/host/ipc.d.ts +11 -0
- package/dist/host/ipc.js +20 -0
- package/dist/host/pack-assembly.d.ts +86 -0
- package/dist/host/pack-assembly.js +32 -0
- package/dist/host/runtime.d.ts +69 -0
- package/dist/host/runtime.js +207 -0
- package/dist/host/visible-terminal-exec.d.ts +21 -0
- package/dist/host/visible-terminal-exec.js +151 -0
- package/dist/hosted-web-search.d.ts +13 -0
- package/dist/hosted-web-search.js +82 -0
- package/dist/image-attachment.d.ts +39 -0
- package/dist/image-attachment.js +133 -0
- package/dist/insights/flush.d.ts +10 -0
- package/dist/insights/flush.js +139 -0
- package/dist/insights/record.d.ts +17 -0
- package/dist/insights/record.js +44 -0
- package/dist/json-store.d.ts +9 -0
- package/dist/json-store.js +22 -0
- package/dist/llm/index.d.ts +23 -0
- package/dist/llm/index.js +105 -0
- package/dist/llm/ollama.d.ts +23 -0
- package/dist/llm/ollama.js +242 -0
- package/dist/llm/openai-compat.d.ts +20 -0
- package/dist/llm/openai-compat.js +199 -0
- package/dist/llm/sidecar-provider.d.ts +37 -0
- package/dist/llm/sidecar-provider.js +163 -0
- package/dist/llm/tool-choice.d.ts +35 -0
- package/dist/llm/tool-choice.js +85 -0
- package/dist/llm/types.d.ts +122 -0
- package/dist/llm/types.js +1 -0
- package/dist/local-backend/agent-capability.d.ts +101 -0
- package/dist/local-backend/agent-capability.js +174 -0
- package/dist/local-backend/ai-title.d.ts +43 -0
- package/dist/local-backend/ai-title.js +173 -0
- package/dist/local-backend/auto-continue-helper.d.ts +80 -0
- package/dist/local-backend/auto-continue-helper.js +83 -0
- package/dist/local-backend/branch-helper.d.ts +24 -0
- package/dist/local-backend/branch-helper.js +27 -0
- package/dist/local-backend/context-compactor.d.ts +81 -0
- package/dist/local-backend/context-compactor.js +213 -0
- package/dist/local-backend/coreloop-stream.d.ts +245 -0
- package/dist/local-backend/coreloop-stream.js +277 -0
- package/dist/local-backend/deferred-detector.d.ts +15 -0
- package/dist/local-backend/deferred-detector.js +124 -0
- package/dist/local-backend/history-helper.d.ts +30 -0
- package/dist/local-backend/history-helper.js +34 -0
- package/dist/local-backend/interrupted-helper.d.ts +29 -0
- package/dist/local-backend/interrupted-helper.js +25 -0
- package/dist/local-backend/live-stream.d.ts +36 -0
- package/dist/local-backend/live-stream.js +23 -0
- package/dist/local-backend/llm-diagnose.d.ts +37 -0
- package/dist/local-backend/llm-diagnose.js +284 -0
- package/dist/local-backend/message-triggers.d.ts +27 -0
- package/dist/local-backend/message-triggers.js +67 -0
- package/dist/local-backend/pack-backend-routes.d.ts +31 -0
- package/dist/local-backend/pack-backend-routes.js +62 -0
- package/dist/local-backend/pack-turn-hooks.d.ts +37 -0
- package/dist/local-backend/pack-turn-hooks.js +67 -0
- package/dist/local-backend/prompt-builder.d.ts +101 -0
- package/dist/local-backend/prompt-builder.js +246 -0
- package/dist/local-backend/regenerate-helper.d.ts +62 -0
- package/dist/local-backend/regenerate-helper.js +75 -0
- package/dist/local-backend/router.d.ts +175 -0
- package/dist/local-backend/router.js +3139 -0
- package/dist/local-backend/skill-install.d.ts +19 -0
- package/dist/local-backend/skill-install.js +71 -0
- package/dist/local-backend/skill-loader.d.ts +92 -0
- package/dist/local-backend/skill-loader.js +146 -0
- package/dist/local-backend/skills/00-identity/SKILL.md +32 -0
- package/dist/local-backend/skills/10-goal/SKILL.md +59 -0
- package/dist/local-backend/skills/11-loop/SKILL.md +71 -0
- package/dist/local-backend/skills/12-create-skill/SKILL.md +88 -0
- package/dist/local-backend/skills/70-plan-mode/SKILL.md +58 -0
- package/dist/local-backend/skills/80-tool-usage/SKILL.md +70 -0
- package/dist/local-backend/skills/81-anti-deferred/SKILL.md +53 -0
- package/dist/local-backend/skills/82-data-grounding/SKILL.md +56 -0
- package/dist/local-backend/skills/85-local-exec/SKILL.md +86 -0
- package/dist/local-backend/skills/86-proactive-coding/SKILL.md +51 -0
- package/dist/local-backend/subagent-profiles.d.ts +30 -0
- package/dist/local-backend/subagent-profiles.js +74 -0
- package/dist/local-backend/task-process.d.ts +12 -0
- package/dist/local-backend/task-process.js +176 -0
- package/dist/local-backend/task-service.d.ts +135 -0
- package/dist/local-backend/task-service.js +565 -0
- package/dist/local-backend/turn-duration.d.ts +2 -0
- package/dist/local-backend/turn-duration.js +9 -0
- package/dist/local-backend/turn-timeline.d.ts +16 -0
- package/dist/local-backend/turn-timeline.js +42 -0
- package/dist/local-backend/worktree-service.d.ts +84 -0
- package/dist/local-backend/worktree-service.js +243 -0
- package/dist/local-edit.d.ts +48 -0
- package/dist/local-edit.js +44 -0
- package/dist/local-executor.d.ts +255 -0
- package/dist/local-executor.js +881 -0
- package/dist/local-script-registry.d.ts +28 -0
- package/dist/local-script-registry.js +63 -0
- package/dist/log.d.ts +13 -0
- package/dist/log.js +12 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +855 -0
- package/dist/mcp-executor.d.ts +45 -0
- package/dist/mcp-executor.js +241 -0
- package/dist/mcp-server-registry.d.ts +104 -0
- package/dist/mcp-server-registry.js +234 -0
- package/dist/preload-default.d.ts +1 -0
- package/dist/preload-default.js +9 -0
- package/dist/preload.cjs +395 -0
- package/dist/preload.d.ts +20 -0
- package/dist/preload.js +411 -0
- package/dist/product-config.d.ts +43 -0
- package/dist/product-config.js +26 -0
- package/dist/project-registry.d.ts +55 -0
- package/dist/project-registry.js +106 -0
- package/dist/project-rules.d.ts +15 -0
- package/dist/project-rules.js +102 -0
- package/dist/runtime.d.ts +62 -0
- package/dist/runtime.js +217 -0
- package/dist/scenario/pack.d.ts +8 -0
- package/dist/scenario/pack.js +1 -0
- package/dist/scenario/registry.d.ts +24 -0
- package/dist/scenario/registry.js +31 -0
- package/dist/server/http-server.d.ts +39 -0
- package/dist/server/http-server.js +361 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +107 -0
- package/dist/server/sse-bus.d.ts +14 -0
- package/dist/server/sse-bus.js +31 -0
- package/dist/shell-adapt.d.ts +21 -0
- package/dist/shell-adapt.js +104 -0
- package/dist/sidecar/boot.d.ts +36 -0
- package/dist/sidecar/boot.js +343 -0
- package/dist/sidecar/egress-hint.d.ts +15 -0
- package/dist/sidecar/egress-hint.js +46 -0
- package/dist/sidecar/egress-proxy.d.ts +183 -0
- package/dist/sidecar/egress-proxy.js +419 -0
- package/dist/sidecar/errors.d.ts +22 -0
- package/dist/sidecar/errors.js +38 -0
- package/dist/sidecar/exec-sandbox.d.ts +48 -0
- package/dist/sidecar/exec-sandbox.js +94 -0
- package/dist/sidecar/handle.d.ts +32 -0
- package/dist/sidecar/handle.js +53 -0
- package/dist/sidecar/index.d.ts +14 -0
- package/dist/sidecar/index.js +13 -0
- package/dist/sidecar/proxy-detect.d.ts +50 -0
- package/dist/sidecar/proxy-detect.js +182 -0
- package/dist/sidecar/reverse-approval.d.ts +55 -0
- package/dist/sidecar/reverse-approval.js +86 -0
- package/dist/sidecar/reverse-ask-user.d.ts +34 -0
- package/dist/sidecar/reverse-ask-user.js +59 -0
- package/dist/sidecar/reverse-spawn.d.ts +19 -0
- package/dist/sidecar/reverse-spawn.js +161 -0
- package/dist/sidecar/reverse-tools.d.ts +29 -0
- package/dist/sidecar/reverse-tools.js +106 -0
- package/dist/sidecar/safety-patterns.d.ts +41 -0
- package/dist/sidecar/safety-patterns.js +157 -0
- package/dist/sidecar/storage-path.d.ts +14 -0
- package/dist/sidecar/storage-path.js +35 -0
- package/dist/sidecar/supervisor.d.ts +218 -0
- package/dist/sidecar/supervisor.js +932 -0
- package/dist/sidecar/types.d.ts +601 -0
- package/dist/sidecar/types.js +1 -0
- package/dist/single-instance.d.ts +11 -0
- package/dist/single-instance.js +21 -0
- package/dist/storage/empty-chats.d.ts +9 -0
- package/dist/storage/empty-chats.js +16 -0
- package/dist/storage/index.d.ts +373 -0
- package/dist/storage/index.js +1158 -0
- package/dist/storage/insights-redact.d.ts +2 -0
- package/dist/storage/insights-redact.js +30 -0
- package/dist/storage/insights-settings.d.ts +53 -0
- package/dist/storage/insights-settings.js +92 -0
- package/dist/storage/llm-settings.d.ts +120 -0
- package/dist/storage/llm-settings.js +233 -0
- package/dist/storage/local-store-singleton.d.ts +28 -0
- package/dist/storage/local-store-singleton.js +38 -0
- package/dist/storage/message-order.d.ts +25 -0
- package/dist/storage/message-order.js +27 -0
- package/dist/storage/pack-migrations.d.ts +22 -0
- package/dist/storage/pack-migrations.js +24 -0
- package/dist/storage/pack-seeds.d.ts +36 -0
- package/dist/storage/pack-seeds.js +42 -0
- package/dist/storage/telemetry-settings.d.ts +38 -0
- package/dist/storage/telemetry-settings.js +59 -0
- package/dist/storage/usage-summary.d.ts +55 -0
- package/dist/storage/usage-summary.js +38 -0
- package/dist/storage/web-search-settings.d.ts +38 -0
- package/dist/storage/web-search-settings.js +74 -0
- package/dist/storage/write-lease.d.ts +26 -0
- package/dist/storage/write-lease.js +74 -0
- package/dist/terminal-manager.d.ts +83 -0
- package/dist/terminal-manager.js +506 -0
- package/dist/tool-router.d.ts +228 -0
- package/dist/tool-router.js +930 -0
- package/dist/tool-search-rank.d.ts +42 -0
- package/dist/tool-search-rank.js +96 -0
- package/package.json +67 -0
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { buildProjectRootViolation, isPathWithinRoot, } from './local-executor.js';
|
|
4
|
+
import { mcpExecutor } from './mcp-executor.js';
|
|
5
|
+
import { rankTools, resolveMaxResults, TOOL_SEARCH_DEFAULT_MAX_RESULTS, } from './tool-search-rank.js';
|
|
6
|
+
import { maybeAutoInstallWrittenSkill } from './local-backend/skill-install.js';
|
|
7
|
+
import { filterToolsByPolicy, isToolAllowed, } from './local-backend/agent-capability.js';
|
|
8
|
+
/** 已注册 MCP 服务的动态工具名前缀:`mcp__<serverKey>__<toolName>`。 */
|
|
9
|
+
export const MCP_DYNAMIC_TOOL_PREFIX = 'mcp__';
|
|
10
|
+
/**
|
|
11
|
+
* 工具曝光分层(steerable-framework Wave 2 同构):`direct` 进模型可见列表;
|
|
12
|
+
* `deferred` 可分发、可经 `tool_search` 发现,但不占每轮工具列表的 token;
|
|
13
|
+
* `hidden` 仅可分发。缺省 `direct`。
|
|
14
|
+
*
|
|
15
|
+
* ToolMode / ToolExposure 的单一真源在 scenario/pack.ts(pack-sdk),
|
|
16
|
+
* 本模块 re-export 兼容既有调用方。
|
|
17
|
+
*/
|
|
18
|
+
/** deferred 层的发现缝:一个 direct 层搜索工具,关键词命中即调。 */
|
|
19
|
+
export const TOOL_SEARCH_TOOL_NAME = 'tool_search';
|
|
20
|
+
/** 前转 RPC 超时:sidecar 侧单次请求上限默认 30s(可配到 600s),这里给足余量。 */
|
|
21
|
+
export const WEB_TOOL_RPC_TIMEOUT_MS = 130_000;
|
|
22
|
+
/**
|
|
23
|
+
* 本机是否提供网络读取工具(`STEERABLE_WEB_TOOLS=0` 关闭)。
|
|
24
|
+
*
|
|
25
|
+
* 同一个开关同时管住两件事:`tool.list` 握手是否把 web 工具装进本路由,
|
|
26
|
+
* 以及 layer-1 Seatbelt profile 是否放行它们所需的出网(解析器 + 80/443)。
|
|
27
|
+
* 两者必须同源——只放宽不暴露是白开的口子,只暴露不放宽则每次抓取都死在
|
|
28
|
+
* 名字解析上,而模型会把它解释成用户断网。
|
|
29
|
+
*/
|
|
30
|
+
export function webToolsEnabled() {
|
|
31
|
+
return process.env.STEERABLE_WEB_TOOLS !== '0';
|
|
32
|
+
}
|
|
33
|
+
export class ToolRouter {
|
|
34
|
+
localExecutor;
|
|
35
|
+
localScriptRegistry;
|
|
36
|
+
shellExecutor;
|
|
37
|
+
mcpRegistry;
|
|
38
|
+
projectRegistry;
|
|
39
|
+
constructor(localExecutor, localScriptRegistry, shellExecutor,
|
|
40
|
+
/** 可选:已注册的外部 MCP 服务(设置界面导入)。有缓存工具的服务会追加为一等工具。 */
|
|
41
|
+
mcpRegistry,
|
|
42
|
+
/** 可选:项目注册表(项目模式)。路由层用它把 chat.projectId 解析成沙箱根目录。 */
|
|
43
|
+
projectRegistry) {
|
|
44
|
+
this.localExecutor = localExecutor;
|
|
45
|
+
this.localScriptRegistry = localScriptRegistry;
|
|
46
|
+
this.shellExecutor = shellExecutor;
|
|
47
|
+
this.mcpRegistry = mcpRegistry;
|
|
48
|
+
this.projectRegistry = projectRegistry;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 场景包贡献的工具(0.3a 开槽)。schema 进 listSchemas,分发在 execute
|
|
52
|
+
* 的 default 分支——内置工具优先,名字冲突在注册时响亮失败。
|
|
53
|
+
*/
|
|
54
|
+
contributedTools = new Map();
|
|
55
|
+
/**
|
|
56
|
+
* 注册一组场景工具贡献。与既有工具(内置 / 任务 / web / MCP / 插件 /
|
|
57
|
+
* 已注册贡献)重名即抛错——一个包装不上却静默丢工具是能力暗损。
|
|
58
|
+
*/
|
|
59
|
+
registerToolContributions(contributions) {
|
|
60
|
+
const existing = new Set(this.listSchemas().map((s) => s.name));
|
|
61
|
+
for (const contribution of contributions) {
|
|
62
|
+
if (existing.has(contribution.name)) {
|
|
63
|
+
throw new Error(`[tool-router] duplicate tool name from contribution: ${contribution.name}`);
|
|
64
|
+
}
|
|
65
|
+
this.contributedTools.set(contribution.name, contribution);
|
|
66
|
+
existing.add(contribution.name);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* sidecar 握手得到的网络读取工具可用集。web_search 只在 sidecar 配好
|
|
71
|
+
* 搜索后端(设置页免费 DuckDuckGo / Tavily 钥 / STEERABLE_WEB_SEARCH_API_KEY /
|
|
72
|
+
* TAVILY_API_KEY,或 STEERABLE_WEB_SEARCH_PROVIDER=host)时注册——
|
|
73
|
+
* 宿主不猜,避免出现"工具在列表里但一调就报未配置"。
|
|
74
|
+
*/
|
|
75
|
+
webToolDelegate = null;
|
|
76
|
+
webToolNames = new Set();
|
|
77
|
+
/**
|
|
78
|
+
* OpenAI hosted search executed in this process with the chat credential.
|
|
79
|
+
* Set when the sidecar registered `web_search` via provider=host so the
|
|
80
|
+
* host must not forward the call back into the sidecar.
|
|
81
|
+
*/
|
|
82
|
+
hostedWebSearch = null;
|
|
83
|
+
/**
|
|
84
|
+
* 4.6a/4.6b:跨 turn 任务与 git worktree 服务。setter 注入(与
|
|
85
|
+
* setWebTools 同模式)——main.ts 里 ToolRouter 先于 TaskService 构造
|
|
86
|
+
* (TaskService 反向依赖 ToolRouter 的工具列表),构造后再接线。
|
|
87
|
+
* 未接线时任务/worktree 工具不出场(schema 都不出,模型看不到)。
|
|
88
|
+
*/
|
|
89
|
+
taskService = null;
|
|
90
|
+
worktreeService = null;
|
|
91
|
+
setTaskServices(services) {
|
|
92
|
+
this.taskService = services?.taskService ?? null;
|
|
93
|
+
this.worktreeService = services?.worktreeService ?? null;
|
|
94
|
+
}
|
|
95
|
+
setWebTools(delegate, names = []) {
|
|
96
|
+
this.webToolDelegate = delegate;
|
|
97
|
+
this.webToolNames = new Set(names);
|
|
98
|
+
}
|
|
99
|
+
setHostedWebSearch(handler) {
|
|
100
|
+
this.hostedWebSearch = handler;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* sidecar 握手后注入插件 RPC 缝。未注入(sidecar 缺席/握手失败)时
|
|
104
|
+
* plugin_* 工具一个 schema 都不出——模型看不到不可用的工具。
|
|
105
|
+
*/
|
|
106
|
+
pluginRpc = null;
|
|
107
|
+
setPluginRpc(delegate) {
|
|
108
|
+
this.pluginRpc = delegate;
|
|
109
|
+
}
|
|
110
|
+
listSchemas() {
|
|
111
|
+
return [
|
|
112
|
+
{
|
|
113
|
+
name: 'local_exec_shell',
|
|
114
|
+
description: 'Execute a local shell command',
|
|
115
|
+
mode: 'destructive',
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
command: { type: 'string' },
|
|
120
|
+
cwd: { type: 'string' },
|
|
121
|
+
timeout: { type: 'number', description: 'Timeout in milliseconds (e.g. 30000). If a small number like 15 or 30 is provided, it is assumed to be in seconds and automatically multiplied by 1000.' },
|
|
122
|
+
},
|
|
123
|
+
required: ['command'],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'local_read_file',
|
|
128
|
+
description: 'Read a local text file',
|
|
129
|
+
mode: 'read',
|
|
130
|
+
inputSchema: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: { path: { type: 'string' } },
|
|
133
|
+
required: ['path'],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'local_write_file',
|
|
138
|
+
description: 'Write content to local file path',
|
|
139
|
+
mode: 'destructive',
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: 'object',
|
|
142
|
+
properties: {
|
|
143
|
+
path: { type: 'string' },
|
|
144
|
+
content: { type: 'string' },
|
|
145
|
+
createDirs: { type: 'boolean' },
|
|
146
|
+
expectedVersion: {
|
|
147
|
+
type: 'string',
|
|
148
|
+
description: 'Optional. The `version` returned by your last local_read_file of this path. When provided, the write is rejected if the file changed since — prevents overwriting external edits.',
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
required: ['path', 'content'],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'local_edit_file',
|
|
156
|
+
description: 'Make targeted edits to an existing local file WITHOUT rewriting the whole file. ' +
|
|
157
|
+
'Provide one or more {oldText, newText} edits: each oldText is located in the current file ' +
|
|
158
|
+
'(exact match, then whitespace-tolerant, then Unicode-punctuation-normalized) and replaced by newText. ' +
|
|
159
|
+
'Prefer this over local_write_file for modifying existing files — it is safer (no full-file retype) and cheaper. ' +
|
|
160
|
+
'All edits are matched against the original file, must not overlap, and apply together; if any oldText is not found or ambiguous the whole call fails and nothing is written. ' +
|
|
161
|
+
'For safety, first local_read_file the path, then pass its `version` as expectedVersion so the edit is rejected if the file changed underneath you.',
|
|
162
|
+
mode: 'destructive',
|
|
163
|
+
inputSchema: {
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: {
|
|
166
|
+
path: { type: 'string', description: 'File path to edit.' },
|
|
167
|
+
edits: {
|
|
168
|
+
type: 'array',
|
|
169
|
+
description: 'One or more targeted replacements, applied together.',
|
|
170
|
+
items: {
|
|
171
|
+
type: 'object',
|
|
172
|
+
properties: {
|
|
173
|
+
oldText: {
|
|
174
|
+
type: 'string',
|
|
175
|
+
description: 'Exact snippet to replace. Include enough surrounding lines to be unique.',
|
|
176
|
+
},
|
|
177
|
+
newText: { type: 'string', description: 'Replacement text.' },
|
|
178
|
+
},
|
|
179
|
+
required: ['oldText', 'newText'],
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
expectedVersion: {
|
|
183
|
+
type: 'string',
|
|
184
|
+
description: 'Optional. The `version` returned by your last local_read_file of this path. When provided, the edit is rejected on conflict.',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
required: ['path', 'edits'],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'local_open_path',
|
|
192
|
+
description: 'Open local path or URL',
|
|
193
|
+
mode: 'local',
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: {
|
|
197
|
+
target: { type: 'string' },
|
|
198
|
+
},
|
|
199
|
+
required: ['target'],
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
// R14 P1 同名物理清理:原名 local_run_code 与 sidecar 的 run_code 同名
|
|
204
|
+
// 并列、模型难辨。更名为 local_run_snippet(跑独立代码片段 + 装依赖,
|
|
205
|
+
// 主机侧、不可链工具),与 run_code(程序化工具调用、受限子进程、可链
|
|
206
|
+
// 工具)职责清晰区分。direct 曝光保留,86-proactive-coding 技能条件不变。
|
|
207
|
+
name: 'local_run_snippet',
|
|
208
|
+
description: 'Write a code snippet to a scratch file and run it with the local Python/Node interpreter, ' +
|
|
209
|
+
'optionally installing dependencies first (pipPackages / npmPackages). ' +
|
|
210
|
+
'Use this to proactively solve tasks no dedicated tool covers: data processing, format conversion, ' +
|
|
211
|
+
'parsing, computation, batch file operations, automation glue. ' +
|
|
212
|
+
'Returns stdout/stderr/exitCode plus scriptPath (snippet is kept on disk for inspection).',
|
|
213
|
+
mode: 'destructive',
|
|
214
|
+
inputSchema: {
|
|
215
|
+
type: 'object',
|
|
216
|
+
properties: {
|
|
217
|
+
language: {
|
|
218
|
+
type: 'string',
|
|
219
|
+
enum: ['python', 'node'],
|
|
220
|
+
description: 'Interpreter for the snippet.',
|
|
221
|
+
},
|
|
222
|
+
code: { type: 'string', description: 'Complete source code of the snippet.' },
|
|
223
|
+
cwd: {
|
|
224
|
+
type: 'string',
|
|
225
|
+
description: 'Working directory. Project-bound chats: defaults to the project root and must stay inside it.',
|
|
226
|
+
},
|
|
227
|
+
timeout: {
|
|
228
|
+
type: 'number',
|
|
229
|
+
description: 'Run timeout in milliseconds (a small number like 30 is treated as seconds). Dependency installs get their own generous timeout.',
|
|
230
|
+
},
|
|
231
|
+
pipPackages: {
|
|
232
|
+
type: 'array',
|
|
233
|
+
items: { type: 'string' },
|
|
234
|
+
description: 'Python deps to `pip install` before running (e.g. ["pandas", "openpyxl"]). Uses --user, with an automatic venv fallback on PEP 668 systems.',
|
|
235
|
+
},
|
|
236
|
+
npmPackages: {
|
|
237
|
+
type: 'array',
|
|
238
|
+
items: { type: 'string' },
|
|
239
|
+
description: 'Node deps to `npm install` into the scratch dir before running (e.g. ["xlsx"]). Scripts can require/import them directly.',
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
required: ['language', 'code'],
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: 'local_list_scripts',
|
|
247
|
+
description: 'List all saved local scripts',
|
|
248
|
+
mode: 'read',
|
|
249
|
+
inputSchema: {
|
|
250
|
+
type: 'object',
|
|
251
|
+
properties: {},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: 'local_run_script',
|
|
256
|
+
description: 'Run a saved local script by scriptId',
|
|
257
|
+
mode: 'local',
|
|
258
|
+
inputSchema: {
|
|
259
|
+
type: 'object',
|
|
260
|
+
properties: {
|
|
261
|
+
scriptId: { type: 'string' },
|
|
262
|
+
},
|
|
263
|
+
required: ['scriptId'],
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: 'mcp_list_tools',
|
|
268
|
+
description: 'List tools from one MCP server',
|
|
269
|
+
mode: 'external',
|
|
270
|
+
inputSchema: {
|
|
271
|
+
type: 'object',
|
|
272
|
+
properties: {
|
|
273
|
+
command: { type: 'string' },
|
|
274
|
+
args: { type: 'array', items: { type: 'string' } },
|
|
275
|
+
cwd: { type: 'string' },
|
|
276
|
+
},
|
|
277
|
+
required: ['command'],
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: 'mcp_tool_exec',
|
|
282
|
+
description: 'Execute one MCP tool',
|
|
283
|
+
mode: 'external',
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: 'object',
|
|
286
|
+
properties: {
|
|
287
|
+
command: { type: 'string' },
|
|
288
|
+
args: { type: 'array', items: { type: 'string' } },
|
|
289
|
+
cwd: { type: 'string' },
|
|
290
|
+
toolName: { type: 'string' },
|
|
291
|
+
toolArgs: { type: 'object' },
|
|
292
|
+
},
|
|
293
|
+
required: ['command', 'toolName'],
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
// deferred 层的发现缝(Wave 2 工具分层):MCP 动态工具不占每轮
|
|
297
|
+
// 列表,模型用这个工具按相关度找到它们,命中即调。
|
|
298
|
+
{
|
|
299
|
+
name: TOOL_SEARCH_TOOL_NAME,
|
|
300
|
+
description: 'Search for additional tools not in the initial tool list. Returns full tool schemas; a matched tool can be called immediately by name.',
|
|
301
|
+
mode: 'read',
|
|
302
|
+
inputSchema: {
|
|
303
|
+
type: 'object',
|
|
304
|
+
properties: {
|
|
305
|
+
query: {
|
|
306
|
+
type: 'string',
|
|
307
|
+
description: 'Search terms ranked against deferred tool names and descriptions, best matches first.',
|
|
308
|
+
},
|
|
309
|
+
max_results: {
|
|
310
|
+
type: 'integer',
|
|
311
|
+
description: `Cap on returned schemas (default ${TOOL_SEARCH_DEFAULT_MAX_RESULTS}).`,
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
required: ['query'],
|
|
315
|
+
additionalProperties: false,
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
// ─── 4.6a/4.6b 跨 turn 后台任务 + git worktree 隔离 ─────────────
|
|
319
|
+
// 服务未接线(CLI/test)时一个 schema 都不出——模型看不到不可用的工具。
|
|
320
|
+
...this.listTaskAndWorktreeSchemas(),
|
|
321
|
+
...this.listWebToolSchemas(),
|
|
322
|
+
...this.listRegisteredMcpToolSchemas(),
|
|
323
|
+
...this.listPluginToolSchemas(),
|
|
324
|
+
// ─── 场景包贡献的工具(0.3a 开槽) ─────────────────
|
|
325
|
+
...[...this.contributedTools.values()].map((c) => ({
|
|
326
|
+
name: c.name,
|
|
327
|
+
description: c.description,
|
|
328
|
+
inputSchema: c.inputSchema,
|
|
329
|
+
mode: c.mode,
|
|
330
|
+
...(c.exposure ? { exposure: c.exposure } : {}),
|
|
331
|
+
})),
|
|
332
|
+
];
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* 插件生命周期工具(框架 plugin.* RPC 的模型面)。全在 deferred 层:
|
|
336
|
+
* 插件管理是低频运维动作,不值每轮工具列表的 token——模型经
|
|
337
|
+
* tool_search 发现,命中即调。写操作(enable/disable/reload)标
|
|
338
|
+
* destructive,走宿主审批链。
|
|
339
|
+
*/
|
|
340
|
+
listPluginToolSchemas() {
|
|
341
|
+
if (!this.pluginRpc)
|
|
342
|
+
return [];
|
|
343
|
+
const nameParam = {
|
|
344
|
+
type: 'object',
|
|
345
|
+
properties: {
|
|
346
|
+
name: { type: 'string', description: 'The plugin name from plugin_list.' },
|
|
347
|
+
},
|
|
348
|
+
required: ['name'],
|
|
349
|
+
additionalProperties: false,
|
|
350
|
+
};
|
|
351
|
+
return [
|
|
352
|
+
{
|
|
353
|
+
name: 'plugin_list',
|
|
354
|
+
description: 'List every tool plugin the agent runtime has loaded, with lifecycle state (enabled, reloadable) and the tools each provides.',
|
|
355
|
+
mode: 'read',
|
|
356
|
+
exposure: 'deferred',
|
|
357
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
name: 'plugin_enable',
|
|
361
|
+
description: 'Enable a disabled plugin so its tools become dispatchable again. Use plugin_list first for the exact name.',
|
|
362
|
+
mode: 'destructive',
|
|
363
|
+
exposure: 'deferred',
|
|
364
|
+
inputSchema: nameParam,
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: 'plugin_disable',
|
|
368
|
+
description: 'Disable a plugin without unloading its code; its tools leave the dispatch surface until re-enabled. Use plugin_list first for the exact name.',
|
|
369
|
+
mode: 'destructive',
|
|
370
|
+
exposure: 'deferred',
|
|
371
|
+
inputSchema: nameParam,
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
name: 'plugin_reload',
|
|
375
|
+
description: 'Hot-reload a reloadable plugin from its source, picking up code changes without a restart. Use plugin_list first for the exact name.',
|
|
376
|
+
mode: 'destructive',
|
|
377
|
+
exposure: 'deferred',
|
|
378
|
+
inputSchema: nameParam,
|
|
379
|
+
},
|
|
380
|
+
];
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* 4.6a/4.6b 任务与 worktree 工具的 schema。task_run 立即返回
|
|
384
|
+
* (任务在后台独立流里跨 turn 跑完);worktree_* 操作绑定项目的
|
|
385
|
+
* git 仓库。description 用英文(面向模型),与 local_* 工具同款。
|
|
386
|
+
*/
|
|
387
|
+
listTaskAndWorktreeSchemas() {
|
|
388
|
+
if (!this.taskService || !this.worktreeService)
|
|
389
|
+
return [];
|
|
390
|
+
return [
|
|
391
|
+
{
|
|
392
|
+
name: 'task_run',
|
|
393
|
+
description: 'Run a self-contained task in the background as an independent agent run that keeps going after this turn ends. '
|
|
394
|
+
+ 'Returns immediately with {taskId, status:"running"}; check progress with task_status and collect the final answer with task_result. '
|
|
395
|
+
+ 'No project binding is required — omit worktree for a plain background run. '
|
|
396
|
+
+ 'worktree:true is OPTIONAL isolation for code changes: it needs the chat bound to a git project and runs the task '
|
|
397
|
+
+ 'in a fresh worktree (its own branch) that the user can merge or discard from the task panel. '
|
|
398
|
+
+ 'If a worktree run fails on project binding, retry WITHOUT worktree instead of refusing the task. '
|
|
399
|
+
+ 'Orchestration: dependsOn (taskIds of THIS chat) makes the task wait (status:"blocked") until every dependency completes, '
|
|
400
|
+
+ 'then it starts automatically — build pipelines like "run tests after the fix task". A failed dependency fails the task fast.',
|
|
401
|
+
mode: 'local',
|
|
402
|
+
inputSchema: {
|
|
403
|
+
type: 'object',
|
|
404
|
+
properties: {
|
|
405
|
+
task: {
|
|
406
|
+
type: 'string',
|
|
407
|
+
description: 'Complete, self-contained instructions — the background run sees none of this conversation.',
|
|
408
|
+
},
|
|
409
|
+
worktree: {
|
|
410
|
+
type: 'boolean',
|
|
411
|
+
description: 'Run inside a fresh isolated git worktree (requires the chat to be bound to a project that is a git repo).',
|
|
412
|
+
},
|
|
413
|
+
worktreeName: {
|
|
414
|
+
type: 'string',
|
|
415
|
+
description: 'Optional worktree/branch name slug (default: auto).',
|
|
416
|
+
},
|
|
417
|
+
dependsOn: {
|
|
418
|
+
type: 'array',
|
|
419
|
+
items: { type: 'string' },
|
|
420
|
+
description: 'Optional taskIds (from earlier task_run results in this chat) that must all complete before this task starts.',
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
required: ['task'],
|
|
424
|
+
additionalProperties: false,
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
name: 'task_send',
|
|
429
|
+
description: 'Send a message into a RUNNING background task — the task receives it mid-run as steering (agent-to-agent messaging). '
|
|
430
|
+
+ 'Use it to narrow scope, add findings, or redirect a running task. Fails with needsFollowup while the task is blocked or still starting; '
|
|
431
|
+
+ 'a finished task cannot receive messages — run a new task instead.',
|
|
432
|
+
mode: 'local',
|
|
433
|
+
inputSchema: {
|
|
434
|
+
type: 'object',
|
|
435
|
+
properties: {
|
|
436
|
+
taskId: { type: 'string' },
|
|
437
|
+
message: {
|
|
438
|
+
type: 'string',
|
|
439
|
+
description: 'The steering message the running task receives.',
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
required: ['taskId', 'message'],
|
|
443
|
+
additionalProperties: false,
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
name: 'task_status',
|
|
448
|
+
description: 'Check background task status. With taskId returns that task; without it lists this chat\'s tasks (running first by recency).',
|
|
449
|
+
mode: 'read',
|
|
450
|
+
inputSchema: {
|
|
451
|
+
type: 'object',
|
|
452
|
+
properties: {
|
|
453
|
+
taskId: { type: 'string' },
|
|
454
|
+
},
|
|
455
|
+
additionalProperties: false,
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
name: 'task_result',
|
|
460
|
+
description: 'Collect the final answer of a finished background task. Fails with needsFollowup while the task is still running — poll later instead of re-running the task.',
|
|
461
|
+
mode: 'read',
|
|
462
|
+
inputSchema: {
|
|
463
|
+
type: 'object',
|
|
464
|
+
properties: {
|
|
465
|
+
taskId: { type: 'string' },
|
|
466
|
+
},
|
|
467
|
+
required: ['taskId'],
|
|
468
|
+
additionalProperties: false,
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
name: 'worktree_create',
|
|
473
|
+
description: 'Create an isolated git worktree of the bound project (directory <project>/.steerable/worktrees/<name> on a new steerable/<name> branch). '
|
|
474
|
+
+ 'Returns its path. Idempotent on the name — an existing worktree is returned as-is.',
|
|
475
|
+
mode: 'safe_write',
|
|
476
|
+
inputSchema: {
|
|
477
|
+
type: 'object',
|
|
478
|
+
properties: {
|
|
479
|
+
name: { type: 'string', description: 'Worktree name slug (default: auto).' },
|
|
480
|
+
},
|
|
481
|
+
additionalProperties: false,
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
name: 'worktree_list',
|
|
486
|
+
description: 'List the bound project\'s managed worktrees (name, path, branch).',
|
|
487
|
+
mode: 'read',
|
|
488
|
+
inputSchema: {
|
|
489
|
+
type: 'object',
|
|
490
|
+
properties: {},
|
|
491
|
+
additionalProperties: false,
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
name: 'worktree_remove',
|
|
496
|
+
description: 'Remove a managed worktree and delete its branch — the DISCARD path, any unmerged changes are lost. '
|
|
497
|
+
+ 'To keep the changes, ask the user to merge from the task panel instead.',
|
|
498
|
+
mode: 'destructive',
|
|
499
|
+
inputSchema: {
|
|
500
|
+
type: 'object',
|
|
501
|
+
properties: {
|
|
502
|
+
name: { type: 'string', description: 'Worktree name or path.' },
|
|
503
|
+
},
|
|
504
|
+
required: ['name'],
|
|
505
|
+
additionalProperties: false,
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
];
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* 网络读取对(W5-2):schema 与 sidecar `web_tools.py` 保持一致;description
|
|
512
|
+
* 用英文(面向模型),与 local_* 工具同款。可用集由握手决定,未握手/未注册
|
|
513
|
+
* 时一个 schema 都不出——模型看不到不可用的工具。
|
|
514
|
+
*/
|
|
515
|
+
listWebToolSchemas() {
|
|
516
|
+
if (!this.webToolDelegate)
|
|
517
|
+
return [];
|
|
518
|
+
const schemas = [];
|
|
519
|
+
if (this.webToolNames.has('web_fetch')) {
|
|
520
|
+
schemas.push({
|
|
521
|
+
name: 'web_fetch',
|
|
522
|
+
description: 'Fetch one public web page over http(s) and return its text (HTML is converted to plain text). '
|
|
523
|
+
+ 'Private/loopback/link-local targets are refused; cross-origin redirects are reported, not followed — re-issue the call with the reported URL.',
|
|
524
|
+
mode: 'read',
|
|
525
|
+
inputSchema: {
|
|
526
|
+
type: 'object',
|
|
527
|
+
properties: {
|
|
528
|
+
url: { type: 'string', description: 'The http(s) URL to fetch.' },
|
|
529
|
+
},
|
|
530
|
+
required: ['url'],
|
|
531
|
+
additionalProperties: false,
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
if (this.webToolNames.has('web_search')) {
|
|
536
|
+
schemas.push({
|
|
537
|
+
name: 'web_search',
|
|
538
|
+
description: 'Search the public web. Returns titled results with URLs and snippets; follow up with web_fetch on a result URL to read the page.',
|
|
539
|
+
mode: 'read',
|
|
540
|
+
inputSchema: {
|
|
541
|
+
type: 'object',
|
|
542
|
+
properties: {
|
|
543
|
+
query: { type: 'string', description: 'The search query.' },
|
|
544
|
+
max_results: {
|
|
545
|
+
type: 'integer',
|
|
546
|
+
description: 'Cap on returned results (default 8, ceiling 20).',
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
required: ['query'],
|
|
550
|
+
additionalProperties: false,
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
return schemas;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* 已注册 MCP 服务的动态工具。服务的工具列表来自注册表的内存缓存
|
|
558
|
+
* (启动后台刷新 + 设置界面"测试连接"刷新),没缓存或已禁用的服务不出场。
|
|
559
|
+
* 全部落在 deferred 层:MCP 目录是最大的无界第三方工具来源,不为每个
|
|
560
|
+
* schema 每轮付 token——模型经 `tool_search` 发现,命中即调。
|
|
561
|
+
*/
|
|
562
|
+
listRegisteredMcpToolSchemas() {
|
|
563
|
+
if (!this.mcpRegistry)
|
|
564
|
+
return [];
|
|
565
|
+
const out = [];
|
|
566
|
+
for (const server of this.mcpRegistry.list()) {
|
|
567
|
+
if (!server.enabled)
|
|
568
|
+
continue;
|
|
569
|
+
const cached = this.mcpRegistry.getCachedTools(server.id);
|
|
570
|
+
if (!cached || cached.tools.length === 0)
|
|
571
|
+
continue;
|
|
572
|
+
const key = this.mcpRegistry.serverKey(server);
|
|
573
|
+
for (const tool of cached.tools) {
|
|
574
|
+
out.push({
|
|
575
|
+
name: `${MCP_DYNAMIC_TOOL_PREFIX}${key}__${tool.name}`,
|
|
576
|
+
description: `[MCP:${server.name}] ${tool.description || tool.name}`,
|
|
577
|
+
inputSchema: tool.inputSchema && typeof tool.inputSchema === 'object'
|
|
578
|
+
? tool.inputSchema
|
|
579
|
+
: { type: 'object', properties: {} },
|
|
580
|
+
mode: 'external',
|
|
581
|
+
exposure: 'deferred',
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return out;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* 模型可见列表:仅 direct 层。`tool_search` 只有在存在 deferred 层时
|
|
589
|
+
* 才占一个列表槽——没有可发现的东西时它是每轮请求里的死重。
|
|
590
|
+
* 分发不按层设卡:被发现的 deferred 工具按名直调(execute 不查层)。
|
|
591
|
+
*
|
|
592
|
+
* @param policy 本轮智能体的工具准入策略;被拒的工具连 schema 都不出。
|
|
593
|
+
* deferred 层同样按策略过滤,所以策略拒掉全部 deferred 工具时
|
|
594
|
+
* `tool_search` 也一并退场(没有可发现的东西)。缺省 = 不限制。
|
|
595
|
+
*/
|
|
596
|
+
listModelSchemas(policy) {
|
|
597
|
+
const effective = policy ?? { mode: 'all', tools: [] };
|
|
598
|
+
const all = filterToolsByPolicy(this.listSchemas(), effective);
|
|
599
|
+
const hasDeferred = all.some(s => s.exposure === 'deferred');
|
|
600
|
+
return all.filter(s => {
|
|
601
|
+
if ((s.exposure ?? 'direct') !== 'direct')
|
|
602
|
+
return false;
|
|
603
|
+
if (s.name === TOOL_SEARCH_TOOL_NAME && !hasDeferred)
|
|
604
|
+
return false;
|
|
605
|
+
return true;
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
getSchemaByName(toolName) {
|
|
609
|
+
return this.listSchemas().find(schema => schema.name === toolName) || null;
|
|
610
|
+
}
|
|
611
|
+
async execute(call, context) {
|
|
612
|
+
const args = call.arguments || {};
|
|
613
|
+
const projectRoot = context?.projectRoot ?? null;
|
|
614
|
+
// 智能体工具策略的分发层复检:广告层(listModelSchemas / tool_search)
|
|
615
|
+
// 已经把被拒的工具藏了,但模型可能从历史或幻觉里捞出名字直调。
|
|
616
|
+
const toolPolicy = context?.toolPolicy ?? null;
|
|
617
|
+
if (toolPolicy && !isToolAllowed(toolPolicy, call.name)) {
|
|
618
|
+
throw new Error(`工具 ${call.name} 被当前智能体的工具策略拒绝(智能体管理 → 工具权限)`);
|
|
619
|
+
}
|
|
620
|
+
if (call.name.startsWith(MCP_DYNAMIC_TOOL_PREFIX)) {
|
|
621
|
+
return await this.executeRegisteredMcpTool(call.name, args);
|
|
622
|
+
}
|
|
623
|
+
switch (call.name) {
|
|
624
|
+
case TOOL_SEARCH_TOOL_NAME:
|
|
625
|
+
return this.executeToolSearch(args, toolPolicy);
|
|
626
|
+
case 'web_fetch':
|
|
627
|
+
case 'web_search':
|
|
628
|
+
return await this.executeWebTool(call.name, args);
|
|
629
|
+
case 'local_exec_shell':
|
|
630
|
+
return await this.executeShell({
|
|
631
|
+
command: String(args.command || ''),
|
|
632
|
+
cwd: typeof args.cwd === 'string' ? args.cwd : undefined,
|
|
633
|
+
timeout: typeof args.timeout === 'number' ? args.timeout : undefined,
|
|
634
|
+
}, projectRoot);
|
|
635
|
+
case 'local_read_file':
|
|
636
|
+
return await this.localExecutor.readLocalFile({
|
|
637
|
+
path: String(args.path || ''),
|
|
638
|
+
}, projectRoot, context?.additionalReadRoots ?? null);
|
|
639
|
+
case 'local_write_file': {
|
|
640
|
+
const written = await this.localExecutor.writeLocalFile({
|
|
641
|
+
path: String(args.path || ''),
|
|
642
|
+
content: String(args.content || ''),
|
|
643
|
+
createDirs: Boolean(args.createDirs),
|
|
644
|
+
expectedVersion: typeof args.expectedVersion === 'string' ? args.expectedVersion : undefined,
|
|
645
|
+
}, projectRoot);
|
|
646
|
+
this.autoInstallSkillOnSuccess(written, args.path);
|
|
647
|
+
return written;
|
|
648
|
+
}
|
|
649
|
+
case 'local_edit_file': {
|
|
650
|
+
const edits = Array.isArray(args.edits)
|
|
651
|
+
? args.edits
|
|
652
|
+
.filter(e => e && typeof e === 'object')
|
|
653
|
+
.map(e => ({
|
|
654
|
+
oldText: String(e.oldText ?? ''),
|
|
655
|
+
newText: String(e.newText ?? ''),
|
|
656
|
+
}))
|
|
657
|
+
: [];
|
|
658
|
+
const edited = await this.localExecutor.editLocalFile({
|
|
659
|
+
path: String(args.path || ''),
|
|
660
|
+
edits,
|
|
661
|
+
expectedVersion: typeof args.expectedVersion === 'string' ? args.expectedVersion : undefined,
|
|
662
|
+
}, projectRoot);
|
|
663
|
+
this.autoInstallSkillOnSuccess(edited, args.path);
|
|
664
|
+
return edited;
|
|
665
|
+
}
|
|
666
|
+
case 'local_open_path':
|
|
667
|
+
return await this.localExecutor.openLocalTarget({
|
|
668
|
+
target: String(args.target || ''),
|
|
669
|
+
});
|
|
670
|
+
case 'local_run_snippet': {
|
|
671
|
+
// 与 local_exec_shell 同款项目沙箱:cwd 默认项目根,显式越界拒绝。
|
|
672
|
+
const sandboxed = this.applyProjectCwdSandbox({ cwd: typeof args.cwd === 'string' ? args.cwd : undefined }, projectRoot);
|
|
673
|
+
if ('error' in sandboxed)
|
|
674
|
+
return sandboxed.error;
|
|
675
|
+
return await this.localExecutor.runCode({
|
|
676
|
+
language: String(args.language || ''),
|
|
677
|
+
code: String(args.code || ''),
|
|
678
|
+
cwd: sandboxed.request.cwd,
|
|
679
|
+
timeout: typeof args.timeout === 'number' ? args.timeout : undefined,
|
|
680
|
+
pipPackages: Array.isArray(args.pipPackages)
|
|
681
|
+
? args.pipPackages.map(String)
|
|
682
|
+
: undefined,
|
|
683
|
+
npmPackages: Array.isArray(args.npmPackages)
|
|
684
|
+
? args.npmPackages.map(String)
|
|
685
|
+
: undefined,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
case 'local_list_scripts':
|
|
689
|
+
return { success: true, scripts: this.localScriptRegistry.list() };
|
|
690
|
+
case 'local_run_script': {
|
|
691
|
+
const scriptId = String(args.scriptId || '');
|
|
692
|
+
const script = this.localScriptRegistry.getById(scriptId);
|
|
693
|
+
if (!script) {
|
|
694
|
+
return { success: false, error: `Script not found: ${scriptId}` };
|
|
695
|
+
}
|
|
696
|
+
return await this.localExecutor.executeShell({
|
|
697
|
+
command: script.command,
|
|
698
|
+
cwd: script.cwd,
|
|
699
|
+
timeout: script.timeout,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
case 'mcp_list_tools': {
|
|
703
|
+
const cfg = this.resolveMcpConfig(args);
|
|
704
|
+
return await mcpExecutor.listTools(cfg);
|
|
705
|
+
}
|
|
706
|
+
case 'mcp_tool_exec': {
|
|
707
|
+
const cfg = this.resolveMcpConfig(args);
|
|
708
|
+
const toolName = String(args.toolName || '');
|
|
709
|
+
const toolArgs = (args.toolArgs || {});
|
|
710
|
+
return await mcpExecutor.executeTool(cfg, toolName, toolArgs);
|
|
711
|
+
}
|
|
712
|
+
case 'plugin_list':
|
|
713
|
+
case 'plugin_enable':
|
|
714
|
+
case 'plugin_disable':
|
|
715
|
+
case 'plugin_reload': {
|
|
716
|
+
if (!this.pluginRpc) {
|
|
717
|
+
throw new Error('plugin tools unavailable: sidecar plugin registry not wired');
|
|
718
|
+
}
|
|
719
|
+
const method = call.name.replace('_', '.');
|
|
720
|
+
const params = call.name === 'plugin_list' ? undefined : { name: String(args.name || '') };
|
|
721
|
+
return await this.pluginRpc(method, params);
|
|
722
|
+
}
|
|
723
|
+
case 'task_run': {
|
|
724
|
+
const chatId = this.requireTaskContext(context);
|
|
725
|
+
const dependsOn = Array.isArray(args.dependsOn)
|
|
726
|
+
? args.dependsOn.filter((x) => typeof x === 'string' && x.length > 0)
|
|
727
|
+
: undefined;
|
|
728
|
+
const result = await this.taskService.runTask({
|
|
729
|
+
chatId,
|
|
730
|
+
task: String(args.task || ''),
|
|
731
|
+
worktree: args.worktree === true,
|
|
732
|
+
worktreeName: typeof args.worktreeName === 'string' ? args.worktreeName : undefined,
|
|
733
|
+
dependsOn,
|
|
734
|
+
});
|
|
735
|
+
return {
|
|
736
|
+
success: true,
|
|
737
|
+
...result,
|
|
738
|
+
hint: result.status === 'blocked'
|
|
739
|
+
? '任务已登记为 blocked:依赖任务全部完成后会自动点火,不需要你轮询催促。'
|
|
740
|
+
: '任务已在后台独立运行(不随本轮对话结束而停止)。用 task_status 查进度、task_result 取最终结果;不要在本轮等待它。',
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
case 'task_send': {
|
|
744
|
+
const chatId = this.requireTaskContext(context);
|
|
745
|
+
return this.taskService.sendMessage(chatId, String(args.taskId || ''), String(args.message || ''));
|
|
746
|
+
}
|
|
747
|
+
case 'task_status': {
|
|
748
|
+
const chatId = this.requireTaskContext(context);
|
|
749
|
+
return this.taskService.status(chatId, typeof args.taskId === 'string' && args.taskId ? args.taskId : undefined);
|
|
750
|
+
}
|
|
751
|
+
case 'task_result': {
|
|
752
|
+
const chatId = this.requireTaskContext(context);
|
|
753
|
+
return this.taskService.result(chatId, String(args.taskId || ''));
|
|
754
|
+
}
|
|
755
|
+
case 'worktree_create': {
|
|
756
|
+
const chatId = this.requireTaskContext(context);
|
|
757
|
+
const wt = await this.worktreeService.createWorktree(chatId, typeof args.name === 'string' && args.name ? args.name : undefined);
|
|
758
|
+
return { success: true, ...wt };
|
|
759
|
+
}
|
|
760
|
+
case 'worktree_list': {
|
|
761
|
+
const chatId = this.requireTaskContext(context);
|
|
762
|
+
const worktrees = await this.worktreeService.listWorktrees(chatId);
|
|
763
|
+
return { success: true, total: worktrees.length, worktrees };
|
|
764
|
+
}
|
|
765
|
+
case 'worktree_remove': {
|
|
766
|
+
const chatId = this.requireTaskContext(context);
|
|
767
|
+
const result = await this.worktreeService.removeWorktree(chatId, String(args.name || ''));
|
|
768
|
+
return { success: true, ...result };
|
|
769
|
+
}
|
|
770
|
+
default: {
|
|
771
|
+
// 场景包贡献的工具在 default 分发:内置工具优先(注册时已拒重名)。
|
|
772
|
+
const contributed = this.contributedTools.get(call.name);
|
|
773
|
+
if (contributed) {
|
|
774
|
+
return await contributed.handler(args, context ?? {});
|
|
775
|
+
}
|
|
776
|
+
return { success: false, error: `Unknown tool: ${call.name}` };
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* 任务/worktree 工具的调用上下文:必须有来源 chatId(任务表按 chat 归组、
|
|
782
|
+
* 项目围栏按 chat 解析)。schema 出场 ⇒ 服务已接线,这里只缺 chatId 时
|
|
783
|
+
* 抛错(reverse-tools 总是透传;缺了就是接线 bug,响亮失败)。
|
|
784
|
+
*/
|
|
785
|
+
requireTaskContext(context) {
|
|
786
|
+
if (!this.taskService || !this.worktreeService) {
|
|
787
|
+
throw new Error('任务/worktree 服务未接线(main.ts setTaskServices)');
|
|
788
|
+
}
|
|
789
|
+
const chatId = context?.chatId;
|
|
790
|
+
if (!chatId) {
|
|
791
|
+
throw new Error('任务/worktree 工具需要 chatId 调用上下文');
|
|
792
|
+
}
|
|
793
|
+
return chatId;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Agent 写出 `…/skills/<name>/SKILL.md` 且该目录还不在已知 skill root 上时,
|
|
797
|
+
* 拷进用户技能目录,Skill 设置 / `/` 菜单不必再手动导入。
|
|
798
|
+
*/
|
|
799
|
+
autoInstallSkillOnSuccess(result, writtenPath) {
|
|
800
|
+
if (!result || typeof result !== 'object' || result.success !== true) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (typeof writtenPath !== 'string' || !writtenPath)
|
|
804
|
+
return;
|
|
805
|
+
try {
|
|
806
|
+
maybeAutoInstallWrittenSkill(writtenPath);
|
|
807
|
+
}
|
|
808
|
+
catch (err) {
|
|
809
|
+
console.warn('[skill-install] auto-install failed', err);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* 项目模式硬沙箱(cwd 维度):projectRoot 非空时,cwd 默认收到项目根;
|
|
814
|
+
* 模型显式给的 cwd 越界则返回错误结果。相对 cwd 按项目根解析(而不是
|
|
815
|
+
* 进程 cwd),符合"在项目里干活"的直觉。
|
|
816
|
+
*
|
|
817
|
+
* 注意边界:shell 命令文本里内嵌的绝对路径无法可靠解析,围栏只覆盖
|
|
818
|
+
* cwd 与文件读写工具——系统提示里已告知模型只在项目目录内操作。
|
|
819
|
+
*
|
|
820
|
+
* local_exec_shell 与 local_run_snippet 共用本 helper。
|
|
821
|
+
*/
|
|
822
|
+
applyProjectCwdSandbox(request, projectRoot) {
|
|
823
|
+
if (!projectRoot)
|
|
824
|
+
return { request };
|
|
825
|
+
const root = path.resolve(projectRoot.startsWith('~') ? path.join(os.homedir(), projectRoot.slice(1)) : projectRoot);
|
|
826
|
+
if (request.cwd) {
|
|
827
|
+
const expanded = request.cwd.startsWith('~')
|
|
828
|
+
? path.join(os.homedir(), request.cwd.slice(1))
|
|
829
|
+
: request.cwd;
|
|
830
|
+
const resolvedCwd = path.isAbsolute(expanded)
|
|
831
|
+
? path.resolve(expanded)
|
|
832
|
+
: path.resolve(root, expanded);
|
|
833
|
+
if (!isPathWithinRoot(resolvedCwd, root)) {
|
|
834
|
+
return {
|
|
835
|
+
error: {
|
|
836
|
+
success: false,
|
|
837
|
+
error: buildProjectRootViolation(resolvedCwd, root),
|
|
838
|
+
platform: process.platform,
|
|
839
|
+
},
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
return { request: { ...request, cwd: resolvedCwd } };
|
|
843
|
+
}
|
|
844
|
+
return { request: { ...request, cwd: root } };
|
|
845
|
+
}
|
|
846
|
+
async executeShell(request, projectRoot) {
|
|
847
|
+
const sandboxed = this.applyProjectCwdSandbox(request, projectRoot);
|
|
848
|
+
if ('error' in sandboxed)
|
|
849
|
+
return sandboxed.error;
|
|
850
|
+
if (this.shellExecutor) {
|
|
851
|
+
return this.shellExecutor(sandboxed.request);
|
|
852
|
+
}
|
|
853
|
+
return this.localExecutor.executeShell(sandboxed.request);
|
|
854
|
+
}
|
|
855
|
+
resolveMcpConfig(args) {
|
|
856
|
+
return {
|
|
857
|
+
command: String(args.command || ''),
|
|
858
|
+
args: Array.isArray(args.args) ? args.args.map(item => String(item)) : undefined,
|
|
859
|
+
cwd: typeof args.cwd === 'string' ? args.cwd : undefined,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* `tool_search` 处理器:BM25 排序 deferred 名录(name 分词计两次),返回
|
|
864
|
+
* 完整 schema,结果有界(默认 8,封顶 20)。算法、常量、默认上限与返回
|
|
865
|
+
* 文案都与 steerable-framework `tool_search.py` 对齐——同名工具在两侧给
|
|
866
|
+
* 出同样的名次;hidden 层不进搜索。
|
|
867
|
+
*
|
|
868
|
+
* 智能体工具策略在这里同样生效:否则每轮列表藏起来的工具会从这条发现缝
|
|
869
|
+
* 漏回去,「限制」只剩一半。
|
|
870
|
+
*/
|
|
871
|
+
executeToolSearch(args, policy) {
|
|
872
|
+
const cap = resolveMaxResults(args.max_results);
|
|
873
|
+
const deferred = filterToolsByPolicy(this.listSchemas().filter(s => s.exposure === 'deferred'), policy ?? { mode: 'all', tools: [] });
|
|
874
|
+
const ranked = rankTools(deferred, String(args.query ?? ''));
|
|
875
|
+
const matches = ranked.slice(0, cap).map(schema => ({
|
|
876
|
+
name: schema.name,
|
|
877
|
+
description: schema.description,
|
|
878
|
+
parameters: schema.inputSchema,
|
|
879
|
+
}));
|
|
880
|
+
return {
|
|
881
|
+
matches,
|
|
882
|
+
deferredCount: deferred.length,
|
|
883
|
+
note: matches.length
|
|
884
|
+
? 'Matched tools are registered; call them by name with the returned schema.'
|
|
885
|
+
: 'No deferred tools matched; try different search terms.',
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* 前转网络读取调用到 sidecar(W5-2)。走到这里说明 schema 已出场,
|
|
890
|
+
* 即握手时 sidecar 注册了该工具;delegate 缺失/集合漂移是编程错误或
|
|
891
|
+
* sidecar 中途重启,按响亮失败处理(reverse-tools 会把抛错包成
|
|
892
|
+
* ToolResult)。
|
|
893
|
+
*/
|
|
894
|
+
async executeWebTool(name, args) {
|
|
895
|
+
if (!this.webToolDelegate || !this.webToolNames.has(name)) {
|
|
896
|
+
throw new Error(`工具 ${name} 当前不可用:sidecar 未注册该工具` +
|
|
897
|
+
(name === 'web_search'
|
|
898
|
+
? '(web_search 需要在设置页选择免费搜索或填写 Tavily 钥,或配置 STEERABLE_WEB_SEARCH_API_KEY / TAVILY_API_KEY;OpenAI 可用聊天凭证走托管搜索)'
|
|
899
|
+
: '') +
|
|
900
|
+
'。');
|
|
901
|
+
}
|
|
902
|
+
if (name === 'web_search' && this.hostedWebSearch) {
|
|
903
|
+
return await this.hostedWebSearch(args);
|
|
904
|
+
}
|
|
905
|
+
return await this.webToolDelegate(name, args);
|
|
906
|
+
}
|
|
907
|
+
/** 执行注册表里的 MCP 动态工具(`mcp__<serverKey>__<toolName>`)。 */
|
|
908
|
+
async executeRegisteredMcpTool(name, args) {
|
|
909
|
+
if (!this.mcpRegistry) {
|
|
910
|
+
return { success: false, error: 'MCP 服务注册表不可用' };
|
|
911
|
+
}
|
|
912
|
+
const rest = name.slice(MCP_DYNAMIC_TOOL_PREFIX.length);
|
|
913
|
+
const sep = rest.indexOf('__');
|
|
914
|
+
if (sep === -1) {
|
|
915
|
+
return { success: false, error: `无法解析的 MCP 工具名: ${name}` };
|
|
916
|
+
}
|
|
917
|
+
const serverKey = rest.slice(0, sep);
|
|
918
|
+
const toolName = rest.slice(sep + 2);
|
|
919
|
+
const server = this.mcpRegistry
|
|
920
|
+
.list()
|
|
921
|
+
.find((s) => s.enabled && this.mcpRegistry.serverKey(s) === serverKey);
|
|
922
|
+
if (!server) {
|
|
923
|
+
return {
|
|
924
|
+
success: false,
|
|
925
|
+
error: `MCP 服务「${serverKey}」不存在或已禁用。请在 设置 → MCP 服务 中检查配置。`,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
return await mcpExecutor.executeTool(this.mcpRegistry.toExecutorConfig(server), toolName, args);
|
|
929
|
+
}
|
|
930
|
+
}
|