@chaoset/adaptive-perf 0.2.2 → 0.3.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/README.md CHANGED
@@ -3,6 +3,20 @@
3
3
  让 **标准模式(standard)** 与 **PTC 模式(code)** 达到 **极简模式(minimal)**
4
4
  级别的高性能,同时保留完整能力——不是静态裁剪,而是**动态自适应**:
5
5
 
6
+ - **首轮锚定(bootstrap)**:决定模型首轮"轨迹"(思维链风格)的是请求 #1 可见的
7
+ **工具 schema** 与**自动注入的上下文提醒**(参照
8
+ [dsh-anchored-standard](https://github.com/xiaobright/dsh-anchored-standard/)
9
+ 的实测:Minimal 真实工具对在 256000 输出预算下 5/5 锚定、任何 standard 系
10
+ schema 11/11 落入 standard-like;技能目录在场时锚定完全无法复现 0/9)。因此:
11
+ - 请求 #1 的工具目录收窄到真实 Minimal 工具对(`bash` + `str_replace_editor`);
12
+ - 请求 #1 剥离自动注入上下文(技能目录提醒 `skill-catalog`、AGENTS.md 摘要
13
+ `agent-instructions`,用户主动的技能手势不过滤);
14
+ - 会话出现首个**持久**晋升信号(首个 `tool/call` 或 `assistant/message`,
15
+ `promoteOn` 可选 `either`/`tool-call`/`assistant-message`)后恢复完整目录;
16
+ 阶段从持久会话事件推导,**resume / reload 不丢状态**;
17
+ - compaction 会把会话打回"第二次首轮":`compaction/end` 之后回到受控目录
18
+ (bootstrap 工具对 + `compactionTools` 核心工作集),直到出现新的晋升信号;
19
+ - `maxTokens > 0` 时可给请求 #1 封顶输出预算(晋升后自动剥离)。
6
20
  - **运行时上下文抑制**:对目标 preset 的会话调用 `suppressRuntimeContext()`
7
21
  (与极简模式的 `includeRuntimeContext: false` 同一机制),每次模型请求省掉
8
22
  "Current runtime context" 快照文本,**零功能损失**。
@@ -16,12 +30,14 @@
16
30
 
17
31
  所有开关与工具族可在设置页配置、随时放行;配置保存后**热生效**(无需重启)。
18
32
 
19
- ## 为什么极简模式最快(源码对照结论)
33
+ ## 为什么极简模式最快(源码对照 + 社区实测结论)
20
34
 
21
35
  | 维度 | 极简模式 | 标准 / PTC 模式 | 本插件做法 |
22
36
  |---|---|---|---|
37
+ | 首轮轨迹(决定后续行为风格) | Minimal 工具对锚定(5/5) | 任何 standard 系 schema 落入 standard-like(11/11) | 请求 #1 收窄到真实 Minimal 工具对,首个持久信号后恢复 |
38
+ | 首轮注入提醒 | 无 skill 目录/AGENTS.md 注入 | 自动注入(在场时锚定 0/9) | 请求 #1 剥离 `skill-catalog` + `agent-instructions` |
23
39
  | 运行时上下文快照 | 关闭(`includeRuntimeContext: false`) | 每次请求注入文件策略/审批策略等快照 | 同一机制:`suppressRuntimeContext()` |
24
- | 模型可见工具目录 | 2 个(bash、str_replace_editor) | ~22 个工具 schema 全部进请求 | 默认隐藏 4 个编排类工具族(~11 个工具),按需放行 |
40
+ | 模型可见工具目录 | 2 个(bash、str_replace_editor) | ~22 个工具 schema 全部进请求 | bootstrap 后默认隐藏 4 个编排类工具族(~11 个工具),按需放行 |
25
41
  | PTC SDK 参考段 | 无 | 全部工具的类型+描述渲染进提示 | 随目录缩小(同一 `visible` 视图驱动) |
26
42
 
27
43
  ## 安装(npm 生态方式)
package/client/client.cjs CHANGED
@@ -43,7 +43,19 @@ window.__ModuleLoader__.load({
43
43
  families: "工具族(高级)",
44
44
  fieldFamilies: "JSON:{ 族名: { enabled, tools: [工具名], keywords: [触发词] } }。保存后热生效。",
45
45
  coreTools: "核心工具(永不隐藏)",
46
- fieldCoreTools: "展示用:这些工具不进入任何限制族。未列入任何族的工具也默认保留。"
46
+ fieldCoreTools: "展示用:这些工具不进入任何限制族。未列入任何族的工具也默认保留。",
47
+ bootstrapEnabled: "首轮锚定(bootstrap)",
48
+ fieldBootstrapEnabled: "请求 #1 只暴露 Minimal 真实工具对并剥离自动注入上下文,首个持久信号(promoteOn)后恢复——让首轮轨迹锚定极简(参照 dsh-anchored-standard 实测:工具 schema + 注入提醒决定轨迹)",
49
+ bootstrapTools: "首轮工具",
50
+ fieldBootstrapTools: "请求 #1 可见的工具(真实 Minimal 对:bash, str_replace_editor)。逗号分隔。",
51
+ bootstrapPromoteOn: "晋升触发",
52
+ fieldBootstrapPromoteOn: "either(默认):首个 tool/call 或 assistant/message 先到者晋升;tool-call / assistant-message 可单独指定",
53
+ bootstrapSuppressed: "剥离的注入上下文",
54
+ fieldBootstrapSuppressed: "请求 #1 剥离的自动注入 source.kind(技能目录提醒 skill-catalog、AGENTS.md 摘要 agent-instructions);逗号分隔,留空=不剥离",
55
+ bootstrapCompactionTools: "压缩后工作集",
56
+ fieldBootstrapCompactionTools: "compaction/end 之后、再次晋升前额外可用的工具(核心工作集)。逗号分隔。",
57
+ bootstrapMaxTokens: "首轮输出预算封顶(token)",
58
+ fieldBootstrapMaxTokens: "请求 #1 的 maxTokens 封顶,晋升后自动剥离;0 = 不封顶(opt-in)"
47
59
  };
48
60
  const en = {
49
61
  title: "Adaptive performance (adaptive-perf)",
@@ -70,7 +82,19 @@ window.__ModuleLoader__.load({
70
82
  families: "Tool families (advanced)",
71
83
  fieldFamilies: "JSON: { familyId: { enabled, tools: [names], keywords: [triggers] } }. Hot-applied on save.",
72
84
  coreTools: "Core tools (never hidden)",
73
- fieldCoreTools: "Informational: these never enter any restriction family. Tools not listed in any family stay visible too."
85
+ fieldCoreTools: "Informational: these never enter any restriction family. Tools not listed in any family stay visible too.",
86
+ bootstrapEnabled: "First-request bootstrap",
87
+ fieldBootstrapEnabled: "Request #1 exposes only the real Minimal tool pair and drops auto-injected context; the first durable signal (promoteOn) restores the catalog — anchors the first-request trajectory to minimal (per dsh-anchored-standard measurements)",
88
+ bootstrapTools: "Bootstrap tools",
89
+ fieldBootstrapTools: "Tools visible on request #1 (the real Minimal pair: bash, str_replace_editor). Comma-separated.",
90
+ bootstrapPromoteOn: "Promotion trigger",
91
+ fieldBootstrapPromoteOn: "either (default): first tool/call or assistant/message promotes; tool-call / assistant-message select one",
92
+ bootstrapSuppressed: "Suppressed injected context",
93
+ fieldBootstrapSuppressed: "Auto-injected source.kinds stripped on request #1 (skill-catalog reminder, agent-instructions digest); comma-separated, empty = strip nothing",
94
+ bootstrapCompactionTools: "Post-compaction work set",
95
+ fieldBootstrapCompactionTools: "Extra tools available after compaction/end until a new promotion signal (core work set). Comma-separated.",
96
+ bootstrapMaxTokens: "First-request output cap (tokens)",
97
+ fieldBootstrapMaxTokens: "maxTokens cap for request #1, stripped after promotion; 0 = no cap (opt-in)"
74
98
  };
75
99
 
76
100
  // ── 表单字段 ─────────────────────────────────────────────────────────
@@ -248,6 +272,51 @@ window.__ModuleLoader__.load({
248
272
  disabled: draft === null,
249
273
  onChange: (v) => setDraft({ ...draft, escalateOnUnknownTool: v })
250
274
  }),
275
+ react.createElement(BoolField, {
276
+ label: t("bootstrapEnabled"),
277
+ hint: t("fieldBootstrapEnabled"),
278
+ value: draft === null ? false : !!draft.bootstrap && draft.bootstrap.enabled,
279
+ disabled: draft === null,
280
+ onChange: (v) => setDraft({ ...draft, bootstrap: { ...(draft.bootstrap || {}), enabled: v } })
281
+ }),
282
+ react.createElement(TextField, {
283
+ label: t("bootstrapTools"),
284
+ hint: t("fieldBootstrapTools"),
285
+ value: draft === null ? "" : ((draft.bootstrap && draft.bootstrap.tools) || []).join(", "),
286
+ disabled: draft === null || (draft.bootstrap && draft.bootstrap.enabled === false),
287
+ onChange: (v) => setDraft({ ...draft, bootstrap: { ...(draft.bootstrap || {}), tools: v.split(/[,,\s]+/).filter((x) => x.length > 0) } })
288
+ }),
289
+ react.createElement(TextField, {
290
+ label: t("bootstrapPromoteOn"),
291
+ hint: t("fieldBootstrapPromoteOn"),
292
+ value: draft === null ? "" : (draft.bootstrap && draft.bootstrap.promoteOn) || "either",
293
+ disabled: draft === null || (draft.bootstrap && draft.bootstrap.enabled === false),
294
+ onChange: (v) => setDraft({ ...draft, bootstrap: { ...(draft.bootstrap || {}), promoteOn: (v.trim() || "either") } })
295
+ }),
296
+ react.createElement(TextField, {
297
+ label: t("bootstrapSuppressed"),
298
+ hint: t("fieldBootstrapSuppressed"),
299
+ value: draft === null ? "" : ((draft.bootstrap && draft.bootstrap.suppressedContextSources) || []).join(", "),
300
+ disabled: draft === null || (draft.bootstrap && draft.bootstrap.enabled === false),
301
+ onChange: (v) => setDraft({ ...draft, bootstrap: { ...(draft.bootstrap || {}), suppressedContextSources: v.split(/[,,\s]+/).filter((x) => x.length > 0) } })
302
+ }),
303
+ react.createElement(TextField, {
304
+ label: t("bootstrapCompactionTools"),
305
+ hint: t("fieldBootstrapCompactionTools"),
306
+ value: draft === null ? "" : ((draft.bootstrap && draft.bootstrap.compactionTools) || []).join(", "),
307
+ disabled: draft === null || (draft.bootstrap && draft.bootstrap.enabled === false),
308
+ onChange: (v) => setDraft({ ...draft, bootstrap: { ...(draft.bootstrap || {}), compactionTools: v.split(/[,,\s]+/).filter((x) => x.length > 0) } })
309
+ }),
310
+ react.createElement(TextField, {
311
+ label: t("bootstrapMaxTokens"),
312
+ hint: t("fieldBootstrapMaxTokens"),
313
+ value: draft === null ? "" : String((draft.bootstrap && draft.bootstrap.maxTokens) || 0),
314
+ disabled: draft === null || (draft.bootstrap && draft.bootstrap.enabled === false),
315
+ onChange: (v) => {
316
+ const n = Number(v.trim());
317
+ setDraft({ ...draft, bootstrap: { ...(draft.bootstrap || {}), maxTokens: Number.isSafeInteger(n) && n > 0 ? n : 0 } });
318
+ }
319
+ }),
251
320
  react.createElement(Field, {
252
321
  label: t("families"),
253
322
  hint: familiesInvalid ? t("invalidFamilies") + (familiesError !== null ? " — " + familiesError : "") : t("fieldFamilies")
package/cordis.patch.yml CHANGED
@@ -15,3 +15,12 @@
15
15
  leanByDefault: true
16
16
  escalateOnKeyword: true
17
17
  escalateOnUnknownTool: true
18
+ # 首轮锚定(参照 dsh-anchored-standard 实测):请求 #1 只暴露 Minimal
19
+ # 真实工具对并剥离自动注入上下文,首个持久信号后恢复完整目录。
20
+ bootstrap:
21
+ enabled: true
22
+ tools: [bash, str_replace_editor]
23
+ promoteOn: either
24
+ suppressedContextSources: [skill-catalog, agent-instructions]
25
+ compactionTools: [read, write, edit, glob, grep, todo_write, ask_user_question]
26
+ maxTokens: 0
package/lib/index.mjs CHANGED
@@ -9,11 +9,27 @@
9
9
  * tool schema 与(PTC 的)SDK 参考段都随之变小;
10
10
  * 3. persona 为 complete 固定提示,无其它全局 prompt 段。
11
11
  *
12
- * 本插件在两个维度上做"动态自适应"(而不是静态裁剪):
12
+ * 本插件在三个维度上做"动态自适应"(而不是静态裁剪):
13
13
  * A. 运行时上下文抑制:对目标 preset 的会话调用
14
14
  * agent.ctx.systemPrompt.suppressRuntimeContext()(与极简模式同一机制),
15
15
  * 每次请求省掉快照文本,零功能损失。
16
- * B. 工具目录自适应精简:会话启动时按"工具族"默认隐藏高开销低频工具
16
+ * B. 首轮锚定(bootstrap,参照 dsh-anchored-standard 的实测结论):
17
+ * 决定首轮"轨迹"(模型思维链风格)的是请求 #1 可见的**工具 schema**
18
+ * 与**自动注入的上下文提醒**,而不是静态目录大小。因此:
19
+ * - 请求 #1 的工具目录收窄到真实 Minimal 工具对
20
+ * (bash + str_replace_editor)——256000 输出预算下 5/5 锚定,
21
+ * 而任何 standard 系 schema 11/11 落入 standard-like 轨迹;
22
+ * - 请求 #1 剥离自动注入的上下文(技能目录提醒 skill-catalog、
23
+ * AGENTS.md 摘要 agent-instructions)——目录在场时锚定完全无法
24
+ * 复现(0/9);用户主动的技能手势不过滤;
25
+ * - 会话出现首次**持久**晋升信号(首个 tool/call 或 assistant/message,
26
+ * promoteOn 可选 either/tool-call/assistant-message)后恢复完整目录;
27
+ * 阶段从持久会话事件推导,resume/reload 不丢状态;
28
+ * - compaction 会把会话打回"第二次首轮":compaction/end 之后回到
29
+ * 受控目录(bootstrap 工具对 + compactionTools 核心工作集),直到
30
+ * 出现新的持久晋升信号(纪元感知);
31
+ * - bootstrap.maxTokens(可选)给请求 #1 封顶输出预算,晋升后剥离。
32
+ * C. 工具目录自适应精简:会话启动时按"工具族"默认隐藏高开销低频工具
17
33
  * (子代理 / 工作流 / ralph / goal 等编排类),只保留核心编码工具,
18
34
  * 让标准模式的目录逼近极简、PTC 的 SDK 段同步缩小;随后根据两类信号
19
35
  * 在会话内"单调升级"放行对应工具族(只升不降,限制请求缓存失效次数):
@@ -26,9 +42,14 @@
26
42
  * - agent.ctx.systemPrompt.suppressRuntimeContext() 抑制上下文快照
27
43
  * - agent.ctx.tools.schemas(agent) 读取该 agent 当前可见工具(限制交集)
28
44
  * - agent.ctx.tools.restrict({ deny }) 按 agent 作用域裁剪继承工具
45
+ * - system-prompt/assemble(waterfall) 按会话阶段收窄装配后的工具目录
46
+ * - agent/pre-step(waterfall) 剥离自动注入的上下文消息(source.kind)
47
+ * - agent/request(waterfall) 首轮输出预算封顶
48
+ * - session/event 增量喂入持久事件(晋升 / compaction 纪元)
29
49
  * - agent/created、agent/disposed、agent/inbox/inserted、tools/result 事件
30
50
  * 监听器都注册在 host 根作用域(与 dsh-agent-presets 自身同款用法),事件按
31
- * scope 过滤分发,子作用域派发的事件根监听器可收到。
51
+ * scope 过滤分发,子作用域派发的事件根监听器可收到;三个 waterfall 均以
52
+ * prepend 注册保证"最外层"(剥离是最后一道变换)。
32
53
  *
33
54
  * 配置:cordis.patch.yml 的 config(安装默认)与
34
55
  * ~/.dsh/plugins/adaptive-perf/config.json(设置页 UI,权威)合并,
@@ -56,6 +77,12 @@ export const name = 'adaptive-perf';
56
77
  * families:工具族。每族可配 tools(该族工具名,需存在于目标 preset 的目录,
57
78
  * 插件运行时按实际可见工具取交集,不会因改名/缺失而崩溃)与 keywords(用户消息
58
79
  * 命中任一触发词即放行该族;大小写不敏感,子串匹配)。
80
+ *
81
+ * bootstrap:首轮锚定(参照 dsh-anchored-standard 的实测结论)。请求 #1 只暴露
82
+ * bootstrap.tools(真实 Minimal 工具对),剥离 suppressedContextSources 列出的
83
+ * 自动注入上下文;首个持久晋升信号(promoteOn)后恢复;compaction/end 之后回到
84
+ * 受控目录(bootstrap 工具对 + compactionTools)直到新的晋升信号;maxTokens>0
85
+ * 时给请求 #1 封顶输出预算(晋升后剥离)。
59
86
  */
60
87
  export const DEFAULT_CONFIG = {
61
88
  /** 总开关:关闭后插件对会话不产生任何影响。 */
@@ -99,6 +126,21 @@ export const DEFAULT_CONFIG = {
99
126
  keywords: ['长期目标', '跨轮次', '目标追踪', 'goal'],
100
127
  },
101
128
  },
129
+ /** 首轮锚定(参照 dsh-anchored-standard)。 */
130
+ bootstrap: {
131
+ /** 是否启用首轮锚定。 */
132
+ enabled: true,
133
+ /** 请求 #1 可见的工具(真实 Minimal 工具对)。 */
134
+ tools: ['bash', 'str_replace_editor'],
135
+ /** 晋升触发:either(默认)/ tool-call / assistant-message。 */
136
+ promoteOn: 'either',
137
+ /** 请求 #1 剥离的自动注入上下文 source.kind;[] 关闭剥离。 */
138
+ suppressedContextSources: ['skill-catalog', 'agent-instructions'],
139
+ /** compaction/end 之后、再次晋升前可用的额外核心工作集。 */
140
+ compactionTools: ['read', 'write', 'edit', 'glob', 'grep', 'todo_write', 'ask_user_question'],
141
+ /** 请求 #1 的输出预算封顶(token);0 = 不封顶(opt-in)。 */
142
+ maxTokens: 0,
143
+ },
102
144
  };
103
145
 
104
146
  /** 轻量配置校验/归一化:非法字段回退默认值,避免 config.json 或 remote.set
@@ -119,6 +161,12 @@ function stringList(value, fallback) {
119
161
  return fallback;
120
162
  }
121
163
 
164
+ /** 与 stringList 相同,但显式空数组保持为空(语义是有意义的配置值)。 */
165
+ function stringListOrEmpty(value, fallback) {
166
+ if (value === undefined) return fallback;
167
+ return stringList(value, []);
168
+ }
169
+
122
170
  export function normalizeConfig(source, defaults = DEFAULT_CONFIG) {
123
171
  const raw = source !== null && typeof source === 'object' && !Array.isArray(source) ? source : {};
124
172
  const merged = { ...defaults, ...raw };
@@ -136,6 +184,23 @@ export function normalizeConfig(source, defaults = DEFAULT_CONFIG) {
136
184
  keywords: stringList(candidate.keywords, fam.keywords),
137
185
  };
138
186
  }
187
+ const rawBootstrap = raw.bootstrap !== null && typeof raw.bootstrap === 'object' && !Array.isArray(raw.bootstrap)
188
+ ? raw.bootstrap
189
+ : defaults.bootstrap;
190
+ const db = defaults.bootstrap;
191
+ const promoteOn = rawBootstrap.promoteOn === 'tool-call' || rawBootstrap.promoteOn === 'assistant-message'
192
+ ? rawBootstrap.promoteOn
193
+ : rawBootstrap.promoteOn === 'either' ? 'either' : db.promoteOn;
194
+ const maxTokensRaw = rawBootstrap.maxTokens;
195
+ const maxTokens = Number.isSafeInteger(maxTokensRaw) && maxTokensRaw > 0 ? maxTokensRaw : 0;
196
+ const bootstrap = {
197
+ enabled: boolValue(rawBootstrap.enabled, db.enabled),
198
+ tools: stringList(rawBootstrap.tools, db.tools),
199
+ promoteOn,
200
+ suppressedContextSources: stringListOrEmpty(rawBootstrap.suppressedContextSources, db.suppressedContextSources),
201
+ compactionTools: stringListOrEmpty(rawBootstrap.compactionTools, db.compactionTools),
202
+ maxTokens,
203
+ };
139
204
  return {
140
205
  enabled: boolValue(merged.enabled, defaults.enabled),
141
206
  presets: stringList(merged.presets, defaults.presets),
@@ -145,6 +210,7 @@ export function normalizeConfig(source, defaults = DEFAULT_CONFIG) {
145
210
  escalateOnUnknownTool: boolValue(merged.escalateOnUnknownTool, defaults.escalateOnUnknownTool),
146
211
  coreTools: stringList(merged.coreTools, defaults.coreTools),
147
212
  families,
213
+ bootstrap,
148
214
  };
149
215
  }
150
216
 
@@ -182,6 +248,26 @@ export function validateConfig(partial) {
182
248
  }
183
249
  }
184
250
  }
251
+ if (partial.bootstrap !== void 0) {
252
+ const b = partial.bootstrap;
253
+ if (b === null || typeof b !== 'object' || Array.isArray(b)) {
254
+ throw new TypeError('adaptive-perf config field "bootstrap" must be an object');
255
+ }
256
+ if (b.enabled !== void 0 && typeof b.enabled !== 'boolean') {
257
+ throw new TypeError('adaptive-perf config field "bootstrap.enabled" must be a boolean');
258
+ }
259
+ if (b.promoteOn !== void 0 && !['either', 'tool-call', 'assistant-message'].includes(b.promoteOn)) {
260
+ throw new TypeError('adaptive-perf config field "bootstrap.promoteOn" must be "either" | "tool-call" | "assistant-message"');
261
+ }
262
+ if (b.maxTokens !== void 0 && !(Number.isSafeInteger(b.maxTokens) && b.maxTokens >= 0)) {
263
+ throw new TypeError('adaptive-perf config field "bootstrap.maxTokens" must be a non-negative safe integer');
264
+ }
265
+ for (const key of ['tools', 'suppressedContextSources', 'compactionTools']) {
266
+ if (b[key] !== void 0 && !(Array.isArray(b[key]) && b[key].every((v) => typeof v === 'string'))) {
267
+ throw new TypeError(`adaptive-perf config field "bootstrap.${key}" must be an array of strings`);
268
+ }
269
+ }
270
+ }
185
271
  }
186
272
 
187
273
  // ── 纯函数(导出便于回归测试)────────────────────────────────────────────
@@ -225,6 +311,89 @@ export function collectFailureText(result) {
225
311
  return parts.join('\n');
226
312
  }
227
313
 
314
+ // ── bootstrap 锚定(参照 dsh-anchored-standard 的实测结论)──────────────
315
+
316
+ /** promoteOn 配置 → 晋升的持久事件类型集合。 */
317
+ export const PROMOTE_EVENTS = {
318
+ 'tool-call': ['tool/call'],
319
+ 'assistant-message': ['assistant/message'],
320
+ either: ['tool/call', 'assistant/message'],
321
+ };
322
+
323
+ /**
324
+ * 扫描一段持久会话日志,推导 { boundary, promoted } 阶段。
325
+ * compaction/end 之后需要新的晋升信号(纪元感知);boundary 为 -1 表示
326
+ * 尚无压缩。事件无 seq 时按"边界之后"处理(与参考实现一致)。
327
+ */
328
+ export function scanPhase(events, promoteEvents) {
329
+ let boundary = -1;
330
+ let promoted = false;
331
+ if (Array.isArray(events)) {
332
+ for (const event of events) {
333
+ const seq = typeof event?.seq === 'number' ? event.seq : 0;
334
+ if (event?.type === 'compaction/end') {
335
+ boundary = seq;
336
+ promoted = false;
337
+ continue;
338
+ }
339
+ if (promoteEvents.includes(event?.type) && seq > boundary) promoted = true;
340
+ }
341
+ }
342
+ return { boundary, promoted };
343
+ }
344
+
345
+ /** 增量喂入一个持久事件(仅更新已存在条目;冷会话由 scanPhase 全量推导)。 */
346
+ export function observePhase(state, sessionId, event) {
347
+ const entry = state.get(sessionId);
348
+ if (entry === void 0) return;
349
+ const seq = typeof event?.seq === 'number' ? event.seq : 0;
350
+ if (event?.type === 'compaction/end') {
351
+ state.set(sessionId, { boundary: seq, promoted: false, promoteEvents: entry.promoteEvents });
352
+ return;
353
+ }
354
+ if (entry.promoted) return;
355
+ const promoteEvents = entry.promoteEvents;
356
+ if (promoteEvents.includes(event?.type) && seq > entry.boundary) {
357
+ state.set(sessionId, { ...entry, promoted: true });
358
+ }
359
+ }
360
+
361
+ /**
362
+ * 按 keep 集合收窄装配后的工具目录。
363
+ * @returns { tools, missing } —— missing 非空表示 keep 中的工具在目录里缺失,
364
+ * 调用方决定是否降级(全目录放行)。
365
+ */
366
+ export function filterBootstrapTools(assembly, keep) {
367
+ const keepSet = keep instanceof Set ? keep : new Set(keep);
368
+ const tools = Array.isArray(assembly?.tools) ? assembly.tools : [];
369
+ const available = new Set(tools.map((tool) => tool?.name).filter((n) => typeof n === 'string'));
370
+ const missing = [...keepSet].filter((n) => !available.has(n));
371
+ return {
372
+ tools: tools.filter((tool) => keepSet.has(tool.name)),
373
+ missing,
374
+ };
375
+ }
376
+
377
+ /** 按 source.kind 剥离自动注入的上下文消息(保留主动的用户消息)。 */
378
+ export function filterBootstrapMessages(messages, suppressedSources) {
379
+ if (!Array.isArray(messages) || suppressedSources.size === 0) return messages;
380
+ const kept = messages.filter((message) => {
381
+ const kind = message?.source?.kind;
382
+ return typeof kind !== 'string' || !suppressedSources.has(kind);
383
+ });
384
+ return kept.length === messages.length ? messages : kept;
385
+ }
386
+
387
+ /** 首轮输出预算:未晋升 → 封顶;已晋升 → 若仍带着封顶值则剥离。 */
388
+ export function applyBootstrapBudget(config, promoted, maxTokens) {
389
+ if (!promoted) return { ...config, maxTokens };
390
+ if (config?.maxTokens === maxTokens) {
391
+ const { maxTokens: _drop, ...rest } = config;
392
+ return rest;
393
+ }
394
+ return config;
395
+ }
396
+
228
397
  // ── host 插件 ────────────────────────────────────────────────────────────
229
398
 
230
399
  export function apply(ctx, config) {
@@ -374,8 +543,9 @@ export function apply(ctx, config) {
374
543
  /** 释放一个 agent 的全部副作用。 */
375
544
  function releaseAgent(agentId) {
376
545
  const a = agents.get(agentId);
377
- if (a === void 0) return;
378
546
  agents.delete(agentId);
547
+ bootstrapState.delete(agentId);
548
+ if (a === void 0) return;
379
549
  if (a.suppressDisposer !== null) {
380
550
  try { a.suppressDisposer(); } catch {}
381
551
  }
@@ -429,6 +599,108 @@ export function apply(ctx, config) {
429
599
  }
430
600
  });
431
601
 
602
+ // ── 首轮锚定(bootstrap,参照 dsh-anchored-standard 的实测结论)─────
603
+ // 阶段状态:sessionId -> { boundary, promoted, promoteEvents }。冷会话在
604
+ // handleAgent 时全量扫描持久日志(resume 安全),此后 session/event 增量
605
+ // 更新。子代理(delegationDepth > 0)恒为已晋升。所有 waterfall 用
606
+ // prepend 注册,保证本插件是"最外层"变换(剥离是最后一道)。
607
+ const bootstrapState = new Map();
608
+ let bootstrapWarned = false;
609
+ const warnBootstrapOnce = (message) => {
610
+ if (bootstrapWarned) return;
611
+ bootstrapWarned = true;
612
+ warn(message);
613
+ };
614
+
615
+ function bootstrapPhaseOf(agent) {
616
+ const session = agent !== null && typeof agent === 'object' ? agent.session : void 0;
617
+ if (session === void 0 || typeof session.id !== 'string') return { boundary: -1, promoted: true };
618
+ // 子代理首轮即可用工具(与参考实现一致)。
619
+ if ((session.header?.delegationDepth ?? 0) > 0) return { boundary: -1, promoted: true };
620
+ let entry = bootstrapState.get(session.id);
621
+ if (entry === void 0) {
622
+ entry = {
623
+ ...scanPhase(session.events, PROMOTE_EVENTS[cfg.bootstrap.promoteOn]),
624
+ promoteEvents: PROMOTE_EVENTS[cfg.bootstrap.promoteOn],
625
+ };
626
+ bootstrapState.set(session.id, entry);
627
+ }
628
+ return entry;
629
+ }
630
+
631
+ /** bootstrap 是否对该会话生效(总开关 + 目标 preset + 无会话时放行)。 */
632
+ function bootstrapActiveFor(agent) {
633
+ if (!cfg.enabled || !cfg.bootstrap.enabled) return false;
634
+ if (agent === void 0) return true;
635
+ return isTarget(agent);
636
+ }
637
+
638
+ // 持久事件增量喂入:晋升信号 / compaction 纪元。
639
+ ctx.on('session/event', (session, event) => {
640
+ if (session === null || typeof session !== 'object' || typeof session.id !== 'string') return;
641
+ observePhase(bootstrapState, session.id, event);
642
+ });
643
+
644
+ // 请求装配时按阶段收窄工具目录。请求 #1(未晋升):只保留 bootstrap
645
+ // 工具对(compaction/end 之后额外放行 compactionTools 核心工作集);
646
+ // 已晋升:放行(leanByDefault 的族限制仍由 tools.restrict 生效)。
647
+ ctx.on('system-prompt/assemble', async (assembly, _context, next) => {
648
+ const resolved = await next();
649
+ try {
650
+ const agent = agentsService?.currentInitiator?.() ?? void 0;
651
+ if (!bootstrapActiveFor(agent)) return resolved;
652
+ const phase = bootstrapPhaseOf(agent);
653
+ if (phase.promoted) return resolved;
654
+ const keep = new Set(cfg.bootstrap.tools);
655
+ if (phase.boundary >= 0) for (const name of cfg.bootstrap.compactionTools) keep.add(name);
656
+ const filtered = filterBootstrapTools(resolved, keep);
657
+ if (filtered.missing.length > 0) {
658
+ // 目录缺失 bootstrap 工具对:降级为全目录(一次告警),绝不能
659
+ // 把每个请求都卡死在空目录。
660
+ warnBootstrapOnce(`bootstrap tools missing from catalog (${JSON.stringify(filtered.missing)}); exposing the full catalog`);
661
+ return resolved;
662
+ }
663
+ return { ...resolved, tools: filtered.tools };
664
+ } catch (error) {
665
+ warnBootstrapOnce(`bootstrap tool filter failed, exposing the full catalog: ${String((error && error.message) || error)}`);
666
+ return resolved;
667
+ }
668
+ }, { prepend: true });
669
+
670
+ // 首轮剥离自动注入的上下文(技能目录提醒 / AGENTS.md 摘要)。prepend +
671
+ // 根作用域注册保证是最后一道变换(后注册的注入者无法再补回)。
672
+ ctx.on('agent/pre-step', async ({ agent }, next) => {
673
+ const decision = await next();
674
+ if (decision === null || typeof decision !== 'object' || decision.kind === 'reject') return decision;
675
+ try {
676
+ if (!bootstrapActiveFor(agent)) return decision;
677
+ const phase = bootstrapPhaseOf(agent);
678
+ if (phase.promoted || cfg.bootstrap.suppressedContextSources.length === 0) return decision;
679
+ if (!Array.isArray(decision.messages)) return decision;
680
+ const kept = filterBootstrapMessages(decision.messages, new Set(cfg.bootstrap.suppressedContextSources));
681
+ return kept === decision.messages ? decision : { ...decision, messages: kept };
682
+ } catch (error) {
683
+ // 过滤失败绝不吞掉用户的上下文:原样放行。
684
+ warnBootstrapOnce(`bootstrap context filter failed, keeping injected context: ${String((error && error.message) || error)}`);
685
+ return decision;
686
+ }
687
+ }, { prepend: true });
688
+
689
+ // 可选:请求 #1 输出预算封顶(晋升后剥离,避免封顶沿 header 种子延续)。
690
+ if (cfg.bootstrap.maxTokens > 0) {
691
+ ctx.on('agent/request', async ({ agent }, next) => {
692
+ const resolved = await next();
693
+ try {
694
+ if (!bootstrapActiveFor(agent)) return resolved;
695
+ const phase = bootstrapPhaseOf(agent);
696
+ return applyBootstrapBudget(resolved, phase.promoted, cfg.bootstrap.maxTokens);
697
+ } catch (error) {
698
+ warnBootstrapOnce(`bootstrap budget filter failed: ${String((error && error.message) || error)}`);
699
+ return resolved;
700
+ }
701
+ }, { prepend: true });
702
+ }
703
+
432
704
  // 已运行会话补挂(插件热重载/重装时)。
433
705
  if (agentsService !== void 0 && typeof agentsService.list === 'function') {
434
706
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chaoset/adaptive-perf",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "DSH host plugin: bring standard / PTC presets to minimal-mode-level performance via runtime-context suppression and adaptive tool-catalog trimming with demand-driven escalation",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,7 +34,7 @@
34
34
  "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6"
35
35
  },
36
36
  "engines": {
37
- "node": ">=20"
37
+ "node": ">=22.19.0"
38
38
  },
39
39
  "scripts": {
40
40
  "test": "node ../../scripts/test.mjs",