dsh-plugin-prompt-tool 0.4.2 → 0.6.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.
Files changed (191) hide show
  1. package/README.md +131 -222
  2. package/engine/anchor-match.mjs +117 -0
  3. package/engine/compaction-epoch.mjs +139 -0
  4. package/engine/compositions/library/bootstrap-filesystem.yml +19 -0
  5. package/engine/compositions/library/compaction.yml +52 -0
  6. package/engine/compositions/library/context-gate.yml +40 -0
  7. package/engine/compositions/library/custom-bash.yml +14 -0
  8. package/engine/compositions/library/delegation.yml +85 -0
  9. package/engine/compositions/library/official-agent-instructions.yml +17 -0
  10. package/engine/compositions/library/official-persistent-shell.yml +56 -0
  11. package/engine/compositions/library/official-skill-filesystem-cordis.yml +10 -0
  12. package/engine/compositions/library/official-tool-bash.yml +6 -0
  13. package/engine/compositions/library/official-tool-cordis.yml +13 -0
  14. package/engine/compositions/library/official-tool-presentation.yml +7 -0
  15. package/engine/compositions/library/official-tool-skill.yml +14 -0
  16. package/engine/compositions/library/persistent-shell.yml +36 -0
  17. package/engine/compositions/library/persona.yml +9 -0
  18. package/engine/compositions/library/planning.yml +41 -0
  19. package/engine/compositions/library/prompt-config-engine.yml +16 -0
  20. package/engine/compositions/library/run-code-env.yml +15 -0
  21. package/engine/compositions/library/skill-filesystem.yml +13 -0
  22. package/engine/compositions/library/skill-search.yml +14 -0
  23. package/engine/compositions/library/str-replace-editor.yml +10 -0
  24. package/engine/compositions/library/tool-ask-user.yml +8 -0
  25. package/engine/compositions/library/tool-bash.yml +16 -0
  26. package/engine/compositions/library/tool-bootstrap.yml +16 -0
  27. package/engine/compositions/library/tool-filter.yml +12 -0
  28. package/engine/compositions/library/tool-fs-search.yml +18 -0
  29. package/engine/compositions/library/tool-fs.yml +10 -0
  30. package/engine/compositions/library/tool-goal.yml +19 -0
  31. package/engine/compositions/library/tool-jobs.yml +23 -0
  32. package/engine/compositions/library/tool-pwsh.yml +12 -0
  33. package/engine/compositions/library/tool-todo.yml +11 -0
  34. package/engine/compositions/library/tool-web.yml +9 -0
  35. package/engine/compositions/source/local/context-gate.yml +37 -0
  36. package/engine/compositions/source/local/custom-bash.yml +11 -0
  37. package/engine/compositions/source/local/prompt-config-engine.yml +13 -0
  38. package/engine/compositions/source/local/run-code-env.yml +12 -0
  39. package/engine/compositions/source/local/skill-search.yml +11 -0
  40. package/engine/compositions/source/local/tool-bootstrap.yml +13 -0
  41. package/engine/context-gate.mjs +305 -0
  42. package/{preset → engine}/custom-bash.mjs +243 -243
  43. package/engine/executor.mjs +278 -0
  44. package/engine/fillers.mjs +273 -0
  45. package/engine/interpolate.mjs +66 -0
  46. package/engine/layers.mjs +224 -0
  47. package/engine/prompt-config-engine.mjs +49 -0
  48. package/engine/run-code-env.mjs +208 -0
  49. package/engine/schema.mjs +312 -0
  50. package/engine/session-vars.mjs +52 -0
  51. package/{preset → engine}/shared.mjs +35 -0
  52. package/engine/strategies.mjs +213 -0
  53. package/{preset → engine}/tool-bootstrap.mjs +350 -282
  54. package/engine/tool-filter.mjs +80 -0
  55. package/engine/vendor/yaml/LICENSE +13 -0
  56. package/engine/vendor/yaml/dist/compose/compose-collection.js +88 -0
  57. package/engine/vendor/yaml/dist/compose/compose-doc.js +43 -0
  58. package/engine/vendor/yaml/dist/compose/compose-node.js +109 -0
  59. package/engine/vendor/yaml/dist/compose/compose-scalar.js +86 -0
  60. package/engine/vendor/yaml/dist/compose/composer.js +219 -0
  61. package/engine/vendor/yaml/dist/compose/resolve-block-map.js +115 -0
  62. package/engine/vendor/yaml/dist/compose/resolve-block-scalar.js +198 -0
  63. package/engine/vendor/yaml/dist/compose/resolve-block-seq.js +49 -0
  64. package/engine/vendor/yaml/dist/compose/resolve-end.js +37 -0
  65. package/engine/vendor/yaml/dist/compose/resolve-flow-collection.js +207 -0
  66. package/engine/vendor/yaml/dist/compose/resolve-flow-scalar.js +225 -0
  67. package/engine/vendor/yaml/dist/compose/resolve-props.js +146 -0
  68. package/engine/vendor/yaml/dist/compose/util-contains-newline.js +34 -0
  69. package/engine/vendor/yaml/dist/compose/util-empty-scalar-position.js +26 -0
  70. package/engine/vendor/yaml/dist/compose/util-flow-indent-check.js +15 -0
  71. package/engine/vendor/yaml/dist/compose/util-map-includes.js +13 -0
  72. package/engine/vendor/yaml/dist/doc/Document.js +335 -0
  73. package/engine/vendor/yaml/dist/doc/anchors.js +71 -0
  74. package/engine/vendor/yaml/dist/doc/applyReviver.js +55 -0
  75. package/engine/vendor/yaml/dist/doc/createNode.js +88 -0
  76. package/engine/vendor/yaml/dist/doc/directives.js +176 -0
  77. package/engine/vendor/yaml/dist/errors.js +57 -0
  78. package/engine/vendor/yaml/dist/index.js +17 -0
  79. package/engine/vendor/yaml/dist/log.js +11 -0
  80. package/engine/vendor/yaml/dist/nodes/Alias.js +116 -0
  81. package/engine/vendor/yaml/dist/nodes/Collection.js +147 -0
  82. package/engine/vendor/yaml/dist/nodes/Node.js +38 -0
  83. package/engine/vendor/yaml/dist/nodes/Pair.js +36 -0
  84. package/engine/vendor/yaml/dist/nodes/Scalar.js +24 -0
  85. package/engine/vendor/yaml/dist/nodes/YAMLMap.js +144 -0
  86. package/engine/vendor/yaml/dist/nodes/YAMLSeq.js +113 -0
  87. package/engine/vendor/yaml/dist/nodes/addPairToJSMap.js +63 -0
  88. package/engine/vendor/yaml/dist/nodes/identity.js +36 -0
  89. package/engine/vendor/yaml/dist/nodes/toJS.js +37 -0
  90. package/engine/vendor/yaml/dist/parse/cst-scalar.js +214 -0
  91. package/engine/vendor/yaml/dist/parse/cst-stringify.js +61 -0
  92. package/engine/vendor/yaml/dist/parse/cst-visit.js +97 -0
  93. package/engine/vendor/yaml/dist/parse/cst.js +98 -0
  94. package/engine/vendor/yaml/dist/parse/lexer.js +721 -0
  95. package/engine/vendor/yaml/dist/parse/line-counter.js +39 -0
  96. package/engine/vendor/yaml/dist/parse/parser.js +975 -0
  97. package/engine/vendor/yaml/dist/public-api.js +102 -0
  98. package/engine/vendor/yaml/dist/schema/Schema.js +37 -0
  99. package/engine/vendor/yaml/dist/schema/common/map.js +17 -0
  100. package/engine/vendor/yaml/dist/schema/common/null.js +15 -0
  101. package/engine/vendor/yaml/dist/schema/common/seq.js +17 -0
  102. package/engine/vendor/yaml/dist/schema/common/string.js +14 -0
  103. package/engine/vendor/yaml/dist/schema/core/bool.js +19 -0
  104. package/engine/vendor/yaml/dist/schema/core/float.js +43 -0
  105. package/engine/vendor/yaml/dist/schema/core/int.js +38 -0
  106. package/engine/vendor/yaml/dist/schema/core/schema.js +23 -0
  107. package/engine/vendor/yaml/dist/schema/json/schema.js +62 -0
  108. package/engine/vendor/yaml/dist/schema/tags.js +96 -0
  109. package/engine/vendor/yaml/dist/schema/yaml-1.1/binary.js +58 -0
  110. package/engine/vendor/yaml/dist/schema/yaml-1.1/bool.js +26 -0
  111. package/engine/vendor/yaml/dist/schema/yaml-1.1/float.js +46 -0
  112. package/engine/vendor/yaml/dist/schema/yaml-1.1/int.js +71 -0
  113. package/engine/vendor/yaml/dist/schema/yaml-1.1/merge.js +67 -0
  114. package/engine/vendor/yaml/dist/schema/yaml-1.1/omap.js +74 -0
  115. package/engine/vendor/yaml/dist/schema/yaml-1.1/pairs.js +78 -0
  116. package/engine/vendor/yaml/dist/schema/yaml-1.1/schema.js +39 -0
  117. package/engine/vendor/yaml/dist/schema/yaml-1.1/set.js +93 -0
  118. package/engine/vendor/yaml/dist/schema/yaml-1.1/timestamp.js +101 -0
  119. package/engine/vendor/yaml/dist/stringify/foldFlowLines.js +146 -0
  120. package/engine/vendor/yaml/dist/stringify/stringify.js +129 -0
  121. package/engine/vendor/yaml/dist/stringify/stringifyCollection.js +153 -0
  122. package/engine/vendor/yaml/dist/stringify/stringifyComment.js +20 -0
  123. package/engine/vendor/yaml/dist/stringify/stringifyDocument.js +85 -0
  124. package/engine/vendor/yaml/dist/stringify/stringifyNumber.js +25 -0
  125. package/engine/vendor/yaml/dist/stringify/stringifyPair.js +150 -0
  126. package/engine/vendor/yaml/dist/stringify/stringifyString.js +336 -0
  127. package/engine/vendor/yaml/dist/util.js +11 -0
  128. package/engine/vendor/yaml/dist/visit.js +233 -0
  129. package/engine/vendor/yaml/index.js +5 -0
  130. package/engine/vendor/yaml/package.json +11 -0
  131. package/lib/client.js +4724 -775
  132. package/lib/client.js.map +1 -1
  133. package/lib/index.d.mts +530 -51
  134. package/lib/index.mjs +4164 -740
  135. package/lib/preset-core.d.mts +7 -37
  136. package/lib/preset-core.mjs +43 -285
  137. package/lib/prompt-configs-B4vH09wx.d.mts +100 -0
  138. package/lib/prompt-configs-ThS4iXPg.mjs +771 -0
  139. package/package.json +36 -29
  140. package/preset/anchored/preset.yml +342 -0
  141. package/preset/creative/preset.yml +65 -0
  142. package/preset/creative/skills/cordis-plugin-development/SKILL.md +420 -0
  143. package/preset/creative/skills/editing-cordis-compositions/SKILL.md +165 -0
  144. package/preset/custom/preset.yml +13 -0
  145. package/preset/liangshen/preset.yml +72 -0
  146. package/preset/minimal/preset.yml +44 -0
  147. package/preset/ptc/preset.yml +56 -0
  148. package/preset/standard/preset.yml +55 -0
  149. package/skills/manifest.json +7 -0
  150. package/skills/sandboxmod/SKILL.md +49 -49
  151. package/skills/web ui/SKILL.md +42 -0
  152. package/templates/10-pre-step.yml +28 -0
  153. package/templates/11-merged-a.yml +11 -0
  154. package/templates/13-anchor.yml +19 -0
  155. package/templates/14-first-turn-anchor.yml +27 -0
  156. package/templates/15-guide-auto.yml +25 -0
  157. package/templates/16-custom-fallback.yml +21 -0
  158. package/templates/17-instruction-hint.yml +23 -0
  159. package/templates/18-placeholder-env-facts.yml +11 -0
  160. package/templates/19-placeholder-skill-catalog.yml +19 -0
  161. package/templates/20-system-section.yml +19 -0
  162. package/templates/30-runtime-context.yml +10 -0
  163. package/templates/31-runtime-context-placeholder.yml +18 -0
  164. package/templates/40-agent-request.yml +11 -0
  165. package/templates/50-llm-stream.yml +9 -0
  166. package/templates/60-tool-pipeline.yml +12 -0
  167. package/AGENTS.md +0 -4
  168. package/plan.md +0 -312
  169. package/preset/agent.cordis.yml +0 -443
  170. package/preset/compaction-epoch.mjs +0 -81
  171. package/preset/context-gate.mjs +0 -165
  172. package/preset/instruction-hint.mjs +0 -217
  173. package/preset/near-anchor.mjs +0 -101
  174. package/preset/preset.yml +0 -3
  175. package/preset/prompt-injector.mjs +0 -112
  176. package/preset/router-first-turn.mjs +0 -73
  177. package/preset/router-guide.mjs +0 -79
  178. package/preset.md +0 -115
  179. package/upstream/dsh-anchored-standard/LICENSE +0 -22
  180. package/upstream/dsh-anchored-standard/NOTICE +0 -19
  181. package/upstream/dsh-anchored-standard/REVISION +0 -1
  182. package/upstream/dsh-anchored-standard/preset/agent.cordis.yml +0 -440
  183. package/upstream/dsh-anchored-standard/preset/compaction-epoch.mjs +0 -81
  184. package/upstream/dsh-anchored-standard/preset/context-gate.mjs +0 -202
  185. package/upstream/dsh-anchored-standard/preset/custom-bash.mjs +0 -219
  186. package/upstream/dsh-anchored-standard/preset/dev-tool-search.mjs +0 -131
  187. package/upstream/dsh-anchored-standard/preset/instruction-hint.mjs +0 -231
  188. package/upstream/dsh-anchored-standard/preset/preset.yml +0 -3
  189. package/upstream/dsh-anchored-standard/preset/skill-search.mjs +0 -142
  190. package/upstream/dsh-anchored-standard/preset/tool-bootstrap.mjs +0 -301
  191. /package/{preset → engine}/skill-search.mjs +0 -0
@@ -1,282 +1,350 @@
1
- /**
2
- * Anchored tool bootstrap — keep the FIRST model request on the Minimal
3
- * preset's REAL tool schema (persistent `bash` + `str_replace_editor`), then
4
- * keep the assembled catalog once the session has produced its first durable
5
- * promotion signal; `usePtcMode` optionally switches the wire presentation to
6
- * Code Mode (PTC). Injected-context control lives in the companion
7
- * `context-gate` plugin, not here.
8
- *
9
- * The phase is derived from durable session events, so resume and reload
10
- * preserve it. By default (`promoteOn: 'either'`) a session promotes after the
11
- * first `tool/call` OR the first `assistant/message`, whichever comes first:
12
- * request #1 always sees the bootstrap catalog and later requests keep the
13
- * assembled catalog. The original `'tool-call'` mode is kept for compatibility,
14
- * but it can trap a session in bootstrap forever when the first model reply
15
- * makes no tool call — the `'either'` default removes that trap while keeping
16
- * the first-request anchor intact.
17
- *
18
- * First-request conditions established by the reproduction work (issues #6
19
- * and #11, 2026-08-15):
20
- *
21
- * 1. Tool schema. The API-visible first-request catalog decides whether the
22
- * session anchors on the Minimal trajectory. At the adapter-default
23
- * maxTokens (256000 on the official endpoint) the Minimal tool pair —
24
- * persistent `bash` + `str_replace_editor` — anchored 5/5 runs with zero
25
- * `let me` first-lines, while every standard-family schema (pwsh/read,
26
- * pwsh only, sandboxed bash/read) fell into standard-like behavior
27
- * (11/11). Bootstrap therefore exposes exactly the Minimal pair, not
28
- * Standard's `pwsh`/`read`.
29
- *
30
- * 2. Output budget. On the official endpoint the first request's `max_tokens`
31
- * also dominated the trajectory anchor at 1024 (`We need` style in 26/32
32
- * runs against 0/5 at 256000, independent of tool descriptions). The
33
- * Minimal tool schema, however, anchors at 256000 WITHOUT any cap, and the
34
- * cap's delivery depends on the profile package's `prepareCall` behavior
35
- * (it reaches the request on the 0.1.0-rc.5 source checkout; a prebuilt
36
- * rc.6-reporting profile package observed in issue #11 overwrote it with
37
- * `adapterDefaults.maxTokens`). `bootstrapMaxTokens` is therefore OPT-IN:
38
- * leave it unset to run the Minimal schema at the adapter default, or set
39
- * it to cap the first request. When set, the cap is stripped after
40
- * promotion — the next request's seed proposal carries the previous
41
- * header's maxTokens forward, so the release must be explicit.
42
- *
43
- * 3. Injected context is NOT this plugin's concern: the companion
44
- * `context-gate` plugin (shared/context-gate.mjs, mounted as the FIRST
45
- * row) owns the unified injection control — runtime-context suppression
46
- * on the assembly path and a claimed-baseline deny on the pre-step
47
- * waterfall, both keyed to the same epoch-aware promotion phase. Mount it
48
- * separately for context control alone; this file narrows only the tool
49
- * catalog (plus the optional output cap below).
50
- *
51
- * SUBAGENTS: by default subagents (delegationDepth > 0) are always promoted
52
- * (assembled catalog from their first request). `includeSubagents: true`
53
- * makes them follow the same bootstrap phase — their first request also sees
54
- * the bootstrap pair, and their own first reply or tool call promotes them.
55
- * Keep this flag in sync with the context-gate row's flag.
56
- *
57
- * POST-PROMOTION CATALOG (prompt-tool patch): after promotion both modes
58
- * keep the assembled catalog. `usePtcMode` switches the wire presentation
59
- * to Code Mode (PTC, single run_code) instead of narrowing the resident set.
60
- * The controlled phase below still narrows the catalog before promotion and
61
- * after compaction.
62
- * COMPACTION (local addition): a compaction rewrites the whole surface, so the
63
- * first post-compaction request is a "second first request". Promotion is
64
- * epoch-aware (see compaction-epoch.mjs): after `compaction/end` the session
65
- * falls back to the controlled phase — the bootstrap pair plus
66
- * `compactionTools` (a core work set, default none) — until a NEW durable
67
- * promotion signal exists past that boundary. The model is mid-task and needs
68
- * to keep working, but still faces a small catalog instead of the full
69
- * Standard set.
70
- *
71
- * Robustness:
72
- * - Promotion decisions are memoized per session id for this process; the
73
- * durable event scan runs once per session per process, then O(1).
74
- * - Subagents (delegationDepth > 0) are always promoted (assembled catalog)
75
- * unless `includeSubagents: true`.
76
- * - A missing bootstrap tool degrades to the full catalog with a one-time
77
- * warning instead of throwing, so a composition drift can never brick
78
- * every request of a session.
79
- * - Invalid config (bad tool lists, unknown `promoteOn`, malformed flags,
80
- * non-positive `bootstrapMaxTokens`) fails at apply time, i.e. at preset
81
- * mount, where it is visible and fixable.
82
- */
83
-
84
- import { createEpochPromotion } from './compaction-epoch.mjs'
85
- import { booleanOption, createWarnOnce, parsePromoteOn, validateConfig } from './shared.mjs'
86
-
87
- /** Cordis plugin name used by loader diagnostics. */
88
- export const name = 'anchored-tool-bootstrap'
89
-
90
- /**
91
- * Deliberately NO inject list: the listeners only touch services at event
92
- * time. Keep this row right AFTER the context-gate row in agent.cordis.yml:
93
- * waterfall after-next transforms apply in reverse registration order, so the
94
- * tool filter here must register before any plugin that touches the same
95
- * assembly. The optional budget listener registers with `prepend: true` so a
96
- * later listener can never override the first-round cap after we set it.
97
- */
98
- export const inject = []
99
-
100
- /** Every config key this plugin accepts — anything else is a typo. */
101
- const ALLOWED_KEYS = new Set(['bootstrapTools', 'promoteOn', 'bootstrapMaxTokens', 'compactionTools', 'includeSubagents', 'usePtcMode'])
102
-
103
-
104
- /**
105
- * The default first-request catalog: the OFFICIAL Minimal preset's exact tool
106
- * pair — the persistent `bash` shell and `str_replace_editor`. Issue #11
107
- * measured this schema anchoring 5/5 at the adapter-default maxTokens while
108
- * every standard-family schema failed 11/11.
109
- */
110
- const DEFAULT_BOOTSTRAP_TOOLS = ['bash', 'str_replace_editor']
111
-
112
- /** Non-empty string list config validator. */
113
- function stringList(value, field) {
114
- if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== 'string' || item.length === 0)) {
115
- throw new TypeError(`${name}: ${field} must be a non-empty array of non-empty strings`)
116
- }
117
- return [...new Set(value)]
118
- }
119
-
120
- function stringListOrEmpty(value, field) {
121
- if (value === undefined) return []
122
- return stringList(value, field)
123
- }
124
-
125
-
126
- /**
127
- * Validate the optional first-request output cap. `undefined` means NO cap:
128
- * the Minimal tool schema anchors at the adapter-default maxTokens, and the
129
- * cap's delivery is profile-package dependent (see the header note), so it is
130
- * opt-in rather than the default.
131
- */
132
- function optionalPositiveInt(value, field) {
133
- if (value === undefined) return undefined
134
- if (!Number.isSafeInteger(value) || value <= 0) {
135
- throw new TypeError(`${name}: ${field} must be a positive safe integer`)
136
- }
137
- return value
138
- }
139
-
140
- /** Register the per-session bootstrap filters. */
141
- export function apply(ctx, config) {
142
- const source = validateConfig(name, config, ALLOWED_KEYS)
143
- const bootstrapTools = stringList(source.bootstrapTools, 'bootstrapTools')
144
- const promoteEvents = parsePromoteOn(name, source.promoteOn)
145
- const bootstrapMaxTokens = optionalPositiveInt(source.bootstrapMaxTokens, 'bootstrapMaxTokens')
146
- const includeSubagents = booleanOption(name, source.includeSubagents, 'includeSubagents', false)
147
- const usePtcMode = booleanOption(name, source.usePtcMode, 'usePtcMode', true)
148
- // Core work set exposed after a compaction, before re-promotion. Empty
149
- // means "no compaction recovery catalog": the session stays on the
150
- // bootstrap pair until a new promotion signal.
151
- const compactionTools = stringListOrEmpty(source.compactionTools, 'compactionTools')
152
-
153
- const promotion = createEpochPromotion(promoteEvents, { includeSubagents })
154
-
155
- // prompt-tool patch: optional Code Mode (PTC) wire presentation after promotion.
156
- const presentationBySession = new WeakMap()
157
- const agentBySession = new WeakMap()
158
- const presentationState = (session) => {
159
- let state = presentationBySession.get(session)
160
- if (state === undefined) {
161
- state = { applied: false, disposer: undefined }
162
- presentationBySession.set(session, state)
163
- }
164
- return state
165
- }
166
- const applyCodePresentation = (agent) => {
167
- const session = agent?.session
168
- if (session === undefined) return
169
- const state = presentationState(session)
170
- if (state.applied) return
171
- const tools = agent.ctx?.tools
172
- if (tools === undefined || typeof tools.presentAs !== 'function') return
173
- state.disposer = tools.presentAs('code')
174
- state.applied = true
175
- }
176
- const releaseCodePresentation = (session) => {
177
- const state = presentationBySession.get(session)
178
- if (state === undefined) return
179
- if (typeof state.disposer === 'function') {
180
- try { state.disposer() } catch { /* never brick the session */ }
181
- }
182
- state.disposer = undefined
183
- state.applied = false
184
- }
185
- ctx.on('session/event', (session, event) => promotion.observe(session, event))
186
-
187
- ctx.on('session/event', (session, event) => {
188
- if (!usePtcMode) return
189
- if (event.type === 'compaction/end') {
190
- releaseCodePresentation(session)
191
- return
192
- }
193
- if (event.type !== 'step/end' && event.type !== 'turn/end') return
194
- const agent = agentBySession.get(session)
195
- if (agent !== undefined && promotion.status(agent).promoted) applyCodePresentation(agent)
196
- })
197
-
198
- const warnOnce = createWarnOnce(ctx, name)
199
-
200
- /** Narrow the assembled catalog to a keep-set; validate required names. */
201
- const keepTools = (assembled, keep, missingAllowsFullCatalog) => {
202
- const available = new Set(assembled.tools.map((tool) => tool.name))
203
- const missing = [...keep].filter((toolName) => !available.has(toolName))
204
- if (missing.length > 0) {
205
- warnOnce(
206
- `${name}: expected every phase tool; missing=${JSON.stringify(missing)} — `
207
- + (missingAllowsFullCatalog ? 'bootstrap disabled, full catalog exposed' : 'continuing with what is available'),
208
- )
209
- if (missingAllowsFullCatalog) return assembled
210
- }
211
- return {
212
- ...assembled,
213
- tools: assembled.tools.filter((tool) => keep.has(tool.name)),
214
- }
215
- }
216
-
217
- ctx.on('system-prompt/assemble', async (_assembly, context, next) => {
218
- // Downstream errors propagate untouched; only this filter's own logic is guarded.
219
- const assembled = await next()
220
- try {
221
- const agent = context.agent
222
- if (agent === undefined) return assembled
223
- // prompt-tool patch: subagents skip catalog narrowing and use assembled tools directly.
224
- // Callers (such as dsh-mnemon) already filter assembled.tools through their own whitelists.
225
- // New plugin tools with any prefix therefore appear in the subagent first session automatically.
226
- agentBySession.set(agent.session, agent)
227
- if ((agent.session?.header?.delegationDepth ?? 0) > 0) {
228
- if (usePtcMode) applyCodePresentation(agent)
229
- return assembled
230
- }
231
- const status = promotion.status(agent)
232
- if (status.promoted) {
233
- // prompt-tool patch: both modes keep the assembled catalog after promotion.
234
- // usePtcMode switches the wire presentation instead of narrowing resident tools.
235
- if (usePtcMode) applyCodePresentation(agent)
236
- return assembled
237
- }
238
- // Controlled phase: the bootstrap pair; after a compaction, plus the
239
- // compaction work set so mid-task work can continue. Context control is
240
- // NOT here: the companion `context-gate` plugin owns it (see the header
241
- // note), so this filter touches only the tool catalog.
242
- const { boundary } = status
243
- const keep = new Set(bootstrapTools)
244
- if (boundary >= 0) for (const toolName of compactionTools) keep.add(toolName)
245
- return keepTools(assembled, keep, true)
246
- } catch (error) {
247
- // A filter bug must never brick a session: degrade to the full catalog.
248
- warnOnce(`${name}: bootstrap filter failed, exposing the full catalog: ${String((error && error.message) || error)}`)
249
- return assembled
250
- }
251
- })
252
-
253
- // Optionally cap the first model request's output budget while bootstrapping.
254
- // Unset (`bootstrapMaxTokens` omitted) means the adapter default flows — the
255
- // Minimal tool schema anchors at 256000 without a cap (issue #11).
256
- if (bootstrapMaxTokens !== undefined) {
257
- // Same registration discipline as the pre-step strip below: `prepend`
258
- // keeps this listener the OUTERMOST transform of the agent/request
259
- // waterfall for the same registration-order reasons (loader row
260
- // application is concurrent; row order alone does not decide listener
261
- // order — see issue #6 and upstream PR #13), so a later listener can
262
- // never override the first-round budget after we set it.
263
- ctx.on('agent/request', async (payload, next) => {
264
- const resolved = await next()
265
- const agent = payload.agent
266
- if (promotion.status(agent).promoted) {
267
- // The next request's seed proposal carries the previous header's
268
- // maxTokens forward, so the injected cap must be stripped explicitly —
269
- // otherwise it would persist for the whole session.
270
- if (resolved.maxTokens === bootstrapMaxTokens) {
271
- const { maxTokens: _bootstrap, ...rest } = resolved
272
- return rest
273
- }
274
- return resolved
275
- }
276
- return {
277
- ...resolved,
278
- maxTokens: bootstrapMaxTokens,
279
- }
280
- }, { prepend: true })
281
- }
282
- }
1
+ /**
2
+ * Anchored tool bootstrap — keep the FIRST model request on the Minimal
3
+ * preset's REAL tool schema (persistent `bash` + `str_replace_editor`), then
4
+ * keep the assembled catalog once the session has produced its first durable
5
+ * promotion signal; `usePtcMode` optionally switches the wire presentation to
6
+ * Code Mode (PTC). Injected-context control lives in the companion
7
+ * `context-gate` plugin, not here.
8
+ *
9
+ * GATE MODE (liangshen stabilize 扩展, source: xiaobright/dsh-anchored-standard
10
+ * MIT + phase-1 quarantine): `promoteGate: true` gates the promotion on the
11
+ * first reasoning block classifying minimal-like (`we` present, no `let me`),
12
+ * `maxPromoteSteps` (default 4) is the gate fallback, and
13
+ * `promoteAfterFirstResponse: true` promotes a tool-less first response and
14
+ * releases an anchor-gated session at its first `turn/end`.
15
+ * `personaSectionsOnly: true` narrows phase-1 prompt sections to the persona
16
+ * (plan-mode policy and other sections return after promotion);
17
+ * `workspaceLine: true` appends the session's working directory to the
18
+ * promoted persona; `phase1FirstCallInstruction` is an opt-in extra line
19
+ * appended to the phase-1 persona (test builds, issue #274) — unset keeps
20
+ * the exact one-line Minimal anchor.
21
+ *
22
+ * The phase is derived from durable session events, so resume and reload
23
+ * preserve it. By default (`promoteOn: 'either'`) a session promotes after the
24
+ * first `tool/call` OR the first `assistant/message`, whichever comes first:
25
+ * request #1 always sees the bootstrap catalog and later requests keep the
26
+ * assembled catalog. The original `'tool-call'` mode is kept for compatibility,
27
+ * but it can trap a session in bootstrap forever when the first model reply
28
+ * makes no tool call — the `'either'` default removes that trap while keeping
29
+ * the first-request anchor intact.
30
+ *
31
+ * First-request conditions established by the reproduction work (issues #6
32
+ * and #11, 2026-08-15):
33
+ *
34
+ * 1. Tool schema. The API-visible first-request catalog decides whether the
35
+ * session anchors on the Minimal trajectory. At the adapter-default
36
+ * maxTokens (256000 on the official endpoint) the Minimal tool pair —
37
+ * persistent `bash` + `str_replace_editor` — anchored 5/5 runs with zero
38
+ * `let me` first-lines, while every standard-family schema (pwsh/read,
39
+ * pwsh only, sandboxed bash/read) fell into standard-like behavior
40
+ * (11/11). Bootstrap therefore exposes exactly the Minimal pair, not
41
+ * Standard's `pwsh`/`read`.
42
+ *
43
+ * 2. Output budget. On the official endpoint the first request's `max_tokens`
44
+ * also dominated the trajectory anchor at 1024 (`We need` style in 26/32
45
+ * runs against 0/5 at 256000, independent of tool descriptions). The
46
+ * Minimal tool schema, however, anchors at 256000 WITHOUT any cap, and the
47
+ * cap's delivery depends on the profile package's `prepareCall` behavior
48
+ * (it reaches the request on the 0.1.0-rc.5 source checkout; a prebuilt
49
+ * rc.6-reporting profile package observed in issue #11 overwrote it with
50
+ * `adapterDefaults.maxTokens`). `bootstrapMaxTokens` is therefore OPT-IN:
51
+ * leave it unset to run the Minimal schema at the adapter default, or set
52
+ * it to cap the first request. When set, the cap is stripped after
53
+ * promotion — the next request's seed proposal carries the previous
54
+ * header's maxTokens forward, so the release must be explicit.
55
+ *
56
+ * 3. Injected context is NOT this plugin's concern: the companion
57
+ * `context-gate` plugin (shared/context-gate.mjs, mounted as the FIRST
58
+ * row) owns the unified injection control — runtime-context suppression
59
+ * on the assembly path and a claimed-baseline deny on the pre-step
60
+ * waterfall, both keyed to the same epoch-aware promotion phase. Mount it
61
+ * separately for context control alone; this file narrows only the tool
62
+ * catalog (plus the optional output cap below).
63
+ *
64
+ * SUBAGENTS: `includeSubagents` (default false) controls whether subagents
65
+ * (delegationDepth > 0) follow the same bootstrap phase as the main session —
66
+ * false (default) keeps the assembled catalog from their very first request
67
+ * (the historical prompt-tool default: delegated agents inherit the full
68
+ * tool surface); true makes their first request see the bootstrap pair and
69
+ * their own first reply or tool call promotes them. Keep in sync with the
70
+ * context-gate row's flag when both rows are present.
71
+ *
72
+ * POST-PROMOTION CATALOG (prompt-tool patch): after promotion both modes
73
+ * keep the assembled catalog. `usePtcMode` switches the wire presentation
74
+ * to Code Mode (PTC, single run_code) instead of narrowing the resident set.
75
+ * The controlled phase below still narrows the catalog before promotion and
76
+ * after compaction.
77
+ * COMPACTION (local addition): a compaction rewrites the whole surface, so the
78
+ * first post-compaction request is a "second first request". Promotion is
79
+ * epoch-aware (see compaction-epoch.mjs): after `compaction/end` the session
80
+ * falls back to the controlled phase — the bootstrap pair plus
81
+ * `compactionTools` (a core work set, default none) — until a NEW durable
82
+ * promotion signal exists past that boundary. The model is mid-task and needs
83
+ * to keep working, but still faces a small catalog instead of the full
84
+ * Standard set.
85
+ *
86
+ * Robustness:
87
+ * - Promotion decisions are memoized per session id for this process; the
88
+ * durable event scan runs once per session per process, then O(1).
89
+ * - Subagents keep the assembled catalog unless `includeSubagents: true`.
90
+ * - A missing bootstrap tool degrades to the full catalog with a one-time
91
+ * warning instead of throwing, so a composition drift can never brick
92
+ * every request of a session.
93
+ * - Invalid config (bad tool lists, unknown `promoteOn`, malformed flags,
94
+ * non-positive `bootstrapMaxTokens`) fails at apply time, i.e. at preset
95
+ * mount, where it is visible and fixable.
96
+ */
97
+
98
+ import { createEpochPromotion } from './compaction-epoch.mjs'
99
+ import { booleanOption, createWarnOnce, parsePromoteOn, validateConfig } from './shared.mjs'
100
+
101
+ /** Cordis plugin name used by loader diagnostics. */
102
+ export const name = 'anchored-tool-bootstrap'
103
+
104
+ /**
105
+ * Deliberately NO inject list: the listeners only touch services at event
106
+ * time. Keep this row right AFTER the context-gate row in agent.cordis.yml:
107
+ * waterfall after-next transforms apply in reverse registration order, so the
108
+ * tool filter here must register before any plugin that touches the same
109
+ * assembly. The optional budget listener registers with `prepend: true` so a
110
+ * later listener can never override the first-round cap after we set it.
111
+ */
112
+ export const inject = []
113
+
114
+ /** Every config key this plugin accepts — anything else is a typo. */
115
+ const ALLOWED_KEYS = new Set([
116
+ 'bootstrapTools', 'promoteOn', 'bootstrapMaxTokens', 'compactionTools',
117
+ 'includeSubagents', 'usePtcMode',
118
+ 'promoteGate', 'maxPromoteSteps', 'promoteAfterFirstResponse',
119
+ 'personaSectionsOnly', 'workspaceLine', 'phase1FirstCallInstruction',
120
+ ])
121
+
122
+ /** 预设 persona section 名(官方注册名 + 旧名)。 */
123
+ const PERSONA_SECTION_NAMES = new Set(['deployment:persona', 'persona'])
124
+
125
+ /** 晋升后 persona 附加的工作目录行前缀。 */
126
+ const WORKSPACE_LINE_PREFIX = '\n\nYour working directory is '
127
+
128
+
129
+ /** Non-empty string list config validator. */
130
+ function stringList(value, field) {
131
+ if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== 'string' || item.length === 0)) {
132
+ throw new TypeError(`${name}: ${field} must be a non-empty array of non-empty strings`)
133
+ }
134
+ return [...new Set(value)]
135
+ }
136
+
137
+ function stringListOrEmpty(value, field) {
138
+ if (value === undefined) return []
139
+ return stringList(value, field)
140
+ }
141
+
142
+
143
+ /**
144
+ * Validate the optional first-request output cap. `undefined` means NO cap:
145
+ * the Minimal tool schema anchors at the adapter-default maxTokens, and the
146
+ * cap's delivery is profile-package dependent (see the header note), so it is
147
+ * opt-in rather than the default.
148
+ */
149
+ function optionalPositiveInt(value, field) {
150
+ if (value === undefined) return undefined
151
+ if (!Number.isSafeInteger(value) || value <= 0) {
152
+ throw new TypeError(`${name}: ${field} must be a positive safe integer`)
153
+ }
154
+ return value
155
+ }
156
+
157
+ /** Register the per-session bootstrap filters. */
158
+ export function apply(ctx, config) {
159
+ const source = validateConfig(name, config, ALLOWED_KEYS)
160
+ // 门控模式(promoteGate / promoteAfterFirstResponse)固定 either 晋升语义,
161
+ // promoteOn 显式非 either 时与门控互斥,fail loud 而非静默忽略。
162
+ if ((source.promoteGate === true || source.promoteAfterFirstResponse === true)
163
+ && source.promoteOn !== undefined && source.promoteOn !== 'either') {
164
+ throw new TypeError(`${name}: promoteGate/promoteAfterFirstResponse 门控模式固定 either 晋升语义,promoteOn 必须省略或为 "either"`)
165
+ }
166
+ const bootstrapTools = stringList(source.bootstrapTools, 'bootstrapTools')
167
+ const promoteEvents = parsePromoteOn(name, source.promoteOn)
168
+ const bootstrapMaxTokens = optionalPositiveInt(source.bootstrapMaxTokens, 'bootstrapMaxTokens')
169
+ const includeSubagents = booleanOption(name, source.includeSubagents, 'includeSubagents', false)
170
+ const usePtcMode = booleanOption(name, source.usePtcMode, 'usePtcMode', true)
171
+ const personaSectionsOnly = booleanOption(name, source.personaSectionsOnly, 'personaSectionsOnly', false)
172
+ const workspaceLine = booleanOption(name, source.workspaceLine, 'workspaceLine', false)
173
+ const phase1FirstCallInstruction = typeof source.phase1FirstCallInstruction === 'string'
174
+ ? source.phase1FirstCallInstruction
175
+ : ''
176
+ // Core work set exposed after a compaction, before re-promotion. Empty
177
+ // means "no compaction recovery catalog": the session stays on the
178
+ // bootstrap pair until a new promotion signal.
179
+ const compactionTools = stringListOrEmpty(source.compactionTools, 'compactionTools')
180
+
181
+ const promotion = createEpochPromotion(promoteEvents, {
182
+ includeSubagents,
183
+ promoteGate: source.promoteGate === true,
184
+ promoteAfterFirstResponse: source.promoteAfterFirstResponse === true,
185
+ maxPromoteSteps: source.maxPromoteSteps,
186
+ })
187
+
188
+ // prompt-tool patch: optional Code Mode (PTC) wire presentation after promotion.
189
+ const presentationBySession = new WeakMap()
190
+ const agentBySession = new WeakMap()
191
+ const presentationState = (session) => {
192
+ let state = presentationBySession.get(session)
193
+ if (state === undefined) {
194
+ state = { applied: false, disposer: undefined }
195
+ presentationBySession.set(session, state)
196
+ }
197
+ return state
198
+ }
199
+ const applyCodePresentation = (agent) => {
200
+ const session = agent?.session
201
+ if (session === undefined) return
202
+ const state = presentationState(session)
203
+ if (state.applied) return
204
+ const tools = agent.ctx?.tools
205
+ if (tools === undefined || typeof tools.presentAs !== 'function') return
206
+ state.disposer = tools.presentAs('code')
207
+ state.applied = true
208
+ }
209
+ const releaseCodePresentation = (session) => {
210
+ const state = presentationBySession.get(session)
211
+ if (state === undefined) return
212
+ if (typeof state.disposer === 'function') {
213
+ try { state.disposer() } catch { /* never brick the session */ }
214
+ }
215
+ state.disposer = undefined
216
+ state.applied = false
217
+ }
218
+ ctx.on('session/event', (session, event) => promotion.observe(session, event))
219
+
220
+ ctx.on('session/event', (session, event) => {
221
+ if (!usePtcMode) return
222
+ if (event.type === 'compaction/end') {
223
+ releaseCodePresentation(session)
224
+ return
225
+ }
226
+ if (event.type !== 'step/end' && event.type !== 'turn/end') return
227
+ const agent = agentBySession.get(session)
228
+ if (agent !== undefined && promotion.status(agent).promoted) applyCodePresentation(agent)
229
+ })
230
+
231
+ const warnOnce = createWarnOnce(ctx, name)
232
+
233
+ /** 晋升后给 persona 追加工作目录行(无 persona/无 cwd/已含则原样返回)。 */
234
+ const withWorkspaceLine = (assembly, agent) => {
235
+ const cwd = agent?.session?.header?.cwd
236
+ if (typeof cwd !== 'string' || cwd.length === 0) return assembly
237
+ if (!Array.isArray(assembly.sections)) return assembly
238
+ const line = `${WORKSPACE_LINE_PREFIX}${cwd}.`
239
+ const persona = assembly.sections.find((section) =>
240
+ PERSONA_SECTION_NAMES.has(section?.name)
241
+ && typeof section?.text === 'string'
242
+ && !section.text.includes(line))
243
+ if (persona === undefined) return assembly
244
+ return {
245
+ ...assembly,
246
+ sections: assembly.sections.map((section) => section === persona
247
+ ? { ...section, text: `${persona.text}${line}` }
248
+ : section),
249
+ }
250
+ }
251
+
252
+ /** Narrow the assembled catalog to a keep-set; validate required names. */
253
+ const keepTools = (assembled, keep, missingAllowsFullCatalog) => {
254
+ const available = new Set(assembled.tools.map((tool) => tool.name))
255
+ const missing = [...keep].filter((toolName) => !available.has(toolName))
256
+ if (missing.length > 0) {
257
+ warnOnce(
258
+ `${name}: expected every phase tool; missing=${JSON.stringify(missing)} — `
259
+ + (missingAllowsFullCatalog ? 'bootstrap disabled, full catalog exposed' : 'continuing with what is available'),
260
+ )
261
+ if (missingAllowsFullCatalog) return assembled
262
+ }
263
+ return {
264
+ ...assembled,
265
+ tools: assembled.tools.filter((tool) => keep.has(tool.name)),
266
+ }
267
+ }
268
+
269
+ ctx.on('system-prompt/assemble', async (_assembly, context, next) => {
270
+ // Downstream errors propagate untouched; only this filter's own logic is guarded.
271
+ const assembled = await next()
272
+ try {
273
+ const agent = context.agent
274
+ if (agent === undefined) return assembled
275
+ if (agent.session === undefined) return assembled
276
+ agentBySession.set(agent.session, agent)
277
+ if ((agent.session?.header?.delegationDepth ?? 0) > 0 && !includeSubagents) {
278
+ // 默认:子代理继承完整目录(历史 prompt-tool 默认);
279
+ // includeSubagents=true 时落到下方正常相位逻辑(首轮裁剪 + 晋升)。
280
+ if (usePtcMode) applyCodePresentation(agent)
281
+ return assembled
282
+ }
283
+ const status = promotion.status(agent)
284
+ if (status.promoted) {
285
+ // prompt-tool patch: both modes keep the assembled catalog after promotion.
286
+ // usePtcMode switches the wire presentation instead of narrowing resident tools.
287
+ if (usePtcMode) applyCodePresentation(agent)
288
+ return workspaceLine ? withWorkspaceLine(assembled, agent) : assembled
289
+ }
290
+ // Controlled phase: the bootstrap pair; after a compaction, plus the
291
+ // compaction work set so mid-task work can continue. Context control is
292
+ // NOT here: the companion `context-gate` plugin owns it (see the header
293
+ // note), so this filter touches only the tool catalog.
294
+ const { boundary } = status
295
+ const keep = new Set(bootstrapTools)
296
+ if (boundary >= 0) for (const toolName of compactionTools) keep.add(toolName)
297
+ let next = keepTools(assembled, keep, true)
298
+ if (personaSectionsOnly) {
299
+ // Phase-1 提示词段只留 persona(plan-mode 策略等晋升后才恢复)。
300
+ const sections = Array.isArray(next.sections)
301
+ ? next.sections.filter((section) => PERSONA_SECTION_NAMES.has(section?.name))
302
+ : undefined
303
+ if (sections !== undefined) {
304
+ const phase1Sections = phase1FirstCallInstruction === ''
305
+ ? sections
306
+ : sections.map((section) => {
307
+ if (typeof section?.text !== 'string' || section.text.includes(phase1FirstCallInstruction)) return section
308
+ return { ...section, text: `${section.text}${phase1FirstCallInstruction}` }
309
+ })
310
+ next = { ...next, sections: phase1Sections }
311
+ }
312
+ }
313
+ return next
314
+ } catch (error) {
315
+ // A filter bug must never brick a session: degrade to the full catalog.
316
+ warnOnce(`${name}: bootstrap filter failed, exposing the full catalog: ${String((error && error.message) || error)}`)
317
+ return assembled
318
+ }
319
+ })
320
+
321
+ // Optionally cap the first model request's output budget while bootstrapping.
322
+ // Unset (`bootstrapMaxTokens` omitted) means the adapter default flows — the
323
+ // Minimal tool schema anchors at 256000 without a cap (issue #11).
324
+ if (bootstrapMaxTokens !== undefined) {
325
+ // Same registration discipline as the pre-step strip below: `prepend`
326
+ // keeps this listener the OUTERMOST transform of the agent/request
327
+ // waterfall for the same registration-order reasons (loader row
328
+ // application is concurrent; row order alone does not decide listener
329
+ // order — see issue #6 and upstream PR #13), so a later listener can
330
+ // never override the first-round budget after we set it.
331
+ ctx.on('agent/request', async (payload, next) => {
332
+ const resolved = await next()
333
+ const agent = payload.agent
334
+ if (promotion.status(agent).promoted) {
335
+ // The next request's seed proposal carries the previous header's
336
+ // maxTokens forward, so the injected cap must be stripped explicitly —
337
+ // otherwise it would persist for the whole session.
338
+ if (resolved.maxTokens === bootstrapMaxTokens) {
339
+ const { maxTokens: _bootstrap, ...rest } = resolved
340
+ return rest
341
+ }
342
+ return resolved
343
+ }
344
+ return {
345
+ ...resolved,
346
+ maxTokens: bootstrapMaxTokens,
347
+ }
348
+ }, { prepend: true })
349
+ }
350
+ }