@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,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SystemPromptBuilder for the local desktop agent.
|
|
3
|
+
*
|
|
4
|
+
* Assembles a system prompt by loading skill modules from
|
|
5
|
+
* `src/local-backend/skills/<dir>/SKILL.md` and concatenating their bodies
|
|
6
|
+
* in directory-name order. Skills can be filtered by runtime conditions
|
|
7
|
+
* (e.g. `tool:<tool_name>`) and trimmed by character budget when too
|
|
8
|
+
* large (lowest-priority skills are dropped first).
|
|
9
|
+
*
|
|
10
|
+
* This is the TS counterpart of
|
|
11
|
+
* 上游 Python 服务的 SystemPromptBuilder。
|
|
12
|
+
*/
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { loadSkills } from './skill-loader.js';
|
|
15
|
+
import { isSkillPinned } from './agent-capability.js';
|
|
16
|
+
import { getBrand } from '../brand.js';
|
|
17
|
+
const FALLBACK_PROMPT = (() => {
|
|
18
|
+
const brand = getBrand();
|
|
19
|
+
return [
|
|
20
|
+
'# 角色',
|
|
21
|
+
'',
|
|
22
|
+
`你是 ${brand.agentName},${brand.tagline},运行在用户自己的 Windows / macOS / Linux 机器上。`,
|
|
23
|
+
'通过工具直接操作用户的本地环境(shell、文件、当前产品安装的场景工具等)。用中文回复。',
|
|
24
|
+
'不要凭空编造任何工具返回值。',
|
|
25
|
+
].join('\n');
|
|
26
|
+
})();
|
|
27
|
+
const DEFAULT_CHAR_BUDGET = 60_000;
|
|
28
|
+
/**
|
|
29
|
+
* Derive condition strings from the set of tool names available this turn.
|
|
30
|
+
*
|
|
31
|
+
* ['some_pack_tool', 'local_exec_shell']
|
|
32
|
+
* → ['tool:some_pack_tool', 'tool:local_exec_shell']
|
|
33
|
+
*/
|
|
34
|
+
export function conditionsFromTools(toolNames, autoHasTools = true) {
|
|
35
|
+
const out = new Set();
|
|
36
|
+
let hasAny = false;
|
|
37
|
+
for (const name of toolNames) {
|
|
38
|
+
if (!name)
|
|
39
|
+
continue;
|
|
40
|
+
out.add(`tool:${name}`);
|
|
41
|
+
hasAny = true;
|
|
42
|
+
}
|
|
43
|
+
if (autoHasTools && hasAny)
|
|
44
|
+
out.add('has-tools');
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Assemble the final system prompt:
|
|
49
|
+
*
|
|
50
|
+
* [persona preamble]? (agent role / DSL)
|
|
51
|
+
* [chat-level prompt]? (per-chat custom override)
|
|
52
|
+
* [skill 1 body]
|
|
53
|
+
* ...
|
|
54
|
+
* [skill N body]
|
|
55
|
+
* [reality check]? (turn-specific tool list / pollution warning)
|
|
56
|
+
*/
|
|
57
|
+
export async function buildSystemPrompt(options = {}) {
|
|
58
|
+
const conditions = new Set(options.conditions ?? []);
|
|
59
|
+
if (options.toolNames) {
|
|
60
|
+
for (const c of conditionsFromTools(options.toolNames, options.autoHasTools ?? true)) {
|
|
61
|
+
conditions.add(c);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
let modules = await loadSkills({
|
|
65
|
+
conditions,
|
|
66
|
+
ignoreConditions: options.ignoreConditions,
|
|
67
|
+
excludeSkillNames: options.excludeSkillNames,
|
|
68
|
+
skillsDir: options.skillsDir,
|
|
69
|
+
});
|
|
70
|
+
const droppedModules = [];
|
|
71
|
+
// 智能体勾选的技能:再取一次全量(无视条件)捞出勾选项并并入。追加在条件
|
|
72
|
+
// 命中的模块之后——recency 更强,且这些是用户明确为该智能体选的。
|
|
73
|
+
const pinnedAliases = Array.from(options.pinnedSkillNames ?? []);
|
|
74
|
+
const pinnedDirNames = new Set();
|
|
75
|
+
if (pinnedAliases.length > 0) {
|
|
76
|
+
const all = await loadSkills({
|
|
77
|
+
ignoreConditions: true,
|
|
78
|
+
excludeSkillNames: options.excludeSkillNames,
|
|
79
|
+
skillsDir: options.skillsDir,
|
|
80
|
+
});
|
|
81
|
+
const present = new Set(modules.map((m) => m.dirName));
|
|
82
|
+
for (const module of all) {
|
|
83
|
+
if (!isSkillPinned(module, pinnedAliases))
|
|
84
|
+
continue;
|
|
85
|
+
pinnedDirNames.add(module.dirName);
|
|
86
|
+
if (!present.has(module.dirName))
|
|
87
|
+
modules.push(module);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// A6 分层披露:catalog 层技能的正文不再进系统提示词——sidecar 注入目录
|
|
91
|
+
// 清单,模型调 `skill` 工具按需加载(用户显式 "/技能名" 走 router 的一次性
|
|
92
|
+
// 消息注入,也不经过这里)。勾选的技能是例外:用户为这个智能体点名了,正文
|
|
93
|
+
// 直接常驻,不必等模型自己去目录里找。
|
|
94
|
+
if (options.eagerOnly) {
|
|
95
|
+
modules = modules.filter((m) => m.layer === 'eager' || pinnedDirNames.has(m.dirName));
|
|
96
|
+
}
|
|
97
|
+
if (modules.length === 0) {
|
|
98
|
+
return {
|
|
99
|
+
prompt: assembleFinal({
|
|
100
|
+
personaPreamble: options.personaPreamble ?? '',
|
|
101
|
+
chatSystemPrompt: options.chatSystemPrompt ?? '',
|
|
102
|
+
body: FALLBACK_PROMPT,
|
|
103
|
+
realityCheckSuffix: options.realityCheckSuffix ?? '',
|
|
104
|
+
}),
|
|
105
|
+
modules,
|
|
106
|
+
droppedModules,
|
|
107
|
+
conditions: Array.from(conditions),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const charBudget = options.charBudget ?? DEFAULT_CHAR_BUDGET;
|
|
111
|
+
modules = applyBudget(modules, charBudget, droppedModules);
|
|
112
|
+
// 用户显式指定的 MCP 工具("/mcp__srv__tool" 触发):合成一个指令模块放到
|
|
113
|
+
// 最末尾(recency 最强)。在 applyBudget 之后追加,天然免疫预算裁剪。与
|
|
114
|
+
// tool-router 的动态工具命名 mcp__<serverKey>__<toolName> 一致,首轮
|
|
115
|
+
// tool_choice='required' 由 router 的回合路由负责施加。
|
|
116
|
+
if (options.forcedMcpTool) {
|
|
117
|
+
modules.push(buildForcedMcpToolModule(options.forcedMcpTool));
|
|
118
|
+
}
|
|
119
|
+
// 品牌占位符({agentName} 等)由产品注入的品牌渲染——shell 技能正文
|
|
120
|
+
// 保持产品中立(3.2 中性化,见 skills/00-identity)。
|
|
121
|
+
const body = modules.map((m) => renderSkillContent(m, brandSkillVars())).join('\n\n');
|
|
122
|
+
return {
|
|
123
|
+
prompt: assembleFinal({
|
|
124
|
+
personaPreamble: options.personaPreamble ?? '',
|
|
125
|
+
chatSystemPrompt: options.chatSystemPrompt ?? '',
|
|
126
|
+
body,
|
|
127
|
+
realityCheckSuffix: options.realityCheckSuffix ?? '',
|
|
128
|
+
}),
|
|
129
|
+
modules,
|
|
130
|
+
droppedModules,
|
|
131
|
+
conditions: Array.from(conditions),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** 显式指定 MCP 工具的末尾指令模块(合成 SkillModule,不来自 skill-loader)。 */
|
|
135
|
+
function buildForcedMcpToolModule(t) {
|
|
136
|
+
const content = t.available
|
|
137
|
+
? [
|
|
138
|
+
`# ⚡ 本轮指定 MCP 工具(最高优先级)`,
|
|
139
|
+
``,
|
|
140
|
+
`用户通过 \`/${t.token}\` 显式指定了 MCP 工具 \`${t.toolName}\`` +
|
|
141
|
+
`${t.serverName ? `(来自服务「${t.serverName}」)` : ''}` +
|
|
142
|
+
`${t.description ? `,功能:${t.description}` : ''}。本轮规则:`,
|
|
143
|
+
`1. 第一步必须调用工具 \`${t.token}\`(发起 tool_call),参数根据用户问题构造;`,
|
|
144
|
+
`2. 回答必须以该工具本次返回的数据为准——不得编造结果,也不得用历史/缓存数据冒充本次调用;`,
|
|
145
|
+
`3. 若工具调用失败,如实报告错误信息,不要假装执行成功;`,
|
|
146
|
+
`4. 用户问题中超出该工具能力的部分,可用其他工具或知识补充,但要明确区分来源。`,
|
|
147
|
+
].join('\n')
|
|
148
|
+
: [
|
|
149
|
+
`# ⚡ 本轮指定 MCP 工具(当前不可用)`,
|
|
150
|
+
``,
|
|
151
|
+
`用户通过 \`/${t.token}\` 显式指定的 MCP 工具当前不可用(服务未启用、未连接成功或工具不存在)。本轮规则:`,
|
|
152
|
+
`1. **不要**假装调用该工具,也不要编造它的返回结果;`,
|
|
153
|
+
`2. 直接告知用户该工具不可用,建议其在「设置 → MCP 服务」中检查该服务的启用状态并重试「测试连接」;`,
|
|
154
|
+
`3. 其余可回答的部分用中文正常回答。`,
|
|
155
|
+
].join('\n');
|
|
156
|
+
return {
|
|
157
|
+
name: `forced-mcp-tool`,
|
|
158
|
+
displayName: '',
|
|
159
|
+
description: t.description,
|
|
160
|
+
priority: 999,
|
|
161
|
+
tags: [],
|
|
162
|
+
conditions: [],
|
|
163
|
+
match: 'any',
|
|
164
|
+
layer: 'eager',
|
|
165
|
+
modelInvocable: true,
|
|
166
|
+
content,
|
|
167
|
+
dirName: `forced-mcp:${t.token}`,
|
|
168
|
+
skillsDir: '',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 品牌占位符变量:技能正文里的 `{agentName}` / `{brandDisplayName}` /
|
|
173
|
+
* `{brandTagline}` 渲染为当前产品注入的品牌(brand.ts;未注入 = shell
|
|
174
|
+
* 中性默认)。eager 系统提示词路径统一注入;包技能同理可用。
|
|
175
|
+
*/
|
|
176
|
+
export function brandSkillVars() {
|
|
177
|
+
const brand = getBrand();
|
|
178
|
+
return {
|
|
179
|
+
agentName: brand.agentName,
|
|
180
|
+
brandDisplayName: brand.displayName,
|
|
181
|
+
brandTagline: brand.tagline,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/** 技能正文渲染:把 `{scripts}` 及额外占位符解析为绝对路径。 */
|
|
185
|
+
export function renderSkillContent(m, vars) {
|
|
186
|
+
const scriptsPath = path.join(m.skillsDir, m.dirName, 'scripts');
|
|
187
|
+
// Replace {scripts}/ or {scripts}\ with the correct path to ensure uniform slashes
|
|
188
|
+
let content = m.content;
|
|
189
|
+
content = content.replace(/\{scripts\}[\/\\]/g, scriptsPath + path.sep);
|
|
190
|
+
content = content.replace(/\{scripts\}/g, scriptsPath);
|
|
191
|
+
if (vars) {
|
|
192
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
193
|
+
content = content.replaceAll(`{${key}}`, value);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return content;
|
|
197
|
+
}
|
|
198
|
+
/** 用户显式指定技能的优先级指令头,渲染在该技能内容之前。 */
|
|
199
|
+
export function buildForcedSkillHeader(m) {
|
|
200
|
+
const label = m.displayName || m.name;
|
|
201
|
+
return [
|
|
202
|
+
`# ⚡ 本轮指定技能(最高优先级)`,
|
|
203
|
+
``,
|
|
204
|
+
`用户通过 \`/${label}\` 显式指定了本技能。本轮规则:`,
|
|
205
|
+
`1. 必须优先、严格地遵循本技能的流程、约束与工具选择来完成任务;`,
|
|
206
|
+
`2. 本技能与其他内嵌技能冲突时,**一律以本技能为准**;`,
|
|
207
|
+
`3. 除非本技能明确要求,**不要**改用其他工具来完成本技能已覆盖的任务;`,
|
|
208
|
+
`4. 只有本技能未覆盖的辅助步骤,才允许使用其他工具补充。`,
|
|
209
|
+
].join('\n');
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* "/技能名" 触发的一次性注入文本(A6):指令头 + 技能正文,由 router 并入
|
|
213
|
+
* 本轮最后一条用户消息——不再塞系统提示词尾部(系统提示词保持稳定,是
|
|
214
|
+
* prompt cache 的净收益;历史消息里也不会残留技能正文)。
|
|
215
|
+
*/
|
|
216
|
+
export function buildForcedSkillMessage(m, vars) {
|
|
217
|
+
return `${buildForcedSkillHeader(m)}\n\n${renderSkillContent(m, vars)}`;
|
|
218
|
+
}
|
|
219
|
+
function applyBudget(modules, charBudget, droppedModules) {
|
|
220
|
+
const total = modules.reduce((sum, m) => sum + m.content.length, 0);
|
|
221
|
+
if (total <= charBudget)
|
|
222
|
+
return modules;
|
|
223
|
+
const byPriorityAsc = [...modules].sort((a, b) => a.priority - b.priority);
|
|
224
|
+
const dropped = new Set();
|
|
225
|
+
let excess = total - charBudget;
|
|
226
|
+
for (const m of byPriorityAsc) {
|
|
227
|
+
if (excess <= 0)
|
|
228
|
+
break;
|
|
229
|
+
dropped.add(m.name);
|
|
230
|
+
excess -= m.content.length;
|
|
231
|
+
droppedModules.push(m.name);
|
|
232
|
+
}
|
|
233
|
+
return modules.filter((m) => !dropped.has(m.name));
|
|
234
|
+
}
|
|
235
|
+
function assembleFinal(parts) {
|
|
236
|
+
const sections = [];
|
|
237
|
+
if (parts.personaPreamble.trim())
|
|
238
|
+
sections.push(parts.personaPreamble.trim());
|
|
239
|
+
if (parts.chatSystemPrompt.trim()) {
|
|
240
|
+
sections.push(`# 本次对话补充设定\n\n${parts.chatSystemPrompt.trim()}`);
|
|
241
|
+
}
|
|
242
|
+
sections.push(parts.body.trim());
|
|
243
|
+
if (parts.realityCheckSuffix.trim())
|
|
244
|
+
sections.push(parts.realityCheckSuffix.trim());
|
|
245
|
+
return sections.join('\n\n');
|
|
246
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helper for the `/regenerate` route in `router.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Regenerating an assistant reply means: find the user turn that prompted it,
|
|
5
|
+
* delete that assistant message and everything after it, then rerun as if
|
|
6
|
+
* that user turn just happened. This module only computes *which* text to
|
|
7
|
+
* re-run with — the actual deletion is DB I/O and stays in `router.ts` /
|
|
8
|
+
* `storage/index.ts` (which can't be unit-tested here: `better-sqlite3` is
|
|
9
|
+
* compiled against Electron's ABI and can't load under plain Node/vitest).
|
|
10
|
+
*/
|
|
11
|
+
export interface RegenerateMessageLike {
|
|
12
|
+
id: string;
|
|
13
|
+
role: 'user' | 'assistant' | 'system' | 'tool';
|
|
14
|
+
content: string;
|
|
15
|
+
}
|
|
16
|
+
export type RegenerateResolution = {
|
|
17
|
+
ok: true;
|
|
18
|
+
userMessageText: string;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
reason: 'not_found' | 'not_assistant';
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* @param messagesAsc Full chat history, chronologically ascending (oldest first).
|
|
25
|
+
* @param targetMessageId The assistant message being regenerated.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveRegenerateContext(messagesAsc: RegenerateMessageLike[], targetMessageId: string): RegenerateResolution;
|
|
28
|
+
/**
|
|
29
|
+
* W5-2: the fork-point address for a non-destructive regenerate, as a
|
|
30
|
+
* user-message ordinal — the prompting user turn is the last user message
|
|
31
|
+
* before the target, i.e. (count of user messages before the target) - 1.
|
|
32
|
+
* The sidecar's `resolve_fork_seq(user_index=…)` resolves the same ordinal
|
|
33
|
+
* against the durable record (steer injections are a separate record kind,
|
|
34
|
+
* so user ordinals align between the two stores). Returns -1 when no user
|
|
35
|
+
* message precedes the target (the FALLBACK_TEXT case) — not addressable,
|
|
36
|
+
* the caller falls back to legacy truncate-and-rerun.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveRegenerateForkOrdinal(messagesAsc: RegenerateMessageLike[], targetMessageId: string): number;
|
|
39
|
+
export type RegenerateTruncatePlan = {
|
|
40
|
+
proceed: true;
|
|
41
|
+
} | {
|
|
42
|
+
proceed: false;
|
|
43
|
+
message: string;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Whether regenerate may delete the target reply, given the fork attempt that
|
|
47
|
+
* was supposed to preserve it as a branch.
|
|
48
|
+
*
|
|
49
|
+
* A declined address is the protocol's documented fallback and proceeds. A fork
|
|
50
|
+
* that failed instead of answering revokes the permission to delete: the control
|
|
51
|
+
* offers regenerate as non-destructive, and truncating anyway would drop the
|
|
52
|
+
* reply's row and interleave the new one into the same record, with no branch and
|
|
53
|
+
* no notice. Refusing leaves the reply where the user can still see it and lets
|
|
54
|
+
* them retry once the sidecar answers again.
|
|
55
|
+
*
|
|
56
|
+
* @param fork `null` when no sidecar was attached, so no record existed to fork.
|
|
57
|
+
*/
|
|
58
|
+
export declare function planRegenerateTruncate(fork: {
|
|
59
|
+
ok: boolean;
|
|
60
|
+
declined?: boolean;
|
|
61
|
+
reason?: string;
|
|
62
|
+
} | null): RegenerateTruncatePlan;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helper for the `/regenerate` route in `router.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Regenerating an assistant reply means: find the user turn that prompted it,
|
|
5
|
+
* delete that assistant message and everything after it, then rerun as if
|
|
6
|
+
* that user turn just happened. This module only computes *which* text to
|
|
7
|
+
* re-run with — the actual deletion is DB I/O and stays in `router.ts` /
|
|
8
|
+
* `storage/index.ts` (which can't be unit-tested here: `better-sqlite3` is
|
|
9
|
+
* compiled against Electron's ABI and can't load under plain Node/vitest).
|
|
10
|
+
*/
|
|
11
|
+
const FALLBACK_TEXT = '请基于上一轮内容重新生成回复。';
|
|
12
|
+
/**
|
|
13
|
+
* @param messagesAsc Full chat history, chronologically ascending (oldest first).
|
|
14
|
+
* @param targetMessageId The assistant message being regenerated.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveRegenerateContext(messagesAsc, targetMessageId) {
|
|
17
|
+
const targetIdx = messagesAsc.findIndex((m) => m.id === targetMessageId);
|
|
18
|
+
if (targetIdx === -1) {
|
|
19
|
+
return { ok: false, reason: 'not_found' };
|
|
20
|
+
}
|
|
21
|
+
if (messagesAsc[targetIdx].role !== 'assistant') {
|
|
22
|
+
return { ok: false, reason: 'not_assistant' };
|
|
23
|
+
}
|
|
24
|
+
let precedingUserText = null;
|
|
25
|
+
for (let i = targetIdx - 1; i >= 0; i -= 1) {
|
|
26
|
+
if (messagesAsc[i].role === 'user') {
|
|
27
|
+
precedingUserText = messagesAsc[i].content.trim();
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { ok: true, userMessageText: precedingUserText || FALLBACK_TEXT };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* W5-2: the fork-point address for a non-destructive regenerate, as a
|
|
35
|
+
* user-message ordinal — the prompting user turn is the last user message
|
|
36
|
+
* before the target, i.e. (count of user messages before the target) - 1.
|
|
37
|
+
* The sidecar's `resolve_fork_seq(user_index=…)` resolves the same ordinal
|
|
38
|
+
* against the durable record (steer injections are a separate record kind,
|
|
39
|
+
* so user ordinals align between the two stores). Returns -1 when no user
|
|
40
|
+
* message precedes the target (the FALLBACK_TEXT case) — not addressable,
|
|
41
|
+
* the caller falls back to legacy truncate-and-rerun.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveRegenerateForkOrdinal(messagesAsc, targetMessageId) {
|
|
44
|
+
const targetIdx = messagesAsc.findIndex((m) => m.id === targetMessageId);
|
|
45
|
+
if (targetIdx === -1)
|
|
46
|
+
return -1;
|
|
47
|
+
let ordinal = -1;
|
|
48
|
+
for (let i = 0; i < targetIdx; i += 1) {
|
|
49
|
+
if (messagesAsc[i].role === 'user')
|
|
50
|
+
ordinal += 1;
|
|
51
|
+
}
|
|
52
|
+
return ordinal;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether regenerate may delete the target reply, given the fork attempt that
|
|
56
|
+
* was supposed to preserve it as a branch.
|
|
57
|
+
*
|
|
58
|
+
* A declined address is the protocol's documented fallback and proceeds. A fork
|
|
59
|
+
* that failed instead of answering revokes the permission to delete: the control
|
|
60
|
+
* offers regenerate as non-destructive, and truncating anyway would drop the
|
|
61
|
+
* reply's row and interleave the new one into the same record, with no branch and
|
|
62
|
+
* no notice. Refusing leaves the reply where the user can still see it and lets
|
|
63
|
+
* them retry once the sidecar answers again.
|
|
64
|
+
*
|
|
65
|
+
* @param fork `null` when no sidecar was attached, so no record existed to fork.
|
|
66
|
+
*/
|
|
67
|
+
export function planRegenerateTruncate(fork) {
|
|
68
|
+
if (!fork || fork.ok || fork.declined)
|
|
69
|
+
return { proceed: true };
|
|
70
|
+
return {
|
|
71
|
+
proceed: false,
|
|
72
|
+
message: '无法重新生成:旧回复未能保留为分支,' +
|
|
73
|
+
`已放弃改动以免丢失它(${fork.reason ?? '未知原因'})。请稍后重试。`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { ToolRouter } from '../tool-router.js';
|
|
2
|
+
import type { TaskService } from './task-service.js';
|
|
3
|
+
export interface LocalBackendRequest {
|
|
4
|
+
method: string;
|
|
5
|
+
path: string;
|
|
6
|
+
body?: unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface LocalBackendResponse<T = unknown> {
|
|
9
|
+
status: number;
|
|
10
|
+
data: T;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 流式响应中每条 SSE chunk 的 emit 回调。
|
|
14
|
+
* router 在生成过程中每收到一个新片段就立刻调用一次,
|
|
15
|
+
* 由 main 进程把它通过 IPC 事件推到渲染端,避免"齐了再吐"。
|
|
16
|
+
*/
|
|
17
|
+
export type StreamEmit = (sseChunk: string) => void;
|
|
18
|
+
export interface StreamResult {
|
|
19
|
+
status: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 把 CoreLoop/LLM 的底层错误翻译成适合直接显示在助手消息里的中文提示。
|
|
23
|
+
* 原始错误仍保存在 messageMetadata.completionReason 里,便于排查。
|
|
24
|
+
*/
|
|
25
|
+
export declare function userFacingCoreLoopFailure(reason: string): string;
|
|
26
|
+
export interface StreamOptions {
|
|
27
|
+
/**
|
|
28
|
+
* 取消信号。renderer 主动 Stop、窗口被销毁、应用退出时由 main 进程 abort。
|
|
29
|
+
* router 在每个 turn 开始前和每个 tool call 执行前检查;一旦 aborted 就
|
|
30
|
+
* 立刻停止调用 LLM / 工具,把已产出的内容落库后结束流。
|
|
31
|
+
*/
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Out-of-band 事件推送回调。用于把"在请求生命周期之外"才出结果的事件(例如
|
|
36
|
+
* 后台 AI 标题生成)异步广播到所有 renderer。main.ts 注入 `webContents.send`
|
|
37
|
+
* 实现;未注入时事件被静默丢弃(CLI/test 场景下 router 仍可独立工作)。
|
|
38
|
+
*/
|
|
39
|
+
export type LocalBackendBroadcast = (eventName: string, payload: unknown) => void;
|
|
40
|
+
export declare class LocalBackendRouter {
|
|
41
|
+
private readonly toolRouter;
|
|
42
|
+
private readonly broadcast;
|
|
43
|
+
/** 4.6a/4.6c:任务面板的路由入口(列表 + 合并/丢弃)。未注入时任务路由 503。 */
|
|
44
|
+
private readonly taskService;
|
|
45
|
+
constructor(toolRouter: ToolRouter, options?: {
|
|
46
|
+
broadcast?: LocalBackendBroadcast;
|
|
47
|
+
taskService?: TaskService;
|
|
48
|
+
});
|
|
49
|
+
handle(request: LocalBackendRequest): Promise<LocalBackendResponse>;
|
|
50
|
+
/**
|
|
51
|
+
* 对未显式实现的路由做"温柔降级":GET 返回空集合/空对象,不让 UI 因 404 抛错跳到
|
|
52
|
+
* not-found;其他方法返回 success:true 让前端流程继续。所有兜底都在主进程终端
|
|
53
|
+
* 打印一条日志,方便后续按需补真实实现。
|
|
54
|
+
*/
|
|
55
|
+
private fallbackResponse;
|
|
56
|
+
/**
|
|
57
|
+
* 真正流式:每个 SSE chunk 立刻通过 `emit` 推到 IPC 通道,避免"齐了再吐"。
|
|
58
|
+
* 不再返回 chunks 数组,main 进程只需关心最终 status。
|
|
59
|
+
*
|
|
60
|
+
* SSE 协议约定(与 web UI 现有 SSEParser/parseSSEData 对齐):
|
|
61
|
+
* - 正文文本片段:`data: {"content":"..."}` 不带 event 行,前端在
|
|
62
|
+
* `accumulatedContent += parsedData.content` 一行追加渲染。
|
|
63
|
+
* - 同步真实 messageId:`data: {"type":"message_id","messageId":"..."}`
|
|
64
|
+
* - 工具执行结果(无需用户确认):`data: {"type":"executed_actions","actions":[...]}`
|
|
65
|
+
* - 错误:保留 `event:error` + `data: {"message":"..."}`(前端会忽略,但日志/未来 UI 用)
|
|
66
|
+
* - 结束:标准 SSE `[DONE]` 串。
|
|
67
|
+
*/
|
|
68
|
+
handleStream(request: LocalBackendRequest, emit: StreamEmit, options?: StreamOptions): Promise<StreamResult>;
|
|
69
|
+
/**
|
|
70
|
+
* 后台异步生成 chat 标题。**完全 fire-and-forget**——不阻塞 `[DONE]`,
|
|
71
|
+
* 调用方只要 `void this.runTitleGenInBackground(...)` 就行。
|
|
72
|
+
*
|
|
73
|
+
* 设计取舍:
|
|
74
|
+
* - 主对话的 LLM 调用没有 per-call 超时,也没有总时间预算——结束完全由模型
|
|
75
|
+
* 自己判断(不再 emit tool_calls = completed)。之前 title-gen 给 6-12s
|
|
76
|
+
* 单次超时,慢机器 / 冷模型经常拿不到结果就被掐。改成后台跑,**没有时间
|
|
77
|
+
* 压力**:60s 都能等,反正用户已经看到 [DONE]。
|
|
78
|
+
* - 结果通过 `this.broadcast('chat-title-updated', ...)` 推到 renderer。main.ts
|
|
79
|
+
* 把它接到 `mainWindow.webContents.send()`,preload 暴露 `onChatTitleUpdated`。
|
|
80
|
+
* - 写库前再 `getChat()` 校验,避免覆盖用户/其它窗口手改过的标题。
|
|
81
|
+
* - 任何异常都吞掉记日志:title 是 nice-to-have,不该污染日志/影响主流程。
|
|
82
|
+
*/
|
|
83
|
+
private runTitleGenInBackground;
|
|
84
|
+
/**
|
|
85
|
+
* 解析当前对话绑定的项目(项目模式)。chat 没绑项目、项目已被删除、或
|
|
86
|
+
* 注册表未注入时都返回 null(= 不沙箱,行为同旧版本)。
|
|
87
|
+
*
|
|
88
|
+
* Public: main.ts 的反向通道(reverse-tools.ts)经它给 CoreLoop 路径的
|
|
89
|
+
* 工具调用补上 projectRoot 围栏。
|
|
90
|
+
*/
|
|
91
|
+
resolveChatProject(chatId: string): {
|
|
92
|
+
name: string;
|
|
93
|
+
folderPath: string;
|
|
94
|
+
} | null;
|
|
95
|
+
/**
|
|
96
|
+
* 解析本轮生效的智能体:人设前言、技能勾选、工具策略同源于这一次解析。
|
|
97
|
+
*
|
|
98
|
+
* 优先级:payload.mentionedAgentId(@提及)> mentionedAgentIds >
|
|
99
|
+
* payload.agentId > chat.agentId > 无。@提及多个时按提及顺序合并——
|
|
100
|
+
* rolePrompt 逐段拼装(第一个为主角色),能力面取最宽松
|
|
101
|
+
* (见 {@link mergeAgentCapabilities})。
|
|
102
|
+
*
|
|
103
|
+
* 必须在拼装工具列表之前调用:`toolPolicy` 决定每轮 `turnTools`,
|
|
104
|
+
* 而工具列表又反过来决定技能的触发条件。
|
|
105
|
+
*
|
|
106
|
+
* @param chatId 当前对话 id。
|
|
107
|
+
* @param payload 前端提交的流请求体。
|
|
108
|
+
* @returns 本轮的智能体顺序、人设智能体,以及合并后的能力面。
|
|
109
|
+
*/
|
|
110
|
+
private resolveTurnAgents;
|
|
111
|
+
private buildConversationMessages;
|
|
112
|
+
/**
|
|
113
|
+
* 消费前端 @历史对话 引用(LocalChatPanel 发送的 payload.referencedChatIds)。
|
|
114
|
+
*
|
|
115
|
+
* 每个被引用的对话生成一条 user 消息注入上下文:
|
|
116
|
+
* - 附最近若干条 user/assistant 消息的摘录(tool 消息跳过,assistant
|
|
117
|
+
* 文本复用 stripToolSummaryMarkdownForLlm 清理);
|
|
118
|
+
* - 整体截断封顶,防止一次 @ 多个长对话把上下文挤爆。
|
|
119
|
+
*
|
|
120
|
+
* 以 user 角色注入(部分 OpenAI 兼容服务器不接受多条 system 消息)。
|
|
121
|
+
*/
|
|
122
|
+
private buildReferencedChatContextMessages;
|
|
123
|
+
private stripToolSummaryMarkdownForLlm;
|
|
124
|
+
private appendExecutedActionsForLlm;
|
|
125
|
+
/**
|
|
126
|
+
* 检测"deferred-execution 幻觉"——LLM 用文字预告自己即将调用工具但实际没发
|
|
127
|
+
* tool_call。常见于中小尺寸模型:描述完工具参数后写一段
|
|
128
|
+
* "参数:... 现在执行..." 就停笔。
|
|
129
|
+
*
|
|
130
|
+
* 命中条件需要**同时**满足:
|
|
131
|
+
* 1) 包含"意图陈述"句式(现在执行 / 接下来 / 我将 / 准备运行 / now executing 等)
|
|
132
|
+
* 2) 文本以省略号 `...` / `…` 收尾,或以":"/中文冒号收尾
|
|
133
|
+
* 3) 长度 > 20 字符(避免误判极短的"好的,执行...")
|
|
134
|
+
*
|
|
135
|
+
* 调用方负责再加一层"本轮 toolCalls 为空 && usedTool"的语境判断。
|
|
136
|
+
*/
|
|
137
|
+
/**
|
|
138
|
+
* @deprecated Inline helper kept as a thin wrapper for backward compat;
|
|
139
|
+
* real implementation lives in `./deferred-detector.ts` so it's testable.
|
|
140
|
+
*/
|
|
141
|
+
private detectDeferredExecution;
|
|
142
|
+
private detectToolDenialInHistory;
|
|
143
|
+
private buildModePreamble;
|
|
144
|
+
private isLocalSystemInfoRequest;
|
|
145
|
+
private buildRuntimeEnvironmentContext;
|
|
146
|
+
private buildToolRealityCheck;
|
|
147
|
+
/**
|
|
148
|
+
* 把整轮已发生的工具调用浓缩成 narration round 用的纯文本报告。
|
|
149
|
+
* 一条 = 一次工具调用,明显标注 ✅/❌,并尽量保留 error / hint 字段原文。
|
|
150
|
+
*/
|
|
151
|
+
private briefResult;
|
|
152
|
+
private briefScalar;
|
|
153
|
+
private sse;
|
|
154
|
+
/**
|
|
155
|
+
* A4 CoreLoop turn: the sidecar runs the whole agent loop; this method
|
|
156
|
+
* translates its wire notifications into the existing SSE contract and
|
|
157
|
+
* persists the outcome. Tool execution happens back in this process via
|
|
158
|
+
* the reverse-channel handler registered in main.ts.
|
|
159
|
+
*/
|
|
160
|
+
private handleCoreLoopTurn;
|
|
161
|
+
/**
|
|
162
|
+
* 标准"匿名事件" SSE:不带 event 行,前端 SSEParser 会落到默认 onMessage 分支,
|
|
163
|
+
* 用 parseSSEData 把 data 反序列化为 JSON 后用 type/content 字段路由。
|
|
164
|
+
*/
|
|
165
|
+
private sseData;
|
|
166
|
+
private toRecord;
|
|
167
|
+
private safeJson;
|
|
168
|
+
private notFound;
|
|
169
|
+
private badRequest;
|
|
170
|
+
private cleanUserMessage;
|
|
171
|
+
/** 已安装技能的全部可触发别名(name/dirName/displayName,小写)。
|
|
172
|
+
* 除英文 name / dirName 外,也接受 SKILL.md 里 displayName 声明的可读
|
|
173
|
+
* 中文名(如「数据处理链」),这样 "/数据处理链" 同样能触发。 */
|
|
174
|
+
private knownSkillNameSet;
|
|
175
|
+
}
|