@steerable/agent-shell 0.6.50 → 0.6.52
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/dist/brand.d.ts +5 -0
- package/dist/goal-store.d.ts +43 -0
- package/dist/goal-store.js +155 -0
- package/dist/host/runtime.js +13 -3
- package/dist/host-tools-runtime.d.ts +6 -0
- package/dist/host-tools-runtime.js +36 -0
- package/dist/host-tools.d.ts +1 -1
- package/dist/host-tools.js +6 -0
- package/dist/local-backend/router.js +22 -17
- package/dist/local-backend/subagent-profiles.js +4 -0
- package/dist/local-backend/task-service.d.ts +6 -0
- package/dist/local-backend/task-service.js +47 -2
- package/dist/local-executor.d.ts +14 -2
- package/dist/local-executor.js +53 -18
- package/dist/product-config.d.ts +10 -0
- package/dist/project-home.d.ts +1 -1
- package/dist/project-home.js +1 -1
- package/dist/project-registry.d.ts +4 -4
- package/dist/project-registry.js +2 -2
- package/dist/sidecar/boot.d.ts +5 -2
- package/dist/sidecar/boot.js +1 -0
- package/dist/sidecar/reverse-approval.d.ts +2 -0
- package/dist/sidecar/reverse-approval.js +2 -0
- package/dist/sidecar/reverse-tools.d.ts +6 -2
- package/dist/sidecar/reverse-tools.js +9 -1
- package/dist/sidecar/types.d.ts +10 -1
- package/dist/storage/write-lease-error.js +4 -1
- package/dist/tool-presentation.d.ts +23 -0
- package/dist/tool-presentation.js +266 -0
- package/dist/tool-router.d.ts +26 -8
- package/dist/tool-router.js +254 -18
- package/dist/workspace-search.d.ts +35 -0
- package/dist/workspace-search.js +186 -0
- package/package.json +4 -4
package/dist/local-executor.js
CHANGED
|
@@ -180,11 +180,53 @@ export function isPathWithinRoot(resolvedPath, resolvedRoot) {
|
|
|
180
180
|
const rel = path.relative(resolvedRoot, resolvedPath);
|
|
181
181
|
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
182
182
|
}
|
|
183
|
+
/** `~` 展开后 resolve。围栏比较前路径与根都要过这一步。 */
|
|
184
|
+
export function expandFencePath(inputPath) {
|
|
185
|
+
const expanded = inputPath.startsWith('~')
|
|
186
|
+
? path.join(os.homedir(), inputPath.slice(1))
|
|
187
|
+
: inputPath;
|
|
188
|
+
return path.resolve(expanded);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 可写根:项目家目录(或对话工作区)加上额外可写根(源文件夹)。
|
|
192
|
+
* 每个根都包含它自己和全部子目录。空列表表示不设围栏。
|
|
193
|
+
*/
|
|
194
|
+
export function writableFenceRoots(projectRoot, additionalWriteRoots) {
|
|
195
|
+
const roots = [];
|
|
196
|
+
const push = (raw) => {
|
|
197
|
+
if (!raw)
|
|
198
|
+
return;
|
|
199
|
+
const trimmed = raw.trim();
|
|
200
|
+
if (!trimmed)
|
|
201
|
+
return;
|
|
202
|
+
const resolved = expandFencePath(trimmed);
|
|
203
|
+
if (!roots.includes(resolved))
|
|
204
|
+
roots.push(resolved);
|
|
205
|
+
};
|
|
206
|
+
push(projectRoot);
|
|
207
|
+
for (const extra of additionalWriteRoots ?? [])
|
|
208
|
+
push(extra);
|
|
209
|
+
return roots;
|
|
210
|
+
}
|
|
183
211
|
/** 越界错误消息(给模型看,引导它把操作收回项目目录内)。 */
|
|
184
212
|
export function buildProjectRootViolation(resolvedPath, resolvedRoot) {
|
|
185
213
|
return (`路径越界:${resolvedPath} 不在当前对话绑定的项目目录 ${resolvedRoot} 内。` +
|
|
186
|
-
|
|
187
|
-
|
|
214
|
+
`项目模式下文件操作被限制在项目目录及其子目录中——请改用项目内的相对/绝对路径;` +
|
|
215
|
+
`如确需访问目录外文件,请告知用户该限制并请其把目录附加为源文件夹。`);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* 路径不在任一可写根(含其子目录)内时返回给模型的错误;在围栏内或未设围栏时返回 null。
|
|
219
|
+
* `resolvedPath` 与 `roots` 都应已 resolve。
|
|
220
|
+
*/
|
|
221
|
+
export function writableFenceViolation(resolvedPath, roots) {
|
|
222
|
+
if (roots.length === 0)
|
|
223
|
+
return null;
|
|
224
|
+
if (roots.some((root) => isPathWithinRoot(resolvedPath, root)))
|
|
225
|
+
return null;
|
|
226
|
+
if (roots.length === 1)
|
|
227
|
+
return buildProjectRootViolation(resolvedPath, roots[0]);
|
|
228
|
+
return (`路径越界:${resolvedPath} 不在可写目录内(${roots.join('、')})。` +
|
|
229
|
+
`可写范围是这些目录及其子目录。请改用其中的路径。`);
|
|
188
230
|
}
|
|
189
231
|
const BUILTIN_DANGEROUS = [
|
|
190
232
|
{ id: 'rm_rf_root', pattern: 'rm\\s+-rf\\s+\\/(?:\\s|$)', platform: 'unix' },
|
|
@@ -480,14 +522,11 @@ export class LocalExecutor {
|
|
|
480
522
|
return { success: false, error: message };
|
|
481
523
|
}
|
|
482
524
|
}
|
|
483
|
-
async writeLocalFile(request, projectRoot) {
|
|
525
|
+
async writeLocalFile(request, projectRoot, additionalWriteRoots) {
|
|
484
526
|
const filePath = this.resolvePath(request.path);
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
return { success: false, error: buildProjectRootViolation(filePath, root) };
|
|
489
|
-
}
|
|
490
|
-
}
|
|
527
|
+
const violation = writableFenceViolation(filePath, writableFenceRoots(projectRoot, additionalWriteRoots));
|
|
528
|
+
if (violation)
|
|
529
|
+
return { success: false, error: violation };
|
|
491
530
|
// 同文件串行化:读-改-写必须排队,否则并发写会互相覆盖(pi file-mutation-queue)。
|
|
492
531
|
return this.serializeFileOp(filePath, async () => {
|
|
493
532
|
try {
|
|
@@ -544,14 +583,11 @@ export class LocalExecutor {
|
|
|
544
583
|
* read-before-write 冲突检测,再在原文上做三级模糊定位 + 逆序替换,最后
|
|
545
584
|
* 原子落盘。返回新版本令牌与统一 diff。
|
|
546
585
|
*/
|
|
547
|
-
async editLocalFile(request, projectRoot) {
|
|
586
|
+
async editLocalFile(request, projectRoot, additionalWriteRoots) {
|
|
548
587
|
const filePath = this.resolvePath(request.path);
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
return { success: false, error: buildProjectRootViolation(filePath, root) };
|
|
553
|
-
}
|
|
554
|
-
}
|
|
588
|
+
const violation = writableFenceViolation(filePath, writableFenceRoots(projectRoot, additionalWriteRoots));
|
|
589
|
+
if (violation)
|
|
590
|
+
return { success: false, error: violation };
|
|
555
591
|
return this.serializeFileOp(filePath, async () => {
|
|
556
592
|
try {
|
|
557
593
|
const encoding = request.encoding ?? 'utf-8';
|
|
@@ -910,8 +946,7 @@ export class LocalExecutor {
|
|
|
910
946
|
});
|
|
911
947
|
}
|
|
912
948
|
resolvePath(inputPath) {
|
|
913
|
-
|
|
914
|
-
return path.resolve(expanded);
|
|
949
|
+
return expandFencePath(inputPath);
|
|
915
950
|
}
|
|
916
951
|
resolveCwd(cwd, shellType) {
|
|
917
952
|
const resolved = this.resolvePath(cwd || os.homedir());
|
package/dist/product-config.d.ts
CHANGED
|
@@ -106,6 +106,16 @@ export interface ProductConfig {
|
|
|
106
106
|
cwd?: string;
|
|
107
107
|
enabled?: boolean;
|
|
108
108
|
}>;
|
|
109
|
+
/**
|
|
110
|
+
* P3.1 多智能体编排六件套 (agent_spawn / agent_wait / agent_send / agent_interrupt / agent_close / agent_list)。
|
|
111
|
+
* 缺省关(仅开 delegate_subagent);设为 true 或配置对象时在回合中启用。
|
|
112
|
+
* STEERABLE_ORCHESTRATION 环境变量('1' 开启,'0' 显式关闭)优先级更高。
|
|
113
|
+
*/
|
|
114
|
+
orchestration?: boolean | {
|
|
115
|
+
enabled?: boolean;
|
|
116
|
+
maxDepth?: number;
|
|
117
|
+
maxParallel?: number;
|
|
118
|
+
};
|
|
109
119
|
}
|
|
110
120
|
/** shell 内置智能体:只有产品显式 `true` 才开。 */
|
|
111
121
|
export declare function isShellBuiltinAgentEnabled(id: 'local-assistant' | 'all-round-assistant', config?: ProductConfig): boolean;
|
package/dist/project-home.d.ts
CHANGED
package/dist/project-home.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* 项目 = 名字 + 托管家目录 + 可选源文件夹。家目录默认建在
|
|
5
5
|
* `Documents/<应用名>/<项目名>/`(见 project-home.ts)。chat 通过
|
|
6
6
|
* `chat_sessions.project_id` 绑定项目;绑定后文件读写与命令执行被硬限制
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* 在家目录及其子目录内(见 tool-router.ts 的 ToolExecContext.projectRoot);
|
|
8
|
+
* 源文件夹及其子目录同样可写。
|
|
9
9
|
*
|
|
10
10
|
* 持久化在 userData/agent-projects.json。存储通过 {@link ProjectKvStore}
|
|
11
11
|
* 接口注入:main.ts 用 electron-store 实现,单测用内存实现——本模块不
|
|
@@ -18,8 +18,8 @@ export interface ProjectRecord {
|
|
|
18
18
|
/** 托管家目录(绝对路径)。默认 `Documents/<应用名>/<项目名>/`。 */
|
|
19
19
|
folderPath: string;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* 附加源文件夹(已有代码目录)。与家目录一样可读写,范围含各自子目录。
|
|
22
|
+
* 不替代家目录。旧记录没有此字段,读取时按空列表处理。
|
|
23
23
|
*/
|
|
24
24
|
sourceFolders?: string[];
|
|
25
25
|
/**
|
package/dist/project-registry.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* 项目 = 名字 + 托管家目录 + 可选源文件夹。家目录默认建在
|
|
5
5
|
* `Documents/<应用名>/<项目名>/`(见 project-home.ts)。chat 通过
|
|
6
6
|
* `chat_sessions.project_id` 绑定项目;绑定后文件读写与命令执行被硬限制
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* 在家目录及其子目录内(见 tool-router.ts 的 ToolExecContext.projectRoot);
|
|
8
|
+
* 源文件夹及其子目录同样可写。
|
|
9
9
|
*
|
|
10
10
|
* 持久化在 userData/agent-projects.json。存储通过 {@link ProjectKvStore}
|
|
11
11
|
* 接口注入:main.ts 用 electron-store 实现,单测用内存实现——本模块不
|
package/dist/sidecar/boot.d.ts
CHANGED
|
@@ -9,10 +9,13 @@ export interface HostSidecarDeps {
|
|
|
9
9
|
/** W4-2 围栏:chatId → 项目家目录或无项目对话工作区。 */
|
|
10
10
|
resolveProjectRoot: (chatId: string) => Promise<string | null>;
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
* projectRoot 围栏约束;这些根只放宽 local_read_file 的读取范围。
|
|
12
|
+
* 额外只读根(会话附件目录、源文件夹)。只放宽 local_read_file,含子目录。
|
|
14
13
|
*/
|
|
15
14
|
resolveAdditionalReadRoots?: (chatId: string) => string[] | Promise<string[]>;
|
|
15
|
+
/**
|
|
16
|
+
* 额外可写根(项目源文件夹)。放宽写入、编辑和命令 cwd,含各自子目录。
|
|
17
|
+
*/
|
|
18
|
+
resolveAdditionalWriteRoots?: (chatId: string) => string[] | Promise<string[]>;
|
|
16
19
|
/** W4-1 审批反向通道处理器(宿主审批弹窗的应答入口)。 */
|
|
17
20
|
approvalHandler: ReturnType<typeof createApprovalBridge>['handler'];
|
|
18
21
|
/**
|
package/dist/sidecar/boot.js
CHANGED
|
@@ -214,6 +214,7 @@ export async function startHostSidecar(deps) {
|
|
|
214
214
|
// root so ToolRouter's cwd/path confinement applies.
|
|
215
215
|
resolveProjectRoot: deps.resolveProjectRoot,
|
|
216
216
|
resolveAdditionalReadRoots: deps.resolveAdditionalReadRoots,
|
|
217
|
+
resolveAdditionalWriteRoots: deps.resolveAdditionalWriteRoots,
|
|
217
218
|
}));
|
|
218
219
|
// W4-1: approval algebra reverse channel — the sidecar's
|
|
219
220
|
// ApprovalExecutor(HostApprover) asks here; the renderer modal answers
|
|
@@ -43,6 +43,7 @@ export function createApprovalBridge(deps) {
|
|
|
43
43
|
return deny('no renderer window available to approve the call');
|
|
44
44
|
}
|
|
45
45
|
const requestId = randomUUID();
|
|
46
|
+
const chatId = typeof p.chatId === 'string' && p.chatId ? p.chatId : undefined;
|
|
46
47
|
const prompt = {
|
|
47
48
|
requestId,
|
|
48
49
|
toolName,
|
|
@@ -52,6 +53,7 @@ export function createApprovalBridge(deps) {
|
|
|
52
53
|
mode: typeof p.mode === 'string' ? p.mode : 'other',
|
|
53
54
|
category: typeof p.category === 'string' ? p.category : toolName,
|
|
54
55
|
round: typeof p.round === 'number' ? p.round : 0,
|
|
56
|
+
...(chatId ? { chatId } : {}),
|
|
55
57
|
};
|
|
56
58
|
deps.onLog?.(`approval: prompting for ${toolName} (${prompt.mode}/${prompt.category})`);
|
|
57
59
|
return await new Promise((resolve) => {
|
|
@@ -21,9 +21,13 @@ export interface ReverseToolDeps {
|
|
|
21
21
|
*/
|
|
22
22
|
resolveProjectRoot?: (chatId: string) => Promise<string | null>;
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
* 这里只放宽 local_read_file 的读取范围。
|
|
24
|
+
* 额外只读根(会话附件目录、源文件夹)。只放宽读取,含各自子目录。
|
|
26
25
|
*/
|
|
27
26
|
resolveAdditionalReadRoots?: (chatId: string) => string[] | Promise<string[]>;
|
|
27
|
+
/**
|
|
28
|
+
* 额外可写根(项目源文件夹)。放宽写入、编辑和命令 cwd,含各自子目录。
|
|
29
|
+
* 附件目录不进这里。
|
|
30
|
+
*/
|
|
31
|
+
resolveAdditionalWriteRoots?: (chatId: string) => string[] | Promise<string[]>;
|
|
28
32
|
}
|
|
29
33
|
export declare function createToolInvokeHandler(deps: ReverseToolDeps): SidecarReverseHandler;
|
|
@@ -74,6 +74,9 @@ export function createToolInvokeHandler(deps) {
|
|
|
74
74
|
const additionalReadRoots = p.context?.chatId
|
|
75
75
|
? await Promise.resolve(deps.resolveAdditionalReadRoots?.(p.context.chatId) ?? [])
|
|
76
76
|
: [];
|
|
77
|
+
const additionalWriteRoots = p.context?.chatId
|
|
78
|
+
? await Promise.resolve(deps.resolveAdditionalWriteRoots?.(p.context.chatId) ?? [])
|
|
79
|
+
: [];
|
|
77
80
|
if (name === 'local_exec_shell') {
|
|
78
81
|
const classification = classifyShellCommand(String(toolArgs.command ?? ''));
|
|
79
82
|
if (classification.severity === 'critical') {
|
|
@@ -94,7 +97,11 @@ export function createToolInvokeHandler(deps) {
|
|
|
94
97
|
? normalizeToolPolicy(p.context.toolPolicy)
|
|
95
98
|
: null;
|
|
96
99
|
const toolPolicy = normalizedPolicy && normalizedPolicy.mode !== 'all' ? normalizedPolicy : null;
|
|
97
|
-
const execContext = projectRoot ||
|
|
100
|
+
const execContext = projectRoot ||
|
|
101
|
+
additionalReadRoots.length > 0 ||
|
|
102
|
+
additionalWriteRoots.length > 0 ||
|
|
103
|
+
p.context?.chatId ||
|
|
104
|
+
toolPolicy
|
|
98
105
|
? {
|
|
99
106
|
projectRoot,
|
|
100
107
|
chatId: p.context?.chatId,
|
|
@@ -102,6 +109,7 @@ export function createToolInvokeHandler(deps) {
|
|
|
102
109
|
// worktree_* 工具按它把任务绑定到来源对话。
|
|
103
110
|
...(toolPolicy ? { toolPolicy } : {}),
|
|
104
111
|
...(additionalReadRoots.length > 0 ? { additionalReadRoots } : {}),
|
|
112
|
+
...(additionalWriteRoots.length > 0 ? { additionalWriteRoots } : {}),
|
|
105
113
|
}
|
|
106
114
|
: undefined;
|
|
107
115
|
try {
|
package/dist/sidecar/types.d.ts
CHANGED
|
@@ -333,12 +333,15 @@ export interface SidecarChatStreamRequest {
|
|
|
333
333
|
* executor with the six-tool orchestration family (agent_spawn /
|
|
334
334
|
* agent_send / agent_wait / agent_close / agent_list / agent_interrupt).
|
|
335
335
|
* When delegation is also on (it is by default), both surfaces share one
|
|
336
|
-
* agent pool — one maxParallel budget, one lineage space.
|
|
336
|
+
* agent pool — one maxParallel budget, one lineage space.
|
|
337
|
+
* `childMaxRounds` / `childMaxToolErrors` default to the parent loop's
|
|
338
|
+
* limits. */
|
|
337
339
|
orchestration?: {
|
|
338
340
|
enabled?: boolean;
|
|
339
341
|
maxDepth?: number;
|
|
340
342
|
maxParallel?: number;
|
|
341
343
|
childMaxRounds?: number;
|
|
344
|
+
childMaxToolErrors?: number;
|
|
342
345
|
};
|
|
343
346
|
/**
|
|
344
347
|
* Sub-agent delegation (delegate-on-pool). ON BY DEFAULT on the sidecar —
|
|
@@ -451,6 +454,12 @@ export interface SidecarChatStreamRequest {
|
|
|
451
454
|
mode: 'host' | 'auto';
|
|
452
455
|
timeoutMs?: number;
|
|
453
456
|
storePath?: string;
|
|
457
|
+
/**
|
|
458
|
+
* Project home, source folders, and other configured write roots.
|
|
459
|
+
* File tools whose targets all sit inside these directories skip the
|
|
460
|
+
* host prompt. Independent of whether the OS exec sandbox is on.
|
|
461
|
+
*/
|
|
462
|
+
writableRoots?: string[];
|
|
454
463
|
};
|
|
455
464
|
}
|
|
456
465
|
/**
|
|
@@ -6,9 +6,12 @@ export function acquireWriteLeaseOrExit(acquire, loadElectron = () => import('el
|
|
|
6
6
|
}
|
|
7
7
|
catch (error) {
|
|
8
8
|
if (error instanceof StoreAlreadyOwnedError) {
|
|
9
|
+
console.error(`[bs] 本地数据库正在被另一个进程使用(${error.lockPath})。\n` +
|
|
10
|
+
'[bs] 再打开一次桌面应用会回到已经打开的窗口。\n' +
|
|
11
|
+
'[bs] 如果终端里的 pnpm dev:bs 占着数据库,请先在那个终端按 Ctrl+C 停掉,再重新打开。');
|
|
9
12
|
void loadElectron()
|
|
10
13
|
.then(({ app, dialog }) => {
|
|
11
|
-
dialog.showErrorBox('
|
|
14
|
+
dialog.showErrorBox('无法再开一个', '再打开一次应用时,会回到已经打开的窗口。\n\n这次没有回到那个窗口,是因为本地数据库正被另一个进程占用。如果终端里还在跑 pnpm dev:bs,先停掉它,再重新打开。');
|
|
12
15
|
app.exit(1);
|
|
13
16
|
})
|
|
14
17
|
.catch(() => exitProcess(1));
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 一等工具的展示声明。
|
|
3
|
+
*
|
|
4
|
+
* 卡片种类和标题从这里按工具名取,不从名字前缀猜测。
|
|
5
|
+
* 未声明的名字(MCP、插件、场景包)落到 generic / other,`declared: false`。
|
|
6
|
+
* 宿主在 executed_actions 上盖 `view`,卡片读这份声明。
|
|
7
|
+
*/
|
|
8
|
+
export declare const TOOL_CARDS: readonly ["generic", "terminal", "diff", "search", "read", "web"];
|
|
9
|
+
export type ToolCard = (typeof TOOL_CARDS)[number];
|
|
10
|
+
export declare const TOOL_KINDS: readonly ["read", "edit", "delete", "move", "search", "execute", "fetch", "other"];
|
|
11
|
+
export type ToolKind = (typeof TOOL_KINDS)[number];
|
|
12
|
+
export interface ToolCallView {
|
|
13
|
+
card: ToolCard;
|
|
14
|
+
kind: ToolKind;
|
|
15
|
+
title: string;
|
|
16
|
+
/** 名字在声明表里。false 表示通用回退,不是从名字前缀推断出来的。 */
|
|
17
|
+
declared: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 取一次调用的展示。`result` 缺省时只根据参数出标题;结果到达后用同一声明补计数和状态。
|
|
21
|
+
*/
|
|
22
|
+
export declare function presentToolCall(name: string, args: unknown, result?: unknown): ToolCallView;
|
|
23
|
+
export declare function declaredToolNames(): readonly string[];
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 一等工具的展示声明。
|
|
3
|
+
*
|
|
4
|
+
* 卡片种类和标题从这里按工具名取,不从名字前缀猜测。
|
|
5
|
+
* 未声明的名字(MCP、插件、场景包)落到 generic / other,`declared: false`。
|
|
6
|
+
* 宿主在 executed_actions 上盖 `view`,卡片读这份声明。
|
|
7
|
+
*/
|
|
8
|
+
export const TOOL_CARDS = ['generic', 'terminal', 'diff', 'search', 'read', 'web'];
|
|
9
|
+
export const TOOL_KINDS = [
|
|
10
|
+
'read',
|
|
11
|
+
'edit',
|
|
12
|
+
'delete',
|
|
13
|
+
'move',
|
|
14
|
+
'search',
|
|
15
|
+
'execute',
|
|
16
|
+
'fetch',
|
|
17
|
+
'other',
|
|
18
|
+
];
|
|
19
|
+
function clip(text, max = 60) {
|
|
20
|
+
const trimmed = text.replace(/\s+/g, ' ').trim();
|
|
21
|
+
if (!trimmed)
|
|
22
|
+
return '';
|
|
23
|
+
return trimmed.length > max ? `${trimmed.slice(0, max - 1)}…` : trimmed;
|
|
24
|
+
}
|
|
25
|
+
function str(args, key) {
|
|
26
|
+
const value = args[key];
|
|
27
|
+
return typeof value === 'string' ? value : '';
|
|
28
|
+
}
|
|
29
|
+
function dataOf(result) {
|
|
30
|
+
if (!result || typeof result !== 'object')
|
|
31
|
+
return null;
|
|
32
|
+
const data = result.data;
|
|
33
|
+
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
function failed(result) {
|
|
39
|
+
return Boolean(result && typeof result === 'object' && result.success === false);
|
|
40
|
+
}
|
|
41
|
+
function commandTitle(args) {
|
|
42
|
+
return clip(str(args, 'command')) || 'shell';
|
|
43
|
+
}
|
|
44
|
+
function pathTitle(args, fallback) {
|
|
45
|
+
return clip(str(args, 'path') || str(args, 'file') || str(args, 'target'), 72) || fallback;
|
|
46
|
+
}
|
|
47
|
+
function webSearchTitle(args, result) {
|
|
48
|
+
const query = str(args, 'query');
|
|
49
|
+
const head = `搜索“${clip(query, 40) || '…'}”`;
|
|
50
|
+
if (result === undefined || failed(result))
|
|
51
|
+
return head;
|
|
52
|
+
const data = dataOf(result);
|
|
53
|
+
const count = data && typeof data.result_count === 'number' ? data.result_count : null;
|
|
54
|
+
return count === null ? head : `${head} → ${count} 条结果`;
|
|
55
|
+
}
|
|
56
|
+
function shortUrl(raw) {
|
|
57
|
+
if (!raw)
|
|
58
|
+
return '未知地址';
|
|
59
|
+
try {
|
|
60
|
+
const url = new URL(raw);
|
|
61
|
+
const path = url.pathname === '/' ? '' : url.pathname;
|
|
62
|
+
return clip(`${url.host}${path}`, 48);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return clip(raw, 48);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function webFetchTitle(args, result) {
|
|
69
|
+
const head = `抓取 ${shortUrl(str(args, 'url'))}`;
|
|
70
|
+
if (result === undefined || failed(result))
|
|
71
|
+
return head;
|
|
72
|
+
const data = dataOf(result);
|
|
73
|
+
if (!data)
|
|
74
|
+
return head;
|
|
75
|
+
const parts = [];
|
|
76
|
+
if (typeof data.status === 'number')
|
|
77
|
+
parts.push(String(data.status));
|
|
78
|
+
if (typeof data.bytes === 'number' && Number.isFinite(data.bytes)) {
|
|
79
|
+
parts.push(data.bytes < 1024 ? `${data.bytes} B` : `${(data.bytes / 1024).toFixed(1)} KB`);
|
|
80
|
+
}
|
|
81
|
+
if (data.truncated === true)
|
|
82
|
+
parts.push('已截断');
|
|
83
|
+
return parts.length > 0 ? `${head} → ${parts.join(' · ')}` : head;
|
|
84
|
+
}
|
|
85
|
+
function taskRunTitle(args, result) {
|
|
86
|
+
const task = clip(str(args, 'task'));
|
|
87
|
+
if (!task)
|
|
88
|
+
return '后台任务';
|
|
89
|
+
const status = dataOf(result)?.status;
|
|
90
|
+
if (status === 'blocked')
|
|
91
|
+
return `${task} · 等待依赖`;
|
|
92
|
+
if (args.worktree === true)
|
|
93
|
+
return `${task} · 隔离工作区`;
|
|
94
|
+
return task;
|
|
95
|
+
}
|
|
96
|
+
function jobIdTitle(args, fallback) {
|
|
97
|
+
const id = str(args, 'job_id') || str(args, 'taskId');
|
|
98
|
+
return id ? id.slice(0, 8) : fallback;
|
|
99
|
+
}
|
|
100
|
+
function runCodeTitle(args, result) {
|
|
101
|
+
const description = clip(str(args, 'description'), 40) || '程序';
|
|
102
|
+
if (failed(result))
|
|
103
|
+
return `程序「${description}」失败`;
|
|
104
|
+
const calls = dataOf(result)?.calls;
|
|
105
|
+
const count = Array.isArray(calls) ? calls.length : 0;
|
|
106
|
+
return count > 0 ? `程序「${description}」· ${count} 个内层工具` : `程序「${description}」`;
|
|
107
|
+
}
|
|
108
|
+
function goalTitle(args, fallback) {
|
|
109
|
+
return clip(str(args, 'objective'), 72) || fallback;
|
|
110
|
+
}
|
|
111
|
+
const PRESENTERS = {
|
|
112
|
+
local_exec_shell: { card: 'terminal', kind: 'execute', title: commandTitle },
|
|
113
|
+
pwsh: { card: 'terminal', kind: 'execute', title: commandTitle },
|
|
114
|
+
bash: { card: 'terminal', kind: 'execute', title: commandTitle },
|
|
115
|
+
bash_session: { card: 'terminal', kind: 'execute', title: commandTitle },
|
|
116
|
+
write_stdin: {
|
|
117
|
+
card: 'terminal',
|
|
118
|
+
kind: 'execute',
|
|
119
|
+
title: (args) => clip(str(args, 'chars'), 40) || str(args, 'sessionId').slice(0, 8) || 'stdin',
|
|
120
|
+
},
|
|
121
|
+
local_run_snippet: {
|
|
122
|
+
card: 'terminal',
|
|
123
|
+
kind: 'execute',
|
|
124
|
+
title: (args) => clip(str(args, 'code'), 48) || str(args, 'language') || 'snippet',
|
|
125
|
+
},
|
|
126
|
+
local_run_script: {
|
|
127
|
+
card: 'terminal',
|
|
128
|
+
kind: 'execute',
|
|
129
|
+
title: (args) => str(args, 'scriptId') || 'script',
|
|
130
|
+
},
|
|
131
|
+
run_code: { card: 'generic', kind: 'execute', title: runCodeTitle },
|
|
132
|
+
run_js: { card: 'generic', kind: 'execute', title: runCodeTitle },
|
|
133
|
+
wait_js: {
|
|
134
|
+
card: 'generic',
|
|
135
|
+
kind: 'execute',
|
|
136
|
+
title: (args) => str(args, 'id').slice(0, 8) || 'wait_js',
|
|
137
|
+
},
|
|
138
|
+
local_read_file: { card: 'read', kind: 'read', title: (args) => pathTitle(args, 'read') },
|
|
139
|
+
read_file: { card: 'read', kind: 'read', title: (args) => pathTitle(args, 'read') },
|
|
140
|
+
view_image: { card: 'read', kind: 'read', title: (args) => pathTitle(args, 'image') },
|
|
141
|
+
capture_display: {
|
|
142
|
+
card: 'read',
|
|
143
|
+
kind: 'read',
|
|
144
|
+
title: (args) => clip(str(args, 'target'), 48) || 'display',
|
|
145
|
+
},
|
|
146
|
+
local_list_scripts: { card: 'read', kind: 'read', title: () => '已保存的脚本' },
|
|
147
|
+
local_write_file: { card: 'diff', kind: 'edit', title: (args) => pathTitle(args, 'write') },
|
|
148
|
+
write_file: { card: 'diff', kind: 'edit', title: (args) => pathTitle(args, 'write') },
|
|
149
|
+
local_edit_file: { card: 'diff', kind: 'edit', title: (args) => pathTitle(args, 'edit') },
|
|
150
|
+
edit_file: { card: 'diff', kind: 'edit', title: (args) => pathTitle(args, 'edit') },
|
|
151
|
+
apply_patch: { card: 'diff', kind: 'edit', title: () => 'apply_patch' },
|
|
152
|
+
grep: {
|
|
153
|
+
card: 'search',
|
|
154
|
+
kind: 'search',
|
|
155
|
+
title: (args) => clip(str(args, 'pattern')) || 'grep',
|
|
156
|
+
},
|
|
157
|
+
glob: {
|
|
158
|
+
card: 'search',
|
|
159
|
+
kind: 'search',
|
|
160
|
+
title: (args) => clip(str(args, 'pattern')) || 'glob',
|
|
161
|
+
},
|
|
162
|
+
tool_search: {
|
|
163
|
+
card: 'search',
|
|
164
|
+
kind: 'search',
|
|
165
|
+
title: (args) => clip(str(args, 'query')) || 'tool_search',
|
|
166
|
+
},
|
|
167
|
+
web_search: { card: 'web', kind: 'search', title: webSearchTitle },
|
|
168
|
+
web_fetch: { card: 'web', kind: 'fetch', title: webFetchTitle },
|
|
169
|
+
task_run: { card: 'generic', kind: 'other', title: taskRunTitle },
|
|
170
|
+
task_send: {
|
|
171
|
+
card: 'generic',
|
|
172
|
+
kind: 'other',
|
|
173
|
+
title: (args) => clip(str(args, 'message')) || '转达任务',
|
|
174
|
+
},
|
|
175
|
+
task_status: {
|
|
176
|
+
card: 'generic',
|
|
177
|
+
kind: 'read',
|
|
178
|
+
title: (args, result) => {
|
|
179
|
+
const id = str(args, 'taskId');
|
|
180
|
+
if (id)
|
|
181
|
+
return id.slice(0, 8);
|
|
182
|
+
const total = dataOf(result)?.total;
|
|
183
|
+
return typeof total === 'number' ? `${total} 个任务` : '本会话';
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
task_result: {
|
|
187
|
+
card: 'generic',
|
|
188
|
+
kind: 'read',
|
|
189
|
+
title: (args) => jobIdTitle(args, '收取结果'),
|
|
190
|
+
},
|
|
191
|
+
job_list: {
|
|
192
|
+
card: 'generic',
|
|
193
|
+
kind: 'read',
|
|
194
|
+
title: (_args, result) => {
|
|
195
|
+
const total = dataOf(result)?.total;
|
|
196
|
+
return typeof total === 'number' ? `${total} 个任务` : '后台任务';
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
job_output: { card: 'generic', kind: 'read', title: (args) => jobIdTitle(args, '任务输出') },
|
|
200
|
+
job_kill: { card: 'generic', kind: 'delete', title: (args) => jobIdTitle(args, '结束任务') },
|
|
201
|
+
worktree_create: {
|
|
202
|
+
card: 'generic',
|
|
203
|
+
kind: 'other',
|
|
204
|
+
title: (args) => str(args, 'name') || '新建工作区',
|
|
205
|
+
},
|
|
206
|
+
worktree_list: { card: 'read', kind: 'read', title: () => '工作区列表' },
|
|
207
|
+
worktree_remove: {
|
|
208
|
+
card: 'generic',
|
|
209
|
+
kind: 'delete',
|
|
210
|
+
title: (args) => str(args, 'name') || '移除工作区',
|
|
211
|
+
},
|
|
212
|
+
get_goal: { card: 'generic', kind: 'read', title: () => '当前目标' },
|
|
213
|
+
create_goal: { card: 'generic', kind: 'other', title: (args) => goalTitle(args, '新建目标') },
|
|
214
|
+
update_goal: {
|
|
215
|
+
card: 'generic',
|
|
216
|
+
kind: 'other',
|
|
217
|
+
title: (args) => {
|
|
218
|
+
const action = str(args, 'action');
|
|
219
|
+
const objective = clip(str(args, 'objective'), 48);
|
|
220
|
+
return objective ? `${action || 'update'} ${objective}` : action || '更新目标';
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
present_files: { card: 'generic', kind: 'other', title: () => '交付文件' },
|
|
224
|
+
local_open_path: {
|
|
225
|
+
card: 'generic',
|
|
226
|
+
kind: 'other',
|
|
227
|
+
title: (args) => pathTitle(args, 'open'),
|
|
228
|
+
},
|
|
229
|
+
todo_write: { card: 'generic', kind: 'other', title: () => '待办' },
|
|
230
|
+
ask_user: {
|
|
231
|
+
card: 'generic',
|
|
232
|
+
kind: 'other',
|
|
233
|
+
title: (args) => clip(str(args, 'intro') || str(args, 'question')) || '提问',
|
|
234
|
+
},
|
|
235
|
+
skill: { card: 'generic', kind: 'read', title: (args) => str(args, 'name') || 'skill' },
|
|
236
|
+
delegate_subagent: {
|
|
237
|
+
card: 'generic',
|
|
238
|
+
kind: 'other',
|
|
239
|
+
title: (args) => clip(str(args, 'task')) || str(args, 'subagent_type') || '子代理',
|
|
240
|
+
},
|
|
241
|
+
mcp_list_tools: { card: 'generic', kind: 'fetch', title: () => 'MCP 工具' },
|
|
242
|
+
mcp_tool_exec: {
|
|
243
|
+
card: 'generic',
|
|
244
|
+
kind: 'fetch',
|
|
245
|
+
title: (args) => str(args, 'toolName') || 'MCP',
|
|
246
|
+
},
|
|
247
|
+
plugin_list: { card: 'generic', kind: 'read', title: () => '插件' },
|
|
248
|
+
plugin_enable: { card: 'generic', kind: 'other', title: (args) => str(args, 'name') || '启用插件' },
|
|
249
|
+
plugin_disable: { card: 'generic', kind: 'delete', title: (args) => str(args, 'name') || '停用插件' },
|
|
250
|
+
plugin_reload: { card: 'generic', kind: 'other', title: (args) => str(args, 'name') || '重载插件' },
|
|
251
|
+
};
|
|
252
|
+
/**
|
|
253
|
+
* 取一次调用的展示。`result` 缺省时只根据参数出标题;结果到达后用同一声明补计数和状态。
|
|
254
|
+
*/
|
|
255
|
+
export function presentToolCall(name, args, result) {
|
|
256
|
+
const record = args && typeof args === 'object' && !Array.isArray(args) ? args : {};
|
|
257
|
+
const presenter = PRESENTERS[name];
|
|
258
|
+
if (!presenter) {
|
|
259
|
+
return { card: 'generic', kind: 'other', title: name, declared: false };
|
|
260
|
+
}
|
|
261
|
+
const title = presenter.title(record, result).trim() || name;
|
|
262
|
+
return { card: presenter.card, kind: presenter.kind, title, declared: true };
|
|
263
|
+
}
|
|
264
|
+
export function declaredToolNames() {
|
|
265
|
+
return Object.keys(PRESENTERS);
|
|
266
|
+
}
|