@chaoset/adaptive-perf 0.6.0 → 0.7.1
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/CHANGELOG.md +186 -0
- package/README.md +52 -42
- package/client/client.cjs +202 -211
- package/client/index.tsx +477 -0
- package/cordis.patch.yml +21 -12
- package/lib/config-store.d.ts +32 -0
- package/lib/config-store.d.ts.map +1 -0
- package/lib/config-store.js +107 -0
- package/lib/config-store.js.map +1 -0
- package/lib/index.d.ts +350 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +1574 -0
- package/lib/index.js.map +1 -0
- package/lib/remote.d.ts +37 -0
- package/lib/remote.d.ts.map +1 -0
- package/lib/{remote.mjs → remote.js} +50 -52
- package/lib/remote.js.map +1 -0
- package/lib/typert.host.d.ts +55 -0
- package/lib/typert.host.d.ts.map +1 -0
- package/lib/typert.host.js +37 -37
- package/lib/typert.host.js.map +1 -0
- package/package.json +15 -14
- package/lib/config-store.mjs +0 -97
- package/lib/index.mjs +0 -1611
package/lib/index.mjs
DELETED
|
@@ -1,1611 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* adaptive-perf — 极简性能自适应插件(@chaoset/adaptive-perf)
|
|
3
|
-
*
|
|
4
|
-
* 目标:让「标准模式(standard)」与「PTC 模式(code)」达到与「极简模式
|
|
5
|
-
* (minimal)」相同级别的高性能。极简模式快的原因(对源码的对照结论):
|
|
6
|
-
* 1. persona 配置了 includeRuntimeContext: false —— 每次模型请求不注入
|
|
7
|
-
* "Current runtime context" 快照(文件策略 / 审批策略等动态段);
|
|
8
|
-
* 2. 工具目录极小(只有 bash + str_replace_editor)—— 每个请求的
|
|
9
|
-
* tool schema 与(PTC 的)SDK 参考段都随之变小;
|
|
10
|
-
* 3. persona 为 complete 固定提示,无其它全局 prompt 段;
|
|
11
|
-
* 4. 不挂 skill 目录提醒(~9KB)与 AGENTS.md 摘要注入。
|
|
12
|
-
*
|
|
13
|
-
* 本插件在多个维度上做"动态自适应"(而不是静态裁剪),全部参照
|
|
14
|
-
* dsh-anchored-standard(https://github.com/xiaobright/dsh-anchored-standard)
|
|
15
|
-
* 的实测结论:
|
|
16
|
-
* A. 运行时上下文抑制:对目标 preset 的会话调用
|
|
17
|
-
* agent.ctx.systemPrompt.suppressRuntimeContext()(与极简模式同一机制),
|
|
18
|
-
* 每次请求省掉快照文本,零功能损失。
|
|
19
|
-
* B. 真实 Minimal 工具对(bootstrap.realPair,0.5.0 新增):首轮"轨迹"
|
|
20
|
-
* (模型思维链风格)由请求 #1 可见的**工具 schema** 决定,且只有与
|
|
21
|
-
* Minimal 逐字相同的 schema 才能锚定(issue #11:真实工具对 5/5 锚定,
|
|
22
|
-
* 任何 standard 系 schema——包括 sandboxed bash/read——11/11 落入
|
|
23
|
-
* standard-like)。仅收窄目录不够:standard/code preset 根本没有
|
|
24
|
-
* str_replace_editor,其 bash 也是 sandboxed schema。因此本插件把
|
|
25
|
-
* Minimal 的**真实工具对**(持久 PTY bash + str_replace_editor,官方
|
|
26
|
-
* minimal preset 同一批插件、同一份描述)挂进 agent 作用域工具层
|
|
27
|
-
* (scoped registration 按名阴影继承工具),首轮目录 = 真实工具对;
|
|
28
|
-
* PTC 的 SDK 参考段同样按可见目录渲染,同步缩小。
|
|
29
|
-
* C. 首轮锚定(bootstrap):请求 #1 只暴露真实工具对,剥离自动注入上下文
|
|
30
|
-
* (技能目录提醒 skill-catalog、AGENTS.md 摘要 agent-instructions;
|
|
31
|
-
* 用户主动的技能手势不过滤);首个**持久**晋升信号(首个 tool/call 或
|
|
32
|
-
* assistant/message,promoteOn 可选 either/tool-call/assistant-message)
|
|
33
|
-
* 后进入 **resident 目录**:工具对 + 常驻发现工具(dev_tool_search /
|
|
34
|
-
* skill_search / skill_load)+ 已解锁工具,而不是一次性 dump 完整
|
|
35
|
-
* Standard 目录(避免 post-promotion regression);阶段从持久会话事件
|
|
36
|
-
* 推导,resume/reload 不丢状态;compaction/end 之后回到受控目录
|
|
37
|
-
* (工具对 + compactionTools)直到新的晋升信号(纪元感知);
|
|
38
|
-
* bootstrap.maxTokens(可选)给请求 #1 封顶输出预算,晋升后剥离。
|
|
39
|
-
* D. 常驻上下文抑制(suppressInjectedContext,0.5.0 新增):技能目录提醒
|
|
40
|
-
* 与 AGENTS.md 摘要**不再在晋升后恢复**——参考实现实测它们即使在后置
|
|
41
|
-
* 阶段也扰动轨迹且每个请求多耗数千 token。替代品:
|
|
42
|
-
* - skill_search / skill_load:按需发现并注入单个技能的完整说明
|
|
43
|
-
* (晋升后常驻,替代 ~9KB 的 <available_skills> 注入);
|
|
44
|
-
* - instruction-hint:晋升后只注入**一次**的短提示——"这些指令文件
|
|
45
|
-
* 存在,需要时自己读",而不是每请求 dump 全文(文件探测走宿主 fs,
|
|
46
|
-
* 探测失败不注入,绝不抛错)。
|
|
47
|
-
* E. 工具目录自适应精简:会话启动时按"工具族"默认隐藏高开销低频工具
|
|
48
|
-
* (子代理 / 工作流 / ralph / goal 等编排类),只保留核心编码工具,
|
|
49
|
-
* 随后根据两类信号在会话内"单调升级"放行对应工具族(只升不降):
|
|
50
|
-
* - 关键词信号:用户消息命中某工具族的触发词(如"子代理")→ 放行该族;
|
|
51
|
-
* - 失败信号:PTC 的 run_code 程序调用被隐藏工具报 UNKNOWN_TOOL
|
|
52
|
-
* (tools/result 失败文本含工具名)→ 放行该族,下次程序即可调用。
|
|
53
|
-
*
|
|
54
|
-
* 实现全部使用 dsh 公开服务契约(Inspect 确认):
|
|
55
|
-
* - ctx.agentPresets.composedPreset(agent.ctx) 读取会话所属 preset
|
|
56
|
-
* - agent.ctx.systemPrompt.suppressRuntimeContext() 抑制上下文快照
|
|
57
|
-
* - agent.ctx.systemPrompt.section() 按名阴影 prompt 段(persona/引导段)
|
|
58
|
-
* - agent.ctx.tools.schemas(agent) 读取该 agent 当前可见工具(限制交集)
|
|
59
|
-
* - agent.ctx.tools.restrict({ deny }) 按 agent 作用域裁剪继承工具
|
|
60
|
-
* - agent.ctx.tools.register() 在 agent 作用域注册工具(按名阴影继承工具,
|
|
61
|
-
* 自身层注册不受 restrict 影响——view() 对 own layer 豁免)
|
|
62
|
-
* - agent.ctx.plugin() 挂载官方 minimal preset 同款插件(持久 bash /
|
|
63
|
-
* str_replace_editor / 其依赖的 terminals 与本地 fs)
|
|
64
|
-
* - agent/pre-step(waterfall) 剥离自动注入上下文 + 注入 instruction-hint
|
|
65
|
-
* - agent/request(waterfall) 首轮输出预算封顶
|
|
66
|
-
* - session/event 增量喂入持久事件(晋升 / compaction 纪元)
|
|
67
|
-
* - agent/created、agent/disposed、agent/inbox/inserted、tools/result 事件
|
|
68
|
-
* 监听器都注册在 host 根作用域(与 dsh-agent-presets 自身同款用法),事件按
|
|
69
|
-
* scope 过滤分发,子作用域派发的事件根监听器可收到;两个 waterfall 均以
|
|
70
|
-
* prepend 注册保证"最外层"(剥离是最后一道变换)。
|
|
71
|
-
*
|
|
72
|
-
* 配置:cordis.patch.yml 的 config(安装默认)与
|
|
73
|
-
* ~/.dsh/plugins/adaptive-perf/config.json(设置页 UI,权威)合并,
|
|
74
|
-
* 保存后热生效(新会话立即生效;已运行会话按新配置重算限制)。
|
|
75
|
-
*
|
|
76
|
-
* 依赖:真实工具对需要官方插件包(@deepseek-ai/dsh-terminal 等,声明为
|
|
77
|
-
* optionalDependencies)。它们不可用时本插件自动降级(告警 + 退回旧行为:
|
|
78
|
-
* 只收窄目录、不替换 schema),绝不拖垮 harness。
|
|
79
|
-
*/
|
|
80
|
-
|
|
81
|
-
import { createConfigStore } from './config-store.mjs';
|
|
82
|
-
|
|
83
|
-
// remote 服务(设置页 UI 的配置读写)可选:typert-protocol 不可用时
|
|
84
|
-
// 动态 import 失败,核心功能(自适应)照常工作。
|
|
85
|
-
let PluginConfigGateway = null;
|
|
86
|
-
try {
|
|
87
|
-
({ PluginConfigGateway } = await import('./remote.mjs'));
|
|
88
|
-
} catch (error) {
|
|
89
|
-
console.warn('adaptive-perf: settings gateway unavailable: ' + (error && error.message || error));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// 宿主 settings 体系的 schema 库(dsh-settings 0.1.0-rc.7+ 用 schemastery)。
|
|
93
|
-
// DSH profile 的 hoisted node_modules 直接解析;仓库测试环境无此包时为
|
|
94
|
-
// null,settings namespace 注册段静默跳过(fail-safe)。
|
|
95
|
-
let Schema = null;
|
|
96
|
-
try {
|
|
97
|
-
({ default: Schema } = await import('@deepseek-ai/schemastery'));
|
|
98
|
-
} catch {}
|
|
99
|
-
|
|
100
|
-
export const name = 'adaptive-perf';
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* 默认配置。apply 时与 YAML 传入的 config 合并(cordis 不合并小写 config 导出)。
|
|
104
|
-
*
|
|
105
|
-
* families:工具族。每族可配 tools(该族工具名,需存在于目标 preset 的目录,
|
|
106
|
-
* 插件运行时按实际可见工具取交集,不会因改名/缺失而崩溃)与 keywords(用户消息
|
|
107
|
-
* 命中任一触发词即放行该族;大小写不敏感,子串匹配)。
|
|
108
|
-
*
|
|
109
|
-
* bootstrap:首轮锚定(参照 dsh-anchored-standard 的实测结论)。请求 #1 只暴露
|
|
110
|
-
* bootstrap.tools(真实 Minimal 工具对),剥离 suppressedContextSources 列出的
|
|
111
|
-
* 自动注入上下文;首个持久晋升信号(promoteOn)后恢复;compaction/end 之后回到
|
|
112
|
-
* 受控目录(bootstrap 工具对 + compactionTools)直到新的晋升信号;maxTokens>0
|
|
113
|
-
* 时给请求 #1 封顶输出预算(晋升后剥离)。
|
|
114
|
-
*/
|
|
115
|
-
export const DEFAULT_CONFIG = {
|
|
116
|
-
/** 总开关:关闭后插件对会话不产生任何影响。 */
|
|
117
|
-
enabled: true,
|
|
118
|
-
/** 应用自适应的 agent preset 列表(composedPreset 返回值)。 */
|
|
119
|
-
presets: ['standard', 'code'],
|
|
120
|
-
/**
|
|
121
|
-
* 默认策略(0.6.0):功能优先,全部优化 opt-in。旧版把下列机制默认
|
|
122
|
-
* 开启,导致 standard/PTC 模式功能缺失(工具族从目录删除 → PTC 程序
|
|
123
|
-
* 调用直接 UNKNOWN_TOOL;技能目录/指令摘要被剥离;官方引导段被屏蔽;
|
|
124
|
-
* 首轮只剩两个工具)。这些机制的收益本质是"砍上下文换 token",没有
|
|
125
|
-
* 无损形态,因此默认全部关闭,保持官方 preset 的完整功能;需要省
|
|
126
|
-
* token 时按需开启(设置页或 config.json),开启即接受相应的功能
|
|
127
|
-
* 约束(关键词/失败信号放行、skill_search 按需发现等补偿机制仍在)。
|
|
128
|
-
*/
|
|
129
|
-
/** 抑制运行时上下文快照(等同极简模式的 includeRuntimeContext: false)。 */
|
|
130
|
-
suppressRuntimeContext: false,
|
|
131
|
-
/** 会话启动时默认隐藏编排类工具族(核心编码工具始终保留)。 */
|
|
132
|
-
leanByDefault: false,
|
|
133
|
-
/** 用户消息命中工具族触发词时自动放行该族(会话内单调)。 */
|
|
134
|
-
escalateOnKeyword: true,
|
|
135
|
-
/** 工具调用失败(UNKNOWN_TOOL 文本含隐藏工具名)时自动放行该族。 */
|
|
136
|
-
escalateOnUnknownTool: true,
|
|
137
|
-
/**
|
|
138
|
-
* 常驻上下文抑制(0.5.0):true = 整个会话剥离自动注入上下文
|
|
139
|
-
* (skill-catalog 技能目录提醒、agent-instructions AGENTS.md 摘要),
|
|
140
|
-
* 不再像旧版那样在晋升后恢复——替代品是 skill_search/skill_load 按需发现
|
|
141
|
-
* 与 instruction-hint 一次性提示。false = 只在 bootstrap(请求 #1)剥离。
|
|
142
|
-
*/
|
|
143
|
-
suppressInjectedContext: false,
|
|
144
|
-
/** 晋升后常驻 skill_search / skill_load 两个按需技能发现工具。 */
|
|
145
|
-
skillDiscovery: true,
|
|
146
|
-
/** 晋升后注入一次"指令文件存在,需要时自读"的短提示(替代全文注入)。 */
|
|
147
|
-
instructionHint: true,
|
|
148
|
-
/** 核心编码工具(不进入任何限制族;此处仅作文档展示)。 */
|
|
149
|
-
coreTools: [
|
|
150
|
-
'bash', 'read', 'write', 'edit', 'glob', 'grep', 'read_image',
|
|
151
|
-
'job_output', 'job_list', 'job_kill',
|
|
152
|
-
'todo_write', 'ask_user_question', 'web_search', 'skill', 'exit_plan_mode',
|
|
153
|
-
],
|
|
154
|
-
/** 默认隐藏、按需放行的工具族。 */
|
|
155
|
-
families: {
|
|
156
|
-
delegation: {
|
|
157
|
-
enabled: true,
|
|
158
|
-
tools: ['subagent', 'subagent_fork', 'send_message', 'list_agents', 'interrupt_agent'],
|
|
159
|
-
keywords: ['子代理', '子agent', '委托', '分派', '派给', '子任务', 'subagent', 'delegate', 'fork'],
|
|
160
|
-
},
|
|
161
|
-
workflow: {
|
|
162
|
-
enabled: true,
|
|
163
|
-
tools: ['workflow'],
|
|
164
|
-
keywords: ['工作流', 'workflow', '编排', '多阶段'],
|
|
165
|
-
},
|
|
166
|
-
ralph: {
|
|
167
|
-
enabled: true,
|
|
168
|
-
tools: ['ralph'],
|
|
169
|
-
keywords: ['ralph', '全新agent', 'fresh agent'],
|
|
170
|
-
},
|
|
171
|
-
goal: {
|
|
172
|
-
enabled: true,
|
|
173
|
-
tools: ['create_goal', 'get_goal', 'update_goal'],
|
|
174
|
-
keywords: ['长期目标', '跨轮次', '目标追踪', 'goal'],
|
|
175
|
-
},
|
|
176
|
-
},
|
|
177
|
-
/** 首轮锚定(参照 dsh-anchored-standard)。 */
|
|
178
|
-
bootstrap: {
|
|
179
|
-
/** 是否启用首轮锚定。 */
|
|
180
|
-
enabled: false,
|
|
181
|
-
/**
|
|
182
|
-
* 是否挂载真实 Minimal 工具对(0.5.0):把官方 minimal preset 的持久
|
|
183
|
-
* PTY bash + str_replace_editor 挂进 agent 作用域(按名阴影 standard 的
|
|
184
|
-
* sandboxed bash),使请求 #1 的 schema 与 Minimal 逐字相同。依赖
|
|
185
|
-
* @deepseek-ai 官方插件包,不可用时自动降级为旧行为(仅收窄目录)。
|
|
186
|
-
*/
|
|
187
|
-
realPair: true,
|
|
188
|
-
/** 请求 #1 可见的工具(真实 Minimal 工具对)。 */
|
|
189
|
-
tools: ['bash', 'str_replace_editor'],
|
|
190
|
-
/** 晋升触发:either(默认)/ tool-call / assistant-message。 */
|
|
191
|
-
promoteOn: 'either',
|
|
192
|
-
/** 请求 #1 剥离的自动注入上下文 source.kind;[] 关闭剥离。 */
|
|
193
|
-
suppressedContextSources: ['skill-catalog', 'agent-instructions'],
|
|
194
|
-
/** compaction/end 之后、再次晋升前可用的额外核心工作集。 */
|
|
195
|
-
compactionTools: ['read', 'write', 'edit', 'glob', 'grep', 'todo_write', 'ask_user_question'],
|
|
196
|
-
/** 晋升后常驻的按需发现工具(dsh-anchored-standard 的 resident discovery set)。 */
|
|
197
|
-
discoveryTools: ['dev_tool_search', 'skill_search', 'skill_load'],
|
|
198
|
-
/** 请求 #1 的输出预算封顶(token);0 = 不封顶(opt-in)。 */
|
|
199
|
-
maxTokens: 0,
|
|
200
|
-
},
|
|
201
|
-
/**
|
|
202
|
-
* 极简提示词层(语域锚定,参照 dsh-anchored-standard 的完整条件)。
|
|
203
|
-
*
|
|
204
|
-
* 参考实现指出:首轮"轨迹"(思维链风格)锚定需要 **Minimal 的完整 system
|
|
205
|
-
* prompt**——只收窄工具 schema 不够。本层对目标 preset 的会话:
|
|
206
|
-
* - suppressSections:按名阴影屏蔽全局引导段(harness:identity、
|
|
207
|
-
* harness:source、app:web-surface),空段在装配时被丢弃,等同极简
|
|
208
|
-
* complete persona 的效果(plan-mode 段、PTC 的 SDK 段不受影响);
|
|
209
|
-
* - persona:按名阴影替换 deployment:persona 为极简语域的短 persona
|
|
210
|
-
* (默认与极简模式逐字相同;留空则不替换)。
|
|
211
|
-
* 两处都是 agent 作用域的 prompt 段阴影,仅影响目标会话,随会话销毁自动
|
|
212
|
-
* 释放;对已恢复/已晋升的旧会话同样生效(不依赖 bootstrap 阶段)。
|
|
213
|
-
*/
|
|
214
|
-
minimalPrompt: {
|
|
215
|
-
/** 是否启用极简提示词层。 */
|
|
216
|
-
enabled: false,
|
|
217
|
-
/** 替换后的 persona 文本;与极简模式相同可完全对齐语域。留空不替换。 */
|
|
218
|
-
persona: 'You are a helpful software engineer assistant.',
|
|
219
|
-
/** 屏蔽全局引导段(identity / source / web-surface)。 */
|
|
220
|
-
suppressSections: true,
|
|
221
|
-
},
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
/** 轻量配置校验/归一化:非法字段回退默认值,避免 config.json 或 remote.set
|
|
225
|
-
* 写入错误类型后在事件路径上抛 TypeError。 */
|
|
226
|
-
function boolValue(value, fallback) {
|
|
227
|
-
return typeof value === 'boolean' ? value : fallback;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function stringList(value, fallback) {
|
|
231
|
-
if (Array.isArray(value)) {
|
|
232
|
-
const out = value.filter((v) => typeof v === 'string' && v.trim().length > 0).map((v) => v.trim());
|
|
233
|
-
return out.length > 0 ? out : fallback;
|
|
234
|
-
}
|
|
235
|
-
if (typeof value === 'string') {
|
|
236
|
-
const out = value.split(/[,,\s]+/).filter((v) => v.length > 0);
|
|
237
|
-
return out.length > 0 ? out : fallback;
|
|
238
|
-
}
|
|
239
|
-
return fallback;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/** 与 stringList 相同,但显式空数组保持为空(语义是有意义的配置值)。 */
|
|
243
|
-
function stringListOrEmpty(value, fallback) {
|
|
244
|
-
if (value === undefined) return fallback;
|
|
245
|
-
return stringList(value, []);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export function normalizeConfig(source, defaults = DEFAULT_CONFIG) {
|
|
249
|
-
const raw = source !== null && typeof source === 'object' && !Array.isArray(source) ? source : {};
|
|
250
|
-
const merged = { ...defaults, ...raw };
|
|
251
|
-
const families = {};
|
|
252
|
-
const rawFamilies = raw.families !== null && typeof raw.families === 'object' && !Array.isArray(raw.families)
|
|
253
|
-
? raw.families
|
|
254
|
-
: defaults.families;
|
|
255
|
-
for (const [id, fam] of Object.entries(defaults.families)) {
|
|
256
|
-
const candidate = rawFamilies[id] !== null && typeof rawFamilies[id] === 'object' && !Array.isArray(rawFamilies[id])
|
|
257
|
-
? rawFamilies[id]
|
|
258
|
-
: fam;
|
|
259
|
-
families[id] = {
|
|
260
|
-
enabled: boolValue(candidate.enabled, fam.enabled),
|
|
261
|
-
tools: stringList(candidate.tools, fam.tools),
|
|
262
|
-
keywords: stringList(candidate.keywords, fam.keywords),
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
const rawBootstrap = raw.bootstrap !== null && typeof raw.bootstrap === 'object' && !Array.isArray(raw.bootstrap)
|
|
266
|
-
? raw.bootstrap
|
|
267
|
-
: defaults.bootstrap;
|
|
268
|
-
const db = defaults.bootstrap;
|
|
269
|
-
const promoteOn = rawBootstrap.promoteOn === 'tool-call' || rawBootstrap.promoteOn === 'assistant-message'
|
|
270
|
-
? rawBootstrap.promoteOn
|
|
271
|
-
: rawBootstrap.promoteOn === 'either' ? 'either' : db.promoteOn;
|
|
272
|
-
const maxTokensRaw = rawBootstrap.maxTokens;
|
|
273
|
-
const maxTokens = Number.isSafeInteger(maxTokensRaw) && maxTokensRaw > 0 ? maxTokensRaw : 0;
|
|
274
|
-
const bootstrap = {
|
|
275
|
-
enabled: boolValue(rawBootstrap.enabled, db.enabled),
|
|
276
|
-
realPair: boolValue(rawBootstrap.realPair, db.realPair),
|
|
277
|
-
tools: stringList(rawBootstrap.tools, db.tools),
|
|
278
|
-
promoteOn,
|
|
279
|
-
suppressedContextSources: stringListOrEmpty(rawBootstrap.suppressedContextSources, db.suppressedContextSources),
|
|
280
|
-
compactionTools: stringListOrEmpty(rawBootstrap.compactionTools, db.compactionTools),
|
|
281
|
-
discoveryTools: stringListOrEmpty(rawBootstrap.discoveryTools, db.discoveryTools),
|
|
282
|
-
maxTokens,
|
|
283
|
-
};
|
|
284
|
-
const rawMP = raw.minimalPrompt !== null && typeof raw.minimalPrompt === 'object' && !Array.isArray(raw.minimalPrompt)
|
|
285
|
-
? raw.minimalPrompt
|
|
286
|
-
: defaults.minimalPrompt;
|
|
287
|
-
const dm = defaults.minimalPrompt;
|
|
288
|
-
const minimalPrompt = {
|
|
289
|
-
enabled: boolValue(rawMP.enabled, dm.enabled),
|
|
290
|
-
persona: typeof rawMP.persona === 'string' ? rawMP.persona.trim() : dm.persona,
|
|
291
|
-
suppressSections: boolValue(rawMP.suppressSections, dm.suppressSections),
|
|
292
|
-
};
|
|
293
|
-
return {
|
|
294
|
-
enabled: boolValue(merged.enabled, defaults.enabled),
|
|
295
|
-
presets: stringList(merged.presets, defaults.presets),
|
|
296
|
-
suppressRuntimeContext: boolValue(merged.suppressRuntimeContext, defaults.suppressRuntimeContext),
|
|
297
|
-
leanByDefault: boolValue(merged.leanByDefault, defaults.leanByDefault),
|
|
298
|
-
escalateOnKeyword: boolValue(merged.escalateOnKeyword, defaults.escalateOnKeyword),
|
|
299
|
-
escalateOnUnknownTool: boolValue(merged.escalateOnUnknownTool, defaults.escalateOnUnknownTool),
|
|
300
|
-
suppressInjectedContext: boolValue(merged.suppressInjectedContext, defaults.suppressInjectedContext),
|
|
301
|
-
skillDiscovery: boolValue(merged.skillDiscovery, defaults.skillDiscovery),
|
|
302
|
-
instructionHint: boolValue(merged.instructionHint, defaults.instructionHint),
|
|
303
|
-
coreTools: stringList(merged.coreTools, defaults.coreTools),
|
|
304
|
-
families,
|
|
305
|
-
bootstrap,
|
|
306
|
-
minimalPrompt,
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/** remote.set 的严格校验:非法值直接拒绝并返回错误,而不是静默写坏文件。 */
|
|
311
|
-
export function validateConfig(partial) {
|
|
312
|
-
if (partial === null || typeof partial !== 'object' || Array.isArray(partial)) {
|
|
313
|
-
throw new TypeError('adaptive-perf config must be a plain object');
|
|
314
|
-
}
|
|
315
|
-
const boolFields = ['enabled', 'suppressRuntimeContext', 'leanByDefault', 'escalateOnKeyword', 'escalateOnUnknownTool',
|
|
316
|
-
'suppressInjectedContext', 'skillDiscovery', 'instructionHint'];
|
|
317
|
-
for (const key of boolFields) {
|
|
318
|
-
if (partial[key] !== void 0 && typeof partial[key] !== 'boolean') {
|
|
319
|
-
throw new TypeError(`adaptive-perf config field "${key}" must be a boolean`);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
for (const key of ['presets', 'coreTools']) {
|
|
323
|
-
if (partial[key] !== void 0 && !(Array.isArray(partial[key]) && partial[key].every((v) => typeof v === 'string'))) {
|
|
324
|
-
throw new TypeError(`adaptive-perf config field "${key}" must be an array of strings`);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
if (partial.families !== void 0) {
|
|
328
|
-
if (partial.families === null || typeof partial.families !== 'object' || Array.isArray(partial.families)) {
|
|
329
|
-
throw new TypeError('adaptive-perf config field "families" must be an object');
|
|
330
|
-
}
|
|
331
|
-
for (const [id, fam] of Object.entries(partial.families)) {
|
|
332
|
-
if (fam === null || typeof fam !== 'object' || Array.isArray(fam)) {
|
|
333
|
-
throw new TypeError(`adaptive-perf config family "${id}" must be an object { enabled, tools, keywords }`);
|
|
334
|
-
}
|
|
335
|
-
if (fam.enabled !== void 0 && typeof fam.enabled !== 'boolean') {
|
|
336
|
-
throw new TypeError(`adaptive-perf config family "${id}".enabled must be a boolean`);
|
|
337
|
-
}
|
|
338
|
-
for (const key of ['tools', 'keywords']) {
|
|
339
|
-
if (fam[key] !== void 0 && !(Array.isArray(fam[key]) && fam[key].every((v) => typeof v === 'string'))) {
|
|
340
|
-
throw new TypeError(`adaptive-perf config family "${id}".${key} must be an array of strings`);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
if (partial.bootstrap !== void 0) {
|
|
346
|
-
const b = partial.bootstrap;
|
|
347
|
-
if (b === null || typeof b !== 'object' || Array.isArray(b)) {
|
|
348
|
-
throw new TypeError('adaptive-perf config field "bootstrap" must be an object');
|
|
349
|
-
}
|
|
350
|
-
if (b.enabled !== void 0 && typeof b.enabled !== 'boolean') {
|
|
351
|
-
throw new TypeError('adaptive-perf config field "bootstrap.enabled" must be a boolean');
|
|
352
|
-
}
|
|
353
|
-
if (b.realPair !== void 0 && typeof b.realPair !== 'boolean') {
|
|
354
|
-
throw new TypeError('adaptive-perf config field "bootstrap.realPair" must be a boolean');
|
|
355
|
-
}
|
|
356
|
-
if (b.promoteOn !== void 0 && !['either', 'tool-call', 'assistant-message'].includes(b.promoteOn)) {
|
|
357
|
-
throw new TypeError('adaptive-perf config field "bootstrap.promoteOn" must be "either" | "tool-call" | "assistant-message"');
|
|
358
|
-
}
|
|
359
|
-
if (b.maxTokens !== void 0 && !(Number.isSafeInteger(b.maxTokens) && b.maxTokens >= 0)) {
|
|
360
|
-
throw new TypeError('adaptive-perf config field "bootstrap.maxTokens" must be a non-negative safe integer');
|
|
361
|
-
}
|
|
362
|
-
for (const key of ['tools', 'suppressedContextSources', 'compactionTools', 'discoveryTools']) {
|
|
363
|
-
if (b[key] !== void 0 && !(Array.isArray(b[key]) && b[key].every((v) => typeof v === 'string'))) {
|
|
364
|
-
throw new TypeError(`adaptive-perf config field "bootstrap.${key}" must be an array of strings`);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
if (partial.minimalPrompt !== void 0) {
|
|
369
|
-
const m = partial.minimalPrompt;
|
|
370
|
-
if (m === null || typeof m !== 'object' || Array.isArray(m)) {
|
|
371
|
-
throw new TypeError('adaptive-perf config field "minimalPrompt" must be an object');
|
|
372
|
-
}
|
|
373
|
-
if (m.enabled !== void 0 && typeof m.enabled !== 'boolean') {
|
|
374
|
-
throw new TypeError('adaptive-perf config field "minimalPrompt.enabled" must be a boolean');
|
|
375
|
-
}
|
|
376
|
-
if (m.suppressSections !== void 0 && typeof m.suppressSections !== 'boolean') {
|
|
377
|
-
throw new TypeError('adaptive-perf config field "minimalPrompt.suppressSections" must be a boolean');
|
|
378
|
-
}
|
|
379
|
-
if (m.persona !== void 0 && typeof m.persona !== 'string') {
|
|
380
|
-
throw new TypeError('adaptive-perf config field "minimalPrompt.persona" must be a string');
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// ── 纯函数(导出便于回归测试)────────────────────────────────────────────
|
|
386
|
-
|
|
387
|
-
/** 被极简提示词层屏蔽的全局引导段(name, order,与 dsh-app-boot /
|
|
388
|
-
* dsh-web-app / dsh-system-prompt 的注册一致)。 */
|
|
389
|
-
export const SECTION_SHADOWS = [
|
|
390
|
-
['harness:identity', -100],
|
|
391
|
-
['harness:source', -99],
|
|
392
|
-
['app:web-surface', -98],
|
|
393
|
-
];
|
|
394
|
-
|
|
395
|
-
/** persona 段的注册名与顺序(dsh-system-prompt 的 PERSONA_SECTION/PERSONA_ORDER)。 */
|
|
396
|
-
export const PERSONA_SECTION_NAME = 'deployment:persona';
|
|
397
|
-
export const PERSONA_SECTION_ORDER = 0;
|
|
398
|
-
|
|
399
|
-
/** 提取一条用户消息的顶层文本(text 块拼接)。 */
|
|
400
|
-
export function extractUserText(message) {
|
|
401
|
-
if (message === null || typeof message !== 'object' || !Array.isArray(message.content)) return '';
|
|
402
|
-
let text = '';
|
|
403
|
-
for (const block of message.content) {
|
|
404
|
-
if (block !== null && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string') {
|
|
405
|
-
text += block.text;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
return text;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/** 大小写不敏感的子串匹配:任一关键词命中即返回 true。 */
|
|
412
|
-
export function matchKeywords(text, keywords) {
|
|
413
|
-
if (!Array.isArray(keywords) || keywords.length === 0) return false;
|
|
414
|
-
const haystack = String(text ?? '').toLowerCase();
|
|
415
|
-
return keywords.some((k) => typeof k === 'string' && k.length > 0 && haystack.includes(k.toLowerCase()));
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/** 收集一次工具调用失败的模型可见文本(error 字段 + content 文本块)。 */
|
|
419
|
-
export function collectFailureText(result) {
|
|
420
|
-
if (result === null || typeof result !== 'object') return '';
|
|
421
|
-
const parts = [];
|
|
422
|
-
const error = result.error;
|
|
423
|
-
if (error !== null && typeof error === 'object') {
|
|
424
|
-
if (typeof error.code === 'string') parts.push(error.code);
|
|
425
|
-
if (typeof error.message === 'string') parts.push(error.message);
|
|
426
|
-
if (typeof error.text === 'string') parts.push(error.text);
|
|
427
|
-
}
|
|
428
|
-
if (Array.isArray(result.content)) {
|
|
429
|
-
for (const block of result.content) {
|
|
430
|
-
if (block !== null && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string') {
|
|
431
|
-
parts.push(block.text);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
return parts.join('\n');
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// ── 真实 Minimal 工具对(0.5.0,参照 dsh-anchored-standard issue #11)──────
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* 官方 minimal preset 的持久 bash 工具描述,逐字一致。schema(参数)由
|
|
442
|
-
* @deepseek-ai/dsh-tool-bash-persistent 提供,与 minimal preset 挂的是同一
|
|
443
|
-
* 个包;描述在这里显式传入,保证与 minimal 的 agent.cordis.yml 完全一致。
|
|
444
|
-
*/
|
|
445
|
-
export const MINIMAL_BASH_DESCRIPTION = [
|
|
446
|
-
'Run commands in a bash shell',
|
|
447
|
-
'* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.',
|
|
448
|
-
"* You don't have access to the internet via this tool.",
|
|
449
|
-
'* You do have access to a mirror of common linux and python packages via apt and pip.',
|
|
450
|
-
'* State is persistent across command calls and discussions with the user.',
|
|
451
|
-
"* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.",
|
|
452
|
-
'* Please avoid commands that may produce a very large amount of output.',
|
|
453
|
-
"* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.",
|
|
454
|
-
].join('\n');
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* 被真实工具对按名阴影的工具引导段:standard 的 sandboxed bash 注册了
|
|
458
|
-
* `tool:bash`(order 105)引导段,挂载真实工具对后该文本已与可见工具不符,
|
|
459
|
-
* 按名阴影为空段(装配时丢弃)。
|
|
460
|
-
*/
|
|
461
|
-
export const TOOL_GUIDANCE_SHADOWS = [
|
|
462
|
-
['tool:bash', 105],
|
|
463
|
-
];
|
|
464
|
-
|
|
465
|
-
/** 真实工具对挂载所需的官方插件包说明符(声明在 optionalDependencies)。 */
|
|
466
|
-
export const REAL_PAIR_SPECS = {
|
|
467
|
-
terminal: '@deepseek-ai/dsh-terminal',
|
|
468
|
-
terminalBash: '@deepseek-ai/dsh-terminal-bash',
|
|
469
|
-
bashPersistent: '@deepseek-ai/dsh-tool-bash-persistent',
|
|
470
|
-
fsLocal: '@deepseek-ai/dsh-fs-local',
|
|
471
|
-
strReplaceEditor: '@deepseek-ai/dsh-tool-str-replace-editor',
|
|
472
|
-
};
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* 加载真实 Minimal 工具对所需模块。任一包不可用即整体返回 null(调用方
|
|
476
|
-
* 降级为旧行为),绝不抛出。
|
|
477
|
-
* @param importFn - 测试可注入的 import 实现(默认动态 import)。
|
|
478
|
-
*/
|
|
479
|
-
export async function loadRealPairModules(importFn) {
|
|
480
|
-
const dynamicImport = importFn ?? ((spec) => import(spec));
|
|
481
|
-
const modules = {};
|
|
482
|
-
for (const [key, spec] of Object.entries(REAL_PAIR_SPECS)) {
|
|
483
|
-
try {
|
|
484
|
-
const mod = await dynamicImport(spec);
|
|
485
|
-
modules[key] = mod?.default ?? mod;
|
|
486
|
-
} catch {
|
|
487
|
-
modules[key] = null;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
if (Object.keys(REAL_PAIR_SPECS).some((key) => modules[key] === null)) return null;
|
|
491
|
-
return modules;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* 计算真实工具对的挂载清单(纯函数,便于测试):
|
|
496
|
-
* 顺序 = 依赖在前(terminals → backend → bash;fs → editor)。
|
|
497
|
-
* @param modules - loadRealPairModules 的结果。
|
|
498
|
-
* @param cwd - str_replace_editor 本地 fs 的工作目录(会话 cwd)。
|
|
499
|
-
*/
|
|
500
|
-
export function realPairMounts(modules, cwd) {
|
|
501
|
-
if (modules === null || modules === void 0) return [];
|
|
502
|
-
const resolvedCwd = typeof cwd === 'string' && cwd.length > 0
|
|
503
|
-
? cwd
|
|
504
|
-
: (process.env.DSH_CWD && process.env.DSH_CWD.trim().length > 0 ? process.env.DSH_CWD : process.cwd());
|
|
505
|
-
return [
|
|
506
|
-
{ module: modules.terminal, config: {} },
|
|
507
|
-
{ module: modules.terminalBash, config: { timeoutMs: 300000 } },
|
|
508
|
-
{ module: modules.bashPersistent, config: { timeoutMs: 300000, description: MINIMAL_BASH_DESCRIPTION } },
|
|
509
|
-
{ module: modules.fsLocal, config: { cwd: resolvedCwd } },
|
|
510
|
-
{ module: modules.strReplaceEditor, config: { maxOutputChars: 16000 } },
|
|
511
|
-
];
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* 在 agent 作用域挂载真实工具对(scoped registration 按名阴影继承工具)。
|
|
516
|
-
* 每个挂载是一个独立 fiber,dispose 时全部副作用(工具/schema/服务)撤销。
|
|
517
|
-
* @param agent - 目标 agent(其 ctx 必须带 scope 标签)。
|
|
518
|
-
* @param mounts - realPairMounts 的结果。
|
|
519
|
-
* @param warn - 日志回调。
|
|
520
|
-
* @returns 可释放的 disposer 列表。
|
|
521
|
-
*/
|
|
522
|
-
export function mountRealPair(agent, mounts, warn) {
|
|
523
|
-
const disposers = [];
|
|
524
|
-
if (agent === null || typeof agent !== 'object' || typeof agent.ctx?.plugin !== 'function') return disposers;
|
|
525
|
-
for (const mount of mounts) {
|
|
526
|
-
if (mount?.module === null || mount?.module === void 0) continue;
|
|
527
|
-
try {
|
|
528
|
-
const fiber = agent.ctx.plugin(mount.module, mount.config ?? {});
|
|
529
|
-
disposers.push(() => {
|
|
530
|
-
try { fiber.dispose(); } catch {}
|
|
531
|
-
});
|
|
532
|
-
} catch (error) {
|
|
533
|
-
warn?.(`mountRealPair failed for ${String(mount.module?.name ?? 'module')}: ${String((error && error.message) || error)}`);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
return disposers;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// ── instruction-hint(0.5.0,参照 dsh-anchored-standard)──────────────────
|
|
540
|
-
|
|
541
|
-
/** 项目链候选指令文件名(探测顺序)与用户全局候选。 */
|
|
542
|
-
export const INSTRUCTION_PROJECT_CANDIDATES = ['AGENTS.md', 'CLAUDE.md', 'AGENTS.local.md', 'CLAUDE.local.md'];
|
|
543
|
-
export const INSTRUCTION_USER_CANDIDATE = 'AGENTS.md';
|
|
544
|
-
|
|
545
|
-
/** 平台无关的路径拼接(与参考实现一致)。 */
|
|
546
|
-
export function joinPath(dir, segment) {
|
|
547
|
-
if (dir.endsWith('/') || dir.endsWith('\\')) return dir + segment;
|
|
548
|
-
const sep = dir.includes('\\') ? '\\' : '/';
|
|
549
|
-
return dir + sep + segment;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
/** 绝对 POSIX/Windows 路径的父目录。 */
|
|
553
|
-
export function parentPath(path) {
|
|
554
|
-
const idx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
|
|
555
|
-
if (idx <= 0) return path;
|
|
556
|
-
const parent = path.slice(0, idx);
|
|
557
|
-
return parent.length === 0 ? path : parent;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/** 从 cwd 向上找项目根(含 .git/.hg/.svn 标记的最近祖先)。 */
|
|
561
|
-
export async function findProjectRoot(fs, cwd, signal) {
|
|
562
|
-
let current = cwd;
|
|
563
|
-
for (;;) {
|
|
564
|
-
for (const marker of ['.git', '.hg', '.svn']) {
|
|
565
|
-
try {
|
|
566
|
-
const target = await fs.resolve(joinPath(current, marker), { cwd, signal });
|
|
567
|
-
const info = await fs.stat(target, signal);
|
|
568
|
-
if (info !== void 0) return current;
|
|
569
|
-
} catch {
|
|
570
|
-
// 探测失败 = 标记不存在,继续。
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
const parent = parentPath(current);
|
|
574
|
-
if (parent === current || parent.length === 0) return cwd;
|
|
575
|
-
current = parent;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/** 列出某目录下存在的候选指令文件。 */
|
|
580
|
-
export async function presentInDir(fs, dir, candidates, signal) {
|
|
581
|
-
const found = [];
|
|
582
|
-
for (const candidate of candidates) {
|
|
583
|
-
try {
|
|
584
|
-
const target = await fs.resolve(joinPath(dir, candidate), { cwd: dir, signal });
|
|
585
|
-
const info = await fs.stat(target, signal);
|
|
586
|
-
if (info !== void 0 && info.type === 'file') found.push(candidate);
|
|
587
|
-
} catch {
|
|
588
|
-
// 不存在/不可读——跳过。
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
return found;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* 探测会话的指令文件(项目链 + $DSH_HOME/AGENTS.md)。任何失败都返回
|
|
596
|
-
* null/空集——提示注入绝不能让会话受损。
|
|
597
|
-
* @returns { projectFiles, root, userGlobalFiles } 或 null(无 fs / 无 cwd)。
|
|
598
|
-
*/
|
|
599
|
-
export async function probeInstructionFiles(fs, cwd, dshHome, signal) {
|
|
600
|
-
if (fs === null || fs === void 0 || typeof cwd !== 'string' || cwd.length === 0) return null;
|
|
601
|
-
const projectFiles = [];
|
|
602
|
-
let root = cwd;
|
|
603
|
-
try {
|
|
604
|
-
root = await findProjectRoot(fs, cwd, signal);
|
|
605
|
-
projectFiles.push(...await presentInDir(fs, root, INSTRUCTION_PROJECT_CANDIDATES, signal));
|
|
606
|
-
} catch {
|
|
607
|
-
// 项目链探测失败——忽略,仍报告用户全局文件。
|
|
608
|
-
}
|
|
609
|
-
const userGlobalFiles = [];
|
|
610
|
-
if (typeof dshHome === 'string' && dshHome.length > 0) {
|
|
611
|
-
try {
|
|
612
|
-
userGlobalFiles.push(...await presentInDir(fs, dshHome, [INSTRUCTION_USER_CANDIDATE], signal));
|
|
613
|
-
} catch {
|
|
614
|
-
// 不可读——忽略。
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
if (projectFiles.length === 0 && userGlobalFiles.length === 0) return null;
|
|
618
|
-
return { projectFiles, root, userGlobalFiles };
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// ── 按需发现工具(resident discovery set,参照 dsh-anchored-standard)─────
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* 从 agent/pre-step 的 decision.messages 剥离指定 source.kind 的消息。
|
|
625
|
-
* 保留原数组引用(未变化时)便于调用方判断。
|
|
626
|
-
*/
|
|
627
|
-
export function stripSuppressedMessages(messages, suppressedSources) {
|
|
628
|
-
if (!Array.isArray(messages) || suppressedSources.size === 0) return messages;
|
|
629
|
-
const kept = messages.filter((message) => {
|
|
630
|
-
const kind = message?.source?.kind;
|
|
631
|
-
return typeof kind !== 'string' || !suppressedSources.has(kind);
|
|
632
|
-
});
|
|
633
|
-
return kept.length === messages.length ? messages : kept;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
/**
|
|
637
|
-
* dev_tool_search:搜索完整可见目录并按名解锁工具(解锁结果下一请求生效,
|
|
638
|
-
* 经 syncBootstrap 重算 deny 集;解锁记录写入 a.unlocked,resume-safe 由
|
|
639
|
-
* loadUnlockedFromEvents 从持久事件恢复)。
|
|
640
|
-
*/
|
|
641
|
-
export function createDevToolSearch({ schemasOf, onUnlock, catalogHint }) {
|
|
642
|
-
const hint = catalogHint ?? [
|
|
643
|
-
'This session starts with a minimal resident set: bash, str_replace_editor.',
|
|
644
|
-
'Everything else is unlocked on demand through this tool.',
|
|
645
|
-
].join('\n');
|
|
646
|
-
return {
|
|
647
|
-
name: 'dev_tool_search',
|
|
648
|
-
description: [
|
|
649
|
-
'Discover and unlock tools that are NOT currently available.',
|
|
650
|
-
'',
|
|
651
|
-
hint,
|
|
652
|
-
'',
|
|
653
|
-
'If the current task needs internet, delegation, workflows, goals, images, background jobs, or multi-agent coordination, call dev_tool_search FIRST — do not try to work around them with bash.',
|
|
654
|
-
'',
|
|
655
|
-
'Usage: pass `query` to search the catalog (returns matching tool names + descriptions), then pass `toolNames` with exact names to unlock them. Unlocked tools appear from the next request on and stay unlocked for the session.',
|
|
656
|
-
].join('\n'),
|
|
657
|
-
parameters: {
|
|
658
|
-
type: 'object',
|
|
659
|
-
properties: {
|
|
660
|
-
query: { type: 'string', description: 'search keywords (e.g. "web", "subagent")' },
|
|
661
|
-
toolNames: { type: 'array', description: 'exact tool names to unlock', items: { type: 'string' } },
|
|
662
|
-
},
|
|
663
|
-
required: [],
|
|
664
|
-
additionalProperties: false,
|
|
665
|
-
},
|
|
666
|
-
output: {
|
|
667
|
-
schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string' } }, required: ['text'] },
|
|
668
|
-
render: (_a, value) => [{ type: 'text', text: value.text }],
|
|
669
|
-
},
|
|
670
|
-
async execute(args, exec) {
|
|
671
|
-
const query = args !== null && typeof args === 'object' && typeof args.query === 'string' ? args.query.trim() : '';
|
|
672
|
-
const unlock = args !== null && typeof args === 'object' && Array.isArray(args.toolNames)
|
|
673
|
-
? args.toolNames.filter((name) => typeof name === 'string' && name.length > 0)
|
|
674
|
-
: [];
|
|
675
|
-
const lines = [];
|
|
676
|
-
if (unlock.length > 0) {
|
|
677
|
-
let available = new Set();
|
|
678
|
-
try {
|
|
679
|
-
const schemas = typeof schemasOf === 'function' ? (schemasOf(exec) ?? []) : [];
|
|
680
|
-
for (const schema of schemas) {
|
|
681
|
-
if (schema !== null && typeof schema === 'object' && typeof schema.name === 'string') available.add(schema.name);
|
|
682
|
-
}
|
|
683
|
-
} catch {}
|
|
684
|
-
const ok = unlock.filter((name) => available.has(name));
|
|
685
|
-
if (ok.length > 0 && typeof onUnlock === 'function') {
|
|
686
|
-
try { await onUnlock(ok, exec); } catch {}
|
|
687
|
-
}
|
|
688
|
-
if (ok.length > 0) lines.push(`Unlocked for the next request: ${ok.join(', ')}`);
|
|
689
|
-
const missing = unlock.filter((name) => !available.has(name));
|
|
690
|
-
if (missing.length > 0) lines.push(`Not found in catalog: ${missing.join(', ')}`);
|
|
691
|
-
}
|
|
692
|
-
if (query.length === 0 && unlock.length === 0) {
|
|
693
|
-
lines.push('Provide `query` to search the catalog, or `toolNames` to unlock tools.');
|
|
694
|
-
return { text: lines.join('\n') };
|
|
695
|
-
}
|
|
696
|
-
if (query.length === 0) {
|
|
697
|
-
return { text: lines.join('\n') || 'Nothing to do.' };
|
|
698
|
-
}
|
|
699
|
-
try {
|
|
700
|
-
const schemas = typeof schemasOf === 'function' ? (schemasOf(exec) ?? []) : [];
|
|
701
|
-
const wanted = query.toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean);
|
|
702
|
-
const matches = schemas
|
|
703
|
-
.filter((schema) => {
|
|
704
|
-
if (schema === null || typeof schema !== 'object') return false;
|
|
705
|
-
const haystack = `${schema.name || ''} ${schema.description || ''}`.toLowerCase();
|
|
706
|
-
return wanted.every((token) => haystack.includes(token));
|
|
707
|
-
})
|
|
708
|
-
.slice(0, 25);
|
|
709
|
-
if (matches.length === 0) {
|
|
710
|
-
lines.push(`No tools match "${query}".`);
|
|
711
|
-
} else {
|
|
712
|
-
lines.push(`Matching tools (${matches.length}):`);
|
|
713
|
-
for (const schema of matches) {
|
|
714
|
-
const desc = String(schema.description || '').split('\n')[0].slice(0, 90);
|
|
715
|
-
lines.push(`- ${schema.name}: ${desc}`);
|
|
716
|
-
}
|
|
717
|
-
lines.push('Unlock with dev_tool_search({"toolNames": ["<exact name>"]}).');
|
|
718
|
-
}
|
|
719
|
-
} catch (error) {
|
|
720
|
-
lines.push(`catalog search unavailable: ${String((error && error.message) || error)}`);
|
|
721
|
-
}
|
|
722
|
-
return { text: lines.join('\n') };
|
|
723
|
-
},
|
|
724
|
-
};
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
/**
|
|
728
|
-
* skill_search:按关键词搜索可用技能(仅摘要,替代 ~9KB 目录注入)。
|
|
729
|
-
* skillsOf 返回宿主 skills 服务(调用期解析)。
|
|
730
|
-
*/
|
|
731
|
-
export function createSkillSearch({ skillsOf }) {
|
|
732
|
-
const tokens = (text) => (text || '').toLowerCase().split(/[^a-z0-9_-]+/).filter(Boolean);
|
|
733
|
-
return {
|
|
734
|
-
name: 'skill_search',
|
|
735
|
-
description: 'Search the available skills by keyword and return matching skill names with short descriptions. This session keeps NO skill catalog in the prompt — if a task looks like it matches a skill (document conversion, image processing, game reviews, markdown, PDF, spreadsheets, …), call skill_search FIRST to find it, then skill_load to activate it. Do NOT assume skill names from memory.',
|
|
736
|
-
parameters: {
|
|
737
|
-
type: 'object',
|
|
738
|
-
properties: { query: { type: 'string', description: 'search keywords (e.g. "pdf", "obsidian", "game review")' } },
|
|
739
|
-
required: ['query'],
|
|
740
|
-
additionalProperties: false,
|
|
741
|
-
},
|
|
742
|
-
output: {
|
|
743
|
-
schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string' } }, required: ['text'] },
|
|
744
|
-
render: (_a, value) => [{ type: 'text', text: value.text }],
|
|
745
|
-
},
|
|
746
|
-
async execute(args, exec) {
|
|
747
|
-
const wanted = tokens(args?.query);
|
|
748
|
-
try {
|
|
749
|
-
const skills = skillsOf();
|
|
750
|
-
if (skills === void 0 || typeof skills.list !== 'function') {
|
|
751
|
-
return { text: 'skill_search unavailable: skills service is not mounted.' };
|
|
752
|
-
}
|
|
753
|
-
const agent = exec?.agent;
|
|
754
|
-
const lookup = {
|
|
755
|
-
scope: agent,
|
|
756
|
-
cwd: agent?.session?.header?.cwd,
|
|
757
|
-
signal: exec?.signal,
|
|
758
|
-
};
|
|
759
|
-
const all = await skills.list(lookup);
|
|
760
|
-
const matches = (Array.isArray(all) ? all : []).filter((skill) => {
|
|
761
|
-
if (wanted.length === 0) return true;
|
|
762
|
-
const haystack = tokens(`${skill?.name ?? ''} ${skill?.description ?? ''} ${skill?.whenToUse ?? ''}`).join(' ');
|
|
763
|
-
return wanted.every((token) => haystack.includes(token));
|
|
764
|
-
});
|
|
765
|
-
const head = matches.slice(0, 20);
|
|
766
|
-
const lines = head.map((skill) => {
|
|
767
|
-
const desc = String(skill?.description || '').split('\n')[0];
|
|
768
|
-
return `- ${skill?.name}: ${desc}`;
|
|
769
|
-
});
|
|
770
|
-
if (lines.length === 0) return { text: `No skills match "${args?.query}". Use skill_search with other keywords.` };
|
|
771
|
-
const extra = matches.length > 20 ? `\n…(${matches.length - 20} more)` : '';
|
|
772
|
-
return { text: `Matching skills (${matches.length}):\n${lines.join('\n')}${extra}\n\nLoad one with skill_load (exact name).` };
|
|
773
|
-
} catch (error) {
|
|
774
|
-
return { text: `skill_search unavailable: ${String((error && error.message) || error)}` };
|
|
775
|
-
}
|
|
776
|
-
},
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
/**
|
|
781
|
-
* skill_load:按精确名加载单个技能的完整说明,经 agent.inject 作为
|
|
782
|
-
* source.kind='skill-invocation' 的下一条上下文消息注入(用户主动技能手势
|
|
783
|
-
* 同款来源,绝不被默认抑制集剥离)。
|
|
784
|
-
*/
|
|
785
|
-
export function createSkillLoad({ skillsOf }) {
|
|
786
|
-
return {
|
|
787
|
-
name: 'skill_load',
|
|
788
|
-
description: 'Load the full instructions of ONE skill by its exact name (from skill_search results) and inject them for the next request. Call this before acting on a task that matches the skill.',
|
|
789
|
-
parameters: {
|
|
790
|
-
type: 'object',
|
|
791
|
-
properties: { name: { type: 'string', description: 'exact skill name (kebab-case, from skill_search)' } },
|
|
792
|
-
required: ['name'],
|
|
793
|
-
additionalProperties: false,
|
|
794
|
-
},
|
|
795
|
-
output: {
|
|
796
|
-
schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string' } }, required: ['text'] },
|
|
797
|
-
render: (_a, value) => [{ type: 'text', text: value.text }],
|
|
798
|
-
},
|
|
799
|
-
async execute(args, exec) {
|
|
800
|
-
try {
|
|
801
|
-
const agent = exec?.agent;
|
|
802
|
-
if (agent === void 0) return { text: 'skill_load requires an agent context.' };
|
|
803
|
-
const skills = skillsOf();
|
|
804
|
-
if (skills === void 0 || typeof skills.get !== 'function') {
|
|
805
|
-
return { text: 'skill_load unavailable: skills service is not mounted.' };
|
|
806
|
-
}
|
|
807
|
-
const name = typeof args?.name === 'string' ? args.name.trim() : '';
|
|
808
|
-
if (name.length === 0) return { text: 'skill_load requires a skill name.' };
|
|
809
|
-
const skill = await skills.get(name, {
|
|
810
|
-
scope: agent,
|
|
811
|
-
cwd: agent.session?.header?.cwd,
|
|
812
|
-
signal: exec?.signal,
|
|
813
|
-
});
|
|
814
|
-
if (skill === void 0) {
|
|
815
|
-
return { text: `No skill named "${name}". Run skill_search to list available skills.` };
|
|
816
|
-
}
|
|
817
|
-
const body = extractSkillBody(skill);
|
|
818
|
-
if (body.length === 0) {
|
|
819
|
-
return { text: `Skill "${name}" has no loadable body.` };
|
|
820
|
-
}
|
|
821
|
-
if (typeof agent.inject !== 'function') {
|
|
822
|
-
return { text: `Skill "${name}" found, but injection is unavailable in this agent.` };
|
|
823
|
-
}
|
|
824
|
-
agent.inject({
|
|
825
|
-
id: `skill-load-${name}-${Date.now()}`,
|
|
826
|
-
role: 'user',
|
|
827
|
-
content: [{ type: 'text', text: body }],
|
|
828
|
-
source: { kind: 'skill-invocation', name, form: 'instructions' },
|
|
829
|
-
});
|
|
830
|
-
return { text: `Skill "${name}" loaded; its instructions will be injected for the next request.` };
|
|
831
|
-
} catch (error) {
|
|
832
|
-
return { text: `skill_load failed: ${String((error && error.message) || error)}` };
|
|
833
|
-
}
|
|
834
|
-
},
|
|
835
|
-
};
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
/** 提取已加载技能定义的模型可见正文(与参考实现一致)。 */
|
|
839
|
-
export function extractSkillBody(skill) {
|
|
840
|
-
const content = skill?.content ?? skill?.instructions ?? skill?.body;
|
|
841
|
-
if (typeof content === 'string') return content;
|
|
842
|
-
if (Array.isArray(content)) {
|
|
843
|
-
return content.map((part) => (typeof part === 'string' ? part : JSON.stringify(part))).join('\n');
|
|
844
|
-
}
|
|
845
|
-
return '';
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
// ── bootstrap 锚定(参照 dsh-anchored-standard 的实测结论)──────────────
|
|
849
|
-
|
|
850
|
-
/** promoteOn 配置 → 晋升的持久事件类型集合。 */
|
|
851
|
-
export const PROMOTE_EVENTS = {
|
|
852
|
-
'tool-call': ['tool/call'],
|
|
853
|
-
'assistant-message': ['assistant/message'],
|
|
854
|
-
either: ['tool/call', 'assistant/message'],
|
|
855
|
-
};
|
|
856
|
-
|
|
857
|
-
/**
|
|
858
|
-
* 扫描一段持久会话日志,推导 { boundary, promoted } 阶段。
|
|
859
|
-
* compaction/end 之后需要新的晋升信号(纪元感知);boundary 为 -1 表示
|
|
860
|
-
* 尚无压缩。事件无 seq 时按"边界之后"处理(与参考实现一致)。
|
|
861
|
-
*/
|
|
862
|
-
export function scanPhase(events, promoteEvents) {
|
|
863
|
-
let boundary = -1;
|
|
864
|
-
let promoted = false;
|
|
865
|
-
if (Array.isArray(events)) {
|
|
866
|
-
for (const event of events) {
|
|
867
|
-
const seq = typeof event?.seq === 'number' ? event.seq : 0;
|
|
868
|
-
if (event?.type === 'compaction/end') {
|
|
869
|
-
boundary = seq;
|
|
870
|
-
promoted = false;
|
|
871
|
-
continue;
|
|
872
|
-
}
|
|
873
|
-
if (promoteEvents.includes(event?.type) && seq > boundary) promoted = true;
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
return { boundary, promoted };
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
/** 增量喂入一个持久事件(仅更新已存在条目;冷会话由 scanPhase 全量推导)。 */
|
|
880
|
-
export function observePhase(state, sessionId, event) {
|
|
881
|
-
const entry = state.get(sessionId);
|
|
882
|
-
if (entry === void 0) return;
|
|
883
|
-
const seq = typeof event?.seq === 'number' ? event.seq : 0;
|
|
884
|
-
if (event?.type === 'compaction/end') {
|
|
885
|
-
state.set(sessionId, { boundary: seq, promoted: false, promoteEvents: entry.promoteEvents });
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
if (entry.promoted) return;
|
|
889
|
-
const promoteEvents = entry.promoteEvents;
|
|
890
|
-
if (promoteEvents.includes(event?.type) && seq > entry.boundary) {
|
|
891
|
-
state.set(sessionId, { ...entry, promoted: true });
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
/**
|
|
896
|
-
* 按 keep 集合收窄装配后的工具目录。
|
|
897
|
-
* @returns { tools, missing } —— missing 非空表示 keep 中的工具在目录里缺失,
|
|
898
|
-
* 调用方决定是否降级(全目录放行)。
|
|
899
|
-
*/
|
|
900
|
-
export function filterBootstrapTools(assembly, keep) {
|
|
901
|
-
const keepSet = keep instanceof Set ? keep : new Set(keep);
|
|
902
|
-
const tools = Array.isArray(assembly?.tools) ? assembly.tools : [];
|
|
903
|
-
const available = new Set(tools.map((tool) => tool?.name).filter((n) => typeof n === 'string'));
|
|
904
|
-
const missing = [...keepSet].filter((n) => !available.has(n));
|
|
905
|
-
return {
|
|
906
|
-
tools: tools.filter((tool) => keepSet.has(tool.name)),
|
|
907
|
-
missing,
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
/** 按 source.kind 剥离自动注入的上下文消息(保留主动的用户消息)。 */
|
|
912
|
-
export function filterBootstrapMessages(messages, suppressedSources) {
|
|
913
|
-
if (!Array.isArray(messages) || suppressedSources.size === 0) return messages;
|
|
914
|
-
const kept = messages.filter((message) => {
|
|
915
|
-
const kind = message?.source?.kind;
|
|
916
|
-
return typeof kind !== 'string' || !suppressedSources.has(kind);
|
|
917
|
-
});
|
|
918
|
-
return kept.length === messages.length ? messages : kept;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
/** 首轮输出预算:未晋升 → 封顶;已晋升 → 若仍带着封顶值则剥离。 */
|
|
922
|
-
export function applyBootstrapBudget(config, promoted, maxTokens) {
|
|
923
|
-
if (!promoted) return { ...config, maxTokens };
|
|
924
|
-
if (config?.maxTokens === maxTokens) {
|
|
925
|
-
const { maxTokens: _drop, ...rest } = config;
|
|
926
|
-
return rest;
|
|
927
|
-
}
|
|
928
|
-
return config;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
// ── host 插件 ────────────────────────────────────────────────────────────
|
|
932
|
-
|
|
933
|
-
/** 把配置 namespace 注册进宿主 settings 体系(dsh-settings 0.1.0-rc.7+)。
|
|
934
|
-
* 设置页 describe() 只枚举 settings.register 注册过的 namespace;本插件
|
|
935
|
-
* 的卡片读写不经过宿主 settings 文档,注册只为让卡片出现在设置页。
|
|
936
|
-
* options.base 必传当前生效配置快照:宿主 resolve = schema(mergeLayers(base,
|
|
937
|
-
* section)),schema 无默认值时 base 缺失会让 describe() 的 value 为
|
|
938
|
-
* undefined,而设置页 wire 校验要求 value 非空(invalid_type: nonoptional),
|
|
939
|
-
* 一项失败会拖垮整个设置页。本插件的 any() schema 完全依赖 base。
|
|
940
|
-
* fail-safe(schema 库/服务缺失静默跳过)+ 幂等(重复注册忽略)。
|
|
941
|
-
* 导出以便测试注入 Schema stub。 */
|
|
942
|
-
export function registerSettingsNamespace(ctx, ns, schemaLib, buildSchema, options) {
|
|
943
|
-
if (schemaLib === null || schemaLib === undefined) return false;
|
|
944
|
-
if (ctx === null || typeof ctx !== 'object' || typeof ctx.inject !== 'function') return false;
|
|
945
|
-
try {
|
|
946
|
-
ctx.inject(['settings'], (settingsCtx) => {
|
|
947
|
-
try {
|
|
948
|
-
settingsCtx.settings.register(ns, buildSchema(schemaLib), options);
|
|
949
|
-
} catch (error) {
|
|
950
|
-
const message = String(error && error.message || error);
|
|
951
|
-
if (!message.includes('already registered')) throw error;
|
|
952
|
-
}
|
|
953
|
-
});
|
|
954
|
-
return true;
|
|
955
|
-
} catch (error) {
|
|
956
|
-
try { ctx.logger?.warn?.(`adaptive-perf: settings namespace registration skipped: ${error && error.message || error}`); } catch {}
|
|
957
|
-
return false;
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
export async function apply(ctx, config, options = {}) {
|
|
962
|
-
// fail-safe:初始化失败只记录,绝不让本插件拖垮 harness(host 层挂载时
|
|
963
|
-
// entry 异常会导致进程启动失败)。
|
|
964
|
-
try {
|
|
965
|
-
const systemPrompt = ctx.get('systemPrompt');
|
|
966
|
-
const tools = ctx.get('tools');
|
|
967
|
-
if (systemPrompt === void 0 || tools === void 0) {
|
|
968
|
-
ctx.logger?.warn?.('adaptive-perf: systemPrompt/tools service unavailable; plugin inactive');
|
|
969
|
-
return;
|
|
970
|
-
}
|
|
971
|
-
const agentPresets = ctx.get('agentPresets');
|
|
972
|
-
if (agentPresets === void 0) {
|
|
973
|
-
ctx.logger?.warn?.('adaptive-perf: agentPresets service unavailable (no agent presets composed); plugin inactive');
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
const agentsService = ctx.get('agents');
|
|
977
|
-
|
|
978
|
-
const patchConfig = config !== null && typeof config === 'object' && !Array.isArray(config) ? config : {};
|
|
979
|
-
/** 当前生效配置(热更新;后续 store 创建后以 config.json 权威合并结果覆盖)。 */
|
|
980
|
-
let cfg = normalizeConfig(patchConfig);
|
|
981
|
-
|
|
982
|
-
/** 真实 Minimal 工具对模块(0.5.0)。加载失败 = null = 降级旧行为。
|
|
983
|
-
* options.realPairModules 仅供测试注入。 */
|
|
984
|
-
const realPairModules = options.realPairModules === void 0
|
|
985
|
-
? (cfg.bootstrap.enabled && cfg.bootstrap.realPair ? await loadRealPairModules() : null)
|
|
986
|
-
: options.realPairModules;
|
|
987
|
-
/** 是否已尝试加载过真实工具对模块(避免配置热更新时反复重试)。 */
|
|
988
|
-
let realPairModulesTried = options.realPairModules !== void 0 || (cfg.bootstrap.enabled && cfg.bootstrap.realPair);
|
|
989
|
-
if (cfg.bootstrap.enabled && cfg.bootstrap.realPair && realPairModules === null) {
|
|
990
|
-
try {
|
|
991
|
-
ctx.logger?.warn?.('adaptive-perf: real Minimal tool pair modules unavailable (optional @deepseek-ai deps missing?); falling back to catalog-only bootstrap');
|
|
992
|
-
} catch {}
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
/** 已注入过 instruction-hint 的会话(内存去重,与参考实现一致)。 */
|
|
996
|
-
const hintedSessions = new Set();
|
|
997
|
-
|
|
998
|
-
/**
|
|
999
|
-
* 配置热更新把 realPair 从关闭切到开启时补加载官方模块(初始未启用时
|
|
1000
|
-
* 不加载)。加载成功后对已接管会话补挂。
|
|
1001
|
-
*/
|
|
1002
|
-
function ensureRealPairModules() {
|
|
1003
|
-
if (realPairModulesTried || cfg.bootstrap.enabled === false || cfg.bootstrap.realPair === false) return;
|
|
1004
|
-
realPairModulesTried = true;
|
|
1005
|
-
loadRealPairModules().then((mods) => {
|
|
1006
|
-
if (mods === null) return;
|
|
1007
|
-
realPairModules = mods;
|
|
1008
|
-
info('real Minimal tool pair modules loaded; mounting for live agents');
|
|
1009
|
-
for (const a of agents.values()) {
|
|
1010
|
-
syncRealPair(a);
|
|
1011
|
-
applyMinimalPrompt(a);
|
|
1012
|
-
syncBootstrap(a);
|
|
1013
|
-
}
|
|
1014
|
-
}).catch(() => {});
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
/** agentId -> 会话自适应状态。 */
|
|
1018
|
-
const agents = new Map();
|
|
1019
|
-
|
|
1020
|
-
const warn = (message) => { try { ctx.logger?.warn?.(`adaptive-perf: ${message}`); } catch {} };
|
|
1021
|
-
const info = (message) => { try { ctx.logger?.info?.(`adaptive-perf: ${message}`); } catch {} };
|
|
1022
|
-
|
|
1023
|
-
/** 该会话是否为目标 preset。 */
|
|
1024
|
-
function isTarget(agent) {
|
|
1025
|
-
if (!cfg.enabled) return false;
|
|
1026
|
-
try {
|
|
1027
|
-
const presetId = agentPresets.composedPreset(agent.ctx);
|
|
1028
|
-
return presetId !== void 0 && cfg.presets.includes(presetId);
|
|
1029
|
-
} catch (error) {
|
|
1030
|
-
warn(`preset lookup failed for agent ${agent.id}: ${error && error.message || error}`);
|
|
1031
|
-
return false;
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
/** 计算某族当前应隐藏的工具名(与该 agent 实际可见目录取交集)。 */
|
|
1036
|
-
function familyDeny(a, family) {
|
|
1037
|
-
if (!Array.isArray(family.tools)) return [];
|
|
1038
|
-
return family.tools.filter((n) => typeof n === 'string' && a.visibleNames.has(n));
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
/** 应用/更新运行时上下文抑制。总开关关闭时一并释放。 */
|
|
1042
|
-
function applySuppression(a) {
|
|
1043
|
-
const want = cfg.enabled && cfg.suppressRuntimeContext;
|
|
1044
|
-
if (want && a.suppressDisposer === null) {
|
|
1045
|
-
try {
|
|
1046
|
-
a.suppressDisposer = a.agent.ctx.systemPrompt.suppressRuntimeContext();
|
|
1047
|
-
info(`agent ${a.agent.id}: runtime context suppressed`);
|
|
1048
|
-
} catch (error) {
|
|
1049
|
-
warn(`suppressRuntimeContext failed for agent ${a.agent.id}: ${error && error.message || error}`);
|
|
1050
|
-
}
|
|
1051
|
-
} else if (!want && a.suppressDisposer !== null) {
|
|
1052
|
-
try { a.suppressDisposer(); } catch {}
|
|
1053
|
-
a.suppressDisposer = null;
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
/** 应用/更新极简提示词层(persona 阴影 + 全局引导段屏蔽 +
|
|
1058
|
-
* 真实工具对替换后的 tool 引导段阴影)。 */
|
|
1059
|
-
function applyMinimalPrompt(a) {
|
|
1060
|
-
for (const disposer of a.promptDisposers) {
|
|
1061
|
-
try { disposer(); } catch {}
|
|
1062
|
-
}
|
|
1063
|
-
a.promptDisposers = [];
|
|
1064
|
-
if (!cfg.enabled || !cfg.minimalPrompt.enabled) return;
|
|
1065
|
-
const sp = a.agent.ctx.systemPrompt;
|
|
1066
|
-
try {
|
|
1067
|
-
if (cfg.minimalPrompt.suppressSections) {
|
|
1068
|
-
for (const [name, order] of SECTION_SHADOWS) {
|
|
1069
|
-
a.promptDisposers.push(sp.section({ name, order, text: '' }));
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
if (a.realPairActive) {
|
|
1073
|
-
for (const [name, order] of TOOL_GUIDANCE_SHADOWS) {
|
|
1074
|
-
a.promptDisposers.push(sp.section({ name, order, text: '' }));
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
const persona = cfg.minimalPrompt.persona.trim();
|
|
1078
|
-
if (persona.length > 0) {
|
|
1079
|
-
a.promptDisposers.push(sp.section({ name: PERSONA_SECTION_NAME, order: PERSONA_SECTION_ORDER, text: persona }));
|
|
1080
|
-
}
|
|
1081
|
-
info(`agent ${a.agent.id}: minimal prompt layer active`);
|
|
1082
|
-
} catch (error) {
|
|
1083
|
-
warn(`minimalPrompt failed for agent ${a.agent.id}: ${error && error.message || error}`);
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
/** 应用/更新工具族限制(lean 模式)。总开关关闭、族被禁用、已升级、
|
|
1088
|
-
* 已从配置删除的族一律放行。 */ function applyFamilies(a) {
|
|
1089
|
-
if (!cfg.enabled || !cfg.leanByDefault) {
|
|
1090
|
-
for (const [id, disposer] of a.familyDisposers) {
|
|
1091
|
-
try { disposer(); } catch {}
|
|
1092
|
-
a.familyDisposers.delete(id);
|
|
1093
|
-
}
|
|
1094
|
-
return;
|
|
1095
|
-
}
|
|
1096
|
-
// 1) 清理不再需要的限制(族被禁用 / 已升级 / 配置里已不存在)。
|
|
1097
|
-
for (const [id, disposer] of a.familyDisposers) {
|
|
1098
|
-
const family = cfg.families[id];
|
|
1099
|
-
if (family === void 0 || family.enabled === false || a.escalated.has(id)) {
|
|
1100
|
-
try { disposer(); } catch {}
|
|
1101
|
-
a.familyDisposers.delete(id);
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
// 2) 补齐需要的新限制。
|
|
1105
|
-
for (const [id, family] of Object.entries(cfg.families)) {
|
|
1106
|
-
if (a.familyDisposers.has(id)) continue;
|
|
1107
|
-
if (family.enabled === false || a.escalated.has(id)) continue;
|
|
1108
|
-
const deny = familyDeny(a, family);
|
|
1109
|
-
if (deny.length === 0) continue;
|
|
1110
|
-
try {
|
|
1111
|
-
const disposer = a.agent.ctx.tools.restrict({ deny });
|
|
1112
|
-
a.familyDisposers.set(id, disposer);
|
|
1113
|
-
} catch (error) {
|
|
1114
|
-
warn(`restrict failed for agent ${a.agent.id} family "${id}": ${error && error.message || error}`);
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
/** 当前隐藏的工具名(活跃限制族的 deny 并集,供失败信号匹配)。 */
|
|
1120
|
-
function hiddenNames(a) {
|
|
1121
|
-
const out = new Set();
|
|
1122
|
-
for (const [id, family] of Object.entries(cfg.families)) {
|
|
1123
|
-
if (!a.familyDisposers.has(id)) continue;
|
|
1124
|
-
for (const n of familyDeny(a, family)) out.add(n);
|
|
1125
|
-
}
|
|
1126
|
-
return out;
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
/** 放行一个工具族(会话内单调:只升不降)。 */
|
|
1130
|
-
function escalate(a, familyId, trigger) {
|
|
1131
|
-
if (a.escalated.has(familyId)) return;
|
|
1132
|
-
a.escalated.add(familyId);
|
|
1133
|
-
const disposer = a.familyDisposers.get(familyId);
|
|
1134
|
-
if (disposer !== void 0) {
|
|
1135
|
-
try { disposer(); } catch {}
|
|
1136
|
-
a.familyDisposers.delete(familyId);
|
|
1137
|
-
}
|
|
1138
|
-
info(`agent ${a.agent.id}: escalated family "${familyId}" (${trigger})`);
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
/** 从持久事件恢复 dev_tool_search 已解锁的工具(resume-safe)。 */
|
|
1142
|
-
function loadUnlockedFromEvents(a) {
|
|
1143
|
-
const events = a.agent.session && Array.isArray(a.agent.session.events) ? a.agent.session.events : [];
|
|
1144
|
-
for (const event of events) {
|
|
1145
|
-
if (event === null || typeof event !== 'object' || event.type !== 'tool/call') continue;
|
|
1146
|
-
if (event.data === null || typeof event.data !== 'object' || event.data.name !== 'dev_tool_search') continue;
|
|
1147
|
-
let args = event.data.arguments;
|
|
1148
|
-
if (typeof args === 'string') {
|
|
1149
|
-
try { args = JSON.parse(args); } catch { continue; }
|
|
1150
|
-
}
|
|
1151
|
-
if (args === null || typeof args !== 'object' || Array.isArray(args)) continue;
|
|
1152
|
-
if (Array.isArray(args.toolNames)) {
|
|
1153
|
-
for (const name of args.toolNames) {
|
|
1154
|
-
if (typeof name === 'string' && name.length > 0) a.unlocked.add(name);
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
/**
|
|
1161
|
-
* 挂载/卸载真实 Minimal 工具对(0.5.0):把官方 minimal preset 同款插件
|
|
1162
|
-
* (持久 PTY bash + str_replace_editor 及其依赖的 terminals/本地 fs)
|
|
1163
|
-
* 挂进 agent 作用域。scoped registration 按名阴影 standard 的 sandboxed
|
|
1164
|
-
* bash,且自身层注册不受 restrict 影响(view() 对 own layer 豁免)——
|
|
1165
|
-
* 首轮目录因此是"真实工具对 + 被 deny 的其他继承工具"。
|
|
1166
|
-
* 依赖模块缺失(realPairModules === null)或 Windows(PTY 仅 linux/darwin)
|
|
1167
|
-
* 时静默降级:不挂载,退回旧行为(仅收窄目录)。
|
|
1168
|
-
*/
|
|
1169
|
-
function syncRealPair(a) {
|
|
1170
|
-
for (const dispose of a.realPairDisposers) {
|
|
1171
|
-
try { dispose(); } catch {}
|
|
1172
|
-
}
|
|
1173
|
-
a.realPairDisposers = [];
|
|
1174
|
-
const active = cfg.enabled && cfg.bootstrap.enabled && cfg.bootstrap.realPair
|
|
1175
|
-
&& realPairModules !== null && process.platform !== 'win32';
|
|
1176
|
-
if (active) {
|
|
1177
|
-
const mounts = realPairMounts(realPairModules, a.agent.session?.header?.cwd);
|
|
1178
|
-
a.realPairDisposers = mountRealPair(a.agent, mounts, warn);
|
|
1179
|
-
if (a.realPairDisposers.length > 0) {
|
|
1180
|
-
info(`agent ${a.agent.id}: real Minimal tool pair mounted (${mounts.length} plugins)`);
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
a.realPairActive = a.realPairDisposers.length > 0;
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
/**
|
|
1187
|
-
* 给目标 agent 注册 on-demand 发现工具(dsh-anchored-standard 的 resident
|
|
1188
|
-
* discovery pattern):晋升后常驻 bootstrap.discoveryTools 列出的工具——
|
|
1189
|
-
* dev_tool_search(搜索完整目录并按名解锁,解锁结果下一请求生效,写入
|
|
1190
|
-
* a.unlocked 后 syncBootstrap 重算 deny 集)+ skill_search / skill_load
|
|
1191
|
-
* (常驻上下文抑制的替代品:按需发现/加载技能)。
|
|
1192
|
-
* 已存在的同名工具(preset 自带)不重复注册。
|
|
1193
|
-
*/
|
|
1194
|
-
function syncDiscoveryTools(a, enabled) {
|
|
1195
|
-
const want = enabled ? cfg.bootstrap.discoveryTools : [];
|
|
1196
|
-
for (const [name, disposer] of a.toolDisposers) {
|
|
1197
|
-
if (!want.includes(name)) {
|
|
1198
|
-
try { disposer(); } catch {}
|
|
1199
|
-
a.toolDisposers.delete(name);
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
if (!enabled) return;
|
|
1203
|
-
const scopedTools = a.agent.ctx && a.agent.ctx.tools;
|
|
1204
|
-
if (scopedTools === void 0 || typeof scopedTools.register !== 'function') return;
|
|
1205
|
-
for (const name of want) {
|
|
1206
|
-
if (a.toolDisposers.has(name)) continue;
|
|
1207
|
-
if (a.visibleNames.has(name)) continue; // preset/宿主已提供同名工具
|
|
1208
|
-
const def = discoveryToolDef(a, name);
|
|
1209
|
-
if (def === null) continue;
|
|
1210
|
-
try {
|
|
1211
|
-
const disposer = scopedTools.register(def);
|
|
1212
|
-
if (typeof disposer === 'function') a.toolDisposers.set(name, disposer);
|
|
1213
|
-
} catch (error) {
|
|
1214
|
-
warn(`register ${name} failed for agent ${a.agent.id}: ${error && error.message || error}`);
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
/** 构造一个发现工具定义;未知名称返回 null。 */
|
|
1220
|
-
function discoveryToolDef(a, name) {
|
|
1221
|
-
if (name === 'dev_tool_search') {
|
|
1222
|
-
return createDevToolSearch({
|
|
1223
|
-
schemasOf: (exec) => {
|
|
1224
|
-
const agent = exec !== null && typeof exec === 'object' ? exec.agent : void 0;
|
|
1225
|
-
return agent === void 0 ? [] : tools.schemas(agent);
|
|
1226
|
-
},
|
|
1227
|
-
onUnlock: async (names, exec) => {
|
|
1228
|
-
const agent = exec !== null && typeof exec === 'object' ? exec.agent : void 0;
|
|
1229
|
-
const state = agent === void 0 ? void 0 : agents.get(agent.id);
|
|
1230
|
-
if (state === void 0) return;
|
|
1231
|
-
for (const name of names) state.unlocked.add(name);
|
|
1232
|
-
syncBootstrap(state);
|
|
1233
|
-
},
|
|
1234
|
-
catalogHint: 'This session starts with a minimal resident set: bash, str_replace_editor, skill_search, skill_load. Everything else is unlocked on demand through this tool.',
|
|
1235
|
-
});
|
|
1236
|
-
}
|
|
1237
|
-
if (name === 'skill_search' && cfg.skillDiscovery) {
|
|
1238
|
-
return createSkillSearch({ skillsOf: () => ctx.get('skills') });
|
|
1239
|
-
}
|
|
1240
|
-
if (name === 'skill_load' && cfg.skillDiscovery) {
|
|
1241
|
-
return createSkillLoad({ skillsOf: () => ctx.get('skills') });
|
|
1242
|
-
}
|
|
1243
|
-
return null;
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
/** 完整接管一个 agent(幂等)。 */
|
|
1247
|
-
function handleAgent(agent) {
|
|
1248
|
-
if (agents.has(agent.id)) return;
|
|
1249
|
-
if (!isTarget(agent)) return;
|
|
1250
|
-
const a = {
|
|
1251
|
-
agent,
|
|
1252
|
-
suppressDisposer: null,
|
|
1253
|
-
promptDisposers: [],
|
|
1254
|
-
familyDisposers: new Map(),
|
|
1255
|
-
bootstrapDisposer: null,
|
|
1256
|
-
bootstrapKey: null,
|
|
1257
|
-
escalated: new Set(),
|
|
1258
|
-
visibleNames: new Set(),
|
|
1259
|
-
unlocked: new Set(),
|
|
1260
|
-
toolDisposers: new Map(),
|
|
1261
|
-
realPairDisposers: [],
|
|
1262
|
-
realPairActive: false,
|
|
1263
|
-
};
|
|
1264
|
-
// 记录当前可见目录(限制前);仅记录真实存在的工具,后续 restrict 与
|
|
1265
|
-
// 实际目录取交集,preset 升级改名/删除工具不会让本插件崩溃。
|
|
1266
|
-
try {
|
|
1267
|
-
for (const schema of tools.schemas(agent)) {
|
|
1268
|
-
if (schema !== null && typeof schema === 'object' && typeof schema.name === 'string') {
|
|
1269
|
-
a.visibleNames.add(schema.name);
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
} catch (error) {
|
|
1273
|
-
warn(`schemas lookup failed for agent ${agent.id}: ${error && error.message || error}`);
|
|
1274
|
-
}
|
|
1275
|
-
loadUnlockedFromEvents(a);
|
|
1276
|
-
agents.set(agent.id, a);
|
|
1277
|
-
applySuppression(a);
|
|
1278
|
-
syncRealPair(a);
|
|
1279
|
-
applyMinimalPrompt(a);
|
|
1280
|
-
applyFamilies(a);
|
|
1281
|
-
syncBootstrap(a);
|
|
1282
|
-
info(`agent ${agent.id}: adaptive-perf active (preset ${String(agentPresets.composedPreset(agent.ctx))})`);
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
/** 释放一个 agent 的全部副作用。 */
|
|
1286
|
-
function releaseAgent(agentId) {
|
|
1287
|
-
const a = agents.get(agentId);
|
|
1288
|
-
agents.delete(agentId);
|
|
1289
|
-
bootstrapState.delete(agentId);
|
|
1290
|
-
if (a === void 0) return;
|
|
1291
|
-
if (a.suppressDisposer !== null) {
|
|
1292
|
-
try { a.suppressDisposer(); } catch {}
|
|
1293
|
-
}
|
|
1294
|
-
for (const disposer of a.promptDisposers) {
|
|
1295
|
-
try { disposer(); } catch {}
|
|
1296
|
-
}
|
|
1297
|
-
a.promptDisposers = [];
|
|
1298
|
-
for (const disposer of a.toolDisposers.values()) {
|
|
1299
|
-
try { disposer(); } catch {}
|
|
1300
|
-
}
|
|
1301
|
-
a.toolDisposers.clear();
|
|
1302
|
-
for (const dispose of a.realPairDisposers) {
|
|
1303
|
-
try { dispose(); } catch {}
|
|
1304
|
-
}
|
|
1305
|
-
a.realPairDisposers = [];
|
|
1306
|
-
if (a.bootstrapDisposer !== null) {
|
|
1307
|
-
try { a.bootstrapDisposer(); } catch {}
|
|
1308
|
-
}
|
|
1309
|
-
for (const disposer of a.familyDisposers.values()) {
|
|
1310
|
-
try { disposer(); } catch {}
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
// ── 事件监听(注册在 host 根作用域,与 dsh-agent-presets 同款用法)──
|
|
1315
|
-
ctx.on('agent/created', ({ agent }) => {
|
|
1316
|
-
handleAgent(agent);
|
|
1317
|
-
});
|
|
1318
|
-
ctx.on('agent/disposed', ({ agent }) => {
|
|
1319
|
-
releaseAgent(agent.id);
|
|
1320
|
-
});
|
|
1321
|
-
|
|
1322
|
-
// 关键词信号:用户消息进入收件箱(早于回合处理与请求装配)。
|
|
1323
|
-
// 监听器无条件注册,事件时按实时 cfg 判断,热更新开关即时生效。
|
|
1324
|
-
ctx.on('agent/inbox/inserted', ({ agent, message }) => {
|
|
1325
|
-
if (!cfg.enabled || !cfg.escalateOnKeyword) return;
|
|
1326
|
-
const a = agents.get(agent.id);
|
|
1327
|
-
if (a === void 0) return;
|
|
1328
|
-
const text = extractUserText(message);
|
|
1329
|
-
if (text.length === 0) return;
|
|
1330
|
-
for (const [id, family] of Object.entries(cfg.families)) {
|
|
1331
|
-
if (a.escalated.has(id) || family.enabled === false) continue;
|
|
1332
|
-
if (matchKeywords(text, family.keywords)) escalate(a, id, 'keyword');
|
|
1333
|
-
}
|
|
1334
|
-
});
|
|
1335
|
-
|
|
1336
|
-
// 失败信号:PTC 程序调用隐藏工具报错(文本含工具名)时放行对应族。
|
|
1337
|
-
ctx.on('tools/result', (exec, result) => {
|
|
1338
|
-
if (!cfg.enabled || !cfg.escalateOnUnknownTool) return;
|
|
1339
|
-
const agent = exec !== null && typeof exec === 'object' ? exec.agent : void 0;
|
|
1340
|
-
if (agent === void 0) return;
|
|
1341
|
-
const a = agents.get(agent.id);
|
|
1342
|
-
if (a === void 0) return;
|
|
1343
|
-
if (result === null || typeof result !== 'object' || result.isError !== true) return;
|
|
1344
|
-
const text = collectFailureText(result);
|
|
1345
|
-
if (text.length === 0) return;
|
|
1346
|
-
const hidden = hiddenNames(a);
|
|
1347
|
-
if (hidden.size === 0) return;
|
|
1348
|
-
for (const [id, family] of Object.entries(cfg.families)) {
|
|
1349
|
-
if (a.escalated.has(id) || family.enabled === false) continue;
|
|
1350
|
-
for (const n of family.tools) {
|
|
1351
|
-
if (hidden.has(n) && text.includes(n)) {
|
|
1352
|
-
escalate(a, id, 'unknown-tool');
|
|
1353
|
-
break;
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
});
|
|
1358
|
-
|
|
1359
|
-
// ── 首轮锚定(bootstrap,参照 dsh-anchored-standard 的实测结论)─────
|
|
1360
|
-
// 阶段状态:sessionId -> { boundary, promoted, promoteEvents }。冷会话在
|
|
1361
|
-
// handleAgent 时全量扫描持久日志(resume 安全),此后 session/event 增量
|
|
1362
|
-
// 更新。子代理(delegationDepth > 0)恒为已晋升。所有 waterfall 用
|
|
1363
|
-
// prepend 注册,保证本插件是"最外层"变换(剥离是最后一道)。
|
|
1364
|
-
const bootstrapState = new Map();
|
|
1365
|
-
let bootstrapWarned = false;
|
|
1366
|
-
const warnBootstrapOnce = (message) => {
|
|
1367
|
-
if (bootstrapWarned) return;
|
|
1368
|
-
bootstrapWarned = true;
|
|
1369
|
-
warn(message);
|
|
1370
|
-
};
|
|
1371
|
-
|
|
1372
|
-
function bootstrapPhaseOf(agent) {
|
|
1373
|
-
const session = agent !== null && typeof agent === 'object' ? agent.session : void 0;
|
|
1374
|
-
if (session === void 0 || typeof session.id !== 'string') return { boundary: -1, promoted: true };
|
|
1375
|
-
// 子代理首轮即可用工具(与参考实现一致)。
|
|
1376
|
-
if ((session.header?.delegationDepth ?? 0) > 0) return { boundary: -1, promoted: true };
|
|
1377
|
-
let entry = bootstrapState.get(session.id);
|
|
1378
|
-
if (entry === void 0) {
|
|
1379
|
-
entry = {
|
|
1380
|
-
...scanPhase(session.events, PROMOTE_EVENTS[cfg.bootstrap.promoteOn]),
|
|
1381
|
-
promoteEvents: PROMOTE_EVENTS[cfg.bootstrap.promoteOn],
|
|
1382
|
-
};
|
|
1383
|
-
bootstrapState.set(session.id, entry);
|
|
1384
|
-
}
|
|
1385
|
-
return entry;
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
/** bootstrap 是否对该会话生效(总开关 + 目标 preset + 无会话时放行)。 */
|
|
1389
|
-
function bootstrapActiveFor(agent) {
|
|
1390
|
-
if (!cfg.enabled || !cfg.bootstrap.enabled) return false;
|
|
1391
|
-
if (agent === void 0) return true;
|
|
1392
|
-
return isTarget(agent);
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
/**
|
|
1396
|
-
* bootstrap 阶段的保留工具集:
|
|
1397
|
-
* - 未晋升:bootstrap 工具对 + PTC 的直接调用工具(run_code)+
|
|
1398
|
-
* compaction 后恢复期的工作集;
|
|
1399
|
-
* - 已晋升:bootstrap 工具对 + resident discovery 工具 +
|
|
1400
|
-
* dev_tool_search 已解锁工具 + 已升级工具族(dsh-anchored-standard 的
|
|
1401
|
-
* resident catalog,避免晋升后一次性 dump 完整 Standard 目录)。
|
|
1402
|
-
*
|
|
1403
|
-
* 注意:不能靠过滤 system-prompt/assemble 的 assembly.tools 来实现——
|
|
1404
|
-
* PTC 模式下 assembly.tools 只有 [run_code],Minimal 工具对不在其中,
|
|
1405
|
-
* 过滤会触发降级(实测首轮模型仍看到 15 个工具)。正确做法是
|
|
1406
|
-
* tools.restrict(与族限制同一机制):它同时驱动 API 目录和 PTC 的
|
|
1407
|
-
* SDK 参考段,且天然保留 run_code(不在 deny 里)。
|
|
1408
|
-
*/
|
|
1409
|
-
function bootstrapKeepSet(a) {
|
|
1410
|
-
if (!cfg.enabled || !cfg.bootstrap.enabled) return null;
|
|
1411
|
-
const session = a.agent.session;
|
|
1412
|
-
if (session === void 0) return null; // 无会话信息时保持既有行为(测试/未知场景)
|
|
1413
|
-
const phase = bootstrapPhaseOf(a.agent);
|
|
1414
|
-
const keep = new Set(cfg.bootstrap.tools);
|
|
1415
|
-
if (a.visibleNames.has('run_code')) keep.add('run_code');
|
|
1416
|
-
if (phase.promoted) {
|
|
1417
|
-
for (const name of cfg.bootstrap.discoveryTools) keep.add(name);
|
|
1418
|
-
for (const name of a.unlocked) keep.add(name);
|
|
1419
|
-
for (const [id, family] of Object.entries(cfg.families)) {
|
|
1420
|
-
if (a.escalated.has(id)) {
|
|
1421
|
-
for (const name of familyDeny(a, family)) keep.add(name);
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
return keep;
|
|
1425
|
-
}
|
|
1426
|
-
if (phase.boundary >= 0) for (const name of cfg.bootstrap.compactionTools) keep.add(name);
|
|
1427
|
-
return keep;
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
/** 按当前阶段同步 bootstrap 的临时 restrict(幂等;仅阶段/配置变化时重挂)。 */
|
|
1431
|
-
function syncBootstrap(a) {
|
|
1432
|
-
const phase = bootstrapPhaseOf(a.agent);
|
|
1433
|
-
const keep = bootstrapKeepSet(a);
|
|
1434
|
-
// discovery 工具只在晋升后的 resident 阶段注册;bootstrap/compaction 阶段隐藏。
|
|
1435
|
-
syncDiscoveryTools(a, keep !== null && phase.promoted);
|
|
1436
|
-
const key = keep === null ? null : [...keep].sort().join(',');
|
|
1437
|
-
if (key === a.bootstrapKey) return;
|
|
1438
|
-
if (a.bootstrapDisposer !== null) {
|
|
1439
|
-
try { a.bootstrapDisposer(); } catch {}
|
|
1440
|
-
a.bootstrapDisposer = null;
|
|
1441
|
-
a.bootstrapKey = null;
|
|
1442
|
-
}
|
|
1443
|
-
if (keep === null) return;
|
|
1444
|
-
const deny = [...a.visibleNames].filter((name) => !keep.has(name));
|
|
1445
|
-
if (deny.length === 0) return;
|
|
1446
|
-
try {
|
|
1447
|
-
a.bootstrapDisposer = a.agent.ctx.tools.restrict({ deny });
|
|
1448
|
-
a.bootstrapKey = key;
|
|
1449
|
-
info(`agent ${a.agent.id}: bootstrap restrict deny=${deny.length} keep=[${[...keep].join(',')}]`);
|
|
1450
|
-
} catch (error) {
|
|
1451
|
-
warn(`bootstrap restrict failed for agent ${a.agent.id}: ${String((error && error.message) || error)}`);
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
// 持久事件增量喂入:晋升信号 / compaction 纪元;阶段变化时同步 bootstrap 限制。
|
|
1456
|
-
ctx.on('session/event', (session, event) => {
|
|
1457
|
-
if (session === null || typeof session !== 'object' || typeof session.id !== 'string') return;
|
|
1458
|
-
const before = bootstrapState.get(session.id);
|
|
1459
|
-
observePhase(bootstrapState, session.id, event);
|
|
1460
|
-
const a = agents.get(session.id);
|
|
1461
|
-
if (a !== void 0) {
|
|
1462
|
-
const after = bootstrapState.get(session.id);
|
|
1463
|
-
if (before === void 0 || after === void 0 || before.promoted !== after.promoted || before.boundary !== after.boundary) {
|
|
1464
|
-
syncBootstrap(a);
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
});
|
|
1468
|
-
|
|
1469
|
-
// 上下文剥离 + instruction-hint。prepend + 根作用域注册保证是最后一道
|
|
1470
|
-
// 变换(后注册的注入者无法再补回)。两个职责同在一个监听器里,顺序天然
|
|
1471
|
-
// 正确:先剥离被抑制来源,再(晋升后一次性)追加指令文件提示。
|
|
1472
|
-
// 剥离与 bootstrap 开关正交:常驻抑制(suppressInjectedContext)在
|
|
1473
|
-
// bootstrap 关闭时也生效;instruction-hint 依赖晋升阶段(bootstrap 机制)。
|
|
1474
|
-
ctx.on('agent/pre-step', async ({ agent, signal }, next) => {
|
|
1475
|
-
const decision = await next();
|
|
1476
|
-
if (decision === null || typeof decision !== 'object' || decision.kind === 'reject') return decision;
|
|
1477
|
-
try {
|
|
1478
|
-
if (!cfg.enabled || !isTarget(agent)) return decision;
|
|
1479
|
-
const phase = bootstrapPhaseOf(agent);
|
|
1480
|
-
const suppressed = new Set(cfg.bootstrap.suppressedContextSources);
|
|
1481
|
-
let out = decision;
|
|
1482
|
-
// 剥离条件:bootstrap 未晋升(旧行为)或常驻抑制开启(0.5.0 默认:
|
|
1483
|
-
// 整个会话不再注入技能目录与 AGENTS.md 摘要,替代品是按需发现工具)。
|
|
1484
|
-
const stripActive = (cfg.bootstrap.enabled && !phase.promoted) || cfg.suppressInjectedContext;
|
|
1485
|
-
if (suppressed.size > 0 && stripActive && Array.isArray(out.messages)) {
|
|
1486
|
-
const kept = stripSuppressedMessages(out.messages, suppressed);
|
|
1487
|
-
out = kept === out.messages ? out : { ...out, messages: kept };
|
|
1488
|
-
}
|
|
1489
|
-
// instruction-hint:晋升后一次性注入"指令文件存在,需要时自读"。
|
|
1490
|
-
if (cfg.bootstrap.enabled && cfg.instructionHint && phase.promoted && Array.isArray(out.messages)) {
|
|
1491
|
-
const session = agent.session;
|
|
1492
|
-
if (session !== null && typeof session === 'object' && typeof session.id === 'string' && !hintedSessions.has(session.id)) {
|
|
1493
|
-
hintedSessions.add(session.id);
|
|
1494
|
-
const fs = ctx.get('fs');
|
|
1495
|
-
const dshHome = process.env.DSH_HOME?.trim() || `${process.env.HOME || process.env.USERPROFILE || ''}/.dsh`;
|
|
1496
|
-
const probe = await probeInstructionFiles(fs, session.header?.cwd, dshHome, signal);
|
|
1497
|
-
if (probe !== null) {
|
|
1498
|
-
const sections = [];
|
|
1499
|
-
if (probe.projectFiles.length > 0) {
|
|
1500
|
-
sections.push(`Workspace instruction files exist: ${probe.projectFiles.join(', ')} (project root: ${probe.root}).`);
|
|
1501
|
-
}
|
|
1502
|
-
if (probe.userGlobalFiles.length > 0) {
|
|
1503
|
-
sections.push(`A user-global instruction file exists: ${INSTRUCTION_USER_CANDIDATE}.`);
|
|
1504
|
-
}
|
|
1505
|
-
const text = [
|
|
1506
|
-
...sections,
|
|
1507
|
-
'Do NOT assume their content. When a task touches this workspace, read the relevant instruction files first and follow them.',
|
|
1508
|
-
].join(' ');
|
|
1509
|
-
out = {
|
|
1510
|
-
...out,
|
|
1511
|
-
messages: [...out.messages, {
|
|
1512
|
-
id: `instruction-hint-${session.id}`,
|
|
1513
|
-
role: 'user',
|
|
1514
|
-
content: [{ type: 'text', text }],
|
|
1515
|
-
source: { kind: 'instruction-hint', form: 'hint' },
|
|
1516
|
-
}],
|
|
1517
|
-
};
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
return out;
|
|
1522
|
-
} catch (error) {
|
|
1523
|
-
// 过滤失败绝不吞掉用户的上下文:原样放行。
|
|
1524
|
-
warnBootstrapOnce(`bootstrap context filter failed, keeping injected context: ${String((error && error.message) || error)}`);
|
|
1525
|
-
return decision;
|
|
1526
|
-
}
|
|
1527
|
-
}, { prepend: true });
|
|
1528
|
-
|
|
1529
|
-
// 可选:请求 #1 输出预算封顶(晋升后剥离,避免封顶沿 header 种子延续)。
|
|
1530
|
-
if (cfg.bootstrap.maxTokens > 0) {
|
|
1531
|
-
ctx.on('agent/request', async ({ agent }, next) => {
|
|
1532
|
-
const resolved = await next();
|
|
1533
|
-
try {
|
|
1534
|
-
if (!bootstrapActiveFor(agent)) return resolved;
|
|
1535
|
-
const phase = bootstrapPhaseOf(agent);
|
|
1536
|
-
return applyBootstrapBudget(resolved, phase.promoted, cfg.bootstrap.maxTokens);
|
|
1537
|
-
} catch (error) {
|
|
1538
|
-
warnBootstrapOnce(`bootstrap budget filter failed: ${String((error && error.message) || error)}`);
|
|
1539
|
-
return resolved;
|
|
1540
|
-
}
|
|
1541
|
-
}, { prepend: true });
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
// 已运行会话补挂(插件热重载/重装时)。
|
|
1545
|
-
if (agentsService !== void 0 && typeof agentsService.list === 'function') {
|
|
1546
|
-
try {
|
|
1547
|
-
for (const agent of agentsService.list()) handleAgent(agent);
|
|
1548
|
-
} catch (error) {
|
|
1549
|
-
warn(`existing agent sweep failed: ${error && error.message || error}`);
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
// 配置存储:patch config 为低优先级,config.json(设置页 UI)权威。
|
|
1554
|
-
// onUpdate 把 UI 保存的改动热更新进 cfg,并对所有已接管会话重算。
|
|
1555
|
-
const store = createConfigStore({
|
|
1556
|
-
name: 'adaptive-perf',
|
|
1557
|
-
defaults: DEFAULT_CONFIG,
|
|
1558
|
-
patchConfig,
|
|
1559
|
-
validate: validateConfig,
|
|
1560
|
-
warn,
|
|
1561
|
-
onUpdate: (merged) => {
|
|
1562
|
-
cfg = normalizeConfig({ ...patchConfig, ...merged });
|
|
1563
|
-
info('config hot-updated; recomputing restrictions for live agents');
|
|
1564
|
-
// realPair 从关闭切到开启:补加载官方模块(首次尝试失败不反复重试)。
|
|
1565
|
-
ensureRealPairModules();
|
|
1566
|
-
// 补挂此前创建的会话(例如插件从 disabled 切换为 enabled,或新增了
|
|
1567
|
-
// 目标 preset):新符合条件的 agent 立即接管(handleAgent 已应用全部
|
|
1568
|
-
// 层,下面的重算只针对此前已接管的 agent,避免同一 onUpdate 内重复)。
|
|
1569
|
-
const existing = new Set(agents.keys());
|
|
1570
|
-
if (agentsService !== void 0 && typeof agentsService.list === 'function') {
|
|
1571
|
-
try {
|
|
1572
|
-
for (const agent of agentsService.list()) handleAgent(agent);
|
|
1573
|
-
} catch (error) {
|
|
1574
|
-
warn(`config-update agent sweep failed: ${error && error.message || error}`);
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
for (const agentId of existing) {
|
|
1578
|
-
const a = agents.get(agentId);
|
|
1579
|
-
if (a === void 0) continue;
|
|
1580
|
-
applySuppression(a);
|
|
1581
|
-
syncRealPair(a);
|
|
1582
|
-
applyMinimalPrompt(a);
|
|
1583
|
-
applyFamilies(a);
|
|
1584
|
-
syncBootstrap(a);
|
|
1585
|
-
}
|
|
1586
|
-
},
|
|
1587
|
-
});
|
|
1588
|
-
cfg = normalizeConfig(store.effective());
|
|
1589
|
-
|
|
1590
|
-
// 远程配置服务(设置页 UI 读写;typert 不可用时跳过)
|
|
1591
|
-
if (PluginConfigGateway !== null) {
|
|
1592
|
-
ctx.plugin(PluginConfigGateway, { store, serviceKey: 'adaptivePerfConfig' });
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
// 注册进宿主 settings 体系(可见性):设置页用 settings.describe() 枚举
|
|
1596
|
-
// registrations,未注册的 namespace 即使卡片带正确的 key 也不渲染。
|
|
1597
|
-
// 卡片的实际读写仍走上面的 config gateway(config.json 权威、热更新)。
|
|
1598
|
-
// 配置为多层嵌套结构(families/bootstrap/minimalPrompt…),schema 用 any
|
|
1599
|
-
// 透传——describe 元数据只服务于可见性,卡片不读取它。any() 无默认值,
|
|
1600
|
-
// 必须传 base(当前生效配置快照),否则 describe() 的 value 为 undefined,
|
|
1601
|
-
// 设置页 wire 校验(nonoptional)会整体失败。
|
|
1602
|
-
registerSettingsNamespace(ctx, 'adaptivePerfConfig', Schema, (z) => z.any(), { base: cfg });
|
|
1603
|
-
|
|
1604
|
-
// 卸载清理:释放全部会话副作用与监听。
|
|
1605
|
-
ctx.effect(() => () => {
|
|
1606
|
-
for (const agentId of [...agents.keys()]) releaseAgent(agentId);
|
|
1607
|
-
});
|
|
1608
|
-
} catch (error) {
|
|
1609
|
-
try { ctx.logger?.error?.(`adaptive-perf: init failed (harness continues without adaptive performance): ${error && error.message || error}`); } catch {}
|
|
1610
|
-
}
|
|
1611
|
-
}
|