@thincoder/core 0.9.5 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -1
  3. package/abort-provenance.mjs +10 -10
  4. package/advisor/citations.mjs +5 -5
  5. package/advisor/compaction.mjs +16 -15
  6. package/advisor/loop.mjs +12 -12
  7. package/advisor/messages.mjs +6 -6
  8. package/advisor/project-context.mjs +5 -5
  9. package/advisor/repos.mjs +3 -3
  10. package/advisor/run.mjs +22 -12
  11. package/advisor.mjs +4 -4
  12. package/agent/assemble.mjs +111 -0
  13. package/agent/chat-call.mjs +68 -0
  14. package/agent/child-marks.mjs +2 -2
  15. package/agent/completion.mjs +9 -2
  16. package/agent/continue-decision.mjs +25 -0
  17. package/agent/dispatch-gates.mjs +140 -0
  18. package/agent/dispatch-run.mjs +167 -0
  19. package/agent/dispatch.mjs +54 -296
  20. package/agent/family-tools.mjs +5 -5
  21. package/agent/helpers.mjs +53 -3
  22. package/agent/live-beat.mjs +49 -0
  23. package/agent/post-turn.mjs +4 -8
  24. package/agent/record-results.mjs +23 -20
  25. package/agent/relay-prefix.mjs +2 -2
  26. package/agent/run-stages.mjs +15 -11
  27. package/agent/run-start.mjs +111 -0
  28. package/agent/setup-reminders.mjs +9 -9
  29. package/agent/setup.mjs +32 -4
  30. package/agent/spawn-child.mjs +9 -9
  31. package/agent/suspension.mjs +89 -29
  32. package/agent/timers.mjs +113 -0
  33. package/agent/turn-loop.mjs +244 -0
  34. package/agent/write-gate.mjs +103 -6
  35. package/agent-tools/advisor-async.mjs +9 -9
  36. package/agent-tools/advisor-settle.mjs +12 -12
  37. package/agent-tools/advisor.mjs +36 -41
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +21 -18
  40. package/agent-tools/audit-block.mjs +7 -7
  41. package/agent-tools/bash-async.mjs +281 -0
  42. package/agent-tools/batch-lifecycle.mjs +57 -18
  43. package/agent-tools/batch-paths.mjs +52 -8
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +12 -65
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +56 -56
  49. package/agent-tools/context.mjs +4 -20
  50. package/agent-tools/design-token.mjs +3 -3
  51. package/agent-tools/digest-budget.mjs +1 -1
  52. package/agent-tools/eng.mjs +5 -5
  53. package/agent-tools/escalate-async.mjs +16 -11
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +2 -2
  56. package/agent-tools/parent-channel.mjs +3 -13
  57. package/agent-tools/plan.mjs +3 -4
  58. package/agent-tools/read-history.mjs +12 -28
  59. package/agent-tools/recent-changes.mjs +4 -5
  60. package/agent-tools/review-facts.mjs +1 -1
  61. package/agent-tools/settings.mjs +47 -20
  62. package/agent-tools/skill.mjs +3 -5
  63. package/agent-tools/spawn-gates.mjs +3 -3
  64. package/agent-tools/subagent-actions-query.mjs +270 -0
  65. package/agent-tools/subagent-actions.mjs +21 -265
  66. package/agent-tools/subagent-async.mjs +25 -20
  67. package/agent-tools/subagent-panel.mjs +33 -4
  68. package/agent-tools/subagent-run.mjs +36 -21
  69. package/agent-tools/subagent-scheduler.mjs +28 -27
  70. package/agent-tools/subagent-spawn.mjs +45 -14
  71. package/agent-tools/subagent.mjs +33 -53
  72. package/agent-tools/task.mjs +3 -9
  73. package/agent-tools/timer.mjs +12 -12
  74. package/agent-tools/verify.mjs +14 -13
  75. package/agent-tools.mjs +0 -1
  76. package/agent.mjs +18 -355
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +11 -6
  80. package/config-watch.mjs +83 -0
  81. package/config.mjs +17 -4
  82. package/context-degrade.mjs +81 -0
  83. package/context-echo.mjs +88 -0
  84. package/context-push.mjs +51 -0
  85. package/context.mjs +8 -187
  86. package/conventions.mjs +110 -161
  87. package/declaration.mjs +194 -0
  88. package/embedding.mjs +3 -1
  89. package/env-ref.mjs +81 -0
  90. package/explore-distill.mjs +2 -3
  91. package/file-links.mjs +53 -0
  92. package/file-refs.mjs +103 -0
  93. package/generate-title.mjs +28 -4
  94. package/history-window.mjs +17 -2
  95. package/i18n.mjs +2 -0
  96. package/index-discover.mjs +7 -7
  97. package/ledger-cmd.mjs +34 -126
  98. package/ledger-db.mjs +10 -2
  99. package/ledger-tools.mjs +188 -0
  100. package/ledger.mjs +9 -4
  101. package/log.mjs +12 -7
  102. package/manifest-discovery.mjs +131 -0
  103. package/manifest-schema.mjs +166 -0
  104. package/manifest.mjs +18 -217
  105. package/mcp.mjs +4 -1
  106. package/memory/code-search.mjs +149 -0
  107. package/memory/code-sync.mjs +89 -230
  108. package/memory/core.mjs +47 -32
  109. package/memory/docs.mjs +63 -262
  110. package/memory/file-list.mjs +89 -0
  111. package/memory/file-walk.mjs +7 -4
  112. package/memory/memory-tool.mjs +229 -0
  113. package/memory/schema.mjs +18 -3
  114. package/memory/sweep.mjs +289 -0
  115. package/memory/sync-tail.mjs +81 -0
  116. package/memory-status.mjs +69 -0
  117. package/memory.mjs +3 -0
  118. package/model-specs.mjs +16 -7
  119. package/notify-policy.mjs +51 -0
  120. package/package.json +1 -1
  121. package/peer-claims.mjs +2 -2
  122. package/peer-domains.mjs +7 -11
  123. package/peer-instances.mjs +9 -10
  124. package/process-probe-exec.mjs +187 -0
  125. package/process-probe.mjs +27 -175
  126. package/prompt-overlays.mjs +11 -10
  127. package/prompts/advisor-design.md +2 -2
  128. package/prompts/advisor-round2.md +1 -1
  129. package/prompts/advisor-round3.md +1 -1
  130. package/prompts/common.md +16 -8
  131. package/prompts/discipline-engineering.md +57 -16
  132. package/prompts/discipline-normal.md +6 -3
  133. package/prompts/persona-coder.md +0 -1
  134. package/prompts/persona-eng-coder.md +1 -5
  135. package/prompts/persona-eng-designer.md +2 -3
  136. package/prompts/persona-engineering.md +32 -11
  137. package/prompts/persona-explore.md +0 -1
  138. package/prompts/persona-normal.md +1 -1
  139. package/prompts/persona-plan.md +3 -4
  140. package/prompts/subagent-base.md +14 -0
  141. package/provider/core.mjs +11 -48
  142. package/provider/list-models.mjs +3 -0
  143. package/provider/responses-request.mjs +237 -0
  144. package/provider/responses.mjs +6 -228
  145. package/provider-flows.mjs +248 -0
  146. package/queued.mjs +90 -0
  147. package/rules.mjs +209 -2
  148. package/session-guard.mjs +27 -7
  149. package/session-lifecycle.mjs +90 -18
  150. package/session-rename.mjs +5 -1
  151. package/session-slot-claims.mjs +230 -0
  152. package/session-slot-scan.mjs +299 -0
  153. package/session-slot-verify.mjs +304 -0
  154. package/session-slot-write.mjs +74 -8
  155. package/session-slots-manifest.mjs +163 -241
  156. package/session-slots.mjs +99 -71
  157. package/session.mjs +30 -10
  158. package/shell-candidates.mjs +78 -0
  159. package/text-budget.mjs +2 -2
  160. package/think-off.mjs +49 -0
  161. package/token-ttl.mjs +15 -4
  162. package/tool-args.mjs +73 -0
  163. package/tool-docs/advisor.md +9 -0
  164. package/tool-docs/batch.md +1 -0
  165. package/tool-docs/code_search.md +1 -0
  166. package/tool-docs/consult_start.md +5 -0
  167. package/tool-docs/consult_stop.md +3 -0
  168. package/tool-docs/context.md +7 -0
  169. package/tool-docs/doc_search.md +1 -0
  170. package/tool-docs/edit.md +2 -13
  171. package/tool-docs/eng.md +1 -0
  172. package/tool-docs/execute.md +1 -9
  173. package/tool-docs/git.md +25 -51
  174. package/tool-docs/goal.md +6 -0
  175. package/tool-docs/ledger_add.md +1 -0
  176. package/tool-docs/ledger_close.md +1 -0
  177. package/tool-docs/ledger_count.md +1 -0
  178. package/tool-docs/ledger_query.md +1 -0
  179. package/tool-docs/ledger_update.md +1 -0
  180. package/tool-docs/main_history.md +1 -0
  181. package/tool-docs/memory.md +10 -0
  182. package/tool-docs/notify_parent.md +9 -0
  183. package/tool-docs/peer_instances.md +1 -0
  184. package/tool-docs/plan.md +5 -0
  185. package/tool-docs/process.md +7 -3
  186. package/tool-docs/read_history.md +7 -0
  187. package/tool-docs/recent_changes.md +1 -0
  188. package/tool-docs/repo_outline.md +1 -0
  189. package/tool-docs/settings.md +6 -0
  190. package/tool-docs/skill.md +5 -0
  191. package/tool-docs/subagent.md +31 -0
  192. package/tool-docs/task.md +7 -0
  193. package/tool-docs/timer.md +6 -0
  194. package/tool-docs/verify.md +1 -0
  195. package/tool-docs/wait_for.md +2 -1
  196. package/tools/bash.mjs +28 -4
  197. package/tools/edit-batch.mjs +4 -4
  198. package/tools/edit-diff.mjs +4 -23
  199. package/tools/exec-run.mjs +102 -4
  200. package/tools/execute.mjs +5 -2
  201. package/tools/file.mjs +4 -9
  202. package/tools/git-run.mjs +1 -1
  203. package/tools/git.mjs +0 -24
  204. package/tools/index.mjs +2 -1
  205. package/tools/linter.mjs +2 -1
  206. package/tools/node-child.mjs +21 -0
  207. package/tools/ops.mjs +50 -13
  208. package/tools/patch.mjs +4 -2
  209. package/tools/repomap.mjs +20 -9
  210. package/tools/shared.mjs +8 -6
  211. package/tools/web.mjs +5 -2
  212. package/tools/write-path.mjs +1 -1
  213. package/traces/trace-store.mjs +17 -14
  214. package/vision-reader.mjs +76 -0
  215. package/agent-tools/batch-segment.mjs +0 -18
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * consult.mjs — multi-model consultation ("会诊", docs/design/CONSULTATION.md). CLI port.
3
3
  *
4
- * Two tools (AGENT-LOOP.md §25 D-R17a — R17, 2026-09-06): consult_start
4
+ * Two tools (CONSULTATION.md §6.3 — R17, 2026-09-06): consult_start
5
5
  * (non-blocking spawn) / consult_stop (cancel a running session). consult_check
6
6
  * was RETIRED with the digest auto-injection: the mechanism does ZERO judging —
7
7
  * when every model of a session settles (pending 0), the session moves to the
@@ -19,8 +19,9 @@
19
19
  * prefix `consult#<childRelayN>/` (one per consultant child — the shared subagent
20
20
  * relay channel; the child relay number is NOT the session id — sessions key
21
21
  * their own `_consultIdCounter`), not onSubagent/onToolPanel.
22
- * Each child settles its own TUI block with a ⟦ev⟧done event at settle (R17 —
23
- * the old in-turn check consumption is gone).
22
+ * Each child settles its own TUI block with a ⟦ev⟧settled event at settle (consult
23
+ * 同族收齐批 · #748; a cancelled session's children emit ⟦ev⟧stopped — R17: the old
24
+ * in-turn check consumption is gone; consumed blocks archive at the consumption window).
24
25
  */
25
26
  import { createAgent, runAgent, readonlyToolNames, excludeSubagentTools } from "../agent.mjs"
26
27
  import { resolveChildProvider } from "./subagent.mjs"
@@ -29,11 +30,15 @@ import { offloadToolResult, escapeXml } from "../agent/helpers.mjs"
29
30
  import { logEvent, errText } from "../log.mjs"
30
31
  import { deathLine } from "../abort-provenance.mjs"
31
32
  import { makeRelay, wrapChildCallbacks, runWithContinue, ensureChildApiKey, clampEffort } from "../agent/spawn-child.mjs"
32
- // TUI-OOM-ROOTCAUSE §23.3.1:子代理人读线窗口常量(单源——store 零依赖)。
33
+ // TUI-OOM-ROOTCAUSE·AGENT-LOOP.md §6.15:子代理人读线窗口常量(单源——store 零依赖)。
33
34
  import { RECORD_WINDOW_MESSAGES } from "../session-store.mjs"
34
35
  // ASYNC-RESULT-CONTAINER.md D2/D3/D6:pending 单容器停靠 + settle 公共收尾 + child signal 单点
35
36
  import { bindChildController, buildChildSignal, settleAsyncEntry } from "./async-settle.mjs"
36
- import { digestBudgetOver, persistOverflowReport } from "./digest-budget.mjs" // B5(群 B 批 §22 D-DG2):digest 注入预算单源
37
+ import { registerTurnCapCheckpoint } from "./checkpoint.mjs"
38
+ import { drainInjectedQueue } from "./subagent-run.mjs"
39
+ import { digestBudgetOver, persistOverflowReport } from "./digest-budget.mjs" // B5(群 B 批 AGENT-LOOP.md §6.14 D-DG2):digest 注入预算单源
40
+ // #15 描述外置:描述文本单点 = tool-docs/{consult_start,consult_stop,main_history}.md(DESC 单解析面,缺档抛错语义不变)
41
+ import { DESC } from "../tools/shared.mjs"
37
42
 
38
43
  // Named consult defaults (consult P2, 2026-08-30).
39
44
  const CONSULT_TIMEOUT_MS = 600_000 // default consult lifecycle timeout
@@ -83,11 +88,7 @@ export function makeMainHistoryTool(parentAgent) {
83
88
  return {
84
89
  name: "main_history",
85
90
  readonly: true,
86
- description:
87
- "Read the main agent's conversation history — what has been tried, the exact errors, recent context. " +
88
- "Use it to ground your analysis in the actual failure trail instead of guessing.\n" +
89
- "Parameters:\n" +
90
- "- limit: Number of recent messages to return (default 20, max 100)",
91
+ description: DESC("main_history"),
91
92
  parameters: {
92
93
  type: "object",
93
94
  properties: { limit: { type: "number", description: "Recent messages (default 20, max 100)" } },
@@ -132,7 +133,7 @@ export function makeMainHistoryTool(parentAgent) {
132
133
  }
133
134
 
134
135
  /**
135
- * Full-session settle routing (R17 — AGENT-LOOP.md §25 D-R17a): a session whose
136
+ * Full-session settle routing (R17 — CONSULTATION.md §6.2): a session whose
136
137
  * pending count reached 0 has no more replies coming — the session leaves
137
138
  * `_consultSessions` and, unless it was cancelled (consult_stop / turn-end
138
139
  * abort), moves into the pending single container (`_pendingAsyncResults` +role
@@ -144,8 +145,8 @@ export function makeMainHistoryTool(parentAgent) {
144
145
  * is woken (settle-event parity with the async pools) so an idle settle still
145
146
  * triggers the digestion round (T-R17j).
146
147
  * D3:公共收尾统一走 settleAsyncEntry 共享 helper(四族同机制)——consult 族参数:
147
- * 无池(会话池无条目——settle 即出池)、无 ctx(无 TUI 冻结事件——子块各自 settle 时
148
- * 已冻结)、无 onAccounting;helper 按 role "consult" 恒停靠 pending(非挂起期也停靠)。
148
+ * 无池(会话池无条目——settle 即出池)、无 ctx(无 TUI 冻结事件——子块由消费窗三端
149
+ * 收口——#748)、无 onAccounting;helper 按 role "consult" 恒停靠 pending(非挂起期也停靠)。
149
150
  * Cancelled sessions produce no digest (T-R17c).
150
151
  */
151
152
  function sessionSettled(agent, session) {
@@ -156,6 +157,7 @@ function sessionSettled(agent, session) {
156
157
  role: "consult",
157
158
  report: composeConsultDigest(session),
158
159
  error: null, done: true, status: "done", cancelled: false,
160
+ childIds: [...(session.childIds ?? [])], // #748:随 settle 携子块映射表(消费窗三端展开同源)
159
161
  relayPrefix: null, startedAt: null, _settle: null, _settleSeq: 0,
160
162
  }
161
163
  settleAsyncEntry(agent, entry, { pool: null, ctx: null })
@@ -183,7 +185,7 @@ export function composeConsultDigest(session) {
183
185
  */
184
186
  export async function injectConsultResult(agent, entry) {
185
187
  const full = String(entry?.report ?? "(no consultation result)")
186
- // §22 D-DG3(群 B 批 B5):计入预算的 raw = 报告正文(标签行不计)——首行标签拆出;超限
188
+ // §6.14 D-DG3(群 B 批 B5):计入预算的 raw = 报告正文(标签行不计)——首行标签拆出;超限
187
189
  // 路径保留族标签行(与 VSC 镜像同形);落盘内容 = raw(与其余三族同口径)。
188
190
  const at = full.indexOf("\n")
189
191
  const label = at === -1 ? "" : full.slice(0, at + 1)
@@ -197,7 +199,11 @@ export async function injectConsultResult(agent, entry) {
197
199
  })
198
200
  }
199
201
 
200
- function settleChild(agent, session, id, label, ok, payload, emitDone) {
202
+ /** Per-child settle 路由(consult 同族收齐批 · 2026-09-30 · 台账 #748):settle 一律发
203
+ * `⟦ev⟧settled`(区块驻留「done · awaiting digestion」——冻结 ∥ 归档落消费窗,三端补发面同源);
204
+ * 取消径(会话 stopped——consult_stop ∥ cleanup)⇒ `⟦ev⟧stopped` 即折(消费永不来——与四族取消面同判)。
205
+ * 无块子块(relayPrefix 缺省——spawn 前失败,从未开块)零发射;测试缝 = 最小导出接桩(会话桩直调)。 */
206
+ export function settleChild(agent, session, id, label, ok, payload, relayPrefix, emit) {
201
207
  if (ok) {
202
208
  session.received++
203
209
  session.replies.push({ model: label, reply: payload })
@@ -208,7 +214,10 @@ function settleChild(agent, session, id, label, ok, payload, emitDone) {
208
214
  session.replies.push({ model: label, reply: `(consultation failed: ${payload})`, failed: true })
209
215
  }
210
216
  session.pending--
211
- emitDone?.() // per-child TUI block freeze at settle (R17 — child's activity card is done)
217
+ if (relayPrefix) {
218
+ const kind = session.stopped ? "stopped" : "settled" // 块发射两态(消费面口径 = AGENT-LOOP-ASYNC-POOL.md §6.8)
219
+ emit?.(`${relayPrefix}⟦ev⟧${kind}\x1e0\x1e0\x1e${kind}\x1e`)
220
+ }
212
221
  if (session.pending === 0) sessionSettled(agent, session)
213
222
  }
214
223
 
@@ -219,7 +228,7 @@ async function runConsultChild(ctx, session, id, m, problem, ctrl) {
219
228
  const armWatchdog = () => {
220
229
  const t = setTimeout(() => {
221
230
  timedOut = true
222
- // §20.3 站点 #12 信号面(第 24 批):watchdog 中止带 timeout reason
231
+ // AGENT-LOOP-SUBAGENT.md §6.12 站点 #12 信号面(第 24 批):watchdog 中止带 timeout reason
223
232
  try { ctrl.abort({ abortTrigger: "timeout", abortDetail: "consult-watchdog" }) } catch { /* already settled */ }
224
233
  }, timeoutMs)
225
234
  t.unref?.()
@@ -243,11 +252,9 @@ async function runConsultChild(ctx, session, id, m, problem, ctrl) {
243
252
  else logEvent("child:error", { ...base, err: errText(payload, 200) })
244
253
  }
245
254
  // R17: relay prefix assigned before the child runner arms — the per-child TUI
246
- // block freeze emits only when a block actually exists (relay established).
255
+ // block event emits only when a block actually exists (relay established).
247
256
  let relayPrefix = null
248
- const settle = (ok, payload) => settleChild(agent, session, id, label, ok, payload, relayPrefix
249
- ? () => ctx.callbacks?.onToken?.(`${relayPrefix}⟦ev⟧done\x1e0\x1e0\x1edone\x1e`)
250
- : null)
257
+ const settle = (ok, payload) => settleChild(agent, session, id, label, ok, payload, relayPrefix, ctx.callbacks?.onToken)
251
258
  try {
252
259
  // Provider resolution: consultModels entries are { provider, model, effort? } — resolve
253
260
  // via the subagent's provider resolver ("provider:model" handles cross-provider picks).
@@ -282,16 +289,22 @@ async function runConsultChild(ctx, session, id, m, problem, ctrl) {
282
289
  // role "consult" (overlay + base would concatenate it twice).
283
290
  role: "consult",
284
291
  })
285
- // TUI-OOM-ROOTCAUSE §23.3.1:子代理人读线窗口(四处创建点同置)——consult 会话跨回合驻留,
292
+ // TUI-OOM-ROOTCAUSE·AGENT-LOOP.md §6.15:子代理人读线窗口(四处创建点同置)——consult 会话跨回合驻留,
286
293
  // 不置窗则 _fullHistory 仍无界(F-O1 覆盖 depth>0 全部创建点)
287
294
  child._historyWindow = RECORD_WINDOW_MESSAGES
295
+ // SUBAGENT-UPSTREAM-CHANNEL 补赋(TURN-CAP-CONTINUE.md §3.1):会诊 = 后台族(consult_start
296
+ // 发后即返)⇒ 父在飞可答——撞帽检查点的上行 ask 与 notify_parent 同源通道。
297
+ child._upstream = { parent: agent, label, sync: false }
288
298
 
289
- // Activity relay via the unified spawn-child pipeline (§7.2 D3): `consult#<subId>/`
299
+ // Activity relay via the unified spawn-child pipeline (docs/cli/design/TUI.md §6.8 D3): `consult#<subId>/`
290
300
  // prefix (same channel subagent uses — parallel consultants stay independent) +
291
301
  // onToolOutput passthrough so the consultant's tool output lands in its TUI block.
292
302
  relayPrefix = makeRelay(agent, "consult", ctx.callbacks?.onToken, provider.model ?? "")
293
303
  // LOGGING:arm(spawn 事件——relay 建立后;子内事件归属 _logId)
294
304
  childLogId = relayPrefix.slice(0, -1)
305
+ // #748:会话自持 `childIds`(= 子块 relay 号——块键形 `consult#<N>` 的 N);无块子块不入表
306
+ // (relayPrefix 缺省 = spawn 前失败——从未开块)。消费窗三端按该表逐子块展开(同源)。
307
+ session.childIds.push(childLogId.slice(childLogId.lastIndexOf("#") + 1))
295
308
  child._logId = childLogId
296
309
  logT0 = Date.now()
297
310
  logArmed = true
@@ -300,24 +313,29 @@ async function runConsultChild(ctx, session, id, m, problem, ctrl) {
300
313
  let declined = false // review #1: guard against double-settle when onDeclined fired
301
314
 
302
315
  // Turn-cap continue loop (TURN-CAP-CONTINUE.md) via runWithContinue (§7.2 D3): hitting
303
- // the cap asks the user via the SAME y/n panel the main agent uses — unlimited
304
- // continues, each with a fresh turn budget AND a re-armed wall-clock watchdog (a
305
- // continue is a fresh budget, the clock restarts too). Parallel consultants serialize
306
- // their prompts through a session-level queue. Declined / headless → failed reply
307
- // (partial diagnosis).
316
+ // the cap reports to the PARENT agent through the turn-cap checkpoint — the user y/n
317
+ // card is gone (F8; see the askContinue hook below). Each grant = a fresh turn budget
318
+ // AND a re-armed wall-clock watchdog (a continue is a fresh budget, the clock restarts
319
+ // too). Parallel consultants serialize their prompts through a session-level queue.
320
+ // Declined / headless → failed reply (partial diagnosis).
308
321
  const runner = ctx.runAgent ?? runAgent
309
322
  try {
310
323
  const result = await runWithContinue(
311
324
  (childAgent, input, cbs, opts) => runner(childAgent, input, cbs, opts),
312
325
  child, "# Problem\n" + problem,
313
326
  childCallbacks,
314
- { depth: 1, maxTurns: agent?.config?.agent?.consultTurns ?? CONSULT_TURNS, signal: ctrl.signal },
315
327
  {
328
+ depth: 1, maxTurns: agent?.config?.agent?.consultTurns ?? CONSULT_TURNS, signal: ctrl.signal,
329
+ // send 注入队列回合边界消费(缺口补配——续段文本落 rec.child._injected 后由此消费)。
330
+ consumeInjected: (ag) => { drainInjectedQueue(child, ag ?? child) },
331
+ },
332
+ {
333
+ // F8(TURN-CAP-CONTINUE.md §1 #7 / #5):撞帽 ⇒ 报请父代理(检查点)——不再弹
334
+ // continue 卡;报请串行在会话级 `continueQueue` 上(唯一会话级队列消费者)。
335
+ // 续期 ⇒ 新段预算 + watchdog 重挂(N3)。headless 亦登记(答复通道 = 父 send,
336
+ // 非权限 handler——§2.5 用例 13)。
316
337
  askContinue: (e) => {
317
- if (!ctx.onPermissionRequest) return Promise.resolve(false)
318
- // 残环批(2026-09-16):归属键形态(`consult#<relayN>`——端侧键形解析的输入契约;
319
- // 原 label = 显示名不携 id);relay 未建立(spawn 前失败路径不可达此 ask)时回落 label。
320
- const ask = () => ctx.onPermissionRequest("continue", { turns: e.turn, agent: relayPrefix ? relayPrefix.slice(0, -1) : label })
338
+ const ask = () => registerTurnCapCheckpoint(child, session, { turn: e.turn, signal: ctrl.signal })
321
339
  session.continueQueue = (session.continueQueue ?? Promise.resolve()).then(ask, ask)
322
340
  return session.continueQueue.then((go) => {
323
341
  if (go) {
@@ -381,24 +399,12 @@ export const consultStartTool = {
381
399
  name: "consult_start",
382
400
  readonly: false,
383
401
  sideEffectExempt: true,
384
- description:
385
- "Start a parallel multi-model consultation (会诊) for a hard problem you are stuck on (repeated failures, no headway). " +
386
- "Call it directly when the user asks for 会诊 / consult — an explicit user request applies even if you are not 'stuck'. " +
387
- "Several configured models (agent.consultModels) analyze the same problem INDEPENDENTLY and in parallel. " +
388
- "Non-blocking: returns immediately with a consult id; the consultants keep running in the background across turns. " +
389
- "When EVERY model has replied (or failed), the full verdict text is delivered to you automatically — as a system " +
390
- "reminder at the next run start, or digested on its own while the session is idle — judge and adopt each opinion " +
391
- "yourself with your own tools (opinions are suggestions, not gates). To stop a session early (user changed their " +
392
- "mind / wants the tokens back), call consult_stop(id) — a stopped session delivers no digest.\n" +
393
- "Parameters:\n" +
394
- "- problem (required): a brief — the symptom, what you already tried (failure trail), and entry-point files. " +
395
- "Do NOT paste raw error logs; consultants pull the main session history themselves via their main_history tool.\n" +
396
- "- models (optional): subset of agent.consultModels to run — an array of \"provider:model\", bare provider, or bare model names (case-insensitive). Omit to run all.",
402
+ description: DESC("consult_start"),
397
403
  parameters: {
398
404
  type: "object",
399
405
  properties: {
400
406
  problem: { type: "string", description: "Problem brief (symptom + failure trail + entry files)" },
401
- models: { type: "array", items: { type: "string" }, description: 'Optional subset of agent.consultModels to run (default: all). Each entry is "provider:model", a bare provider name, or a bare model name (case-insensitive).' },
407
+ models: { type: "array", items: { type: "string" }, description: 'Optional subset of agent.consultModels to run (default: all) — each entry "provider:model", a bare provider, or a bare model name (case-insensitive).' },
402
408
  },
403
409
  required: ["problem"],
404
410
  },
@@ -419,7 +425,7 @@ export const consultStartTool = {
419
425
  agent._consultSessions ??= new Map()
420
426
  const id = String((agent._consultIdCounter = (agent._consultIdCounter ?? 0) + 1))
421
427
  const session = {
422
- id, controllers: [], replies: [], pending: 0,
428
+ id, controllers: [], replies: [], childIds: [], pending: 0,
423
429
  failed: 0, terminated: 0, stopped: false, received: 0, total: run.length,
424
430
  models: run.map(consultLabel),
425
431
  }
@@ -445,13 +451,7 @@ export const consultStopTool = {
445
451
  name: "consult_stop",
446
452
  readonly: false,
447
453
  sideEffectExempt: true,
448
- description:
449
- "Cancel a still-running consultation session (会诊) — the user changed their mind, the problem resolved, or you want the tokens back. " +
450
- "Aborts every consultant that is still running; a stopped session delivers NO digest (R17 — its already-collected partial replies are dropped). " +
451
- "Sessions that finished on their own are no longer cancellable — their verdict text is delivered automatically.\n" +
452
- "Returns JSON {abandoned: <pending count>, cancelled: true} — or {error: \"unknown consult id\"} (already finished/cancelled).\n" +
453
- "Parameters:\n" +
454
- "- id (required): the consult id from consult_start",
454
+ description: DESC("consult_stop"),
455
455
  parameters: {
456
456
  type: "object",
457
457
  properties: {
@@ -464,7 +464,7 @@ export const consultStopTool = {
464
464
  if (!s) return JSON.stringify({ error: "unknown consult id" })
465
465
  const abandoned = s.pending
466
466
  s.stopped = true
467
- // §20.3 站点 #11(第 24 批):会话级停 = stop
467
+ // §6.12 站点 #11(第 24 批):会话级停 = stop
468
468
  for (const c of s.controllers) { try { c.abort({ abortTrigger: "stop", abortDetail: "consult-stop" }) } catch { /* already settled */ } }
469
469
  return JSON.stringify({ abandoned, cancelled: true })
470
470
  },
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * agent-tools/context.mjs — `context` 工具(CONTEXT-COMPACTION.md §6.16 · context-tool 批 2026-09-21)。
3
3
  * 单一工具三操作(用户定形 F-CC5):`stats`(感知 F-CC1)· `prune`(噪声清理 F-CC3)· `compact`(主动压缩 F-CC2,携 focus + 自动附任务/目标 + 可回查锚——裁令 A/D)。
4
- * 内联 description(**不新增 tool-docs 档**——24 档族计数零动);装配面 = 登记册 `agent-tools.mjs`(#83)+ `agent/family-tools.mjs` 的 depth-0 段(`depthOnly`)——两端自动同表;**depth>0 不给**(§6.16.6)。
4
+ * 描述外置(tool-docs/context.md——#15 统一面:文本单点 = tool-docs,DESC 加载);装配面 = 登记册 `agent-tools.mjs`(#83)+ `agent/family-tools.mjs` 的 depth-0 段(`depthOnly`)——两端自动同表;**depth>0 不给**(§6.16.6)。
5
5
  * 本档同时是**注记文案 / 轻推行 / 回执文案的单源**(核与 VSC 的 run-stages 消费点经动态 import 取用——零新增静态边)。
6
6
  * 机械面:`stats` 不新增第三口径(阈值取本回合判定值;百分比走 `historyPercent` 与状态行同式);`prune` = 合格集(`token-window.mjs` 单源)+「原位换内容」应用面(`context.mjs`);`compact` **当次零执行**(只登记 `agent._pendingCompact`,由下一安全点消费——§6.16.2)。
7
7
  */
8
8
  import { contextUsage, estimateTokens, collectStaleToolOutputs, historyPercent, PRUNE_MIN_TOKENS } from "../token-window.mjs"
9
9
  import { pruneStaleToolOutputs, COMPRESS_FAILURE_LIMIT } from "../context.mjs"
10
10
  import { slotPath } from "../session-slots.mjs"
11
+ // #15 描述外置:描述文本单点 = tool-docs/context.md(DESC 单解析面,缺档抛错语义不变)
12
+ import { DESC } from "../tools/shared.mjs"
11
13
 
12
14
  /** 轻推行前缀(单活体去重判据——§6.16.5「同前缀旧行先滤」)。 */
13
15
  export const CONTEXT_NUDGE_PREFIX = "[System reminder: task/goal changed —"
@@ -125,25 +127,7 @@ function compactReceipt(agent, focus, replaced) {
125
127
 
126
128
  export const contextTool = {
127
129
  name: "context",
128
- description:
129
- "Manage your own context window — see how full it is, drop stale tool output, or compact the earlier " +
130
- "conversation into a summary at a moment of your own choosing.\n" +
131
- "- action='stats': current usage — total vs the compaction threshold, per-segment shares, and how much " +
132
- "is prunable / reclaimable.\n" +
133
- "- action='prune': drop the CONTENT of stale tool results (older than the protected tail) and replace it " +
134
- "with a short stub — tool pairing stays intact, the session record keeps the full text, and the tool can " +
135
- "simply be re-run.\n" +
136
- "- action='compact': compact the earlier conversation now, with 'focus' — write what the UPCOMING work " +
137
- "needs (1–3 sentences: the goal, the files, the constraints that must survive). The summary is written to " +
138
- "serve that focus; the current task list and goal are attached automatically. It runs at the next safe " +
139
- "point (before the next request — never mid-exchange) through the same machine as the automatic " +
140
- "compaction: same summary chain, same panel, same failure chain (3 consecutive failures degrade to a " +
141
- "deterministic truncation). The receipt carries this session's record path, so nothing is lost for good.\n" +
142
- "Nothing here runs on its own — the automatic threshold compaction stays in place as a fallback; whether " +
143
- "to compact is your call. Errors come back as 'Error: ...' and change nothing.\n" +
144
- "Parameters:\n" +
145
- "- action (required): stats | prune | compact\n" +
146
- "- focus (required for compact): what the upcoming work needs — the summary is weighted toward it",
130
+ description: DESC("context"),
147
131
  parameters: {
148
132
  type: "object",
149
133
  properties: {
@@ -23,7 +23,7 @@ export function buildApprovedSuffix(designToken, designId, slotCount) {
23
23
  return `Approved. Pass this exact token to eng-coder (designToken parameter): ${designToken}\ndesignId: ${designId} (pass as the designId parameter when spawning eng-coder — optional while this session holds a single design; ${slotCount} approved design slot(s) held as of this approval, and the count may have changed since — with several designs the spawn gate refuses a missing designId and lists the held ids)`
24
24
  }
25
25
 
26
- /** F2e (§29.1): strip the engine-generated Approved suffix from a report before
26
+ /** F2e: strip the engine-generated Approved suffix from a report before
27
27
  * it becomes a prior — the suffix is deterministic (buildApprovedSuffix), so the
28
28
  * truncation is exact; a text not ending in it passes through untouched. */
29
29
  export function stripApprovedSuffix(text, suffix) {
@@ -83,7 +83,7 @@ export function settleDesignReview(agent, run, designToken, rawResult, opts = {}
83
83
  if (!designToken || typeof rawResult !== "string") {
84
84
  return { passed: false, output: rawResult ?? "" }
85
85
  }
86
- // A(F11/§14.3 消费点 1):未完成即不签发——判据与 code 完成守卫同源(单谓词)。
86
+ // A(F11/ADVISOR-GUARDS.md §1 消费点 1):未完成即不签发——判据与 code 完成守卫同源(单谓词)。
87
87
  const incomplete = opts?.incomplete ?? null
88
88
  if (incomplete) {
89
89
  const stripped = rawResult.replace(makeDesignTokenRegex(designToken, "g"), "").trim()
@@ -107,7 +107,7 @@ export function settleDesignReview(agent, run, designToken, rawResult, opts = {}
107
107
  if (agent._role === "eng-coder") agent._engDesignReviewed = true
108
108
  run.open = false // approval closes this doc-set instance — next review is fresh
109
109
  const clean = rawResult.replace(makeDesignTokenRegex(designToken, "g"), "").trim()
110
- // F2c (§29.1): id echo + omission guidance + point-in-time slot snapshot — the
110
+ // F2c: id echo + omission guidance + point-in-time slot snapshot — the
111
111
  // same suffix the F2e prior stores strip with (stored for the exact truncation).
112
112
  run.approvedSuffix = buildApprovedSuffix(designToken, run.designId, agent._engDesignTokens.size)
113
113
  return {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * digest-budget.mjs — digest 注入批量预算单源(群 B 批 B5——AGENT-LOOP.md §22 语义源)。
2
+ * digest-budget.mjs — digest 注入批量预算单源(群 B 批 B5——AGENT-LOOP.md §6.14 语义源)。
3
3
  * 原预算块居 subagent-async.mjs(只覆盖经 injectAsyncResult 的三族:subagent / advisor /
4
4
  * escalate),consult 族注入器(injectConsultResult)绕过 ⇒ 多族条目同轮合并注入时可累计
5
5
  * 超限(BATCH-3-STRUCTURE F-2 原始事故面 1.3MB 请求体)。本档把常量 / 判超 / 记账 / 落盘
@@ -18,6 +18,8 @@ import { ENG_ON_REMINDER, ENG_OFF_REMINDER } from "../agent.mjs"
18
18
  import { clearPlanMode } from "./plan.mjs"
19
19
  import { purgeExpiredDesignTokens } from "../token-ttl.mjs"
20
20
  import { resolveEngineeringManifest } from "../manifest.mjs"
21
+ // 描述面 = `tool-docs/eng.md`(#15 描述外置统一——DESC 单一解析面,与内置工具族同径)
22
+ import { DESC } from "../tools/shared.mjs"
21
23
 
22
24
  // ─── 持久化镜像 / 面板提示注入缝(#91——「写盘镜像 / 面板提示按端注入」,形态参 §2.13.5 注入缝)──
23
25
  /**
@@ -41,9 +43,7 @@ async function mirrorNotice(enabled, ctx) {
41
43
 
42
44
  export const engTool = {
43
45
  name: "eng",
44
- description:
45
- "Enter or exit engineering mode. In engineering mode, follow design-before-code: write a design document, run advisor design review, get user approval, then implement via eng-coder subagents. " +
46
- "Returns the mode state — 'Engineering mode activated/exited' (an already-active state is acknowledged).",
46
+ description: DESC("eng"),
47
47
  parameters: {
48
48
  type: "object",
49
49
  properties: {
@@ -59,7 +59,7 @@ export const engTool = {
59
59
  // R16 (F-R16a): OFF 不清 token——有效 token 跨模式存活(设计评审过的产物不因
60
60
  // 开关重复烧)。过期清理跑在另外三处(恢复过滤 / 开模式 / spawn 门禁拒)。
61
61
  ctx.agent._advisorRound = 0 // reset convergence budget
62
- ctx.agent._advisorRuns = new Map() // §11.2 D-24b: per-review instances die with the mode (fresh cycles)
62
+ ctx.agent._advisorRuns = new Map() // AGENT-LOOP-ASYNC-POOL.md §6.10 D-24b: per-review instances die with the mode (fresh cycles)
63
63
  ctx.agent._touchedFiles = [] // clear mutation tracking
64
64
  ctx.agent._lastEngState = false
65
65
  ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
@@ -90,7 +90,7 @@ export const engTool = {
90
90
  // R16 (F-R16b ②): off→on 不重评——只清过期 token(用户裁定"打开工程模式时
91
91
  // 应该清理"),有效 token 原样保留——遍历 Map 删过期,返回文案含清理个数。
92
92
  const cleared = purgeExpiredDesignTokens(ctx.agent)
93
- ctx.agent._advisorRuns = new Map() // §11.2 D-24b: per-review instances die with the mode (fresh cycles)
93
+ ctx.agent._advisorRuns = new Map() // §6.10 D-24b: per-review instances die with the mode (fresh cycles)
94
94
  ctx.agent._lastEngState = true
95
95
  ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
96
96
  ctx.agent._pendingReminders.push(ENG_ON_REMINDER)
@@ -1,5 +1,5 @@
1
1
  /**
2
- * escalate-async.mjs — async 飞刀 runner (AGENT-LOOP.md §25 D-R17b — R17, 2026-09-06).
2
+ * escalate-async.mjs — async 飞刀 runner (ESCALATE.md §5 — R17, 2026-09-06).
3
3
  *
4
4
  * The subagent action:"escalate" is DEFAULT-async at depth 0 (decision ③: the
5
5
  * depth-0-only escalate flips to async like every other background family;
@@ -27,7 +27,7 @@
27
27
  import { relative, isAbsolute } from "node:path"
28
28
  import { runAgent, createAgent, DEFAULT_SUBAGENT_TURNS, excludeSubagentTools } from "../agent.mjs"
29
29
  import { runWithContinue, TURN_CAP_MARK, wrapChildCallbacks } from "../agent/spawn-child.mjs"
30
- // TUI-OOM-ROOTCAUSE §23.3.1:子代理人读线窗口常量(单源——store 零依赖)。
30
+ // TUI-OOM-ROOTCAUSE·AGENT-LOOP.md §6.15:子代理人读线窗口常量(单源——store 零依赖)。
31
31
  import { RECORD_WINDOW_MESSAGES } from "../session-store.mjs"
32
32
  import { logEvent } from "../log.mjs"
33
33
  import { deathLine } from "../abort-provenance.mjs"
@@ -38,6 +38,8 @@ import { refreshQueuedTokens, nextSubagentId, consumeSubagentToken, assertPoolKe
38
38
  import { mutationSeqOf } from "./advisor-async.mjs"
39
39
  // ASYNC-RESULT-CONTAINER.md D3/D6:settle 公共收尾单点 + child signal 构建单点
40
40
  import { bindChildController, buildChildSignal, settleAsyncEntry } from "./async-settle.mjs"
41
+ import { registerTurnCapCheckpoint } from "./checkpoint.mjs"
42
+ import { drainInjectedQueue } from "./subagent-run.mjs"
41
43
 
42
44
  /** Parent-side mutations (absolute paths) committed AFTER the escalate launch —
43
45
  * the overlap scan feeds the settle classification (review #4/round2 #4: the
@@ -90,7 +92,7 @@ function touchedFilesNote(child, cwd) {
90
92
  }
91
93
 
92
94
  /**
93
- * Async escalate settle — three-way classification (§25 D-R17b review #4):
95
+ * Async escalate settle — three-way classification (ESCALATE.md §5 D-R17b review #4):
94
96
  * - done: merge ALL mutations (the escalation's changes are the parent's —
95
97
  * verify/advisor guards must see them) + overlap warning into the report
96
98
  * (report-level hint — not a gate — round2 #4);
@@ -104,7 +106,7 @@ function touchedFilesNote(child, cwd) {
104
106
  */
105
107
  export function classifyEscalateSettle(parent, entry) {
106
108
  if (entry.cancelled) return { cancelled: true }
107
- // §23.3.1(TUI-OOM-ROOTCAUSE)——childAgent 语义面:settle 分类(touched 摘要/partial
109
+ // AGENT-LOOP.md §6.15(TUI-OOM-ROOTCAUSE)——childAgent 语义面:settle 分类(touched 摘要/partial
108
110
  // merge)与 status/observe 摘要的读取源;**消化注入完成后由 `releaseSettledEntry`
109
111
  // (async-settle.mjs)置 null**(池内未消化窗口不释放——表 2 候选 2 否决)。
110
112
  const child = entry.childAgent
@@ -148,7 +150,7 @@ export function launchEscalateAsync(parent, ctx, launch) {
148
150
  const { task, provider, tag, effortNote } = launch
149
151
  parent._asyncSubagents ??= new Map()
150
152
  parent._asyncQueue ??= []
151
- // Async id allocation (AGENT-LOOP.md §15 D-A1 precedent): reserve the relay
153
+ // Async id allocation (AGENT-LOOP-SUBAGENT.md §6.7.3 D-A1 precedent): reserve the relay
152
154
  // counter at launch — the returned id stays stable while the entry sits queued.
153
155
  // The [model] token (TUI block creation) is DEFERRED to actual start so queued
154
156
  // flights don't paint an empty panel block (subagent-parity).
@@ -161,7 +163,7 @@ export function launchEscalateAsync(parent, ctx, launch) {
161
163
  const relayPrefix = `escalate#${id}/`
162
164
  const entry = {
163
165
  id, role: "escalate", relayPrefix,
164
- _pool: poolDomainOf("escalate"), // other — shares the domain with explore/plan/coder (§11.1 D-24a)
166
+ _pool: poolDomainOf("escalate"), // other — shares the domain with explore/plan/coder (AGENT-LOOP-ASYNC-POOL.md §6.10 D-24a)
165
167
  status: "queued",
166
168
  position: undefined,
167
169
  report: null, error: null, done: false, cancelled: false,
@@ -200,7 +202,7 @@ export function launchEscalateAsync(parent, ctx, launch) {
200
202
  role: "coder",
201
203
  })
202
204
  entry.childAgent = child // settle 分类/status touched 摘要绑定(start 时刻)
203
- child._historyWindow = RECORD_WINDOW_MESSAGES // TUI-OOM-ROOTCAUSE §23.3.1:子代理人读线窗口(四处创建点同置)
205
+ child._historyWindow = RECORD_WINDOW_MESSAGES // TUI-OOM-ROOTCAUSE·AGENT-LOOP.md §6.15:子代理人读线窗口(四处创建点同置)
204
206
  child._logId = relayPrefix.slice(0, -1)
205
207
  // SUBAGENT-UPSTREAM-CHANNEL(§6.27.4 W3——escalate **async**:工具返回注走异步形)。
206
208
  child._upstream = { parent, label: relayPrefix.slice(0, -1), sync: false }
@@ -224,6 +226,9 @@ export function launchEscalateAsync(parent, ctx, launch) {
224
226
  signal: entry.controller.signal,
225
227
  // §2.5 #78 并入:escalate 子代理输出流式(VSC subagent-escalate-async 同款豁免)。
226
228
  streamOutput: true,
229
+ // SUBAGENT-OBSERVE-SEND 缺口补配(TURN-CAP-CONTINUE.md §3.1 :85):send 注入队列
230
+ // 回合边界消费(否则续段文本静默丢弃)。空队列 no-op。
231
+ consumeInjected: (ag) => { drainInjectedQueue(entry, ag ?? child) },
227
232
  }
228
233
  // 权限按 async 子代理同款装配:AUTO 直放行;手动档经父 _permQueue(并行子代理
229
234
  // 审批不叠弹窗)——背景飞行撞门时无 handler → denied 不悬挂(D-S7 同规则)。
@@ -242,9 +247,9 @@ export function launchEscalateAsync(parent, ctx, launch) {
242
247
  { ...childCallbacks, onPermissionRequest: childPermission },
243
248
  runOpts,
244
249
  {
245
- // 后台飞行不弹 continue 面板(D-A3 §15 例外同款):AUTO && engineering 自动
246
- // resume——escalate 只在 normal 模式可用(engineering 拒)——恒自动拒 → partial。
247
- askContinue: () => Promise.resolve(Boolean(parent.config?.agent?.engineering && parent.autoApprove)),
250
+ // F8(TURN-CAP-CONTINUE.md §1 #7):撞帽 ⇒ 父代理检查点(同池、同载具——send 兑现 /
251
+ // cancel 走既有 abort ⇒ partial + TURN_CAP_MARK);原 AUTO 自动 resume 由 #7 取代。
252
+ askContinue: (e) => registerTurnCapCheckpoint(child, entry, { turn: e.turn, signal: entry.controller.signal }),
248
253
  onDeclined: (e, output) => `escalate (${tag})${entry.effortNote} ${TURN_CAP_MARK} (${e.turn} turns) — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${output.slice(0, 2000)}`,
249
254
  },
250
255
  )
@@ -265,7 +270,7 @@ export function launchEscalateAsync(parent, ctx, launch) {
265
270
  // 运行失败/中止:错误文本落 entry.error(子代理同款——cancel 分支忽略它;
266
271
  // Ctrl+I 中止的残条目由收尾消化带错误文本——不落空 "(no report)" digest)。
267
272
  const child = entry.childAgent
268
- // §20.3 第 3 条合成器(第 24 批):原 message 前缀逐字保留 + 来源后缀
273
+ // AGENT-LOOP-SUBAGENT.md §6.12 第 3 条合成器(第 24 批):原 message 前缀逐字保留 + 来源后缀
269
274
  entry.error = `escalate (${tag}) error: ${deathLine(e, entry.controller?.signal)}\nPartial output: ${(child?._capturedOutput ?? "").slice(0, 2000)}`
270
275
  })
271
276
  .finally(() => {
@@ -1,21 +1,14 @@
1
1
  // F-CC4(CONTEXT-COMPACTION.md §6.16.5):goal 任一状态变更 ⇒ 方向转换轻推(本档只调用;文案/去重单源 = agent-tools/context.mjs)
2
2
  import { pushContextNudge } from "./context.mjs"
3
+ import { DESC } from "../tools/shared.mjs"
3
4
 
4
5
  /**
5
6
  * goal tool: lifecycle management for long-running autonomous goals (completion contract).
6
- * Three states: active / complete / blocked. Completion must pass a verify evidence threshold;
7
- * blocked is only accepted after the same condition persists 3 consecutive times.
8
- * The system injects status + budget progress + audit discipline every turn.
7
+ * 描述面 = `tool-docs/goal.md`(DESC() 加载);完成阈 / 阻塞计次 / 每回合注入 = execute 内实现注。
9
8
  */
10
9
  export const goalTool = {
11
10
  name: "goal",
12
- description:
13
- "Manage a long-running autonomous goal. " +
14
- "action='set': create or replace the goal — must have a verifiable completion criterion (a machine-checkable proof, not vague effort). " +
15
- "action='complete': mark achieved — only after the criterion's check has actually passed. " +
16
- "action='blocked': report an impasse (requires 'reason') — only after 3 genuine attempts. " +
17
- "action='cancel': abandon the goal. " +
18
- "Returns a status line — the goal set/updated/completed/blocked/cancelled confirmation, or Error: ... with the reason.",
11
+ description: DESC("goal"),
19
12
  parameters: {
20
13
  type: "object",
21
14
  properties: {
@@ -8,14 +8,14 @@
8
8
  *
9
9
  * 现算面板块列表(state.subTasks 活值纯推导——无时点快照语义)。
10
10
  * status 三态映射:done+awaitingDigest → "awaitingDigest";done → "done";
11
- * queued(waiting/等位未启动)→ "queued";其余 → "running"(§20 D-SD3b queued 态入镜口径保留)。
11
+ * queued(waiting/等位未启动)→ "queued";其余 → "running"(AGENT-LOOP-SUBAGENT.md §6.9 D-SD3b queued 态入镜口径保留)。
12
12
  */
13
13
  export function computePanelBlocks(state) {
14
14
  const subs = state?.subTasks
15
15
  if (!subs) return null // 未挂载(headless/mock 无 TUI state)——无面板
16
16
  const blocks = []
17
17
  for (const sub of Object.values(subs)) {
18
- // §20 D-SD3b:waiting/等位块(sub.queued——未启动)以 queued 态入镜(视图与面板
18
+ // AGENT-LOOP-SUBAGENT.md §6.9 D-SD3b:waiting/等位块(sub.queued——未启动)以 queued 态入镜(视图与面板
19
19
  // 所见一致——action:'panel' 视图 + freeze 门控读此态)。
20
20
  const status = sub.done ? (sub.awaitingDigest ? "awaitingDigest" : "done") : (sub.queued ? "queued" : "running")
21
21
  blocks.push({ key: sub.key, role: sub.role ?? null, status, startedAt: sub.started ?? Date.now() })
@@ -43,6 +43,8 @@ import { carrierField, getAsyncPool, tombstoneOf, wakeAsyncWaiters } from "./asy
43
43
  import { pushReal } from "../context.mjs"
44
44
  import { escapeXml } from "../agent/helpers.mjs"
45
45
  import { logEvent } from "../log.mjs"
46
+ // #15 描述外置:描述文本单点 = tool-docs/notify_parent.md(DESC 单解析面,缺档抛错语义不变)
47
+ import { DESC } from "../tools/shared.mjs"
46
48
 
47
49
  /** message 长度上限(字符——闸三上界)。 */
48
50
  export const UPSTREAM_MSG_MAX = 1500
@@ -180,19 +182,7 @@ export function drainChildUpstream(agent) {
180
182
  */
181
183
  export const parentChannelTool = {
182
184
  name: "notify_parent",
183
- description:
184
- "A one-way channel to your PARENT (the agent that spawned you) — you are a subagent, so there is no user to ask.\n" +
185
- "Queue a message on the parent's side; your turn is not interrupted and nothing is consumed from it.\n" +
186
- "- kind:'ask' — a question whose answer changes your next step and that you cannot answer from the materials you can read\n" +
187
- " (task book / design doc / repo). The parent replies with subagent action:'send'; you receive it as an ordinary\n" +
188
- " instruction at your next turn boundary. One ask at a time while the previous one is still waiting in the parent's queue.\n" +
189
- "- kind:'note' — an FYI that needs no answer (a premise you found broken, a conflict you resolved and want visible early).\n" +
190
- "- NON-BLOCKING: the call returns immediately. Keep working on the unaffected parts; keep the affected part pending.\n" +
191
- " There is no fetch and no waiting — if you finish first, report the unanswered part as not done (never idle, never poll).\n" +
192
- "- SYNCHRONOUS SPAWN: if the parent is blocked on your run (sync spawn), nothing can be sent back to you — the reply path (`send`) reaches only a RUNNING ASYNC child. The message is read when the parent's call returns (it may re-spawn you with an answer); report the unanswered part as not done.\n" +
193
- "- Out of scope: naming / implementation / wording details, anything a read or a command would answer, trade-offs the\n" +
194
- " task book already states. When in doubt use the stop-and-report discipline — this is not an escape from your own judgment.\n" +
195
- "- Availability: subagents only (depth > 0). At depth 0 you talk to the user through your normal reply or the question tool.",
185
+ description: DESC("notify_parent"),
196
186
  parameters: {
197
187
  type: "object",
198
188
  properties: {
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * plan tool: enter/exit plan mode.
3
- * In plan mode only read-only tools are allowed — explore code, design solutions, no code writing.
4
- * Exit plan mode to present the plan for the user's approval; implement only after they approve.
3
+ * 描述面 = `tool-docs/plan.md`(DESC() 加载)。
5
4
  *
6
5
  * Reminder cadence (kimi-code style): while plan mode is active the agent loop
7
6
  * re-injects reminders — sparse every 2 turns, full every 5 turns or when the
8
7
  * user sends a new message — so the constraint never fades from context.
9
8
  */
9
+ import { DESC } from "../tools/shared.mjs"
10
10
 
11
11
  const PLAN_FULL_REMINDER =
12
12
  "[System reminder: plan mode is ON. Workflow: (1) explore/read codebase with read-only tools, " +
@@ -88,8 +88,7 @@ export function planReminderForTurn(agent, userMessageSince) {
88
88
 
89
89
  export const planTool = {
90
90
  name: "plan",
91
- description:
92
- "Enter or exit plan mode. In plan mode you are restricted to READ-ONLY tools: read files, search code, run read-only shell commands. Use plan mode before complex multi-step tasks — explore the codebase, design the architecture, present a plan to the user. Exit plan mode to present the plan for the user's approval; implement only after they approve. For simple single-file edits, skip plan mode and just make the change.",
91
+ description: DESC("plan"),
93
92
  parameters: {
94
93
  type: "object",
95
94
  properties: {