thincoder 0.12.54 → 0.12.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +98 -0
- package/README.md +1 -1
- package/bin/thincoder.mjs +25 -3
- package/package.json +3 -7
- package/src/acp/bridge.mjs +132 -26
- package/src/advisor/messages.mjs +38 -3
- package/src/advisor/run.mjs +91 -53
- package/src/advisor.mjs +15 -7
- package/src/agent/dispatch.mjs +156 -39
- package/src/agent/helpers.mjs +46 -4
- package/src/agent/setup.mjs +102 -19
- package/src/agent/spawn-child.mjs +28 -1
- package/src/agent-tools/advisor.mjs +43 -11
- package/src/agent-tools/consult.mjs +37 -6
- package/src/agent-tools/eng.mjs +4 -1
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +160 -0
- package/src/agent-tools/settings.mjs +162 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +432 -0
- package/src/agent-tools/subagent-async.mjs +427 -0
- package/src/agent-tools/subagent-scheduler.mjs +319 -0
- package/src/agent-tools/subagent.mjs +565 -128
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +161 -49
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +182 -81
- package/src/auto-think.mjs +14 -0
- package/src/cli/make-agent.mjs +27 -1
- package/src/cli/memory-command.mjs +28 -7
- package/src/cli/permission.mjs +8 -1
- package/src/config.mjs +125 -8
- package/src/context.mjs +115 -34
- package/src/distill.mjs +19 -1
- package/src/escape.mjs +82 -27
- package/src/log.mjs +195 -0
- package/src/mcp/transport-http.mjs +13 -1
- package/src/mcp.mjs +52 -7
- package/src/memory/code-sync.mjs +1 -1
- package/src/memory/core.mjs +204 -10
- package/src/memory/docs.mjs +197 -62
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +38 -1
- package/src/prompts/advisor-design.md +46 -0
- package/src/prompts/advisor-round1.md +49 -2
- package/src/prompts/advisor-round2.md +47 -0
- package/src/prompts/advisor-round3.md +47 -0
- package/src/prompts/coder.md +22 -0
- package/src/prompts/consult-base.md +13 -0
- package/src/prompts/discipline.md +25 -6
- package/src/prompts/eng-coder.md +2 -2
- package/src/prompts/engineering-sub.md +23 -1
- package/src/prompts/engineering.md +157 -50
- package/src/prompts/explore.md +1 -2
- package/src/prompts/main.md +11 -5
- package/src/prompts/methodology-template.md +14 -0
- package/src/prompts/system.md +5 -2
- package/src/provider/anthropic.mjs +7 -5
- package/src/provider/core.mjs +104 -28
- package/src/provider/google.mjs +57 -24
- package/src/provider/normalize.mjs +1 -1
- package/src/provider/rate.mjs +0 -2
- package/src/provider/responses.mjs +8 -13
- package/src/provider/sse.mjs +20 -0
- package/src/session.mjs +15 -0
- package/src/tools/apply_patch.md +5 -1
- package/src/tools/bash.md +3 -3
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +92 -0
- package/src/tools/edit-diff.mjs +265 -0
- package/src/tools/edit.md +11 -6
- package/src/tools/execute.md +8 -8
- package/src/tools/execute.mjs +31 -35
- package/src/tools/file.mjs +26 -114
- package/src/tools/file_ops.md +3 -2
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +8 -16
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -2
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +3 -1
- package/src/tools/linter.mjs +9 -37
- package/src/tools/lsp.md +4 -1
- package/src/tools/patch.mjs +84 -13
- package/src/tools/pdf-parse-text.mjs +497 -0
- package/src/tools/pdf-parse-xref.mjs +499 -0
- package/src/tools/pdf.mjs +155 -0
- package/src/tools/question.md +2 -1
- package/src/tools/read.md +1 -0
- package/src/tools/read_pdf.md +21 -0
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +11 -32
- package/src/tools/system.mjs +6 -21
- package/src/tools/tree.md +2 -1
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +387 -24
- package/src/tui/clipboard.mjs +17 -6
- package/src/tui/cmd-config.mjs +29 -9
- package/src/tui/cmd-eng.mjs +1 -0
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp-form.mjs +197 -0
- package/src/tui/cmd-mcp.mjs +264 -114
- package/src/tui/cmd-think.mjs +1 -1
- package/src/tui/index.mjs +49 -95
- package/src/tui/interaction.mjs +41 -3
- package/src/tui/key-handler.mjs +105 -143
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +41 -1
- package/src/tui/pickers.mjs +73 -7
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +45 -20
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +165 -0
- package/src/tui/render.mjs +4 -4
- package/src/tui/startup.mjs +40 -2
- package/src/tui/subagent-blocks.mjs +404 -111
- package/src/tui/subagent-panel.mjs +88 -13
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-events.mjs +172 -95
- package/src/tui/update-notice.mjs +72 -0
- package/src/tui/wizard.mjs +36 -6
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/tools/exec-prelude.mjs +0 -84
|
@@ -0,0 +1,432 @@
|
|
|
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 摘要助手随行)/ executePanelAction(§19.6——面板镜像
|
|
6
|
+
* view + 门控 freeze——panelFreezeGate/blockKeyIn 随行)/ executeEscalateAction(§19
|
|
7
|
+
* D-M4——飞刀——touchedFilesNote 随行)。
|
|
8
|
+
* check/cancel 动作执行器与共享 post-spawn 管线留在 subagent-async.mjs;
|
|
9
|
+
* §20 调度器 + 文件域组在 ./subagent-scheduler.mjs(describeBlockers 由此导入)。
|
|
10
|
+
*/
|
|
11
|
+
import { isAbsolute, relative } from "node:path"
|
|
12
|
+
import { runAgent, createAgent, CODER_OVERLAY, DEFAULT_SUBAGENT_TURNS } from "../agent.mjs"
|
|
13
|
+
import {
|
|
14
|
+
runWithContinue, TURN_CAP_MARK, makeRelay, wrapChildCallbacks,
|
|
15
|
+
ensureChildApiKey, clampEffort,
|
|
16
|
+
} from "../agent/spawn-child.mjs"
|
|
17
|
+
import { logEvent, errText } from "../log.mjs"
|
|
18
|
+
import { describeBlockers, detectStall, STALL_NOTE } from "./subagent-scheduler.mjs"
|
|
19
|
+
import { resolveChildProvider, mergeChildMutations } from "./subagent-async.mjs"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* subagent action:"status" (§19 D-M2, new): NON-BLOCKING async-pool query —
|
|
23
|
+
* returns immediately, never consumes a result and never touches the check read
|
|
24
|
+
* counter (T-M10). Source of truth = the pool (_asyncSubagents): entries moved
|
|
25
|
+
* to _pendingAsyncResults during a suspension (§17 D-S3 ② — injected at the next
|
|
26
|
+
* run start) are no longer in the pool and are NOT counted as done-waiting.
|
|
27
|
+
* - id given → { id, role, status, model?, elapsedSec?, turn?, maxTurns?,
|
|
28
|
+
* touchedFiles?/touchedMore?/touched? ... } for that entry; unknown id → error
|
|
29
|
+
* (same wording as check — T12 semantics)
|
|
30
|
+
* - id omitted → { overview: { running: [{id, role, model, elapsedSec, turn,
|
|
31
|
+
* maxTurns, touchedFiles?/touched?}], queued: [{id, role, position, touched?}],
|
|
32
|
+
* done: [{id, role}] } } — live queue positions (index in _asyncQueue + 1).
|
|
33
|
+
* §19.5.6: running 条目带 touched files 摘要(touchedFiles 前 5 + touchedMore 超出
|
|
34
|
+
* 计数——相对查询方 cwd;0 改动 → touched 占位);queued 条目带 touched 占位
|
|
35
|
+
* "—(未启动)";done/error/取消条目无 touched 字段(round3 #9)。
|
|
36
|
+
* A settled-but-unconsumed entry (settled during a NORMAL turn) reports done
|
|
37
|
+
* with a "not yet consumed" note — check still retrieves it afterwards.
|
|
38
|
+
*/
|
|
39
|
+
/** §19.5 D-M5 decision-field assembly (F9): running entries report
|
|
40
|
+
* {id, role, model, elapsedSec, turn, maxTurns} — the data needed to decide
|
|
41
|
+
* WHO to cancel. Model is recorded at spawn (childProvider), startedAt at
|
|
42
|
+
* ACTUAL start (queued waits don't count), turn/maxTurns mirrored from the
|
|
43
|
+
* child's ⟦ev⟧turn events at the callbacks-wrap layer (subagent.mjs tracker).
|
|
44
|
+
* elapsedSec computed at call time from startedAt. */
|
|
45
|
+
/** §19.5.6 D-SF2/N-SF1 摘要形态:status 调用时实时读 entry.childAgent._touchedFiles
|
|
46
|
+
* (绝对路径——per-run 记账——§18.12)→ 相对查询方 cwd;cwd 之外保留绝对形态 +
|
|
47
|
+
* "../" 前缀;>80 字符截尾(不超行);前 5 个 + 独立截断字段 touchedMore(超出
|
|
48
|
+
* 计数——不混入数组——消费方按类型区分)。占位:running 0 改动 → "—(尚无改动)"
|
|
49
|
+
* (T-SF2a);queued 未启动 → "—(未启动)"(T-SF2b);done/error/取消条目不含
|
|
50
|
+
* 本摘要(round3 #9 明示——本批只做 running/queued)。 */
|
|
51
|
+
function touchedSummary(entry, cwd) {
|
|
52
|
+
if (entry.status !== "running") return { touched: "—(未启动)" }
|
|
53
|
+
const files = entry.childAgent?._touchedFiles ?? []
|
|
54
|
+
if (files.length === 0) return { touched: "—(尚无改动)" }
|
|
55
|
+
const shown = files.slice(0, 5).map((f) => shortTouchedPath(f, cwd))
|
|
56
|
+
const out = { touchedFiles: shown }
|
|
57
|
+
if (files.length > 5) out.touchedMore = files.length - 5
|
|
58
|
+
return out
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** N-SF1 单路径显示形态:cwd 内 → 相对路径;cwd 外 → "../" + 绝对路径;>80 截尾。 */
|
|
62
|
+
function shortTouchedPath(f, cwd) {
|
|
63
|
+
let p = f
|
|
64
|
+
if (cwd) {
|
|
65
|
+
const rel = relative(cwd, f)
|
|
66
|
+
p = rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : `../${f}`
|
|
67
|
+
}
|
|
68
|
+
return p.length > 80 ? `${p.slice(0, 79)}…` : p
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function statusFields(entry, cwd) {
|
|
72
|
+
const base = { id: String(entry.id), role: entry.role }
|
|
73
|
+
if (entry.status === "running") {
|
|
74
|
+
base.model = entry.model ?? null
|
|
75
|
+
base.elapsedSec = entry.startedAt ? Math.max(0, Math.floor((Date.now() - entry.startedAt) / 1000)) : 0
|
|
76
|
+
base.turn = entry.turn ?? 0
|
|
77
|
+
base.maxTurns = entry.maxTurns ?? 0
|
|
78
|
+
// §19.5.6:touched files 摘要(T-SF1..4——新字段追加——既有字段零破坏)
|
|
79
|
+
Object.assign(base, touchedSummary(entry, cwd))
|
|
80
|
+
} else if (entry.status === "queued") {
|
|
81
|
+
// §19.5.6 T-SF2b:未启动——确定性占位(不崩;无对象可读)
|
|
82
|
+
base.touched = "—(未启动)"
|
|
83
|
+
}
|
|
84
|
+
return base
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function executeStatusAction(args, ctx) {
|
|
88
|
+
const agent = ctx.agent
|
|
89
|
+
const map = agent._asyncSubagents ?? new Map()
|
|
90
|
+
const queue = agent._asyncQueue ?? []
|
|
91
|
+
const queuedPosition = (id) => {
|
|
92
|
+
const i = queue.findIndex((e) => String(e.id) === id)
|
|
93
|
+
return i >= 0 ? i + 1 : undefined
|
|
94
|
+
}
|
|
95
|
+
const { id } = args ?? {}
|
|
96
|
+
if (id !== undefined && id !== null && String(id) !== "") {
|
|
97
|
+
const key = String(id)
|
|
98
|
+
const entry = map.get(key)
|
|
99
|
+
if (!entry) {
|
|
100
|
+
return JSON.stringify({ id: key, status: "error", error: `unknown async subagent id: ${key}` })
|
|
101
|
+
}
|
|
102
|
+
const target = statusFields(entry, agent.cwd)
|
|
103
|
+
if (entry.status === "running") return JSON.stringify({ ...target, status: "running" })
|
|
104
|
+
if (entry.status === "queued") {
|
|
105
|
+
// §20 F-SD4/D-SD3b:waiting 语义对模型可见——排队原因(冲突对象/依赖对象)随
|
|
106
|
+
// status 返回;纯槽满等位(kind slot)无 waiting 字段(position 已足够)。
|
|
107
|
+
const blk = describeBlockers(agent, entry)
|
|
108
|
+
const out = { ...target, status: "queued", position: queuedPosition(key) ?? entry.position }
|
|
109
|
+
if (blk.kind !== "slot") {
|
|
110
|
+
out.waiting = blk.kind === "depc" ? "dependency-cancelled" : "waiting-deps"
|
|
111
|
+
out.reason = blk.detail
|
|
112
|
+
}
|
|
113
|
+
// §21.1 P-SL2(D-SL2):停滞机械标记——池停滞时本任务所在闭环阻塞链随 status
|
|
114
|
+
// 返回(F-SL2——status 视图可显示停滞——模型可见——零正常路径字段变化)。
|
|
115
|
+
const stall = detectStall(agent)
|
|
116
|
+
if (stall) {
|
|
117
|
+
const chain = stall.chains.find((c) => c.task === entry)?.text
|
|
118
|
+
if (chain) out.stall = { chain, note: STALL_NOTE }
|
|
119
|
+
}
|
|
120
|
+
return JSON.stringify(out)
|
|
121
|
+
}
|
|
122
|
+
// done = settled during this turn and not yet consumed — check still retrieves it
|
|
123
|
+
// (§17.5: at a driven turn end it stays pooled → the suspension digest consumes it).
|
|
124
|
+
target.status = "done"
|
|
125
|
+
target.done = true
|
|
126
|
+
if (entry.error) target.error = entry.error
|
|
127
|
+
target.note = "settled, not yet consumed — retrieve via check or the suspension digest injects it"
|
|
128
|
+
return JSON.stringify(target)
|
|
129
|
+
}
|
|
130
|
+
const overview = { running: [], queued: [], done: [] }
|
|
131
|
+
for (const entry of map.values()) {
|
|
132
|
+
if (entry.status === "running") overview.running.push(statusFields(entry, agent.cwd))
|
|
133
|
+
else if (entry.status === "queued") {
|
|
134
|
+
// §20:queued 条目补 waiting/reason(F-SD4——依赖/冲突原因模型可见);
|
|
135
|
+
// §19.5.6 T-SF2b:未启动占位(确定性——不崩)。
|
|
136
|
+
const blk = describeBlockers(agent, entry)
|
|
137
|
+
const row = statusFields(entry, agent.cwd)
|
|
138
|
+
row.position = queuedPosition(String(entry.id)) ?? entry.position
|
|
139
|
+
if (blk.kind !== "slot") {
|
|
140
|
+
row.waiting = blk.kind === "depc" ? "dependency-cancelled" : "waiting-deps"
|
|
141
|
+
row.reason = blk.detail
|
|
142
|
+
}
|
|
143
|
+
overview.queued.push(row)
|
|
144
|
+
}
|
|
145
|
+
else if (entry.done) overview.done.push({ id: String(entry.id), role: entry.role })
|
|
146
|
+
}
|
|
147
|
+
// §21.1 P-SL2(D-SL2):停滞机械标记——overview 级标注(可显示停滞——F-SL2)——
|
|
148
|
+
// 链文本随视图返回(逐条——每 queued 条目一行——闭环可破环引导同挂)。
|
|
149
|
+
const stall = detectStall(agent)
|
|
150
|
+
if (stall) {
|
|
151
|
+
overview.stall = {
|
|
152
|
+
chains: stall.chains.map((c) => c.text),
|
|
153
|
+
note: STALL_NOTE,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return JSON.stringify({ overview })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
160
|
+
// §19.6 subagent panel 检查工具(AGENT-LOOP.md §19.6——F-P1..P3/D-P1..P4)
|
|
161
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
162
|
+
|
|
163
|
+
/** 面板块 key(role#N)在池(Map——条目值)/pending(数组)中的归属判定。 */
|
|
164
|
+
function blockKeyIn(container, key) {
|
|
165
|
+
const entries = container instanceof Map ? [...container.values()] : (container ?? [])
|
|
166
|
+
return entries.some((e) => `${e.role}#${e.id}` === key)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* §19.6 D-P3 冻结门控(安全):仅允许冻结 awaitingDigest 且池(_asyncSubagents)
|
|
171
|
+
* 无对应运行条目 + pending(_pendingAsyncResults)无对应条目的块(= 已消化驻留块
|
|
172
|
+
* ——报告已入模型上下文——pending 已消费——状态滞后——补发冻结不破坏任何顺序)。
|
|
173
|
+
* - pending 仍有对应(报告未达模型)→ 拒绝(提前回收破坏消化顺序——T-P3)
|
|
174
|
+
* - 不存在的 key / 仍 running / done 的块 → 拒绝(T-P4——running 块 settle 时自冻)
|
|
175
|
+
* - 无镜像 → 拒绝(headless/VS Code——freeze 不可用——T-P5)
|
|
176
|
+
* 错误信息明确(模型可解释 + 自助修正)。返回 { ok:true } 或 { err }。
|
|
177
|
+
*/
|
|
178
|
+
function panelFreezeGate(agent, key) {
|
|
179
|
+
const snap = agent._panelSnapshot
|
|
180
|
+
if (!Array.isArray(snap)) {
|
|
181
|
+
return { err: "panel unavailable — no CLI TUI panel mirror in this session (headless / VS Code / subagent contexts — freeze unavailable; panel is CLI-TUI-only, AC-P4)" }
|
|
182
|
+
}
|
|
183
|
+
const block = snap.find((b) => b.key === key)
|
|
184
|
+
if (!block) {
|
|
185
|
+
const live = snap.map((b) => `${b.key}(${b.status})`).join(", ")
|
|
186
|
+
return { err: `unknown panel block key: ${key} — the live panel holds: ${live || "(no blocks)"}` }
|
|
187
|
+
}
|
|
188
|
+
if (block.status !== "awaitingDigest") {
|
|
189
|
+
if (block.status === "done") {
|
|
190
|
+
return { err: `block ${key} is already done — nothing to freeze; it was (or is about to be) reclaimed into the conversation by the freeze sweep at the turn end / settle (only digested-stuck awaitingDigest blocks need a manual freeze)` }
|
|
191
|
+
}
|
|
192
|
+
if (block.status === "running") {
|
|
193
|
+
return { err: `block ${key} is still running — freeze only reclaims awaitingDigest blocks whose report is already digested; a running block freezes on its own settle (or stop it with action:'cancel' if it is a background async child)` }
|
|
194
|
+
}
|
|
195
|
+
return { err: `block ${key} is in state ${block.status} — freeze only reclaims awaitingDigest blocks whose report is already digested` }
|
|
196
|
+
}
|
|
197
|
+
if (blockKeyIn(agent._asyncSubagents, key)) {
|
|
198
|
+
return { err: `block ${key} still has a live pool entry — it is NOT a digested-stuck block (freeze refused; status action shows the pool)` }
|
|
199
|
+
}
|
|
200
|
+
if (blockKeyIn(agent._pendingAsyncResults, key)) {
|
|
201
|
+
return { err: `block ${key} is still genuinely awaiting digestion — its report is still in _pendingAsyncResults and has NOT reached the model yet; freezing now would break the digestion order (wait for the digest run, which reclaims it automatically — §17.5.5)` }
|
|
202
|
+
}
|
|
203
|
+
return { ok: true }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* §19.6 subagent action:"panel"(D-P2——readonly 视图面 + 门控干预面——单动作双参,
|
|
208
|
+
* freeze 优先):
|
|
209
|
+
* - view(缺省——返回镜像区块列表):agent._panelSnapshot = TUI 面板镜像(块级
|
|
210
|
+
* 状态变更点由 subagent-blocks syncPanelSnapshot 同步刷新——与用户所见一致——
|
|
211
|
+
* index.mjs 装配 state._agent)。awaitingDigest 条目**读时交叉**
|
|
212
|
+
* _pendingAsyncResults/_asyncSubagents 标注 digested(round1 #3——digested:true
|
|
213
|
+
* = 报告已消化但块仍驻留——异常块——freeze 候选;模型可定位解释 UI 怪相)。
|
|
214
|
+
* - freeze:key(D-P3 门控通过 → 发 key + "/" + ⟦ev⟧done 哨兵字面 token——
|
|
215
|
+
* onToken——TUI routeSubToken 冻结回收——落位复用 sub._freezeAt settle 锚点
|
|
216
|
+
* splice,无锚点尾推兜底——§17.5.5 同口径——round1 #2)。
|
|
217
|
+
* 无镜像(headless/VS Code——D-P2 round1 #1:webview 无 state.subTasks 对应物——
|
|
218
|
+
* 7.2.3.2 #8 先例)→ view 恒降级池视图(_asyncSubagents + _pendingAsyncResults
|
|
219
|
+
* 合成)+ no panel 注;freeze 报不可用。CLI-only 完整能力(AC-P4)。
|
|
220
|
+
*/
|
|
221
|
+
export function executePanelAction(args, ctx) {
|
|
222
|
+
const agent = ctx.agent
|
|
223
|
+
const freezeKey = (args?.freeze !== undefined && args?.freeze !== null && String(args.freeze) !== "")
|
|
224
|
+
? String(args.freeze)
|
|
225
|
+
: null
|
|
226
|
+
// ── freeze 面(优先——D-P2 单动作双参互斥)──
|
|
227
|
+
if (freezeKey) {
|
|
228
|
+
if ((ctx.depth ?? 0) > 0) {
|
|
229
|
+
return JSON.stringify({ status: "error", error: "panel freeze is only available at depth 0 — a child agent has no panel of its own (AGENT-LOOP.md §19.6 D-P2)" })
|
|
230
|
+
}
|
|
231
|
+
const gate = panelFreezeGate(agent, freezeKey)
|
|
232
|
+
if (gate.err) return JSON.stringify({ status: "error", error: gate.err })
|
|
233
|
+
if (!ctx.callbacks?.onToken) {
|
|
234
|
+
return JSON.stringify({ status: "error", error: `panel mirror present but no token relay in this context — the freeze of ${freezeKey} cannot reach the TUI` })
|
|
235
|
+
}
|
|
236
|
+
// 门控通过 → 发 done 冻结事件(settle 同机制字面格式——TUI routeSubToken done
|
|
237
|
+
// 分支冻结回收——落位 _freezeAt settle 锚点 splice;无锚点(旧会话残留)时
|
|
238
|
+
// freezeSubTaskLines 尾推兜底——注明两种落位,模型不被误导(advisor 🟡1)。
|
|
239
|
+
ctx.callbacks.onToken(`${freezeKey}/⟦ev⟧done\x1e0\x1e0\x1edone\x1e`)
|
|
240
|
+
return JSON.stringify({
|
|
241
|
+
key: freezeKey,
|
|
242
|
+
status: "frozen",
|
|
243
|
+
note: "done freeze event issued — the TUI reclaimed the block into the conversation (spliced at its settle anchor when one is recorded, else appended at the current stream end — §17.5.5 same-rule position)",
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
// ── view 面(缺省——readonly)──
|
|
247
|
+
// 双参互斥(D-P2):freeze 优先;显式 view:false 且无 freeze = 无请求可执行——报错。
|
|
248
|
+
if (args?.view === false) {
|
|
249
|
+
return JSON.stringify({ status: "error", error: "panel has nothing to do — view:false with no freeze key; pass freeze:'role#N' to reclaim a digested-stuck block, or omit view (defaults to true)" })
|
|
250
|
+
}
|
|
251
|
+
const snap = agent._panelSnapshot
|
|
252
|
+
if (!Array.isArray(snap)) {
|
|
253
|
+
// F-P3 降级:无镜像(headless/VS Code/子代理上下文——CLI TUI-only 完整能力)→
|
|
254
|
+
// 池视图(_asyncSubagents 运行/排队条目 + _pendingAsyncResults 待消化条目)
|
|
255
|
+
const blocks = []
|
|
256
|
+
const queue = agent._asyncQueue ?? []
|
|
257
|
+
for (const e of [...(agent._asyncSubagents?.values() ?? [])]) {
|
|
258
|
+
const b = { key: `${e.role}#${e.id}`, role: e.role }
|
|
259
|
+
if (e.status === "running") {
|
|
260
|
+
b.status = "running"
|
|
261
|
+
b.elapsedSec = e.startedAt ? Math.max(0, Math.floor((Date.now() - e.startedAt) / 1000)) : 0
|
|
262
|
+
} else if (e.status === "queued") {
|
|
263
|
+
b.status = "queued"
|
|
264
|
+
const qi = queue.indexOf(e)
|
|
265
|
+
b.position = qi >= 0 ? qi + 1 : (e.position ?? null)
|
|
266
|
+
} else {
|
|
267
|
+
b.status = "done" // 回合内 settle 未取——status action 可查/check 可取回
|
|
268
|
+
}
|
|
269
|
+
blocks.push(b)
|
|
270
|
+
}
|
|
271
|
+
for (const e of agent._pendingAsyncResults ?? []) {
|
|
272
|
+
blocks.push({ key: `${e.role}#${e.id}`, role: e.role, status: "awaitingDigest", note: "report pending — injected at the next run start (§17)" })
|
|
273
|
+
}
|
|
274
|
+
return JSON.stringify({
|
|
275
|
+
degraded: true,
|
|
276
|
+
note: "no panel — this session has no CLI TUI panel mirror (headless / VS Code / subagent context — panel view is CLI-TUI-only, AC-P4); pool-derived view below; action:'status' shows the full pool",
|
|
277
|
+
panel: blocks,
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
const panel = snap.map((b) => {
|
|
281
|
+
const out = { key: b.key, role: b.role, status: b.status }
|
|
282
|
+
if (b.status === "running") {
|
|
283
|
+
out.elapsedSec = b.startedAt ? Math.max(0, Math.floor((Date.now() - b.startedAt) / 1000)) : 0
|
|
284
|
+
} else if (b.status === "awaitingDigest") {
|
|
285
|
+
// 读时交叉(round1 #3):pending/池均无对应 = 报告已消化(注入即从两者移除)——
|
|
286
|
+
// 块驻留 = 状态滞后——digested:true(freeze 候选——模型可定位异常块)。
|
|
287
|
+
out.digested = !blockKeyIn(agent._pendingAsyncResults, b.key) && !blockKeyIn(agent._asyncSubagents, b.key)
|
|
288
|
+
}
|
|
289
|
+
return out
|
|
290
|
+
})
|
|
291
|
+
return JSON.stringify({ panel })
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* subagent action:"escalate"(§19 D-M4——退役 escalate 工具语义原样,ESCALATE.md):
|
|
296
|
+
* 飞刀——交给 consultModels 池里更强模型(WRITE + 术后报告)。约束全保留:depth-0
|
|
297
|
+
* only / 工程模式拒 / consultModels 空拒 / relay 前缀 `escalate#N/`(与既有前缀同名
|
|
298
|
+
* ——TUI 路由零改动)/ 无 permQueue(continue 直达用户)/ mutations merge 回父。
|
|
299
|
+
*/
|
|
300
|
+
export async function executeEscalateAction(args, ctx) {
|
|
301
|
+
const parent = ctx.agent
|
|
302
|
+
if ((ctx.depth ?? 0) > 0) return "Error: escalate is only available at depth 0 (an escalate's work cannot be delegated again)"
|
|
303
|
+
if (parent?.config?.agent?.engineering) {
|
|
304
|
+
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."
|
|
305
|
+
}
|
|
306
|
+
const pool = parent?.config?.agent?.consultModels ?? []
|
|
307
|
+
if (pool.length === 0) return "Error: no escalate candidates — configure at least one consult model (agent.consultModels)"
|
|
308
|
+
|
|
309
|
+
const { task, model } = args ?? {}
|
|
310
|
+
// task 机械必填(多动作 schema 的 required 只是建议——缺 task 会以晦涩 child-run 错浮现)
|
|
311
|
+
if (typeof task !== "string" || !task.trim()) {
|
|
312
|
+
return "Error: escalate requires a task — the task description with goal, constraints, entry files and acceptance criteria"
|
|
313
|
+
}
|
|
314
|
+
const label = (m) => `${m.provider}:${m.model}`
|
|
315
|
+
const wanted = typeof model === "string" ? model.replace(/\s+\([^)]*\)\s*$/, "").trim() : model
|
|
316
|
+
const pick = wanted ? pool.find((m) => label(m) === wanted) : pool[0]
|
|
317
|
+
if (!pick) {
|
|
318
|
+
return `Error: "${model}" is not a consult candidate. Available: ${pool.map(label).join(", ")}`
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
let provider
|
|
322
|
+
try {
|
|
323
|
+
provider = resolveChildProvider(parent, `${pick.provider}:${pick.model}`)
|
|
324
|
+
} catch (e) {
|
|
325
|
+
return `Error: ${e.message}`
|
|
326
|
+
}
|
|
327
|
+
if (!ensureChildApiKey(provider)) {
|
|
328
|
+
return `Error: provider "${pick.provider}" has no API key — set it in config.json before flying it in`
|
|
329
|
+
}
|
|
330
|
+
let effortNote = ""
|
|
331
|
+
if (pick.effort && !clampEffort(provider, pick.model, pick.effort)) {
|
|
332
|
+
// enum 外 effort 丢弃(preset 默认也可能对 override model 是 enum 外值)
|
|
333
|
+
effortNote = ` (effort "${pick.effort}" unsupported by ${pick.model}, dropped)`
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const tag = label(pick)
|
|
337
|
+
const relayPrefix = makeRelay(parent, "escalate", ctx.callbacks?.onToken, provider.model ?? tag)
|
|
338
|
+
|
|
339
|
+
// 无墙钟 watchdog——turn cap 即成本预算(2026-08-16 rationale:固定墙钟会误杀正常慢速
|
|
340
|
+
// 手术;挂起防护 = FETCH_TIMEOUT_MS + 父 signal 直传)
|
|
341
|
+
|
|
342
|
+
// 不自建 onToken(consult P2):wrapChildCallbacks 已承担前缀 relay + D7 哨兵剥除,
|
|
343
|
+
// runWithContinue 拥有 capture(stripEventTokensForCapture)——手写副本会双剥+双缓冲
|
|
344
|
+
const childCallbacks = wrapChildCallbacks(relayPrefix, ctx.callbacks ?? {})
|
|
345
|
+
|
|
346
|
+
// try 外声明:catch 也能在部分失败时 merge mutations
|
|
347
|
+
let child = null
|
|
348
|
+
let escErr = null // LOGGING outcome(string 形态返回 vs 异常——见下方事件点)
|
|
349
|
+
const escId = relayPrefix.slice(0, -1)
|
|
350
|
+
let escT0 = Date.now()
|
|
351
|
+
try {
|
|
352
|
+
// 全写路径(role "coder"):权限经父 onPermissionRequest,mutations merge 回父
|
|
353
|
+
child = createAgent({
|
|
354
|
+
provider,
|
|
355
|
+
tools: parent.tools,
|
|
356
|
+
config: parent.config,
|
|
357
|
+
cwd: parent.cwd,
|
|
358
|
+
memory: parent.memory,
|
|
359
|
+
overlay: CODER_OVERLAY,
|
|
360
|
+
role: "coder",
|
|
361
|
+
})
|
|
362
|
+
child._logId = escId // LOGGING:子内事件归属(escalate#N)
|
|
363
|
+
escT0 = Date.now()
|
|
364
|
+
logEvent("child:spawn", { role: "escalate", id: escId, kind: "escalate" })
|
|
365
|
+
const runner = ctx.runAgent ?? runAgent
|
|
366
|
+
const runOpts = {
|
|
367
|
+
depth: 1,
|
|
368
|
+
maxTurns: parent.config?.agent?.subagentTurns ?? DEFAULT_SUBAGENT_TURNS, // review #7: constant, not literal (single source with subagent)
|
|
369
|
+
signal: ctx.signal ?? null,
|
|
370
|
+
}
|
|
371
|
+
// Continue 经 runWithContinue(§7.2 D3,主会话同等 y/n 面板):resume:true 不重注入
|
|
372
|
+
// task 文本(setup 跳 input)且保留 child history + mutation 记账,刷新 turn 预算;
|
|
373
|
+
// 无权限 handler(headless)或拒绝 → 部分工作返回;continue 次数无限(每轮可拒)。
|
|
374
|
+
const report = await runWithContinue(
|
|
375
|
+
async (childAgent, input, cbs, opts) => {
|
|
376
|
+
// Merge mid-run mutations even when the run throws — the outer catch keeps
|
|
377
|
+
// handling createAgent failures; AbortError still propagates (user Stop).
|
|
378
|
+
try {
|
|
379
|
+
return await runner(childAgent, input, cbs, opts)
|
|
380
|
+
} catch (e) {
|
|
381
|
+
mergeChildMutations(parent, childAgent)
|
|
382
|
+
throw e
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
child, task, { ...childCallbacks, onPermissionRequest: parent.autoApprove ? async () => true : (ctx.onPermissionRequest ?? null) },
|
|
386
|
+
runOpts,
|
|
387
|
+
{
|
|
388
|
+
// escalate has NO permQueue: prompts go straight to the user (T-L spec).
|
|
389
|
+
askContinue: (e) => (ctx.onPermissionRequest
|
|
390
|
+
? ctx.onPermissionRequest("continue", { turns: e.turn, agent: tag })
|
|
391
|
+
: Promise.resolve(false)),
|
|
392
|
+
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)}`,
|
|
393
|
+
},
|
|
394
|
+
).catch((e) => {
|
|
395
|
+
// 非 ContinueError 运行失败:错误文本 + partial 输出(mutations 已在 runner 包装层 merge)
|
|
396
|
+
if (ctx.signal?.aborted || e?.name === "AbortError") throw e
|
|
397
|
+
escErr = { err: e?.message ?? String(e) } // LOGGING:错误路径(返回形态——不抛)
|
|
398
|
+
return `escalate (${tag}) error: ${e?.message ?? String(e)}\nPartial output: ${(child._capturedOutput ?? "").slice(0, 2000)}`
|
|
399
|
+
})
|
|
400
|
+
// Escalate mutations are the parent's mutations: verify/advisor guards must see them
|
|
401
|
+
mergeChildMutations(parent, child)
|
|
402
|
+
if (escErr) logEvent("child:error", { role: "escalate", id: escId, ms: Date.now() - escT0, err: errText(escErr.err, 200) })
|
|
403
|
+
else logEvent("child:done", { role: "escalate", id: escId, ms: Date.now() - escT0, kind: String(report).includes(TURN_CAP_MARK) ? "partial" : "ok" })
|
|
404
|
+
// §7.2.3(round1 #2):escalate 与 spawn 同享 ctx._subagentKey——同步完成精确冻
|
|
405
|
+
// (relayPrefix 去尾 = `escalate#N`)。仅成功路径(escErr = 运行中途失败——不设
|
|
406
|
+
// key——TUI 回落 escalate 角色启发式:escalate 串行 + 角色限定,天然精确——legacy
|
|
407
|
+
// 行为不变——错误路径不触发冻结 round1 #1)。
|
|
408
|
+
if (!escErr) ctx._subagentKey = escId
|
|
409
|
+
return `escalate (${tag})${effortNote} post-op report:\n${report || (child._capturedOutput ?? "").slice(0, 4000)}${touchedFilesNote(child, parent.cwd)}`
|
|
410
|
+
} catch (e) {
|
|
411
|
+
// 仅 createAgent 失败/continue 询问抛出才到这(运行失败已在上面 catch 处理)
|
|
412
|
+
if (child) {
|
|
413
|
+
mergeChildMutations(parent, child)
|
|
414
|
+
if (!escErr && !(ctx.signal?.aborted) && e?.name !== "AbortError") {
|
|
415
|
+
logEvent("child:error", { role: "escalate", id: escId, ms: Date.now() - escT0, err: errText(e, 200) })
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (ctx.signal?.aborted || e?.name === "AbortError") throw e
|
|
419
|
+
return `escalate (${tag}) error: ${e?.message ?? String(e)}`
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Relative touched-file list appended to every escalate return (child paths are absolute). */
|
|
424
|
+
function touchedFilesNote(child, cwd) {
|
|
425
|
+
const touched = child?._touchedFiles ?? []
|
|
426
|
+
if (touched.length === 0) return ""
|
|
427
|
+
const shown = touched.map((f) => {
|
|
428
|
+
const r = relative(cwd ?? process.cwd(), f)
|
|
429
|
+
return r && !r.startsWith("..") && !isAbsolute(r) ? r : f
|
|
430
|
+
})
|
|
431
|
+
return `\nTouched files: ${shown.join(", ")}`
|
|
432
|
+
}
|