@thincoder/core 0.9.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 (203) hide show
  1. package/abort-provenance.mjs +116 -0
  2. package/advisor/citations.mjs +139 -0
  3. package/advisor/compaction.mjs +174 -0
  4. package/advisor/convergence.mjs +80 -0
  5. package/advisor/history.mjs +77 -0
  6. package/advisor/loop.mjs +288 -0
  7. package/advisor/messages.mjs +299 -0
  8. package/advisor/notice.mjs +141 -0
  9. package/advisor/project-context.mjs +197 -0
  10. package/advisor/repos.mjs +150 -0
  11. package/advisor/run.mjs +190 -0
  12. package/advisor/truncate.mjs +57 -0
  13. package/advisor.mjs +281 -0
  14. package/agent/child-marks.mjs +24 -0
  15. package/agent/completion.mjs +145 -0
  16. package/agent/dispatch.mjs +493 -0
  17. package/agent/family-tools.mjs +174 -0
  18. package/agent/helpers.mjs +412 -0
  19. package/agent/post-turn.mjs +70 -0
  20. package/agent/record-results.mjs +174 -0
  21. package/agent/relay-prefix.mjs +39 -0
  22. package/agent/run-stages.mjs +244 -0
  23. package/agent/setup-reminders.mjs +199 -0
  24. package/agent/setup.mjs +234 -0
  25. package/agent/spawn-child.mjs +258 -0
  26. package/agent/suspension.mjs +240 -0
  27. package/agent/write-gate.mjs +87 -0
  28. package/agent-tools/advisor-async.mjs +481 -0
  29. package/agent-tools/advisor-settle.mjs +240 -0
  30. package/agent-tools/advisor.mjs +280 -0
  31. package/agent-tools/async-discard.mjs +143 -0
  32. package/agent-tools/async-settle.mjs +299 -0
  33. package/agent-tools/batch-segment.mjs +265 -0
  34. package/agent-tools/child-permission.mjs +45 -0
  35. package/agent-tools/consult.mjs +471 -0
  36. package/agent-tools/design-token.mjs +117 -0
  37. package/agent-tools/digest-budget.mjs +76 -0
  38. package/agent-tools/eng.mjs +102 -0
  39. package/agent-tools/escalate-async.mjs +302 -0
  40. package/agent-tools/goal.mjs +119 -0
  41. package/agent-tools/panel-blocks.mjs +24 -0
  42. package/agent-tools/parent-channel.mjs +231 -0
  43. package/agent-tools/plan.mjs +86 -0
  44. package/agent-tools/read-history.mjs +309 -0
  45. package/agent-tools/recent-changes.mjs +24 -0
  46. package/agent-tools/review-facts.mjs +31 -0
  47. package/agent-tools/settings.mjs +268 -0
  48. package/agent-tools/skill.mjs +63 -0
  49. package/agent-tools/spawn-gates.mjs +109 -0
  50. package/agent-tools/subagent-actions.mjs +496 -0
  51. package/agent-tools/subagent-async.mjs +456 -0
  52. package/agent-tools/subagent-panel.mjs +160 -0
  53. package/agent-tools/subagent-run.mjs +208 -0
  54. package/agent-tools/subagent-scheduler.mjs +446 -0
  55. package/agent-tools/subagent-spawn.mjs +478 -0
  56. package/agent-tools/subagent.mjs +419 -0
  57. package/agent-tools/task.mjs +87 -0
  58. package/agent-tools/timer.mjs +46 -0
  59. package/agent-tools/verify.mjs +295 -0
  60. package/agent-tools.mjs +23 -0
  61. package/agent.mjs +430 -0
  62. package/auto-think.mjs +115 -0
  63. package/compress-form.mjs +24 -0
  64. package/config-io.mjs +277 -0
  65. package/config-migrate.mjs +178 -0
  66. package/config-presets.mjs +49 -0
  67. package/config.mjs +419 -0
  68. package/context.mjs +495 -0
  69. package/conventions.mjs +223 -0
  70. package/embedding.mjs +120 -0
  71. package/escape.mjs +152 -0
  72. package/expand-home.mjs +16 -0
  73. package/explore-distill.mjs +152 -0
  74. package/generate-title.mjs +123 -0
  75. package/git/checkpoint.mjs +448 -0
  76. package/git/gitmem.mjs +100 -0
  77. package/history-window.mjs +179 -0
  78. package/hooks.mjs +108 -0
  79. package/i18n.mjs +106 -0
  80. package/index-bin.mjs +48 -0
  81. package/index-discover.mjs +176 -0
  82. package/ledger-cmd.mjs +209 -0
  83. package/ledger-db.mjs +86 -0
  84. package/ledger-surface.mjs +76 -0
  85. package/ledger.mjs +202 -0
  86. package/log.mjs +195 -0
  87. package/manifest.mjs +338 -0
  88. package/markdown.mjs +106 -0
  89. package/mcp/helpers.mjs +51 -0
  90. package/mcp/transport-http.mjs +248 -0
  91. package/mcp/transport-stdio.mjs +140 -0
  92. package/mcp/transport-ws.mjs +122 -0
  93. package/mcp.mjs +295 -0
  94. package/memory/code-index.mjs +219 -0
  95. package/memory/code-sync.mjs +427 -0
  96. package/memory/core.mjs +318 -0
  97. package/memory/delete.mjs +242 -0
  98. package/memory/docs.mjs +431 -0
  99. package/memory/file-walk.mjs +109 -0
  100. package/memory/origin.mjs +24 -0
  101. package/memory/scan.mjs +177 -0
  102. package/memory/schema.mjs +460 -0
  103. package/memory.mjs +21 -0
  104. package/model-ref.mjs +66 -0
  105. package/model-specs.mjs +277 -0
  106. package/package.json +32 -0
  107. package/peer-domains.mjs +265 -0
  108. package/peer-instances.mjs +178 -0
  109. package/permission.mjs +79 -0
  110. package/process-probe.mjs +315 -0
  111. package/prompt-files.mjs +113 -0
  112. package/prompt-overlays.mjs +78 -0
  113. package/prompts/advisor-design.md +43 -0
  114. package/prompts/advisor-round1.md +41 -0
  115. package/prompts/advisor-round2.md +46 -0
  116. package/prompts/advisor-round3.md +42 -0
  117. package/prompts/common.md +158 -0
  118. package/prompts/consult-base.md +19 -0
  119. package/prompts/discipline-engineering.md +123 -0
  120. package/prompts/discipline-normal.md +206 -0
  121. package/prompts/persona-coder.md +21 -0
  122. package/prompts/persona-eng-coder.md +41 -0
  123. package/prompts/persona-eng-designer.md +80 -0
  124. package/prompts/persona-engineering.md +160 -0
  125. package/prompts/persona-explore.md +15 -0
  126. package/prompts/persona-normal.md +35 -0
  127. package/prompts/persona-plan.md +27 -0
  128. package/provider/anthropic.mjs +225 -0
  129. package/provider/core.mjs +491 -0
  130. package/provider/errors.mjs +101 -0
  131. package/provider/google.mjs +257 -0
  132. package/provider/index.mjs +7 -0
  133. package/provider/list-models.mjs +163 -0
  134. package/provider/normalize.mjs +81 -0
  135. package/provider/rate.mjs +168 -0
  136. package/provider/responses.mjs +495 -0
  137. package/provider/retry.mjs +88 -0
  138. package/provider/sse.mjs +264 -0
  139. package/provider/wait-status.mjs +59 -0
  140. package/proxy.mjs +274 -0
  141. package/rules.mjs +53 -0
  142. package/session-gc.mjs +248 -0
  143. package/session-guard.mjs +59 -0
  144. package/session-lifecycle.mjs +305 -0
  145. package/session-migrate.mjs +48 -0
  146. package/session-rename.mjs +38 -0
  147. package/session-segments.mjs +99 -0
  148. package/session-slot-write.mjs +168 -0
  149. package/session-slots-manifest.mjs +264 -0
  150. package/session-slots.mjs +298 -0
  151. package/session-store.mjs +441 -0
  152. package/session.mjs +244 -0
  153. package/skills.mjs +234 -0
  154. package/text-budget.mjs +79 -0
  155. package/token-ttl.mjs +285 -0
  156. package/tool-docs/apply_patch.md +15 -0
  157. package/tool-docs/bash.md +38 -0
  158. package/tool-docs/delete.md +13 -0
  159. package/tool-docs/edit.md +30 -0
  160. package/tool-docs/execute.md +21 -0
  161. package/tool-docs/fetch.md +12 -0
  162. package/tool-docs/file_ops.md +17 -0
  163. package/tool-docs/get_current_time.md +8 -0
  164. package/tool-docs/git.md +54 -0
  165. package/tool-docs/glob.md +11 -0
  166. package/tool-docs/grep.md +19 -0
  167. package/tool-docs/hashline_edit.md +14 -0
  168. package/tool-docs/insert_after.md +15 -0
  169. package/tool-docs/lint.md +10 -0
  170. package/tool-docs/ls.md +12 -0
  171. package/tool-docs/lsp.md +10 -0
  172. package/tool-docs/process.md +10 -0
  173. package/tool-docs/question.md +16 -0
  174. package/tool-docs/read.md +20 -0
  175. package/tool-docs/read_image.md +8 -0
  176. package/tool-docs/tree.md +14 -0
  177. package/tool-docs/wait_for.md +22 -0
  178. package/tool-docs/websearch.md +16 -0
  179. package/tool-docs/write.md +11 -0
  180. package/tools/bash.mjs +276 -0
  181. package/tools/edit-batch.mjs +204 -0
  182. package/tools/edit-diff.mjs +388 -0
  183. package/tools/exec-run.mjs +43 -0
  184. package/tools/execute.mjs +243 -0
  185. package/tools/file.mjs +464 -0
  186. package/tools/git-checkpoint.mjs +143 -0
  187. package/tools/git-ext.mjs +173 -0
  188. package/tools/git.mjs +415 -0
  189. package/tools/glob-dialect.mjs +130 -0
  190. package/tools/index.mjs +76 -0
  191. package/tools/linter.mjs +120 -0
  192. package/tools/lsp.mjs +335 -0
  193. package/tools/ops.mjs +293 -0
  194. package/tools/patch.mjs +290 -0
  195. package/tools/question.mjs +26 -0
  196. package/tools/repomap.mjs +314 -0
  197. package/tools/search.mjs +248 -0
  198. package/tools/shared.mjs +467 -0
  199. package/tools/tree.mjs +81 -0
  200. package/tools/web.mjs +224 -0
  201. package/tools/write-path.mjs +191 -0
  202. package/traces/trace-store.mjs +303 -0
  203. package/undo-stack.mjs +47 -0
@@ -0,0 +1,496 @@
1
+ /**
2
+ * subagent-actions.mjs — subagent 动作执行器(§19/§19.5/§19.6——2026-09-05 自
3
+ * subagent-async.mjs 拆分——Module Split Policy §20.9——纯迁移零行为变化)。
4
+ * 内容:executeStatusAction(§19.5 D-M5——非阻塞池查询——touchedSummary/
5
+ * shortTouchedPath/statusFields 摘要助手随行)/ executeObserveAction + executeSendAction
6
+ * (SUBAGENT-OBSERVE-SEND——observe 只读/send 控制豁免注入)/ executeEscalateAction(§19
7
+ * D-M4——飞刀——touchedFilesNote 随行)。
8
+ * §19.8 check 删除后仅 cancel 动作执行器与共享 post-spawn 管线留在 subagent-async.mjs;
9
+ * §20 调度器 + 文件域组在 ./subagent-scheduler.mjs(describeBlockers 由此导入)。
10
+ * 2026-09-08 二次拆分:§19.6 面板段迁至 ./subagent-panel.mjs(601 > 500 硬限跨档——
11
+ * 尾部 re-export executePanelAction 保 subagent.mjs 既有 import 面)+ ASYNC-RESULT-
12
+ * CONTAINER.md D1 落地(池访问点改 getAsyncPool accessor)。
13
+ */
14
+ import { isAbsolute, relative } from "node:path"
15
+ import { runAgent, createAgent, DEFAULT_SUBAGENT_TURNS } from "../agent.mjs"
16
+ import {
17
+ runWithContinue, TURN_CAP_MARK, makeRelay, wrapChildCallbacks,
18
+ ensureChildApiKey, clampEffort,
19
+ } from "../agent/spawn-child.mjs"
20
+ import { logEvent, errText } from "../log.mjs"
21
+ import { describeBlockers, detectStall, STALL_NOTE } from "./subagent-scheduler.mjs"
22
+ import { resolveChildProvider, mergeChildMutations } from "./subagent-async.mjs"
23
+ import { launchEscalateAsync } from "./escalate-async.mjs"
24
+ // TUI-OOM-ROOTCAUSE §23.3.1:子代理人读线窗口常量(单源——store 零依赖)。
25
+ import { RECORD_WINDOW_MESSAGES } from "../session-store.mjs"
26
+ // ASYNC-RESULT-CONTAINER.md D1:池 accessor(absorb 双池——advisor 独立池无队列)
27
+ import { getAsyncPool } from "./async-settle.mjs"
28
+
29
+ /**
30
+ * subagent action:"status" (§19 D-M2, new): NON-BLOCKING async-pool query —
31
+ * returns immediately and never consumes a result (results belong to the auto
32
+ * channel — §19.8: no check action any more). Source of truth = the pool
33
+ * (_asyncSubagents): entries moved to _pendingAsyncResults during a suspension
34
+ * (§17 D-S3 ② — injected at the next run start) are no longer in the pool and
35
+ * are NOT counted as done-waiting.
36
+ * - id given → { id, role, status, model?, elapsedSec?, turn?, maxTurns?,
37
+ * touchedFiles?/touchedMore?/touched? ... } for that entry; unknown id → error
38
+ * (T12 semantics — unknown async-subagent id error, same wording as the pool).
39
+ * - id omitted → { overview: { running: [{id, role, model, elapsedSec, turn,
40
+ * maxTurns, touchedFiles?/touched?}], queued: [{id, role, position, touched?}],
41
+ * done: [{id, role}] } } — live queue positions (index in _asyncQueue + 1).
42
+ * §19.5.6: running 条目带 touched files 摘要(touchedFiles 前 5 + touchedMore 超出
43
+ * 计数——相对查询方 cwd;0 改动 → touched 占位);queued 条目带 touched 占位
44
+ * "—(未启动)";done/error/取消条目无 touched 字段(round3 #9)。
45
+ * A settled-but-unconsumed entry (settled during a NORMAL turn) reports done
46
+ * with a "not yet consumed" note — the auto channel (turn-end collection / the
47
+ * suspension digest) still delivers it afterwards (§19.8: sole consumption path).
48
+ */
49
+ /** §19.5 D-M5 decision-field assembly (F9): running entries report
50
+ * {id, role, model, elapsedSec, turn, maxTurns} — the data needed to decide
51
+ * WHO to cancel. Model is recorded at spawn (childProvider), startedAt at
52
+ * ACTUAL start (queued waits don't count), turn/maxTurns mirrored from the
53
+ * child's ⟦ev⟧turn events at the callbacks-wrap layer (subagent.mjs tracker).
54
+ * elapsedSec computed at call time from startedAt. */
55
+ /** §19.5.6 D-SF2/N-SF1 摘要形态:status 调用时实时读 entry.childAgent._touchedFiles
56
+ * (绝对路径——per-run 记账——§18.12)→ 相对查询方 cwd;cwd 之外保留绝对形态 +
57
+ * "../" 前缀;>80 字符截尾(不超行);前 5 个 + 独立截断字段 touchedMore(超出
58
+ * 计数——不混入数组——消费方按类型区分)。占位:running 0 改动 → "—(尚无改动)"
59
+ * (T-SF2a);queued 未启动 → "—(未启动)"(T-SF2b);done/error/取消条目不含
60
+ * 本摘要(round3 #9 明示——本批只做 running/queued)。 */
61
+ function touchedSummary(entry, cwd) {
62
+ if (entry.status !== "running") return { touched: "—(未启动)" }
63
+ const files = entry.childAgent?._touchedFiles ?? []
64
+ if (files.length === 0) return { touched: "—(尚无改动)" }
65
+ const shown = files.slice(0, 5).map((f) => shortTouchedPath(f, cwd))
66
+ const out = { touchedFiles: shown }
67
+ if (files.length > 5) out.touchedMore = files.length - 5
68
+ return out
69
+ }
70
+
71
+ /** N-SF1 单路径显示形态:cwd 内 → 相对路径;cwd 外 → "../" + 绝对路径;>80 截尾。 */
72
+ function shortTouchedPath(f, cwd) {
73
+ let p = f
74
+ if (cwd) {
75
+ const rel = relative(cwd, f)
76
+ p = rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : `../${f}`
77
+ }
78
+ return p.length > 80 ? `${p.slice(0, 79)}…` : p
79
+ }
80
+
81
+ function statusFields(entry, cwd) {
82
+ // §18.3 #1(第 10 批——双池并表):评审池条目专属字段面——role 区分两池;子代理条目
83
+ // 字段面零变化(NFR-B2)。reviewType(design|code) / round / elapsedSec——与子代理
84
+ // 的 turn/maxTurns 而非(评审无子回合预算语义)。
85
+ if (entry.role === "advisor") {
86
+ const base = { id: String(entry.id), role: entry.role, reviewType: entry.reviewType ?? null, round: entry.round ?? entry.run?.round ?? null }
87
+ if (entry.model != null) base.model = entry.model
88
+ if (entry.startedAt) base.elapsedSec = Math.max(0, Math.round((Date.now() - entry.startedAt) / 1000))
89
+ return base
90
+ }
91
+ const base = { id: String(entry.id), role: entry.role }
92
+ if (entry.status === "running") {
93
+ base.model = entry.model ?? null
94
+ base.elapsedSec = entry.startedAt ? Math.max(0, Math.floor((Date.now() - entry.startedAt) / 1000)) : 0
95
+ base.turn = entry.turn ?? 0
96
+ base.maxTurns = entry.maxTurns ?? 0
97
+ // §19.5.6:touched files 摘要(T-SF1..4——新字段追加——既有字段零破坏)
98
+ Object.assign(base, touchedSummary(entry, cwd))
99
+ } else if (entry.status === "queued") {
100
+ // §19.5.6 T-SF2b:未启动——确定性占位(不崩;无对象可读)
101
+ base.touched = "—(未启动)"
102
+ }
103
+ return base
104
+ }
105
+
106
+ export function executeStatusAction(args, ctx) {
107
+ // #46(批 TOOLFACE-FIXES ①——设计 AGENT-LOOP-SUBAGENT.md §6.25 ①):七动作族 depth 门收口——
108
+ // status 与 observe/send/escalate/cancel/panel freeze 同为 depth-0 专有(子代无自有异步池);
109
+ // 缺门时子代静默空 overview(把「不可用」读成「无在飞」)。族锚取现行可解析节号(§6.7.2)。
110
+ if ((ctx.depth ?? 0) > 0) {
111
+ return JSON.stringify({ status: "error", error: "status is only available at depth 0 — a child agent has no async pool of its own" })
112
+ }
113
+ const agent = ctx.agent
114
+ const map = getAsyncPool(agent, "subagent") ?? new Map()
115
+ const advisors = getAsyncPool(agent, "advisor") ?? new Map()
116
+ const queue = agent._asyncQueue ?? []
117
+ const queuedPosition = (id) => {
118
+ const i = queue.findIndex((e) => String(e.id) === id)
119
+ return i >= 0 ? i + 1 : undefined
120
+ }
121
+ const { id } = args ?? {}
122
+ if (id !== undefined && id !== null && String(id) !== "") {
123
+ const key = String(id)
124
+ // §11.2 D-24b: by-id queries fall through to the advisor pool (shared counter —
125
+ // ids are unique across both pools; role identifies the kind).
126
+ const entry = map.get(key) ?? advisors.get(key)
127
+ if (!entry) {
128
+ // Wording kept exact — locked by subagent-async/tool tests (unknown-id error
129
+ // semantics); an unknown id simply names neither pool (the pools share the
130
+ // id counter, so the message stays unambiguous).
131
+ return JSON.stringify({ id: key, status: "error", error: `unknown async subagent id: ${key}` })
132
+ }
133
+ const target = statusFields(entry, agent.cwd)
134
+ if (entry.status === "running") return JSON.stringify({ ...target, status: "running" })
135
+ if (entry.status === "queued") {
136
+ // §20 F-SD4/D-SD3b:waiting 语义对模型可见——排队原因(冲突对象/依赖对象)随
137
+ // status 返回;纯槽满等位(kind slot)无 waiting 字段(position 已足够)。
138
+ const blk = describeBlockers(agent, entry)
139
+ const out = { ...target, status: "queued", position: queuedPosition(key) ?? entry.position }
140
+ if (blk.kind !== "slot") {
141
+ out.waiting = blk.kind === "depc" ? "dependency-cancelled" : "waiting-deps"
142
+ out.reason = blk.detail
143
+ }
144
+ // §21.1 P-SL2(D-SL2):停滞机械标记——池停滞时本任务所在闭环阻塞链随 status
145
+ // 返回(F-SL2——status 视图可显示停滞——模型可见——零正常路径字段变化)。
146
+ const stall = detectStall(agent)
147
+ if (stall) {
148
+ const chain = stall.chains.find((c) => c.task === entry)?.text
149
+ if (chain) out.stall = { chain, note: STALL_NOTE }
150
+ }
151
+ return JSON.stringify(out)
152
+ }
153
+ // done = settled during this turn and not yet consumed — the auto channel still
154
+ // delivers it (§19.8: turn-end collection / the suspension digest — no fetch action).
155
+ target.status = "done"
156
+ target.done = true
157
+ if (entry.error) target.error = entry.error
158
+ target.note = "settled, not yet consumed — delivered by the auto channel (turn-end collection or the suspension digest injects it)"
159
+ return JSON.stringify(target)
160
+ }
161
+ const overview = { running: [], queued: [], done: [] }
162
+ const mapEntries = [...map.values(), ...advisors.values()]
163
+ for (const entry of mapEntries) {
164
+ if (entry.status === "running") overview.running.push(statusFields(entry, agent.cwd))
165
+ else if (entry.status === "queued") {
166
+ // §20:queued 条目补 waiting/reason(F-SD4——依赖/冲突原因模型可见);
167
+ // §19.5.6 T-SF2b:未启动占位(确定性——不崩)。
168
+ const blk = describeBlockers(agent, entry)
169
+ const row = statusFields(entry, agent.cwd)
170
+ row.position = queuedPosition(String(entry.id)) ?? entry.position
171
+ if (blk.kind !== "slot") {
172
+ row.waiting = blk.kind === "depc" ? "dependency-cancelled" : "waiting-deps"
173
+ row.reason = blk.detail
174
+ }
175
+ overview.queued.push(row)
176
+ }
177
+ else if (entry.done) overview.done.push({ id: String(entry.id), role: entry.role })
178
+ }
179
+ // §21.1 P-SL2(D-SL2):停滞机械标记——overview 级标注(可显示停滞——F-SL2)——
180
+ // 链文本随视图返回(逐条——每 queued 条目一行——闭环可破环引导同挂)。
181
+ const stall = detectStall(agent)
182
+ if (stall) {
183
+ overview.stall = {
184
+ chains: stall.chains.map((c) => c.text),
185
+ note: STALL_NOTE,
186
+ }
187
+ }
188
+ return JSON.stringify({ overview })
189
+ }
190
+
191
+ // ═══════════════════════════════════════════════════════════════════════════
192
+ // SUBAGENT-OBSERVE-SEND(SUBAGENT-OBSERVE-SEND.md——CLI 端)
193
+ // observe(D1——readonly 查询)+ send(D2——控制类豁免注入引导)——动作执行器。
194
+ // 目标 = 父自身 spawn 的异步子代理池条目(_asyncSubagents——非 advisor/escalate——
195
+ // 后者共享 id 计数但非"读写子代理",错误路径明示)。
196
+ // ═══════════════════════════════════════════════════════════════════════════
197
+
198
+ /** D1/N2 摘要形态:从子代理 _fullHistory(real 消息——pushReal 累积)倒序收最近 N 个
199
+ * assistant 回合的一行描述(newest-first;assistant 消息 = 一回合——带 tool_calls =
200
+ * 工具轮、独立 content = 回复首行)。返回纯摘要非全量(N2 隔离——不把子代理噪音灌父)。 */
201
+ function recentTurnLines(child, limit) {
202
+ const hist = child?._fullHistory
203
+ if (!Array.isArray(hist)) return []
204
+ const out = []
205
+ for (let i = hist.length - 1; i >= 0 && out.length < limit; i--) {
206
+ const m = hist[i]
207
+ if (m?.role !== "assistant") continue
208
+ const tcs = Array.isArray(m.tool_calls)
209
+ ? m.tool_calls.filter((t) => typeof t?.function?.name === "string")
210
+ : []
211
+ if (tcs.length) out.push(`tools: ${tcs.map((t) => t.function.name).join(", ")}`)
212
+ else if (typeof m.content === "string" && m.content.trim()) {
213
+ const first = m.content.trim().split(/\r?\n/).find((l) => l.trim())
214
+ out.push((first ?? m.content.trim()).slice(0, 120))
215
+ } else out.push("(assistant — no content)")
216
+ }
217
+ return out
218
+ }
219
+
220
+ function queuedPositionOf(agent, key) {
221
+ const i = (agent?._asyncQueue ?? []).findIndex((e) => String(e.id) === key)
222
+ return i >= 0 ? i + 1 : undefined
223
+ }
224
+
225
+ /** recent 参数钳制(默认 ~5 条摘要——可参数;钳到 1..20 防超长 N2 失控)。 */
226
+ function clampRecent(n) {
227
+ const v = Number.parseInt(n, 10)
228
+ if (!Number.isFinite(v)) return 5
229
+ return Math.max(1, Math.min(20, v))
230
+ }
231
+
232
+ /**
233
+ * subagent action:"observe"(D1——READONLY 查询,父回合内):按 id 拉运行中异步子代理的
234
+ * 最近活动快照——从 entry.childAgent(subagent-run.mjs start() 绑定)读**已落回合**摘要
235
+ * (_fullHistory 最近 N 条)+ _touchedFiles + turn/maxTurns + status + **in-flight 当前
236
+ * 工具**(评审 #1——从 dispatch runOne 维护的 child._inflightTools 读——非只读已落
237
+ * history:卡在长工具调用时 history 无新回合、恰是 observe 要检测的卡死态)。返摘要非
238
+ * 全量(N2)。observe = readonly——running/queued/done 均可查(done 终报 / queued 占位)。
239
+ */
240
+ export function executeObserveAction(args, ctx) {
241
+ if ((ctx.depth ?? 0) > 0) {
242
+ return JSON.stringify({ status: "error", error: "observe is only available at depth 0 — a child agent has no async pool of its own" })
243
+ }
244
+ const agent = ctx.agent
245
+ const id = args?.id
246
+ if (id === undefined || id === null || String(id) === "") {
247
+ return JSON.stringify({ status: "error", error: "observe requires the id of the async subagent to inspect (from the async spawn return) — pass the id; omitting it observes nothing" })
248
+ }
249
+ const key = String(id)
250
+ const entry = getAsyncPool(agent, "subagent")?.get(key)
251
+ if (!entry) {
252
+ if (getAsyncPool(agent, "advisor")?.has(key)) {
253
+ return JSON.stringify({ status: "error", error: `id ${key} is an async ADVISOR review — observe is for async subagents (read/edit children); track an advisor with action:'status' or wait for its auto-delivered report` })
254
+ }
255
+ return JSON.stringify({ status: "error", error: `unknown async subagent id: ${key}` })
256
+ }
257
+ const child = entry.childAgent
258
+ const out = { id: key, role: entry.role, status: entry.status }
259
+ if (entry.status === "running") {
260
+ out.turn = entry.turn ?? 0
261
+ out.maxTurns = entry.maxTurns ?? 0
262
+ Object.assign(out, touchedSummary(entry, agent.cwd))
263
+ // in-flight 当前工具(评审 #1):child._inflightTools Set——dispatch runOne 在工具
264
+ // 执行前后维护——LLM 生成/工具间空隙为空;子代理 await 长工具调用时父回合可见它。
265
+ const inflight = child?._inflightTools
266
+ if (inflight instanceof Set && inflight.size > 0) out.currentTool = [...inflight]
267
+ const n = clampRecent(args?.recent)
268
+ out.recentTurns = recentTurnLines(child, n)
269
+ out.note = `observing running ${entry.role}#${key} — recentTurns newest-first (cap ${n}); currentTool shown only while a tool is executing (stuck detection); sent directions consume at the next turn boundary`
270
+ } else if (entry.status === "queued") {
271
+ out.position = queuedPositionOf(agent, key) ?? entry.position ?? null
272
+ out.note = "queued — not started yet; no activity to observe (starts when the slot frees / dependencies clear; action:'send' targets running only)"
273
+ } else {
274
+ // done = settled this turn, not yet consumed — final report rides the auto channel.
275
+ out.done = true
276
+ out.turn = entry.turn ?? 0
277
+ out.maxTurns = entry.maxTurns ?? 0
278
+ out.recentTurns = child ? recentTurnLines(child, clampRecent(args?.recent)) : []
279
+ out.note = "settled — the final report is delivered by the auto channel (turn-end collection or the suspension digest); observe returns the activity summary, not the full report (N2)"
280
+ }
281
+ return JSON.stringify(out)
282
+ }
283
+
284
+ /**
285
+ * subagent action:"send"(D2——控制类豁免,同 cancel/panel-freeze——父回合内显式调用即
286
+ * 授权):按 id 向运行中异步子代理注入一条引导消息——push 进 entry._injected(仅 running
287
+ * 异步可 send);子回合边界经 consumeInjected 回调消费 → pushReal 成 user 回合进子历史
288
+ * → 当作普通指令处理(注入不等同偏离豁免——子收敛/审计纪律不变)。settle/cancel/unknown
289
+ * /queued → 明确错误。send→settle 竞态:入队后子未及下回合边界即 settle → 消息未投递
290
+ * → settle 收尾附报告提示(不在此报错——send 返回时无法预知)。
291
+ */
292
+ export function executeSendAction(args, ctx) {
293
+ if ((ctx.depth ?? 0) > 0) {
294
+ return JSON.stringify({ status: "error", error: "send is only available at depth 0 — a child agent has no async pool of its own" })
295
+ }
296
+ const agent = ctx.agent
297
+ const id = args?.id
298
+ if (id === undefined || id === null || String(id) === "") {
299
+ return JSON.stringify({ status: "error", error: "send requires the id of the running async subagent to direct (from the async spawn return) — omitting it means an unspecified target" })
300
+ }
301
+ const message = args?.message
302
+ if (typeof message !== "string" || !message.trim()) {
303
+ return JSON.stringify({ status: "error", error: "send requires the message to inject — the direction the running subagent should treat as an ordinary user instruction at its next turn boundary" })
304
+ }
305
+ const key = String(id)
306
+ const entry = getAsyncPool(agent, "subagent")?.get(key)
307
+ if (!entry) {
308
+ if (getAsyncPool(agent, "advisor")?.has(key)) {
309
+ return JSON.stringify({ status: "error", error: `id ${key} is an async ADVISOR review — send is for async subagents; you cannot inject direction into a running review — track it with action:'status' (role:"advisor") or wait for its report to arrive automatically` })
310
+ }
311
+ return JSON.stringify({ status: "error", error: `unknown async subagent id: ${key}` })
312
+ }
313
+ if (entry.done || entry.cancelled) {
314
+ return JSON.stringify({ status: "error", error: `async subagent #${key} has ${entry.cancelled ? "been cancelled" : "already settled"} — nothing to send (you cannot inject into a finished subagent; re-spawn with the direction instead)` })
315
+ }
316
+ if (entry.status !== "running") {
317
+ return JSON.stringify({ status: "error", error: `async subagent #${key} is ${entry.status} (not running) — send only targets a RUNNING async subagent; a queued one has not started its turn loop yet` })
318
+ }
319
+ entry._injected ??= []
320
+ entry._injected.push(String(message).trim())
321
+ return JSON.stringify({
322
+ id: key,
323
+ status: "delivered",
324
+ queued: entry._injected.length,
325
+ note: `message queued for ${entry.role}#${key} — consumed as an ordinary user instruction at its next turn boundary (after its current tool finishes — non-interrupting). If it settles first, its report carries an "undelivered" note.`,
326
+ })
327
+ }
328
+
329
+ // ═══════════════════════════════════════════════════════════════════════════
330
+ // §19.6 subagent panel 检查工具(AGENT-LOOP.md §19.6——F-P1..P3/D-P1..P4)
331
+ // ═══════════════════════════════════════════════════════════════════════════
332
+ // 2026-09-08 拆分(Module Split Policy——601 > 500 硬限跨档):§19.6 面板段迁至
333
+ // ./subagent-panel.mjs(executePanelAction/panelFreezeGate/blockKeyIn——verbatim +
334
+ // ASYNC-RESULT-CONTAINER D1/D2 落地);本面保留 re-export 保 subagent.mjs 既有
335
+ // import 面(subagent-async 尾部 re-export 先例)。
336
+ export { executePanelAction } from "./subagent-panel.mjs"
337
+
338
+ /**
339
+ * subagent action:"escalate"(§19 D-M4——退役 escalate 工具语义原样,ESCALATE.md;
340
+ * §25 D-R17b——R17:缺省 async——后台飞刀 + settle 三分类 digest——async:false 保
341
+ * 同步旧路径)。飞刀——交给 consultModels 池里更强模型(WRITE + 术后报告)。约束全
342
+ * 保留:depth-0 only / 工程模式拒 / consultModels 空拒 / relay 前缀 `escalate#N/`
343
+ * (与既有前缀同名——TUI 路由零改动)/ mutations merge 回父(async 路径在 settle 分类)。
344
+ */
345
+ export async function executeEscalateAction(args, ctx) {
346
+ const parent = ctx.agent
347
+ if ((ctx.depth ?? 0) > 0) return "Error: escalate is only available at depth 0 (an escalate's work cannot be delegated again)"
348
+ if (parent?.config?.agent?.engineering) {
349
+ return "Error: engineering mode is ON — escalate is unavailable (it spawns a coder sub-agent, which engineering mode forbids). Use subagent with role='eng-coder' and a designToken from advisor(type='design') instead."
350
+ }
351
+ const pool = parent?.config?.agent?.consultModels ?? []
352
+ if (pool.length === 0) return "Error: no escalate candidates — configure at least one consult model (agent.consultModels)"
353
+
354
+ const { task, model } = args ?? {}
355
+ // task 机械必填(多动作 schema 的 required 只是建议——缺 task 会以晦涩 child-run 错浮现)
356
+ if (typeof task !== "string" || !task.trim()) {
357
+ return "Error: escalate requires a task — the task description with goal, constraints, entry files and acceptance criteria"
358
+ }
359
+ const label = (m) => `${m.provider}:${m.model}`
360
+ const wanted = typeof model === "string" ? model.replace(/\s+\([^)]*\)\s*$/, "").trim() : model
361
+ const pick = wanted ? pool.find((m) => label(m) === wanted) : pool[0]
362
+ if (!pick) {
363
+ return `Error: "${model}" is not a consult candidate. Available: ${pool.map(label).join(", ")}`
364
+ }
365
+
366
+ let provider
367
+ try {
368
+ provider = resolveChildProvider(parent, `${pick.provider}:${pick.model}`)
369
+ } catch (e) {
370
+ return `Error: ${e.message}`
371
+ }
372
+ if (!ensureChildApiKey(provider)) {
373
+ return `Error: provider "${pick.provider}" has no API key — set it in config.json before flying it in`
374
+ }
375
+ let effortNote = ""
376
+ if (pick.effort && !clampEffort(provider, pick.model, pick.effort)) {
377
+ // enum 外 effort 丢弃(preset 默认也可能对 override model 是 enum 外值)
378
+ effortNote = ` (effort "${pick.effort}" unsupported by ${pick.model}, dropped)`
379
+ }
380
+
381
+ const tag = label(pick)
382
+
383
+ // §25 D-R17b (R17 — 决策点 ③): escalate 缺省 async — the launch returns an ack
384
+ // {id, role:"escalate", status:"running"|"queued"} and the flight runs in the
385
+ // background (shared other pool; settle 三分类 → pending 单容器 digest——
386
+ // ASYNC-RESULT-CONTAINER.md D2)。
387
+ // `async: false` keeps the legacy synchronous flight below (backward compat).
388
+ if (args?.async !== false) {
389
+ return launchEscalateAsync(parent, ctx, { task: String(task), provider, tag, effortNote })
390
+ }
391
+
392
+ const relayPrefix = makeRelay(parent, "escalate", ctx.callbacks?.onToken, provider.model ?? tag)
393
+
394
+ // 无墙钟 watchdog——turn cap 即成本预算(2026-08-16 rationale:固定墙钟会误杀正常慢速
395
+ // 手术;挂起防护 = FETCH_TIMEOUT_MS + 父 signal 直传)
396
+
397
+ // 不自建 onToken(consult P2):wrapChildCallbacks 已承担前缀 relay + D7 哨兵剥除,
398
+ // runWithContinue 拥有 capture(stripEventTokensForCapture)——手写副本会双剥+双缓冲
399
+ const childCallbacks = wrapChildCallbacks(relayPrefix, ctx.callbacks ?? {})
400
+
401
+ // try 外声明:catch 也能在部分失败时 merge mutations
402
+ let child = null
403
+ let escErr = null // LOGGING outcome(string 形态返回 vs 异常——见下方事件点)
404
+ const escId = relayPrefix.slice(0, -1)
405
+ let escT0 = Date.now()
406
+ try {
407
+ // 全写路径(role "coder"):权限经父 onPermissionRequest,mutations merge 回父
408
+ // G3(施工②):overlay 摘除——coder 人格槽由 assemblePrompt 场景表承载(G3 映射)。
409
+ child = createAgent({
410
+ provider,
411
+ tools: parent.tools,
412
+ config: parent.config,
413
+ cwd: parent.cwd,
414
+ memory: parent.memory,
415
+ role: "coder",
416
+ })
417
+ child._historyWindow = RECORD_WINDOW_MESSAGES // TUI-OOM-ROOTCAUSE §23.3.1:子代理人读线窗口(四处创建点同置)
418
+ child._logId = escId // LOGGING:子内事件归属(escalate#N)
419
+ // SUBAGENT-UPSTREAM-CHANNEL(§6.27.4 W2——escalate **sync**:单向;工具返回注走同步形)。
420
+ child._upstream = { parent, label: escId, sync: true }
421
+ escT0 = Date.now()
422
+ logEvent("child:spawn", { role: "escalate", id: escId, kind: "escalate" })
423
+ const runner = ctx.runAgent ?? runAgent
424
+ const runOpts = {
425
+ depth: 1,
426
+ maxTurns: parent.config?.agent?.subagentTurns ?? DEFAULT_SUBAGENT_TURNS, // review #7: constant, not literal (single source with subagent)
427
+ signal: ctx.signal ?? null,
428
+ // §2.5 #78 并入:escalate 子代理输出流式(VSC subagent-escalate 同款豁免)。
429
+ streamOutput: true,
430
+ }
431
+ // Continue 经 runWithContinue(§7.2 D3,主会话同等 y/n 面板):resume:true 不重注入
432
+ // task 文本(setup 跳 input)且保留 child history + mutation 记账,刷新 turn 预算;
433
+ // 无权限 handler(headless)或拒绝 → 部分工作返回;continue 次数无限(每轮可拒)。
434
+ // 残环批(2026-09-16):飞刀询问名同规包装(`escalate#<id>/<tool>`——端侧键形解析的
435
+ // 输入契约;原裸工具名直通无归属);无权限通道 ⇒ 退化 false(`Promise.resolve(false)`——
436
+ // 与 async 路 guard 同语义,T-A4n)。
437
+ const askPermission = (n, a) => (ctx.onPermissionRequest ? ctx.onPermissionRequest(`${escId}/${n}`, a) : Promise.resolve(false))
438
+ const report = await runWithContinue(
439
+ async (childAgent, input, cbs, opts) => {
440
+ // Merge mid-run mutations even when the run throws — the outer catch keeps
441
+ // handling createAgent failures; AbortError still propagates (user Stop).
442
+ try {
443
+ return await runner(childAgent, input, cbs, opts)
444
+ } catch (e) {
445
+ mergeChildMutations(parent, childAgent)
446
+ throw e
447
+ }
448
+ },
449
+ child, task, { ...childCallbacks, onPermissionRequest: parent.autoApprove ? async () => true : askPermission },
450
+ runOpts,
451
+ {
452
+ // sync escalate has NO permQueue——async 飞行权限走 _permQueue(escalate-async.mjs): prompts go straight to the user (T-L spec).
453
+ askContinue: (e) => (ctx.onPermissionRequest
454
+ ? ctx.onPermissionRequest("continue", { turns: e.turn, agent: escId })
455
+ : Promise.resolve(false)),
456
+ onDeclined: (e, output) => `escalate (${tag}) ${TURN_CAP_MARK} (${e.turn} turns) — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${output.slice(0, 2000)}`,
457
+ },
458
+ ).catch((e) => {
459
+ // 非 ContinueError 运行失败:错误文本 + partial 输出(mutations 已在 runner 包装层 merge)
460
+ if (ctx.signal?.aborted || e?.name === "AbortError") throw e
461
+ escErr = { err: e?.message ?? String(e) } // LOGGING:错误路径(返回形态——不抛)
462
+ return `escalate (${tag}) error: ${e?.message ?? String(e)}\nPartial output: ${(child._capturedOutput ?? "").slice(0, 2000)}`
463
+ })
464
+ // Escalate mutations are the parent's mutations: verify/advisor guards must see them
465
+ mergeChildMutations(parent, child)
466
+ if (escErr) logEvent("child:error", { role: "escalate", id: escId, ms: Date.now() - escT0, err: errText(escErr.err, 200) })
467
+ else logEvent("child:done", { role: "escalate", id: escId, ms: Date.now() - escT0, kind: String(report).includes(TURN_CAP_MARK) ? "partial" : "ok" })
468
+ // §7.2.3(round1 #2):escalate 与 spawn 同享 ctx._subagentKey——同步完成精确冻
469
+ // (relayPrefix 去尾 = `escalate#N`)。仅成功路径(escErr = 运行中途失败——不设
470
+ // key——TUI 回落 escalate 角色启发式:escalate 串行 + 角色限定,天然精确——legacy
471
+ // 行为不变——错误路径不触发冻结 round1 #1)。
472
+ if (!escErr) ctx._subagentKey = escId
473
+ return `escalate (${tag})${effortNote} post-op report:\n${report || (child._capturedOutput ?? "").slice(0, 4000)}${touchedFilesNote(child, parent.cwd)}`
474
+ } catch (e) {
475
+ // 仅 createAgent 失败/continue 询问抛出才到这(运行失败已在上面 catch 处理)
476
+ if (child) {
477
+ mergeChildMutations(parent, child)
478
+ if (!escErr && !(ctx.signal?.aborted) && e?.name !== "AbortError") {
479
+ logEvent("child:error", { role: "escalate", id: escId, ms: Date.now() - escT0, err: errText(e, 200) })
480
+ }
481
+ }
482
+ if (ctx.signal?.aborted || e?.name === "AbortError") throw e
483
+ return `escalate (${tag}) error: ${e?.message ?? String(e)}`
484
+ }
485
+ }
486
+
487
+ /** Relative touched-file list appended to every escalate return (child paths are absolute). */
488
+ function touchedFilesNote(child, cwd) {
489
+ const touched = child?._touchedFiles ?? []
490
+ if (touched.length === 0) return ""
491
+ const shown = touched.map((f) => {
492
+ const r = relative(cwd ?? process.cwd(), f)
493
+ return r && !r.startsWith("..") && !isAbsolute(r) ? r : f
494
+ })
495
+ return `\nTouched files: ${shown.join(", ")}`
496
+ }