@chaoset/adaptive-perf 0.4.0 → 0.5.0

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,20 +3,36 @@
3
3
  让 **标准模式(standard)** 与 **PTC 模式(code)** 达到 **极简模式(minimal)**
4
4
  级别的高性能,同时保留完整能力——不是静态裁剪,而是**动态自适应**:
5
5
 
6
- - **首轮锚定(bootstrap)**:决定模型首轮"轨迹"(思维链风格)的是请求 #1 可见的
7
- **工具 schema** 与**自动注入的上下文提醒**(参照
6
+ - **真实 Minimal 工具对(realPair)**:首轮"轨迹"由请求 #1 可见的**工具 schema**
7
+ 逐字决定——只有与 Minimal 完全相同的 schema 能锚定(参照
8
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`);
9
+ 实测:真实工具对 5/5 锚定,任何 standard 系 schema——包括 sandboxed
10
+ bash/read——11/11 落入 standard-like)。本插件把官方 minimal preset 同款插件
11
+ (持久 PTY bash + `str_replace_editor`,含逐字相同的描述)挂进会话作用域,
12
+ 按名阴影 standard 的 sandboxed bash——**不是**只把目录收窄到同名工具。
13
+ - **首轮锚定(bootstrap)**:决定首轮"轨迹"(思维链风格)的是请求 #1 可见的
14
+ **工具 schema** 与**自动注入的上下文提醒**。因此:
15
+ - 请求 #1 的工具目录 = 真实 Minimal 工具对(`bash` + `str_replace_editor`);
12
16
  - 请求 #1 剥离自动注入上下文(技能目录提醒 `skill-catalog`、AGENTS.md 摘要
13
17
  `agent-instructions`,用户主动的技能手势不过滤);
14
18
  - 会话出现首个**持久**晋升信号(首个 `tool/call` 或 `assistant/message`,
15
- `promoteOn` 可选 `either`/`tool-call`/`assistant-message`)后恢复完整目录;
16
- 阶段从持久会话事件推导,**resume / reload 不丢状态**;
19
+ `promoteOn` 可选 `either`/`tool-call`/`assistant-message`)后进入 **resident
20
+ 目录**:bootstrap 工具对 + 常驻发现工具(`dev_tool_search` +
21
+ `skill_search` + `skill_load`)+ 已解锁工具,而不是一次性 dump 完整
22
+ Standard 目录(避免 post-promotion regression);阶段从持久会话事件推导,
23
+ **resume / reload 不丢状态**;
24
+ - `dev_tool_search` 可搜索完整目录并按名解锁工具,解锁结果下一请求生效;
17
25
  - compaction 会把会话打回"第二次首轮":`compaction/end` 之后回到受控目录
18
26
  (bootstrap 工具对 + `compactionTools` 核心工作集),直到出现新的晋升信号;
19
27
  - `maxTokens > 0` 时可给请求 #1 封顶输出预算(晋升后自动剥离)。
28
+ - **常驻上下文抑制(suppressInjectedContext)**:技能目录提醒与 AGENTS.md 摘要
29
+ **不再在晋升后恢复**——参考实现实测它们即使在后置阶段也扰动轨迹,且每个请求
30
+ 多耗数千 token。替代品:
31
+ - `skill_search` / `skill_load`:晋升后常驻的两个小工具,按需发现并注入单个
32
+ 技能的完整说明(替代 ~9KB 的 `<available_skills>` 目录注入);
33
+ - `instruction-hint`:晋升后只注入**一次**的短提示——"指令文件存在,需要时
34
+ 自己读",而不是每请求 dump 全文(探测项目链 AGENTS.md/CLAUDE.md 与
35
+ `$DSH_HOME/AGENTS.md`,探测失败不注入,绝不抛错)。
20
36
  - **运行时上下文抑制**:对目标 preset 的会话调用 `suppressRuntimeContext()`
21
37
  (与极简模式的 `includeRuntimeContext: false` 同一机制),每次模型请求省掉
22
38
  "Current runtime context" 快照文本,**零功能损失**。
@@ -28,8 +44,9 @@
28
44
  - 按名阴影屏蔽三个全局引导段(空段在装配时被丢弃,等同极简 `complete`
29
45
  persona 的效果;plan-mode 与 PTC 的 SDK 段不受影响);
30
46
  - 按名阴影替换 `deployment:persona` 为极简语域 persona(默认与极简模式
31
- 逐字相同:`You are a helpful software engineer assistant.`)。
32
- 两处均为 agent 作用域的 prompt 段阴影,仅影响目标会话;**对已恢复/已晋升的
47
+ 逐字相同:`You are a helpful software engineer assistant.`);
48
+ - 挂载真实工具对时按名阴影 `tool:bash` 引导段(与可见工具一致)。
49
+ 各处均为 agent 作用域的 prompt 段阴影,仅影响目标会话;**对已恢复/已晋升的
33
50
  旧会话同样生效**(不依赖 bootstrap 阶段)。
34
51
  - **工具目录自适应精简**:会话启动时按"工具族"默认隐藏高开销低频工具
35
52
  (子代理 / 工作流 / ralph / goal 等编排类),只保留核心编码工具
@@ -45,11 +62,12 @@
45
62
 
46
63
  | 维度 | 极简模式 | 标准 / PTC 模式 | 本插件做法 |
47
64
  |---|---|---|---|
48
- | 首轮轨迹(决定后续行为风格) | Minimal 工具对锚定(5/5) | 任何 standard 系 schema 落入 standard-like(11/11) | 请求 #1 收窄到真实 Minimal 工具对,首个持久信号后恢复 |
65
+ | 首轮轨迹(决定后续行为风格) | Minimal 真实工具对锚定(5/5) | 任何 standard 系 schema(含 sandboxed bash)落入 standard-like(11/11) | realPair 挂载官方同款插件:请求 #1 schema 与 Minimal 逐字相同,首个持久信号后进 resident 目录 |
49
66
  | 首轮注入提醒 | 无 skill 目录/AGENTS.md 注入 | 自动注入(在场时锚定 0/9) | 请求 #1 剥离 `skill-catalog` + `agent-instructions` |
67
+ | 后置阶段注入 | 永不注入 | 晋升后每请求恢复 ~9KB 目录 + AGENTS.md 全文 | `suppressInjectedContext`:整个会话抑制;替代品为 `skill_search`/`skill_load` + 一次性 `instruction-hint` |
50
68
  | 系统提示语域(persona + 全局引导段) | 一句话 persona,无任何引导段 | identity/source/web-surface 引导段 + 标准 persona("Let me…" 叙述来源) | minimalPrompt 层按名阴影屏蔽 3 个全局段 + 替换极简 persona(对旧会话同样生效) |
51
69
  | 运行时上下文快照 | 关闭(`includeRuntimeContext: false`) | 每次请求注入文件策略/审批策略等快照 | 同一机制:`suppressRuntimeContext()` |
52
- | 模型可见工具目录 | 2 个(bash、str_replace_editor) | ~22 个工具 schema 全部进请求 | bootstrap 后默认隐藏 4 个编排类工具族(~11 个工具),按需放行 |
70
+ | 模型可见工具目录 | 2 个(bash、str_replace_editor) | ~22 个工具 schema 全部进请求 | 首轮 2 个;晋升后 resident 目录(工具对 + 发现工具 + 已解锁),编排族默认隐藏按需放行 |
53
71
  | PTC SDK 参考段 | 无 | 全部工具的类型+描述渲染进提示 | 随目录缩小(同一 `visible` 视图驱动) |
54
72
 
55
73
  ## 安装(npm 生态方式)
@@ -96,14 +114,45 @@ dsh plugin --profile web remove @chaoset/adaptive-perf
96
114
  | `enabled` | `true` | 总开关 |
97
115
  | `presets` | `["standard", "code"]` | 应用自适应的 preset id |
98
116
  | `suppressRuntimeContext` | `true` | 抑制运行时上下文快照(零功能损失) |
117
+ | `suppressInjectedContext` | `true` | 整个会话剥离技能目录/AGENTS.md 注入(0.5.0;`false` = 只在请求 #1 剥离) |
118
+ | `skillDiscovery` | `true` | 晋升后常驻 `skill_search` / `skill_load`(0.5.0) |
119
+ | `instructionHint` | `true` | 晋升后一次性注入指令文件提示(0.5.0) |
99
120
  | `leanByDefault` | `true` | 会话启动即隐藏编排类工具族 |
100
121
  | `escalateOnKeyword` | `true` | 用户消息命中触发词 → 放行该族 |
101
122
  | `escalateOnUnknownTool` | `true` | 工具调用失败(UNKNOWN_TOOL)→ 放行该族 |
102
123
  | `coreTools` | 核心编码工具 | 展示用:不进入任何限制族 |
103
124
  | `families` | 见下 | 工具族:`{ 族名: { enabled, tools, keywords } }` |
104
- | `bootstrap` | 见下 | 首轮锚定(工具对 / 晋升信号 / 剥离上下文 / 压缩恢复集 / 预算封顶) |
125
+ | `bootstrap` | 见下 | 首轮锚定(真实工具对 / 晋升信号 / 剥离上下文 / 压缩恢复集 / resident 发现工具 / 预算封顶) |
105
126
  | `minimalPrompt` | 见下 | 极简提示词层(语域锚定) |
106
127
 
128
+ `bootstrap`(首轮锚定):
129
+
130
+ ```json
131
+ {
132
+ "enabled": true,
133
+ "realPair": true,
134
+ "tools": ["bash", "str_replace_editor"],
135
+ "promoteOn": "either",
136
+ "suppressedContextSources": ["skill-catalog", "agent-instructions"],
137
+ "compactionTools": ["read", "write", "edit", "glob", "grep", "todo_write", "ask_user_question"],
138
+ "discoveryTools": ["dev_tool_search", "skill_search", "skill_load"],
139
+ "maxTokens": 0
140
+ }
141
+ ```
142
+
143
+ - `realPair`(0.5.0):把官方 minimal preset 同款插件(持久 PTY bash +
144
+ `str_replace_editor`)挂进会话作用域,按名阴影 standard 的 sandboxed
145
+ bash——请求 #1 的 schema 与 Minimal 逐字相同(只有逐字相同的 schema 能
146
+ 锚定)。依赖 `@deepseek-ai` 官方插件包(optionalDependencies);缺失或
147
+ Windows(无 PTY 后端)时自动降级为"仅收窄目录"并告警。
148
+ - `tools`:请求 #1 可见的工具(真实 Minimal 对)。
149
+ - `promoteOn`:`either`(默认)/ `tool-call` / `assistant-message`。
150
+ - `suppressedContextSources`:剥离的自动注入 source.kind;`[]` 关闭剥离。
151
+ - `compactionTools`:compaction/end 之后、再次晋升前可用的核心工作集。
152
+ - `discoveryTools`:晋升后 resident 目录里的按需发现工具
153
+ (`dev_tool_search` 解锁工具;`skill_search`/`skill_load` 按需加载技能)。
154
+ - `maxTokens`:请求 #1 输出预算封顶(0 = 不封顶,opt-in)。
155
+
107
156
  `minimalPrompt`(极简提示词层,0.4.0 新增):
108
157
 
109
158
  ```json
@@ -142,8 +191,22 @@ dsh plugin --profile web remove @chaoset/adaptive-perf
142
191
  - 监听 host 根作用域的 `agent/created` / `agent/disposed` / `agent/inbox/inserted`
143
192
  / `tools/result`(与 `dsh-agent-presets` 自身同款事件用法),按
144
193
  `agentPresets.composedPreset(agent.ctx)` 识别目标 preset
194
+ - **真实工具对**:`agent.ctx.plugin()` 把 `dsh-terminal` + `dsh-terminal-bash`
195
+ + `dsh-tool-bash-persistent`(Minimal 逐字描述)+ `dsh-fs-local` +
196
+ `dsh-tool-str-replace-editor` 挂进 agent 作用域;scoped registration 按名
197
+ 阴影继承的 sandboxed bash,且 `view()` 对 own layer 豁免 restrict——首轮
198
+ 目录 = 真实工具对 + 被 deny 的其他继承工具;每个挂载是独立 fiber,
199
+ agent 销毁/插件卸载/配置关闭时全部撤销
145
200
  - 限制通过 `agent.ctx.tools.restrict({ deny })` 逐族注册:多族限制自动取交集,
146
201
  升级 = 释放该族 disposer(单会话单调、一次性目录变化,缓存失效有界)
202
+ - 发现工具(`dev_tool_search` / `skill_search` / `skill_load`)同样注册在
203
+ agent 作用域:已解锁工具写入会话状态并重算 deny 集;技能经宿主 `skills`
204
+ 服务按 agent scope 查询,`skill_load` 用 `agent.inject` 以
205
+ `skill-invocation` 来源注入(用户技能手势同款,不被默认抑制集剥离)
206
+ - 上下文剥离与 instruction-hint 都在 `agent/pre-step`(prepend 注册,最后
207
+ 一道变换):剥离失败绝不吞用户上下文;指令文件探测走宿主 `fs`,失败不注入
147
208
  - 会话启动/插件重载时用 `tools.schemas(agent)` 记录可见目录,限制与之取交集
148
- - 全部副作用(抑制器、限制、监听)随 agent 销毁或插件卸载自动释放
149
- - **fail-safe**:插件任何内部错误只记日志,不拖垮 harness
209
+ - 全部副作用(抑制器、限制、工具注册、提示段、监听)随 agent 销毁或插件
210
+ 卸载自动释放
211
+ - **fail-safe**:插件任何内部错误只记日志,不拖垮 harness;官方插件包缺失
212
+ 时自动降级
package/client/client.cjs CHANGED
@@ -40,12 +40,20 @@ window.__ModuleLoader__.load({
40
40
  fieldEscalateOnKeyword: "用户消息命中工具族触发词(如“子代理”)时,该会话放行对应工具族",
41
41
  escalateOnUnknownTool: "失败自动放行",
42
42
  fieldEscalateOnUnknownTool: "PTC 程序调用被隐藏工具报 UNKNOWN_TOOL 时自动放行对应族",
43
+ suppressInjectedContext: "常驻抑制注入上下文",
44
+ fieldSuppressInjectedContext: "整个会话剥离技能目录提醒(skill-catalog)与 AGENTS.md 摘要(agent-instructions),不再在晋升后恢复——替代品是 skill_search/skill_load 按需发现与一次性 instruction-hint(参照 dsh-anchored-standard:注入在场即使后置也扰动轨迹)",
45
+ skillDiscovery: "技能按需发现",
46
+ fieldSkillDiscovery: "晋升后常驻 skill_search / skill_load 两个小工具,替代 ~9KB 的 <available_skills> 目录注入",
47
+ instructionHint: "指令文件一次性提示",
48
+ fieldInstructionHint: "晋升后注入一次“指令文件存在,需要时自读”的短提示(探测项目链 AGENTS.md/CLAUDE.md 与 $DSH_HOME/AGENTS.md),替代全文摘要",
43
49
  families: "工具族(高级)",
44
50
  fieldFamilies: "JSON:{ 族名: { enabled, tools: [工具名], keywords: [触发词] } }。保存后热生效。",
45
51
  coreTools: "核心工具(永不隐藏)",
46
52
  fieldCoreTools: "展示用:这些工具不进入任何限制族。未列入任何族的工具也默认保留。",
47
53
  bootstrapEnabled: "首轮锚定(bootstrap)",
48
54
  fieldBootstrapEnabled: "请求 #1 只暴露 Minimal 真实工具对并剥离自动注入上下文,首个持久信号(promoteOn)后恢复——让首轮轨迹锚定极简(参照 dsh-anchored-standard 实测:工具 schema + 注入提醒决定轨迹)",
55
+ bootstrapRealPair: "挂载真实 Minimal 工具对",
56
+ fieldBootstrapRealPair: "把官方 minimal preset 同款插件(持久 PTY bash + str_replace_editor)挂进会话作用域,按名阴影 standard 的 sandboxed bash——schema 与 Minimal 逐字相同才能锚定(issue #11:任何 standard 系 schema 11/11 落入 standard-like);依赖官方插件包,缺失时自动降级",
49
57
  bootstrapTools: "首轮工具",
50
58
  fieldBootstrapTools: "请求 #1 可见的工具(真实 Minimal 对:bash, str_replace_editor)。逗号分隔。",
51
59
  bootstrapPromoteOn: "晋升触发",
@@ -54,6 +62,8 @@ window.__ModuleLoader__.load({
54
62
  fieldBootstrapSuppressed: "请求 #1 剥离的自动注入 source.kind(技能目录提醒 skill-catalog、AGENTS.md 摘要 agent-instructions);逗号分隔,留空=不剥离",
55
63
  bootstrapCompactionTools: "压缩后工作集",
56
64
  fieldBootstrapCompactionTools: "compaction/end 之后、再次晋升前额外可用的工具(核心工作集)。逗号分隔。",
65
+ bootstrapDiscoveryTools: "晋升后常驻发现工具",
66
+ fieldBootstrapDiscoveryTools: "晋升后保留在 resident 目录里的按需发现工具(如 dev_tool_search);逗号分隔。",
57
67
  bootstrapMaxTokens: "首轮输出预算封顶(token)",
58
68
  fieldBootstrapMaxTokens: "请求 #1 的 maxTokens 封顶,晋升后自动剥离;0 = 不封顶(opt-in)",
59
69
  minimalPromptEnabled: "极简提示词层(语域锚定)",
@@ -85,12 +95,20 @@ window.__ModuleLoader__.load({
85
95
  fieldEscalateOnKeyword: "A user message matching a family's trigger words re-enables that family for the session",
86
96
  escalateOnUnknownTool: "Failure escalation",
87
97
  fieldEscalateOnUnknownTool: "A PTC program calling a hidden tool (UNKNOWN_TOOL) re-enables that family",
98
+ suppressInjectedContext: "Always suppress injected context",
99
+ fieldSuppressInjectedContext: "Strip the skill-catalog reminder and the AGENTS.md digest from every request, not just request #1 — replaced by on-demand skill_search/skill_load and a one-shot instruction hint (per dsh-anchored-standard: the injections perturb the trajectory even after promotion)",
100
+ skillDiscovery: "On-demand skill discovery",
101
+ fieldSkillDiscovery: "Keep skill_search / skill_load resident after promotion as the replacement for the ~9KB <available_skills> catalog injection",
102
+ instructionHint: "One-shot instruction hint",
103
+ fieldInstructionHint: "After promotion, inject ONCE a short hint that instruction files exist and should be read when relevant (probes the project chain AGENTS.md/CLAUDE.md and $DSH_HOME/AGENTS.md), instead of dumping their content",
88
104
  families: "Tool families (advanced)",
89
105
  fieldFamilies: "JSON: { familyId: { enabled, tools: [names], keywords: [triggers] } }. Hot-applied on save.",
90
106
  coreTools: "Core tools (never hidden)",
91
107
  fieldCoreTools: "Informational: these never enter any restriction family. Tools not listed in any family stay visible too.",
92
108
  bootstrapEnabled: "First-request bootstrap",
93
109
  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)",
110
+ bootstrapRealPair: "Mount the real Minimal tool pair",
111
+ fieldBootstrapRealPair: "Mount the official minimal-preset plugins (persistent PTY bash + str_replace_editor) into the session scope, shadowing the standard sandboxed bash by name — only byte-identical schemas anchor (issue #11: every standard-family schema fell standard-like 11/11); degrades gracefully when the official packages are missing",
94
112
  bootstrapTools: "Bootstrap tools",
95
113
  fieldBootstrapTools: "Tools visible on request #1 (the real Minimal pair: bash, str_replace_editor). Comma-separated.",
96
114
  bootstrapPromoteOn: "Promotion trigger",
@@ -99,6 +117,8 @@ window.__ModuleLoader__.load({
99
117
  fieldBootstrapSuppressed: "Auto-injected source.kinds stripped on request #1 (skill-catalog reminder, agent-instructions digest); comma-separated, empty = strip nothing",
100
118
  bootstrapCompactionTools: "Post-compaction work set",
101
119
  fieldBootstrapCompactionTools: "Extra tools available after compaction/end until a new promotion signal (core work set). Comma-separated.",
120
+ bootstrapDiscoveryTools: "Resident discovery tools",
121
+ fieldBootstrapDiscoveryTools: "On-demand discovery tools kept in the resident catalog after promotion (e.g. dev_tool_search). Comma-separated.",
102
122
  bootstrapMaxTokens: "First-request output cap (tokens)",
103
123
  fieldBootstrapMaxTokens: "maxTokens cap for request #1, stripped after promotion; 0 = no cap (opt-in)",
104
124
  minimalPromptEnabled: "Minimal prompt layer (register anchor)",
@@ -284,6 +304,27 @@ window.__ModuleLoader__.load({
284
304
  disabled: draft === null,
285
305
  onChange: (v) => setDraft({ ...draft, escalateOnUnknownTool: v })
286
306
  }),
307
+ react.createElement(BoolField, {
308
+ label: t("suppressInjectedContext"),
309
+ hint: t("fieldSuppressInjectedContext"),
310
+ value: draft === null ? false : draft.suppressInjectedContext,
311
+ disabled: draft === null,
312
+ onChange: (v) => setDraft({ ...draft, suppressInjectedContext: v })
313
+ }),
314
+ react.createElement(BoolField, {
315
+ label: t("skillDiscovery"),
316
+ hint: t("fieldSkillDiscovery"),
317
+ value: draft === null ? false : draft.skillDiscovery,
318
+ disabled: draft === null,
319
+ onChange: (v) => setDraft({ ...draft, skillDiscovery: v })
320
+ }),
321
+ react.createElement(BoolField, {
322
+ label: t("instructionHint"),
323
+ hint: t("fieldInstructionHint"),
324
+ value: draft === null ? false : draft.instructionHint,
325
+ disabled: draft === null,
326
+ onChange: (v) => setDraft({ ...draft, instructionHint: v })
327
+ }),
287
328
  react.createElement(BoolField, {
288
329
  label: t("bootstrapEnabled"),
289
330
  hint: t("fieldBootstrapEnabled"),
@@ -291,6 +332,13 @@ window.__ModuleLoader__.load({
291
332
  disabled: draft === null,
292
333
  onChange: (v) => setDraft({ ...draft, bootstrap: { ...(draft.bootstrap || {}), enabled: v } })
293
334
  }),
335
+ react.createElement(BoolField, {
336
+ label: t("bootstrapRealPair"),
337
+ hint: t("fieldBootstrapRealPair"),
338
+ value: draft === null ? false : !!draft.bootstrap && draft.bootstrap.realPair,
339
+ disabled: draft === null || (draft.bootstrap && draft.bootstrap.enabled === false),
340
+ onChange: (v) => setDraft({ ...draft, bootstrap: { ...(draft.bootstrap || {}), realPair: v } })
341
+ }),
294
342
  react.createElement(TextField, {
295
343
  label: t("bootstrapTools"),
296
344
  hint: t("fieldBootstrapTools"),
@@ -319,6 +367,13 @@ window.__ModuleLoader__.load({
319
367
  disabled: draft === null || (draft.bootstrap && draft.bootstrap.enabled === false),
320
368
  onChange: (v) => setDraft({ ...draft, bootstrap: { ...(draft.bootstrap || {}), compactionTools: v.split(/[,,\s]+/).filter((x) => x.length > 0) } })
321
369
  }),
370
+ react.createElement(TextField, {
371
+ label: t("bootstrapDiscoveryTools"),
372
+ hint: t("fieldBootstrapDiscoveryTools"),
373
+ value: draft === null ? "" : ((draft.bootstrap && draft.bootstrap.discoveryTools) || []).join(", "),
374
+ disabled: draft === null || (draft.bootstrap && draft.bootstrap.enabled === false),
375
+ onChange: (v) => setDraft({ ...draft, bootstrap: { ...(draft.bootstrap || {}), discoveryTools: v.split(/[,,\s]+/).filter((x) => x.length > 0) } })
376
+ }),
322
377
  react.createElement(TextField, {
323
378
  label: t("bootstrapMaxTokens"),
324
379
  hint: t("fieldBootstrapMaxTokens"),
package/cordis.patch.yml CHANGED
@@ -15,12 +15,22 @@
15
15
  leanByDefault: true
16
16
  escalateOnKeyword: true
17
17
  escalateOnUnknownTool: true
18
- # 首轮锚定(参照 dsh-anchored-standard 实测):请求 #1 只暴露 Minimal
19
- # 真实工具对并剥离自动注入上下文,首个持久信号后恢复完整目录。
18
+ # 常驻上下文抑制(参照 dsh-anchored-standard):整个会话剥离技能目录
19
+ # 提醒与 AGENTS.md 摘要注入,替代品是 skill_search/skill_load 按需
20
+ # 发现与 instruction-hint 一次性提示。
21
+ suppressInjectedContext: true
22
+ skillDiscovery: true
23
+ instructionHint: true
24
+ # 首轮锚定(参照 dsh-anchored-standard issue #11):请求 #1 只暴露
25
+ # 真实 Minimal 工具对(realPair 挂载官方 minimal preset 同款插件,
26
+ # schema 与 Minimal 逐字相同)并剥离自动注入上下文,首个持久信号后
27
+ # 进入 resident 目录(工具对 + 发现工具 + 已解锁工具)。
20
28
  bootstrap:
21
29
  enabled: true
30
+ realPair: true
22
31
  tools: [bash, str_replace_editor]
23
32
  promoteOn: either
24
33
  suppressedContextSources: [skill-catalog, agent-instructions]
25
34
  compactionTools: [read, write, edit, glob, grep, todo_write, ask_user_question]
35
+ discoveryTools: [dev_tool_search, skill_search, skill_load]
26
36
  maxTokens: 0
package/lib/index.mjs CHANGED
@@ -7,32 +7,46 @@
7
7
  * "Current runtime context" 快照(文件策略 / 审批策略等动态段);
8
8
  * 2. 工具目录极小(只有 bash + str_replace_editor)—— 每个请求的
9
9
  * tool schema 与(PTC 的)SDK 参考段都随之变小;
10
- * 3. persona 为 complete 固定提示,无其它全局 prompt 段。
10
+ * 3. persona 为 complete 固定提示,无其它全局 prompt 段;
11
+ * 4. 不挂 skill 目录提醒(~9KB)与 AGENTS.md 摘要注入。
11
12
  *
12
- * 本插件在三个维度上做"动态自适应"(而不是静态裁剪):
13
+ * 本插件在多个维度上做"动态自适应"(而不是静态裁剪),全部参照
14
+ * dsh-anchored-standard(https://github.com/xiaobright/dsh-anchored-standard)
15
+ * 的实测结论:
13
16
  * A. 运行时上下文抑制:对目标 preset 的会话调用
14
17
  * agent.ctx.systemPrompt.suppressRuntimeContext()(与极简模式同一机制),
15
18
  * 每次请求省掉快照文本,零功能损失。
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. 工具目录自适应精简:会话启动时按"工具族"默认隐藏高开销低频工具
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. 工具目录自适应精简:会话启动时按"工具族"默认隐藏高开销低频工具
33
48
  * (子代理 / 工作流 / ralph / goal 等编排类),只保留核心编码工具,
34
- * 让标准模式的目录逼近极简、PTC 的 SDK 段同步缩小;随后根据两类信号
35
- * 在会话内"单调升级"放行对应工具族(只升不降,限制请求缓存失效次数):
49
+ * 随后根据两类信号在会话内"单调升级"放行对应工具族(只升不降):
36
50
  * - 关键词信号:用户消息命中某工具族的触发词(如"子代理")→ 放行该族;
37
51
  * - 失败信号:PTC 的 run_code 程序调用被隐藏工具报 UNKNOWN_TOOL
38
52
  * (tools/result 失败文本含工具名)→ 放行该族,下次程序即可调用。
@@ -40,24 +54,28 @@
40
54
  * 实现全部使用 dsh 公开服务契约(Inspect 确认):
41
55
  * - ctx.agentPresets.composedPreset(agent.ctx) 读取会话所属 preset
42
56
  * - agent.ctx.systemPrompt.suppressRuntimeContext() 抑制上下文快照
57
+ * - agent.ctx.systemPrompt.section() 按名阴影 prompt 段(persona/引导段)
43
58
  * - agent.ctx.tools.schemas(agent) 读取该 agent 当前可见工具(限制交集)
44
59
  * - agent.ctx.tools.restrict({ deny }) 按 agent 作用域裁剪继承工具
45
- * - system-prompt/assemble(waterfall) 请求装配(早期实现;bootstrap 目录
46
- * 收窄改用 tools.restrict——PTC 模式下 assembly.tools 只有 run_code,
47
- * 过滤会降级失效,而 restrict 同时驱动 API 目录与 PTC SDK 参考段)
48
- * - agent/pre-step(waterfall) 剥离自动注入的上下文消息(source.kind
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
49
65
  * - agent/request(waterfall) 首轮输出预算封顶
50
66
  * - session/event 增量喂入持久事件(晋升 / compaction 纪元)
51
67
  * - agent/created、agent/disposed、agent/inbox/inserted、tools/result 事件
52
68
  * 监听器都注册在 host 根作用域(与 dsh-agent-presets 自身同款用法),事件按
53
- * scope 过滤分发,子作用域派发的事件根监听器可收到;三个 waterfall 均以
69
+ * scope 过滤分发,子作用域派发的事件根监听器可收到;两个 waterfall 均以
54
70
  * prepend 注册保证"最外层"(剥离是最后一道变换)。
55
71
  *
56
72
  * 配置:cordis.patch.yml 的 config(安装默认)与
57
73
  * ~/.dsh/plugins/adaptive-perf/config.json(设置页 UI,权威)合并,
58
74
  * 保存后热生效(新会话立即生效;已运行会话按新配置重算限制)。
59
75
  *
60
- * 本文件不依赖任何 dsh 内部包(纯 ESM + ctx 服务),可独立安装。
76
+ * 依赖:真实工具对需要官方插件包(@deepseek-ai/dsh-terminal 等,声明为
77
+ * optionalDependencies)。它们不可用时本插件自动降级(告警 + 退回旧行为:
78
+ * 只收窄目录、不替换 schema),绝不拖垮 harness。
61
79
  */
62
80
 
63
81
  import { createConfigStore } from './config-store.mjs';
@@ -99,6 +117,17 @@ export const DEFAULT_CONFIG = {
99
117
  escalateOnKeyword: true,
100
118
  /** 工具调用失败(UNKNOWN_TOOL 文本含隐藏工具名)时自动放行该族。 */
101
119
  escalateOnUnknownTool: true,
120
+ /**
121
+ * 常驻上下文抑制(0.5.0):true = 整个会话剥离自动注入上下文
122
+ * (skill-catalog 技能目录提醒、agent-instructions AGENTS.md 摘要),
123
+ * 不再像旧版那样在晋升后恢复——替代品是 skill_search/skill_load 按需发现
124
+ * 与 instruction-hint 一次性提示。false = 只在 bootstrap(请求 #1)剥离。
125
+ */
126
+ suppressInjectedContext: true,
127
+ /** 晋升后常驻 skill_search / skill_load 两个按需技能发现工具。 */
128
+ skillDiscovery: true,
129
+ /** 晋升后注入一次"指令文件存在,需要时自读"的短提示(替代全文注入)。 */
130
+ instructionHint: true,
102
131
  /** 核心编码工具(不进入任何限制族;此处仅作文档展示)。 */
103
132
  coreTools: [
104
133
  'bash', 'read', 'write', 'edit', 'glob', 'grep', 'read_image',
@@ -132,6 +161,13 @@ export const DEFAULT_CONFIG = {
132
161
  bootstrap: {
133
162
  /** 是否启用首轮锚定。 */
134
163
  enabled: true,
164
+ /**
165
+ * 是否挂载真实 Minimal 工具对(0.5.0):把官方 minimal preset 的持久
166
+ * PTY bash + str_replace_editor 挂进 agent 作用域(按名阴影 standard 的
167
+ * sandboxed bash),使请求 #1 的 schema 与 Minimal 逐字相同。依赖
168
+ * @deepseek-ai 官方插件包,不可用时自动降级为旧行为(仅收窄目录)。
169
+ */
170
+ realPair: true,
135
171
  /** 请求 #1 可见的工具(真实 Minimal 工具对)。 */
136
172
  tools: ['bash', 'str_replace_editor'],
137
173
  /** 晋升触发:either(默认)/ tool-call / assistant-message。 */
@@ -140,6 +176,8 @@ export const DEFAULT_CONFIG = {
140
176
  suppressedContextSources: ['skill-catalog', 'agent-instructions'],
141
177
  /** compaction/end 之后、再次晋升前可用的额外核心工作集。 */
142
178
  compactionTools: ['read', 'write', 'edit', 'glob', 'grep', 'todo_write', 'ask_user_question'],
179
+ /** 晋升后常驻的按需发现工具(dsh-anchored-standard 的 resident discovery set)。 */
180
+ discoveryTools: ['dev_tool_search', 'skill_search', 'skill_load'],
143
181
  /** 请求 #1 的输出预算封顶(token);0 = 不封顶(opt-in)。 */
144
182
  maxTokens: 0,
145
183
  },
@@ -218,10 +256,12 @@ export function normalizeConfig(source, defaults = DEFAULT_CONFIG) {
218
256
  const maxTokens = Number.isSafeInteger(maxTokensRaw) && maxTokensRaw > 0 ? maxTokensRaw : 0;
219
257
  const bootstrap = {
220
258
  enabled: boolValue(rawBootstrap.enabled, db.enabled),
259
+ realPair: boolValue(rawBootstrap.realPair, db.realPair),
221
260
  tools: stringList(rawBootstrap.tools, db.tools),
222
261
  promoteOn,
223
262
  suppressedContextSources: stringListOrEmpty(rawBootstrap.suppressedContextSources, db.suppressedContextSources),
224
263
  compactionTools: stringListOrEmpty(rawBootstrap.compactionTools, db.compactionTools),
264
+ discoveryTools: stringListOrEmpty(rawBootstrap.discoveryTools, db.discoveryTools),
225
265
  maxTokens,
226
266
  };
227
267
  const rawMP = raw.minimalPrompt !== null && typeof raw.minimalPrompt === 'object' && !Array.isArray(raw.minimalPrompt)
@@ -240,6 +280,9 @@ export function normalizeConfig(source, defaults = DEFAULT_CONFIG) {
240
280
  leanByDefault: boolValue(merged.leanByDefault, defaults.leanByDefault),
241
281
  escalateOnKeyword: boolValue(merged.escalateOnKeyword, defaults.escalateOnKeyword),
242
282
  escalateOnUnknownTool: boolValue(merged.escalateOnUnknownTool, defaults.escalateOnUnknownTool),
283
+ suppressInjectedContext: boolValue(merged.suppressInjectedContext, defaults.suppressInjectedContext),
284
+ skillDiscovery: boolValue(merged.skillDiscovery, defaults.skillDiscovery),
285
+ instructionHint: boolValue(merged.instructionHint, defaults.instructionHint),
243
286
  coreTools: stringList(merged.coreTools, defaults.coreTools),
244
287
  families,
245
288
  bootstrap,
@@ -252,7 +295,8 @@ export function validateConfig(partial) {
252
295
  if (partial === null || typeof partial !== 'object' || Array.isArray(partial)) {
253
296
  throw new TypeError('adaptive-perf config must be a plain object');
254
297
  }
255
- const boolFields = ['enabled', 'suppressRuntimeContext', 'leanByDefault', 'escalateOnKeyword', 'escalateOnUnknownTool'];
298
+ const boolFields = ['enabled', 'suppressRuntimeContext', 'leanByDefault', 'escalateOnKeyword', 'escalateOnUnknownTool',
299
+ 'suppressInjectedContext', 'skillDiscovery', 'instructionHint'];
256
300
  for (const key of boolFields) {
257
301
  if (partial[key] !== void 0 && typeof partial[key] !== 'boolean') {
258
302
  throw new TypeError(`adaptive-perf config field "${key}" must be a boolean`);
@@ -289,13 +333,16 @@ export function validateConfig(partial) {
289
333
  if (b.enabled !== void 0 && typeof b.enabled !== 'boolean') {
290
334
  throw new TypeError('adaptive-perf config field "bootstrap.enabled" must be a boolean');
291
335
  }
336
+ if (b.realPair !== void 0 && typeof b.realPair !== 'boolean') {
337
+ throw new TypeError('adaptive-perf config field "bootstrap.realPair" must be a boolean');
338
+ }
292
339
  if (b.promoteOn !== void 0 && !['either', 'tool-call', 'assistant-message'].includes(b.promoteOn)) {
293
340
  throw new TypeError('adaptive-perf config field "bootstrap.promoteOn" must be "either" | "tool-call" | "assistant-message"');
294
341
  }
295
342
  if (b.maxTokens !== void 0 && !(Number.isSafeInteger(b.maxTokens) && b.maxTokens >= 0)) {
296
343
  throw new TypeError('adaptive-perf config field "bootstrap.maxTokens" must be a non-negative safe integer');
297
344
  }
298
- for (const key of ['tools', 'suppressedContextSources', 'compactionTools']) {
345
+ for (const key of ['tools', 'suppressedContextSources', 'compactionTools', 'discoveryTools']) {
299
346
  if (b[key] !== void 0 && !(Array.isArray(b[key]) && b[key].every((v) => typeof v === 'string'))) {
300
347
  throw new TypeError(`adaptive-perf config field "bootstrap.${key}" must be an array of strings`);
301
348
  }
@@ -371,6 +418,416 @@ export function collectFailureText(result) {
371
418
  return parts.join('\n');
372
419
  }
373
420
 
421
+ // ── 真实 Minimal 工具对(0.5.0,参照 dsh-anchored-standard issue #11)──────
422
+
423
+ /**
424
+ * 官方 minimal preset 的持久 bash 工具描述,逐字一致。schema(参数)由
425
+ * @deepseek-ai/dsh-tool-bash-persistent 提供,与 minimal preset 挂的是同一
426
+ * 个包;描述在这里显式传入,保证与 minimal 的 agent.cordis.yml 完全一致。
427
+ */
428
+ export const MINIMAL_BASH_DESCRIPTION = [
429
+ 'Run commands in a bash shell',
430
+ '* When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.',
431
+ "* You don't have access to the internet via this tool.",
432
+ '* You do have access to a mirror of common linux and python packages via apt and pip.',
433
+ '* State is persistent across command calls and discussions with the user.',
434
+ "* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.",
435
+ '* Please avoid commands that may produce a very large amount of output.',
436
+ "* Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.",
437
+ ].join('\n');
438
+
439
+ /**
440
+ * 被真实工具对按名阴影的工具引导段:standard 的 sandboxed bash 注册了
441
+ * `tool:bash`(order 105)引导段,挂载真实工具对后该文本已与可见工具不符,
442
+ * 按名阴影为空段(装配时丢弃)。
443
+ */
444
+ export const TOOL_GUIDANCE_SHADOWS = [
445
+ ['tool:bash', 105],
446
+ ];
447
+
448
+ /** 真实工具对挂载所需的官方插件包说明符(声明在 optionalDependencies)。 */
449
+ export const REAL_PAIR_SPECS = {
450
+ terminal: '@deepseek-ai/dsh-terminal',
451
+ terminalBash: '@deepseek-ai/dsh-terminal-bash',
452
+ bashPersistent: '@deepseek-ai/dsh-tool-bash-persistent',
453
+ fsLocal: '@deepseek-ai/dsh-fs-local',
454
+ strReplaceEditor: '@deepseek-ai/dsh-tool-str-replace-editor',
455
+ };
456
+
457
+ /**
458
+ * 加载真实 Minimal 工具对所需模块。任一包不可用即整体返回 null(调用方
459
+ * 降级为旧行为),绝不抛出。
460
+ * @param importFn - 测试可注入的 import 实现(默认动态 import)。
461
+ */
462
+ export async function loadRealPairModules(importFn) {
463
+ const dynamicImport = importFn ?? ((spec) => import(spec));
464
+ const modules = {};
465
+ for (const [key, spec] of Object.entries(REAL_PAIR_SPECS)) {
466
+ try {
467
+ const mod = await dynamicImport(spec);
468
+ modules[key] = mod?.default ?? mod;
469
+ } catch {
470
+ modules[key] = null;
471
+ }
472
+ }
473
+ if (Object.keys(REAL_PAIR_SPECS).some((key) => modules[key] === null)) return null;
474
+ return modules;
475
+ }
476
+
477
+ /**
478
+ * 计算真实工具对的挂载清单(纯函数,便于测试):
479
+ * 顺序 = 依赖在前(terminals → backend → bash;fs → editor)。
480
+ * @param modules - loadRealPairModules 的结果。
481
+ * @param cwd - str_replace_editor 本地 fs 的工作目录(会话 cwd)。
482
+ */
483
+ export function realPairMounts(modules, cwd) {
484
+ if (modules === null || modules === void 0) return [];
485
+ const resolvedCwd = typeof cwd === 'string' && cwd.length > 0
486
+ ? cwd
487
+ : (process.env.DSH_CWD && process.env.DSH_CWD.trim().length > 0 ? process.env.DSH_CWD : process.cwd());
488
+ return [
489
+ { module: modules.terminal, config: {} },
490
+ { module: modules.terminalBash, config: { timeoutMs: 300000 } },
491
+ { module: modules.bashPersistent, config: { timeoutMs: 300000, description: MINIMAL_BASH_DESCRIPTION } },
492
+ { module: modules.fsLocal, config: { cwd: resolvedCwd } },
493
+ { module: modules.strReplaceEditor, config: { maxOutputChars: 16000 } },
494
+ ];
495
+ }
496
+
497
+ /**
498
+ * 在 agent 作用域挂载真实工具对(scoped registration 按名阴影继承工具)。
499
+ * 每个挂载是一个独立 fiber,dispose 时全部副作用(工具/schema/服务)撤销。
500
+ * @param agent - 目标 agent(其 ctx 必须带 scope 标签)。
501
+ * @param mounts - realPairMounts 的结果。
502
+ * @param warn - 日志回调。
503
+ * @returns 可释放的 disposer 列表。
504
+ */
505
+ export function mountRealPair(agent, mounts, warn) {
506
+ const disposers = [];
507
+ if (agent === null || typeof agent !== 'object' || typeof agent.ctx?.plugin !== 'function') return disposers;
508
+ for (const mount of mounts) {
509
+ if (mount?.module === null || mount?.module === void 0) continue;
510
+ try {
511
+ const fiber = agent.ctx.plugin(mount.module, mount.config ?? {});
512
+ disposers.push(() => {
513
+ try { fiber.dispose(); } catch {}
514
+ });
515
+ } catch (error) {
516
+ warn?.(`mountRealPair failed for ${String(mount.module?.name ?? 'module')}: ${String((error && error.message) || error)}`);
517
+ }
518
+ }
519
+ return disposers;
520
+ }
521
+
522
+ // ── instruction-hint(0.5.0,参照 dsh-anchored-standard)──────────────────
523
+
524
+ /** 项目链候选指令文件名(探测顺序)与用户全局候选。 */
525
+ export const INSTRUCTION_PROJECT_CANDIDATES = ['AGENTS.md', 'CLAUDE.md', 'AGENTS.local.md', 'CLAUDE.local.md'];
526
+ export const INSTRUCTION_USER_CANDIDATE = 'AGENTS.md';
527
+
528
+ /** 平台无关的路径拼接(与参考实现一致)。 */
529
+ export function joinPath(dir, segment) {
530
+ if (dir.endsWith('/') || dir.endsWith('\\')) return dir + segment;
531
+ const sep = dir.includes('\\') ? '\\' : '/';
532
+ return dir + sep + segment;
533
+ }
534
+
535
+ /** 绝对 POSIX/Windows 路径的父目录。 */
536
+ export function parentPath(path) {
537
+ const idx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
538
+ if (idx <= 0) return path;
539
+ const parent = path.slice(0, idx);
540
+ return parent.length === 0 ? path : parent;
541
+ }
542
+
543
+ /** 从 cwd 向上找项目根(含 .git/.hg/.svn 标记的最近祖先)。 */
544
+ export async function findProjectRoot(fs, cwd, signal) {
545
+ let current = cwd;
546
+ for (;;) {
547
+ for (const marker of ['.git', '.hg', '.svn']) {
548
+ try {
549
+ const target = await fs.resolve(joinPath(current, marker), { cwd, signal });
550
+ const info = await fs.stat(target, signal);
551
+ if (info !== void 0) return current;
552
+ } catch {
553
+ // 探测失败 = 标记不存在,继续。
554
+ }
555
+ }
556
+ const parent = parentPath(current);
557
+ if (parent === current || parent.length === 0) return cwd;
558
+ current = parent;
559
+ }
560
+ }
561
+
562
+ /** 列出某目录下存在的候选指令文件。 */
563
+ export async function presentInDir(fs, dir, candidates, signal) {
564
+ const found = [];
565
+ for (const candidate of candidates) {
566
+ try {
567
+ const target = await fs.resolve(joinPath(dir, candidate), { cwd: dir, signal });
568
+ const info = await fs.stat(target, signal);
569
+ if (info !== void 0 && info.type === 'file') found.push(candidate);
570
+ } catch {
571
+ // 不存在/不可读——跳过。
572
+ }
573
+ }
574
+ return found;
575
+ }
576
+
577
+ /**
578
+ * 探测会话的指令文件(项目链 + $DSH_HOME/AGENTS.md)。任何失败都返回
579
+ * null/空集——提示注入绝不能让会话受损。
580
+ * @returns { projectFiles, root, userGlobalFiles } 或 null(无 fs / 无 cwd)。
581
+ */
582
+ export async function probeInstructionFiles(fs, cwd, dshHome, signal) {
583
+ if (fs === null || fs === void 0 || typeof cwd !== 'string' || cwd.length === 0) return null;
584
+ const projectFiles = [];
585
+ let root = cwd;
586
+ try {
587
+ root = await findProjectRoot(fs, cwd, signal);
588
+ projectFiles.push(...await presentInDir(fs, root, INSTRUCTION_PROJECT_CANDIDATES, signal));
589
+ } catch {
590
+ // 项目链探测失败——忽略,仍报告用户全局文件。
591
+ }
592
+ const userGlobalFiles = [];
593
+ if (typeof dshHome === 'string' && dshHome.length > 0) {
594
+ try {
595
+ userGlobalFiles.push(...await presentInDir(fs, dshHome, [INSTRUCTION_USER_CANDIDATE], signal));
596
+ } catch {
597
+ // 不可读——忽略。
598
+ }
599
+ }
600
+ if (projectFiles.length === 0 && userGlobalFiles.length === 0) return null;
601
+ return { projectFiles, root, userGlobalFiles };
602
+ }
603
+
604
+ // ── 按需发现工具(resident discovery set,参照 dsh-anchored-standard)─────
605
+
606
+ /**
607
+ * 从 agent/pre-step 的 decision.messages 剥离指定 source.kind 的消息。
608
+ * 保留原数组引用(未变化时)便于调用方判断。
609
+ */
610
+ export function stripSuppressedMessages(messages, suppressedSources) {
611
+ if (!Array.isArray(messages) || suppressedSources.size === 0) return messages;
612
+ const kept = messages.filter((message) => {
613
+ const kind = message?.source?.kind;
614
+ return typeof kind !== 'string' || !suppressedSources.has(kind);
615
+ });
616
+ return kept.length === messages.length ? messages : kept;
617
+ }
618
+
619
+ /**
620
+ * dev_tool_search:搜索完整可见目录并按名解锁工具(解锁结果下一请求生效,
621
+ * 经 syncBootstrap 重算 deny 集;解锁记录写入 a.unlocked,resume-safe 由
622
+ * loadUnlockedFromEvents 从持久事件恢复)。
623
+ */
624
+ export function createDevToolSearch({ schemasOf, onUnlock, catalogHint }) {
625
+ const hint = catalogHint ?? [
626
+ 'This session starts with a minimal resident set: bash, str_replace_editor.',
627
+ 'Everything else is unlocked on demand through this tool.',
628
+ ].join('\n');
629
+ return {
630
+ name: 'dev_tool_search',
631
+ description: [
632
+ 'Discover and unlock tools that are NOT currently available.',
633
+ '',
634
+ hint,
635
+ '',
636
+ '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.',
637
+ '',
638
+ '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.',
639
+ ].join('\n'),
640
+ parameters: {
641
+ type: 'object',
642
+ properties: {
643
+ query: { type: 'string', description: 'search keywords (e.g. "web", "subagent")' },
644
+ toolNames: { type: 'array', description: 'exact tool names to unlock', items: { type: 'string' } },
645
+ },
646
+ required: [],
647
+ additionalProperties: false,
648
+ },
649
+ output: {
650
+ schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string' } }, required: ['text'] },
651
+ render: (_a, value) => [{ type: 'text', text: value.text }],
652
+ },
653
+ async execute(args, exec) {
654
+ const query = args !== null && typeof args === 'object' && typeof args.query === 'string' ? args.query.trim() : '';
655
+ const unlock = args !== null && typeof args === 'object' && Array.isArray(args.toolNames)
656
+ ? args.toolNames.filter((name) => typeof name === 'string' && name.length > 0)
657
+ : [];
658
+ const lines = [];
659
+ if (unlock.length > 0) {
660
+ let available = new Set();
661
+ try {
662
+ const schemas = typeof schemasOf === 'function' ? (schemasOf(exec) ?? []) : [];
663
+ for (const schema of schemas) {
664
+ if (schema !== null && typeof schema === 'object' && typeof schema.name === 'string') available.add(schema.name);
665
+ }
666
+ } catch {}
667
+ const ok = unlock.filter((name) => available.has(name));
668
+ if (ok.length > 0 && typeof onUnlock === 'function') {
669
+ try { await onUnlock(ok, exec); } catch {}
670
+ }
671
+ if (ok.length > 0) lines.push(`Unlocked for the next request: ${ok.join(', ')}`);
672
+ const missing = unlock.filter((name) => !available.has(name));
673
+ if (missing.length > 0) lines.push(`Not found in catalog: ${missing.join(', ')}`);
674
+ }
675
+ if (query.length === 0 && unlock.length === 0) {
676
+ lines.push('Provide `query` to search the catalog, or `toolNames` to unlock tools.');
677
+ return { text: lines.join('\n') };
678
+ }
679
+ if (query.length === 0) {
680
+ return { text: lines.join('\n') || 'Nothing to do.' };
681
+ }
682
+ try {
683
+ const schemas = typeof schemasOf === 'function' ? (schemasOf(exec) ?? []) : [];
684
+ const wanted = query.toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean);
685
+ const matches = schemas
686
+ .filter((schema) => {
687
+ if (schema === null || typeof schema !== 'object') return false;
688
+ const haystack = `${schema.name || ''} ${schema.description || ''}`.toLowerCase();
689
+ return wanted.every((token) => haystack.includes(token));
690
+ })
691
+ .slice(0, 25);
692
+ if (matches.length === 0) {
693
+ lines.push(`No tools match "${query}".`);
694
+ } else {
695
+ lines.push(`Matching tools (${matches.length}):`);
696
+ for (const schema of matches) {
697
+ const desc = String(schema.description || '').split('\n')[0].slice(0, 90);
698
+ lines.push(`- ${schema.name}: ${desc}`);
699
+ }
700
+ lines.push('Unlock with dev_tool_search({"toolNames": ["<exact name>"]}).');
701
+ }
702
+ } catch (error) {
703
+ lines.push(`catalog search unavailable: ${String((error && error.message) || error)}`);
704
+ }
705
+ return { text: lines.join('\n') };
706
+ },
707
+ };
708
+ }
709
+
710
+ /**
711
+ * skill_search:按关键词搜索可用技能(仅摘要,替代 ~9KB 目录注入)。
712
+ * skillsOf 返回宿主 skills 服务(调用期解析)。
713
+ */
714
+ export function createSkillSearch({ skillsOf }) {
715
+ const tokens = (text) => (text || '').toLowerCase().split(/[^a-z0-9_-]+/).filter(Boolean);
716
+ return {
717
+ name: 'skill_search',
718
+ 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.',
719
+ parameters: {
720
+ type: 'object',
721
+ properties: { query: { type: 'string', description: 'search keywords (e.g. "pdf", "obsidian", "game review")' } },
722
+ required: ['query'],
723
+ additionalProperties: false,
724
+ },
725
+ output: {
726
+ schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string' } }, required: ['text'] },
727
+ render: (_a, value) => [{ type: 'text', text: value.text }],
728
+ },
729
+ async execute(args, exec) {
730
+ const wanted = tokens(args?.query);
731
+ try {
732
+ const skills = skillsOf();
733
+ if (skills === void 0 || typeof skills.list !== 'function') {
734
+ return { text: 'skill_search unavailable: skills service is not mounted.' };
735
+ }
736
+ const agent = exec?.agent;
737
+ const lookup = {
738
+ scope: agent,
739
+ cwd: agent?.session?.header?.cwd,
740
+ signal: exec?.signal,
741
+ };
742
+ const all = await skills.list(lookup);
743
+ const matches = (Array.isArray(all) ? all : []).filter((skill) => {
744
+ if (wanted.length === 0) return true;
745
+ const haystack = tokens(`${skill?.name ?? ''} ${skill?.description ?? ''} ${skill?.whenToUse ?? ''}`).join(' ');
746
+ return wanted.every((token) => haystack.includes(token));
747
+ });
748
+ const head = matches.slice(0, 20);
749
+ const lines = head.map((skill) => {
750
+ const desc = String(skill?.description || '').split('\n')[0];
751
+ return `- ${skill?.name}: ${desc}`;
752
+ });
753
+ if (lines.length === 0) return { text: `No skills match "${args?.query}". Use skill_search with other keywords.` };
754
+ const extra = matches.length > 20 ? `\n…(${matches.length - 20} more)` : '';
755
+ return { text: `Matching skills (${matches.length}):\n${lines.join('\n')}${extra}\n\nLoad one with skill_load (exact name).` };
756
+ } catch (error) {
757
+ return { text: `skill_search unavailable: ${String((error && error.message) || error)}` };
758
+ }
759
+ },
760
+ };
761
+ }
762
+
763
+ /**
764
+ * skill_load:按精确名加载单个技能的完整说明,经 agent.inject 作为
765
+ * source.kind='skill-invocation' 的下一条上下文消息注入(用户主动技能手势
766
+ * 同款来源,绝不被默认抑制集剥离)。
767
+ */
768
+ export function createSkillLoad({ skillsOf }) {
769
+ return {
770
+ name: 'skill_load',
771
+ 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.',
772
+ parameters: {
773
+ type: 'object',
774
+ properties: { name: { type: 'string', description: 'exact skill name (kebab-case, from skill_search)' } },
775
+ required: ['name'],
776
+ additionalProperties: false,
777
+ },
778
+ output: {
779
+ schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string' } }, required: ['text'] },
780
+ render: (_a, value) => [{ type: 'text', text: value.text }],
781
+ },
782
+ async execute(args, exec) {
783
+ try {
784
+ const agent = exec?.agent;
785
+ if (agent === void 0) return { text: 'skill_load requires an agent context.' };
786
+ const skills = skillsOf();
787
+ if (skills === void 0 || typeof skills.get !== 'function') {
788
+ return { text: 'skill_load unavailable: skills service is not mounted.' };
789
+ }
790
+ const name = typeof args?.name === 'string' ? args.name.trim() : '';
791
+ if (name.length === 0) return { text: 'skill_load requires a skill name.' };
792
+ const skill = await skills.get(name, {
793
+ scope: agent,
794
+ cwd: agent.session?.header?.cwd,
795
+ signal: exec?.signal,
796
+ });
797
+ if (skill === void 0) {
798
+ return { text: `No skill named "${name}". Run skill_search to list available skills.` };
799
+ }
800
+ const body = extractSkillBody(skill);
801
+ if (body.length === 0) {
802
+ return { text: `Skill "${name}" has no loadable body.` };
803
+ }
804
+ if (typeof agent.inject !== 'function') {
805
+ return { text: `Skill "${name}" found, but injection is unavailable in this agent.` };
806
+ }
807
+ agent.inject({
808
+ id: `skill-load-${name}-${Date.now()}`,
809
+ role: 'user',
810
+ content: [{ type: 'text', text: body }],
811
+ source: { kind: 'skill-invocation', name, form: 'instructions' },
812
+ });
813
+ return { text: `Skill "${name}" loaded; its instructions will be injected for the next request.` };
814
+ } catch (error) {
815
+ return { text: `skill_load failed: ${String((error && error.message) || error)}` };
816
+ }
817
+ },
818
+ };
819
+ }
820
+
821
+ /** 提取已加载技能定义的模型可见正文(与参考实现一致)。 */
822
+ export function extractSkillBody(skill) {
823
+ const content = skill?.content ?? skill?.instructions ?? skill?.body;
824
+ if (typeof content === 'string') return content;
825
+ if (Array.isArray(content)) {
826
+ return content.map((part) => (typeof part === 'string' ? part : JSON.stringify(part))).join('\n');
827
+ }
828
+ return '';
829
+ }
830
+
374
831
  // ── bootstrap 锚定(参照 dsh-anchored-standard 的实测结论)──────────────
375
832
 
376
833
  /** promoteOn 配置 → 晋升的持久事件类型集合。 */
@@ -456,7 +913,7 @@ export function applyBootstrapBudget(config, promoted, maxTokens) {
456
913
 
457
914
  // ── host 插件 ────────────────────────────────────────────────────────────
458
915
 
459
- export function apply(ctx, config) {
916
+ export async function apply(ctx, config, options = {}) {
460
917
  // fail-safe:初始化失败只记录,绝不让本插件拖垮 harness(host 层挂载时
461
918
  // entry 异常会导致进程启动失败)。
462
919
  try {
@@ -477,6 +934,41 @@ export function apply(ctx, config) {
477
934
  /** 当前生效配置(热更新;后续 store 创建后以 config.json 权威合并结果覆盖)。 */
478
935
  let cfg = normalizeConfig(patchConfig);
479
936
 
937
+ /** 真实 Minimal 工具对模块(0.5.0)。加载失败 = null = 降级旧行为。
938
+ * options.realPairModules 仅供测试注入。 */
939
+ const realPairModules = options.realPairModules === void 0
940
+ ? (cfg.bootstrap.enabled && cfg.bootstrap.realPair ? await loadRealPairModules() : null)
941
+ : options.realPairModules;
942
+ /** 是否已尝试加载过真实工具对模块(避免配置热更新时反复重试)。 */
943
+ let realPairModulesTried = options.realPairModules !== void 0 || (cfg.bootstrap.enabled && cfg.bootstrap.realPair);
944
+ if (cfg.bootstrap.enabled && cfg.bootstrap.realPair && realPairModules === null) {
945
+ try {
946
+ ctx.logger?.warn?.('adaptive-perf: real Minimal tool pair modules unavailable (optional @deepseek-ai deps missing?); falling back to catalog-only bootstrap');
947
+ } catch {}
948
+ }
949
+
950
+ /** 已注入过 instruction-hint 的会话(内存去重,与参考实现一致)。 */
951
+ const hintedSessions = new Set();
952
+
953
+ /**
954
+ * 配置热更新把 realPair 从关闭切到开启时补加载官方模块(初始未启用时
955
+ * 不加载)。加载成功后对已接管会话补挂。
956
+ */
957
+ function ensureRealPairModules() {
958
+ if (realPairModulesTried || cfg.bootstrap.enabled === false || cfg.bootstrap.realPair === false) return;
959
+ realPairModulesTried = true;
960
+ loadRealPairModules().then((mods) => {
961
+ if (mods === null) return;
962
+ realPairModules = mods;
963
+ info('real Minimal tool pair modules loaded; mounting for live agents');
964
+ for (const a of agents.values()) {
965
+ syncRealPair(a);
966
+ applyMinimalPrompt(a);
967
+ syncBootstrap(a);
968
+ }
969
+ }).catch(() => {});
970
+ }
971
+
480
972
  /** agentId -> 会话自适应状态。 */
481
973
  const agents = new Map();
482
974
 
@@ -517,7 +1009,8 @@ export function apply(ctx, config) {
517
1009
  }
518
1010
  }
519
1011
 
520
- /** 应用/更新极简提示词层(persona 阴影 + 全局引导段屏蔽)。 */
1012
+ /** 应用/更新极简提示词层(persona 阴影 + 全局引导段屏蔽 +
1013
+ * 真实工具对替换后的 tool 引导段阴影)。 */
521
1014
  function applyMinimalPrompt(a) {
522
1015
  for (const disposer of a.promptDisposers) {
523
1016
  try { disposer(); } catch {}
@@ -531,6 +1024,11 @@ export function apply(ctx, config) {
531
1024
  a.promptDisposers.push(sp.section({ name, order, text: '' }));
532
1025
  }
533
1026
  }
1027
+ if (a.realPairActive) {
1028
+ for (const [name, order] of TOOL_GUIDANCE_SHADOWS) {
1029
+ a.promptDisposers.push(sp.section({ name, order, text: '' }));
1030
+ }
1031
+ }
534
1032
  const persona = cfg.minimalPrompt.persona.trim();
535
1033
  if (persona.length > 0) {
536
1034
  a.promptDisposers.push(sp.section({ name: PERSONA_SECTION_NAME, order: PERSONA_SECTION_ORDER, text: persona }));
@@ -595,6 +1093,111 @@ export function apply(ctx, config) {
595
1093
  info(`agent ${a.agent.id}: escalated family "${familyId}" (${trigger})`);
596
1094
  }
597
1095
 
1096
+ /** 从持久事件恢复 dev_tool_search 已解锁的工具(resume-safe)。 */
1097
+ function loadUnlockedFromEvents(a) {
1098
+ const events = a.agent.session && Array.isArray(a.agent.session.events) ? a.agent.session.events : [];
1099
+ for (const event of events) {
1100
+ if (event === null || typeof event !== 'object' || event.type !== 'tool/call') continue;
1101
+ if (event.data === null || typeof event.data !== 'object' || event.data.name !== 'dev_tool_search') continue;
1102
+ let args = event.data.arguments;
1103
+ if (typeof args === 'string') {
1104
+ try { args = JSON.parse(args); } catch { continue; }
1105
+ }
1106
+ if (args === null || typeof args !== 'object' || Array.isArray(args)) continue;
1107
+ if (Array.isArray(args.toolNames)) {
1108
+ for (const name of args.toolNames) {
1109
+ if (typeof name === 'string' && name.length > 0) a.unlocked.add(name);
1110
+ }
1111
+ }
1112
+ }
1113
+ }
1114
+
1115
+ /**
1116
+ * 挂载/卸载真实 Minimal 工具对(0.5.0):把官方 minimal preset 同款插件
1117
+ * (持久 PTY bash + str_replace_editor 及其依赖的 terminals/本地 fs)
1118
+ * 挂进 agent 作用域。scoped registration 按名阴影 standard 的 sandboxed
1119
+ * bash,且自身层注册不受 restrict 影响(view() 对 own layer 豁免)——
1120
+ * 首轮目录因此是"真实工具对 + 被 deny 的其他继承工具"。
1121
+ * 依赖模块缺失(realPairModules === null)或 Windows(PTY 仅 linux/darwin)
1122
+ * 时静默降级:不挂载,退回旧行为(仅收窄目录)。
1123
+ */
1124
+ function syncRealPair(a) {
1125
+ for (const dispose of a.realPairDisposers) {
1126
+ try { dispose(); } catch {}
1127
+ }
1128
+ a.realPairDisposers = [];
1129
+ const active = cfg.enabled && cfg.bootstrap.enabled && cfg.bootstrap.realPair
1130
+ && realPairModules !== null && process.platform !== 'win32';
1131
+ if (active) {
1132
+ const mounts = realPairMounts(realPairModules, a.agent.session?.header?.cwd);
1133
+ a.realPairDisposers = mountRealPair(a.agent, mounts, warn);
1134
+ if (a.realPairDisposers.length > 0) {
1135
+ info(`agent ${a.agent.id}: real Minimal tool pair mounted (${mounts.length} plugins)`);
1136
+ }
1137
+ }
1138
+ a.realPairActive = a.realPairDisposers.length > 0;
1139
+ }
1140
+
1141
+ /**
1142
+ * 给目标 agent 注册 on-demand 发现工具(dsh-anchored-standard 的 resident
1143
+ * discovery pattern):晋升后常驻 bootstrap.discoveryTools 列出的工具——
1144
+ * dev_tool_search(搜索完整目录并按名解锁,解锁结果下一请求生效,写入
1145
+ * a.unlocked 后 syncBootstrap 重算 deny 集)+ skill_search / skill_load
1146
+ * (常驻上下文抑制的替代品:按需发现/加载技能)。
1147
+ * 已存在的同名工具(preset 自带)不重复注册。
1148
+ */
1149
+ function syncDiscoveryTools(a, enabled) {
1150
+ const want = enabled ? cfg.bootstrap.discoveryTools : [];
1151
+ for (const [name, disposer] of a.toolDisposers) {
1152
+ if (!want.includes(name)) {
1153
+ try { disposer(); } catch {}
1154
+ a.toolDisposers.delete(name);
1155
+ }
1156
+ }
1157
+ if (!enabled) return;
1158
+ const scopedTools = a.agent.ctx && a.agent.ctx.tools;
1159
+ if (scopedTools === void 0 || typeof scopedTools.register !== 'function') return;
1160
+ for (const name of want) {
1161
+ if (a.toolDisposers.has(name)) continue;
1162
+ if (a.visibleNames.has(name)) continue; // preset/宿主已提供同名工具
1163
+ const def = discoveryToolDef(a, name);
1164
+ if (def === null) continue;
1165
+ try {
1166
+ const disposer = scopedTools.register(def);
1167
+ if (typeof disposer === 'function') a.toolDisposers.set(name, disposer);
1168
+ } catch (error) {
1169
+ warn(`register ${name} failed for agent ${a.agent.id}: ${error && error.message || error}`);
1170
+ }
1171
+ }
1172
+ }
1173
+
1174
+ /** 构造一个发现工具定义;未知名称返回 null。 */
1175
+ function discoveryToolDef(a, name) {
1176
+ if (name === 'dev_tool_search') {
1177
+ return createDevToolSearch({
1178
+ schemasOf: (exec) => {
1179
+ const agent = exec !== null && typeof exec === 'object' ? exec.agent : void 0;
1180
+ return agent === void 0 ? [] : tools.schemas(agent);
1181
+ },
1182
+ onUnlock: async (names, exec) => {
1183
+ const agent = exec !== null && typeof exec === 'object' ? exec.agent : void 0;
1184
+ const state = agent === void 0 ? void 0 : agents.get(agent.id);
1185
+ if (state === void 0) return;
1186
+ for (const name of names) state.unlocked.add(name);
1187
+ syncBootstrap(state);
1188
+ },
1189
+ 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.',
1190
+ });
1191
+ }
1192
+ if (name === 'skill_search' && cfg.skillDiscovery) {
1193
+ return createSkillSearch({ skillsOf: () => ctx.get('skills') });
1194
+ }
1195
+ if (name === 'skill_load' && cfg.skillDiscovery) {
1196
+ return createSkillLoad({ skillsOf: () => ctx.get('skills') });
1197
+ }
1198
+ return null;
1199
+ }
1200
+
598
1201
  /** 完整接管一个 agent(幂等)。 */
599
1202
  function handleAgent(agent) {
600
1203
  if (agents.has(agent.id)) return;
@@ -608,6 +1211,10 @@ export function apply(ctx, config) {
608
1211
  bootstrapKey: null,
609
1212
  escalated: new Set(),
610
1213
  visibleNames: new Set(),
1214
+ unlocked: new Set(),
1215
+ toolDisposers: new Map(),
1216
+ realPairDisposers: [],
1217
+ realPairActive: false,
611
1218
  };
612
1219
  // 记录当前可见目录(限制前);仅记录真实存在的工具,后续 restrict 与
613
1220
  // 实际目录取交集,preset 升级改名/删除工具不会让本插件崩溃。
@@ -620,8 +1227,10 @@ export function apply(ctx, config) {
620
1227
  } catch (error) {
621
1228
  warn(`schemas lookup failed for agent ${agent.id}: ${error && error.message || error}`);
622
1229
  }
1230
+ loadUnlockedFromEvents(a);
623
1231
  agents.set(agent.id, a);
624
1232
  applySuppression(a);
1233
+ syncRealPair(a);
625
1234
  applyMinimalPrompt(a);
626
1235
  applyFamilies(a);
627
1236
  syncBootstrap(a);
@@ -641,6 +1250,14 @@ export function apply(ctx, config) {
641
1250
  try { disposer(); } catch {}
642
1251
  }
643
1252
  a.promptDisposers = [];
1253
+ for (const disposer of a.toolDisposers.values()) {
1254
+ try { disposer(); } catch {}
1255
+ }
1256
+ a.toolDisposers.clear();
1257
+ for (const dispose of a.realPairDisposers) {
1258
+ try { dispose(); } catch {}
1259
+ }
1260
+ a.realPairDisposers = [];
644
1261
  if (a.bootstrapDisposer !== null) {
645
1262
  try { a.bootstrapDisposer(); } catch {}
646
1263
  }
@@ -731,8 +1348,12 @@ export function apply(ctx, config) {
731
1348
  }
732
1349
 
733
1350
  /**
734
- * bootstrap 阶段的保留工具集:bootstrap 工具对 + PTC 的直接调用工具
735
- * (run_code)+ compaction 后恢复期的工作集。已晋升返回 null。
1351
+ * bootstrap 阶段的保留工具集:
1352
+ * - 未晋升:bootstrap 工具对 + PTC 的直接调用工具(run_code)+
1353
+ * compaction 后恢复期的工作集;
1354
+ * - 已晋升:bootstrap 工具对 + resident discovery 工具 +
1355
+ * dev_tool_search 已解锁工具 + 已升级工具族(dsh-anchored-standard 的
1356
+ * resident catalog,避免晋升后一次性 dump 完整 Standard 目录)。
736
1357
  *
737
1358
  * 注意:不能靠过滤 system-prompt/assemble 的 assembly.tools 来实现——
738
1359
  * PTC 模式下 assembly.tools 只有 [run_code],Minimal 工具对不在其中,
@@ -741,17 +1362,32 @@ export function apply(ctx, config) {
741
1362
  * SDK 参考段,且天然保留 run_code(不在 deny 里)。
742
1363
  */
743
1364
  function bootstrapKeepSet(a) {
1365
+ if (!cfg.enabled || !cfg.bootstrap.enabled) return null;
1366
+ const session = a.agent.session;
1367
+ if (session === void 0) return null; // 无会话信息时保持既有行为(测试/未知场景)
744
1368
  const phase = bootstrapPhaseOf(a.agent);
745
- if (phase.promoted) return null;
746
1369
  const keep = new Set(cfg.bootstrap.tools);
747
1370
  if (a.visibleNames.has('run_code')) keep.add('run_code');
1371
+ if (phase.promoted) {
1372
+ for (const name of cfg.bootstrap.discoveryTools) keep.add(name);
1373
+ for (const name of a.unlocked) keep.add(name);
1374
+ for (const [id, family] of Object.entries(cfg.families)) {
1375
+ if (a.escalated.has(id)) {
1376
+ for (const name of familyDeny(a, family)) keep.add(name);
1377
+ }
1378
+ }
1379
+ return keep;
1380
+ }
748
1381
  if (phase.boundary >= 0) for (const name of cfg.bootstrap.compactionTools) keep.add(name);
749
1382
  return keep;
750
1383
  }
751
1384
 
752
1385
  /** 按当前阶段同步 bootstrap 的临时 restrict(幂等;仅阶段/配置变化时重挂)。 */
753
1386
  function syncBootstrap(a) {
1387
+ const phase = bootstrapPhaseOf(a.agent);
754
1388
  const keep = bootstrapKeepSet(a);
1389
+ // discovery 工具只在晋升后的 resident 阶段注册;bootstrap/compaction 阶段隐藏。
1390
+ syncDiscoveryTools(a, keep !== null && phase.promoted);
755
1391
  const key = keep === null ? null : [...keep].sort().join(',');
756
1392
  if (key === a.bootstrapKey) return;
757
1393
  if (a.bootstrapDisposer !== null) {
@@ -785,18 +1421,59 @@ export function apply(ctx, config) {
785
1421
  }
786
1422
  });
787
1423
 
788
- // 首轮剥离自动注入的上下文(技能目录提醒 / AGENTS.md 摘要)。prepend +
789
- // 根作用域注册保证是最后一道变换(后注册的注入者无法再补回)。
790
- ctx.on('agent/pre-step', async ({ agent }, next) => {
1424
+ // 上下文剥离 + instruction-hint。prepend + 根作用域注册保证是最后一道
1425
+ // 变换(后注册的注入者无法再补回)。两个职责同在一个监听器里,顺序天然
1426
+ // 正确:先剥离被抑制来源,再(晋升后一次性)追加指令文件提示。
1427
+ // 剥离与 bootstrap 开关正交:常驻抑制(suppressInjectedContext)在
1428
+ // bootstrap 关闭时也生效;instruction-hint 依赖晋升阶段(bootstrap 机制)。
1429
+ ctx.on('agent/pre-step', async ({ agent, signal }, next) => {
791
1430
  const decision = await next();
792
1431
  if (decision === null || typeof decision !== 'object' || decision.kind === 'reject') return decision;
793
1432
  try {
794
- if (!bootstrapActiveFor(agent)) return decision;
1433
+ if (!cfg.enabled || !isTarget(agent)) return decision;
795
1434
  const phase = bootstrapPhaseOf(agent);
796
- if (phase.promoted || cfg.bootstrap.suppressedContextSources.length === 0) return decision;
797
- if (!Array.isArray(decision.messages)) return decision;
798
- const kept = filterBootstrapMessages(decision.messages, new Set(cfg.bootstrap.suppressedContextSources));
799
- return kept === decision.messages ? decision : { ...decision, messages: kept };
1435
+ const suppressed = new Set(cfg.bootstrap.suppressedContextSources);
1436
+ let out = decision;
1437
+ // 剥离条件:bootstrap 未晋升(旧行为)或常驻抑制开启(0.5.0 默认:
1438
+ // 整个会话不再注入技能目录与 AGENTS.md 摘要,替代品是按需发现工具)。
1439
+ const stripActive = (cfg.bootstrap.enabled && !phase.promoted) || cfg.suppressInjectedContext;
1440
+ if (suppressed.size > 0 && stripActive && Array.isArray(out.messages)) {
1441
+ const kept = stripSuppressedMessages(out.messages, suppressed);
1442
+ out = kept === out.messages ? out : { ...out, messages: kept };
1443
+ }
1444
+ // instruction-hint:晋升后一次性注入"指令文件存在,需要时自读"。
1445
+ if (cfg.bootstrap.enabled && cfg.instructionHint && phase.promoted && Array.isArray(out.messages)) {
1446
+ const session = agent.session;
1447
+ if (session !== null && typeof session === 'object' && typeof session.id === 'string' && !hintedSessions.has(session.id)) {
1448
+ hintedSessions.add(session.id);
1449
+ const fs = ctx.get('fs');
1450
+ const dshHome = process.env.DSH_HOME?.trim() || `${process.env.HOME || process.env.USERPROFILE || ''}/.dsh`;
1451
+ const probe = await probeInstructionFiles(fs, session.header?.cwd, dshHome, signal);
1452
+ if (probe !== null) {
1453
+ const sections = [];
1454
+ if (probe.projectFiles.length > 0) {
1455
+ sections.push(`Workspace instruction files exist: ${probe.projectFiles.join(', ')} (project root: ${probe.root}).`);
1456
+ }
1457
+ if (probe.userGlobalFiles.length > 0) {
1458
+ sections.push(`A user-global instruction file exists: ${INSTRUCTION_USER_CANDIDATE}.`);
1459
+ }
1460
+ const text = [
1461
+ ...sections,
1462
+ 'Do NOT assume their content. When a task touches this workspace, read the relevant instruction files first and follow them.',
1463
+ ].join(' ');
1464
+ out = {
1465
+ ...out,
1466
+ messages: [...out.messages, {
1467
+ id: `instruction-hint-${session.id}`,
1468
+ role: 'user',
1469
+ content: [{ type: 'text', text }],
1470
+ source: { kind: 'instruction-hint', form: 'hint' },
1471
+ }],
1472
+ };
1473
+ }
1474
+ }
1475
+ }
1476
+ return out;
800
1477
  } catch (error) {
801
1478
  // 过滤失败绝不吞掉用户的上下文:原样放行。
802
1479
  warnBootstrapOnce(`bootstrap context filter failed, keeping injected context: ${String((error && error.message) || error)}`);
@@ -839,6 +1516,8 @@ export function apply(ctx, config) {
839
1516
  onUpdate: (merged) => {
840
1517
  cfg = normalizeConfig({ ...patchConfig, ...merged });
841
1518
  info('config hot-updated; recomputing restrictions for live agents');
1519
+ // realPair 从关闭切到开启:补加载官方模块(首次尝试失败不反复重试)。
1520
+ ensureRealPairModules();
842
1521
  // 补挂此前创建的会话(例如插件从 disabled 切换为 enabled,或新增了
843
1522
  // 目标 preset):新符合条件的 agent 立即接管(handleAgent 已应用全部
844
1523
  // 层,下面的重算只针对此前已接管的 agent,避免同一 onUpdate 内重复)。
@@ -854,6 +1533,7 @@ export function apply(ctx, config) {
854
1533
  const a = agents.get(agentId);
855
1534
  if (a === void 0) continue;
856
1535
  applySuppression(a);
1536
+ syncRealPair(a);
857
1537
  applyMinimalPrompt(a);
858
1538
  applyFamilies(a);
859
1539
  syncBootstrap(a);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@chaoset/adaptive-perf",
3
- "version": "0.4.0",
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",
3
+ "version": "0.5.0",
4
+ "description": "DSH host plugin: bring standard / PTC presets to minimal-mode-level performance real Minimal tool pair, permanent injected-context suppression with on-demand discovery, runtime-context suppression, and adaptive tool-catalog trimming with demand-driven escalation",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "files": [
@@ -31,7 +31,12 @@
31
31
  }
32
32
  },
33
33
  "optionalDependencies": {
34
- "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6"
34
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6",
35
+ "@deepseek-ai/dsh-terminal": "^0.1.0-rc.6",
36
+ "@deepseek-ai/dsh-terminal-bash": "^0.1.0-rc.6",
37
+ "@deepseek-ai/dsh-tool-bash-persistent": "^0.1.0-rc.6",
38
+ "@deepseek-ai/dsh-fs-local": "^0.1.0-rc.6",
39
+ "@deepseek-ai/dsh-tool-str-replace-editor": "^0.1.0-rc.6"
35
40
  },
36
41
  "engines": {
37
42
  "node": ">=22.19.0"