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
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
* 运行中区块的渲染行数(F2,会话区被挤小);无运行中区块 → 返回 [](F6 空态,
|
|
7
7
|
* 无悬空分隔线)。子 agent 完成后立即冻结进会话流(subagent-blocks.mjs
|
|
8
8
|
* freezeSubTaskLines,✓ 头 + 可展开,§7.2 D4 现状不变),面板下一帧自然移除
|
|
9
|
-
* 该区块(F5)——本模块只渲染 `!done`
|
|
9
|
+
* 该区块(F5)——本模块只渲染 `!done` 条目。§17 T-S14 中间态例外:挂起期已结算
|
|
10
|
+
* 区块(sub.done && sub.awaitingDigest)冻结被延迟,驻留面板显示
|
|
11
|
+
* "done · awaiting digestion",池空补发冻结后才移除。
|
|
12
|
+
*
|
|
13
|
+
* §19.5 D-M7 ⏹:运行中(非 done)折叠头右缘停止标记(dim,仅折叠头)——点击 =
|
|
14
|
+
* cancel(mouse.mjs 列级命中 _stopCol——不触发折叠翻转)。
|
|
15
|
+
* §19.5 D-M8:本模块同时承载子标行 dim 样式(styleSubLabelRow——面板与冻结渲染共用)。
|
|
10
16
|
*
|
|
11
17
|
* 中立模块(D1 评审 #6):layout.mjs 调 renderSubagentPanel 预计算面板高度
|
|
12
18
|
* (subagentLines → subagentH),render-frame.mjs 直接 put 预计算行(不重复
|
|
@@ -19,9 +25,30 @@
|
|
|
19
25
|
* 折叠状态 key = `sub-${key}` 跨 turn 保持(D5,与冻结区块同一 key——冻结边界
|
|
20
26
|
* 无缝衔接)。
|
|
21
27
|
*/
|
|
22
|
-
import { C } from "./ansi.mjs"
|
|
28
|
+
import { ansi, C } from "./ansi.mjs"
|
|
23
29
|
import { sliceByWidth, stringWidth } from "./render.mjs"
|
|
24
30
|
import { isExpanded, renderBlockTimeline, renderExpandedBlock, foldTailLines } from "./fold-block.mjs"
|
|
31
|
+
import { SUBAGENT_ROLES } from "./subagent-blocks.mjs"
|
|
32
|
+
|
|
33
|
+
/** 子标行行首标记(`explore#1 · `——sublabelLine 写入的字面形态)。 */
|
|
34
|
+
const SUB_LABEL_RE = /^([\w-]+#\d+(?:\/[\w-]+#\d+)* · )/
|
|
35
|
+
|
|
36
|
+
/** §19.5 D-M8 子标行 dim 样式(最终渲染点注入——行对象单色外 wrap 无法表达行内
|
|
37
|
+
* 双色):行首 `explore#1 · ` 子标转 gray(90m),其后内容恢复该行 kind 色
|
|
38
|
+
* (tool=cyan / text=白 / think=默认 fg——dim 属性本就由 C.reason 施加,不受
|
|
39
|
+
* fg 切换影响)。折叠 tail / dim 行不需调用(整行已 dim)。返回新行对象。 */
|
|
40
|
+
export function styleSubLabelRow(row) {
|
|
41
|
+
const content = String(row.text).replace(/^│ /, "")
|
|
42
|
+
const m = content.match(SUB_LABEL_RE)
|
|
43
|
+
if (!m) return row
|
|
44
|
+
const restore = row.color === C.tool ? ansi.fg(6)
|
|
45
|
+
: row.color === C.text ? ansi.fg(7)
|
|
46
|
+
: ansi.fg(9) // 默认 fg(think 行 C.reason=dim 属性——90m 后切回默认即还原 dim 白)
|
|
47
|
+
return {
|
|
48
|
+
...row,
|
|
49
|
+
text: `│ ${ansi.gray}${m[1]}${restore}${content.slice(m[1].length)}`,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
25
52
|
|
|
26
53
|
/**
|
|
27
54
|
* 面板行构建(纯函数):顶部分隔线 `─` + 各运行中区块(折叠头 + tail 3 /
|
|
@@ -30,7 +57,7 @@ import { isExpanded, renderBlockTimeline, renderExpandedBlock, foldTailLines } f
|
|
|
30
57
|
* @returns {Array<{text: string, color: string, ...}>}
|
|
31
58
|
*/
|
|
32
59
|
export function renderSubagentPanel(state, cols, maxRows) {
|
|
33
|
-
const runningSubs = Object.values(state.subTasks ?? {}).filter((s) => !s.done)
|
|
60
|
+
const runningSubs = Object.values(state.subTasks ?? {}).filter((s) => !s.done || s.awaitingDigest)
|
|
34
61
|
if (runningSubs.length === 0) return []
|
|
35
62
|
const out = []
|
|
36
63
|
// 面板顶部边界线(现状分隔线语义迁移,§7.2.1 D2/NF2)——面板存在即画线,
|
|
@@ -41,8 +68,25 @@ export function renderSubagentPanel(state, cols, maxRows) {
|
|
|
41
68
|
// 头部摘要:`[▶ coder#1 · glm-5.3 · 45s · turn 12/100] bash — npm test`
|
|
42
69
|
// ⏸ = 等待审批态(sub.approval 非空,评审 #5 定义);图标在括号内,
|
|
43
70
|
// 与冻结头 `[✓ …]` 格式统一(任务简报 UI 决策)。
|
|
44
|
-
const icon = sub.approval ? "⏸" : "▶"
|
|
45
|
-
const elapsed = Math.floor((Date.now() - sub.started) / 1000)
|
|
71
|
+
const icon = sub.approval ? "⏸" : sub.done ? "✓" : "▶"
|
|
72
|
+
const elapsed = Math.floor(((sub.done ? (sub.doneAt ?? Date.now()) : Date.now()) - sub.started) / 1000)
|
|
73
|
+
// §19.5 D-M7b ②: sync/async 显式头标(B 形态——不靠"没标推断")——async 由
|
|
74
|
+
// ⟦ev⟧async 标记置位;sync 区块(无标记)显式标 sync。仅真实 subagent 角色
|
|
75
|
+
// (escalate/consult/compress 等复用面板槽的条目无语义——非 spawn 角色豁免);
|
|
76
|
+
// 冻结后保留(与 model 标识同生命周期——render-conversation frozenSubTaskLines
|
|
77
|
+
// 同款 modePart)。**颜色后置注入**(code review 🔵#4):bracket 宽度预算用纯文
|
|
78
|
+
// 本(dim ANSI 内嵌会被 sliceByWidth 截断在 restore 之前 → 行尾残留 dim)——
|
|
79
|
+
// 截断后对完整存活的 mode word 单独套 dim + 恢复行色(自闭合——截断落在词内
|
|
80
|
+
// 则 replace 不命中 → 无 ANSI 泄漏,词以行色显示)。
|
|
81
|
+
const isSubRole = SUBAGENT_ROLES.includes(sub.role)
|
|
82
|
+
// §20 D-SD3b waiting 块(sub.queued——排队 spawn 返回即建——未启动无 relay 流):
|
|
83
|
+
// 括号状态词 = waiting(依赖/域冲突等位——detail 即原因)或 queued(槽满等位——
|
|
84
|
+
// 状态区显示 position);不显示 sync/async 词(尚未启动——无 async 标记可言——
|
|
85
|
+
// sync 词会误导:async spawn 排队的块不是 sync);⏹ 门控不变(async 启动后才置)。
|
|
86
|
+
const queued = sub.queued ?? null
|
|
87
|
+
const statusWord = queued ? (queued.kind === "slot" ? "queued" : "waiting") : null
|
|
88
|
+
const modeWord = isSubRole && !queued ? (sub.async === true ? "async" : "sync") : null
|
|
89
|
+
const modePart = modeWord ? ` · ${modeWord}` : (statusWord ? ` · ${statusWord}` : "")
|
|
46
90
|
// 评审 #1 宽度预算:模型名先单独按显示宽度截断([model] token 原样记录可长
|
|
47
91
|
// 20-30+ 字符,不截断则括号前缀宽度不可预算、状态区被挤出终端右边距);
|
|
48
92
|
// 再量括号前缀实际显示宽度,状态区按 cols - bracketWidth - 2 截断——整行
|
|
@@ -52,23 +96,54 @@ export function renderSubagentPanel(state, cols, maxRows) {
|
|
|
52
96
|
? ` · ${sliceByWidth(sub.model, Math.max(8, Math.floor(cols / 3)))}`
|
|
53
97
|
: ""
|
|
54
98
|
const turnPart = sub.maxTurns > 0 ? ` · turn ${sub.turn}/${sub.maxTurns}` : ""
|
|
55
|
-
const
|
|
99
|
+
const bracketRaw = `[${icon} ${sub.key}${modePart}${modelPart} · ${elapsed}s${turnPart}]`
|
|
100
|
+
let bracket = sliceByWidth(bracketRaw, Math.max(1, cols - 2))
|
|
101
|
+
if (modeWord) {
|
|
102
|
+
const painted = bracket.replace(` · ${modeWord}`, ` · ${ansi.dim}${modeWord}${C.tool}`)
|
|
103
|
+
if (painted !== bracket) bracket = painted // 词被截断则保持纯文本(不泄漏 dim)
|
|
104
|
+
}
|
|
105
|
+
if (statusWord) {
|
|
106
|
+
const painted = bracket.replace(` · ${statusWord}`, ` · ${ansi.dim}${statusWord}${C.tool}`)
|
|
107
|
+
if (painted !== bracket) bracket = painted
|
|
108
|
+
}
|
|
56
109
|
const bracketWidth = stringWidth(bracket)
|
|
57
110
|
let statePart
|
|
58
|
-
if (
|
|
111
|
+
if (queued) {
|
|
112
|
+
// waiting 块状态区:detail 即原因文本(waiting for: …/dependency cancelled: …);
|
|
113
|
+
// slot 等位 → queued · position N(槽满)。
|
|
114
|
+
statePart = queued.kind === "slot"
|
|
115
|
+
? `queued · position ${queued.position ?? "?"}(槽满等位)`
|
|
116
|
+
: (queued.detail || "queued")
|
|
117
|
+
} else if (sub.approval) statePart = `等待审批: ${sub.approval}`
|
|
118
|
+
else if (sub.awaitingDigest) statePart = "done · awaiting digestion"
|
|
59
119
|
else if (sub.currentTool) statePart = sub.currentTool
|
|
60
120
|
else statePart = "thinking..."
|
|
61
121
|
const argSummary = sub.currentTool && sub.toolArgs?.command
|
|
62
122
|
? ` — ${String(sub.toolArgs.command).replace(/\s+/g, " ").trim().slice(0, 60)}`
|
|
63
123
|
: ""
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
124
|
+
let headText = `${bracket} ${sliceByWidth(statePart + argSummary, Math.max(0, cols - 2 - bracketWidth))}`
|
|
125
|
+
const line = { color: C.tool, _foldToggle: foldKey }
|
|
126
|
+
// §19.5 D-M7 ⏹ + D-M7b ③ 门控:⏹ 只对 async 区块(running && SUBAGENT_ROLES &&
|
|
127
|
+
// sub.async——sync 区块无 ⏹——杜绝"可见但不可中止"误导——用户裁定 B 形态)。
|
|
128
|
+
// done/awaitingDigest/压缩面板(role compress)/consult 无标记(点击只对池内
|
|
129
|
+
// async 子代理有意义):dim 停止标记钉在折叠头右缘**内收一列**(code review
|
|
130
|
+
// 🟡#1——glyph 在 cols−1、最右列留 margin——避免终端最末列点击不可靠/全角字形
|
|
131
|
+
// 顶格被裁;命中区 = col ≥ _stopCol = cols−1——含 glyph 与其右 margin,左邻
|
|
132
|
+
// padding 空格仍走折叠)。整行 ≤ cols:内容先按 cols−3 截断。
|
|
133
|
+
if (!sub.done && sub.async === true && SUBAGENT_ROLES.includes(sub.role)) {
|
|
134
|
+
const cut = sliceByWidth(headText, Math.max(0, cols - 3))
|
|
135
|
+
headText = cut + " ".repeat(Math.max(0, cols - 3 - stringWidth(cut)))
|
|
136
|
+
line.text = `${headText} ${ansi.dim}⏹${ansi.reset} `
|
|
137
|
+
line._stopSub = sub.key
|
|
138
|
+
line._stopCol = Math.max(1, cols - 1)
|
|
139
|
+
} else {
|
|
140
|
+
line.text = headText
|
|
141
|
+
}
|
|
142
|
+
out.push(line)
|
|
69
143
|
if (isExpanded(state, foldKey)) {
|
|
70
144
|
// 展开态:全量活动时间线(per-kind 着色,60% 屏封顶 + 块内滚动——公共组件)。
|
|
71
|
-
|
|
145
|
+
// §19.5 D-M8:时间线行过 styleSubLabelRow——行首子标 dim、内容恢复 kind 色。
|
|
146
|
+
const body = renderBlockTimeline(sub.blocks, cols).map(styleSubLabelRow)
|
|
72
147
|
out.push(...renderExpandedBlock({ body, foldKey, state, maxRows, cols, label: "subagent activity" }))
|
|
73
148
|
} else {
|
|
74
149
|
// 折叠态:tail 3 非空 block 行(最近活动),dim。
|
package/src/tui/tool-args.mjs
CHANGED
|
@@ -48,8 +48,16 @@ export function describeToolArgs(name, args) {
|
|
|
48
48
|
case "advisor": return String(a.type ?? "review")
|
|
49
49
|
case "read_image": return String(a.path ?? "")
|
|
50
50
|
case "question": return String(a.question ?? "").replace(/\s+/g, " ").trim().slice(0, 60)
|
|
51
|
-
case "
|
|
52
|
-
|
|
51
|
+
case "memory": {
|
|
52
|
+
// §6 action-routed summary (D-M5): one readable line per action
|
|
53
|
+
const action = String(a.action ?? "")
|
|
54
|
+
if (action === "put") return String(a.title ?? "")
|
|
55
|
+
if (action === "search") return String(a.query ?? "")
|
|
56
|
+
if (action === "list") return [a.scope && `scope ${a.scope}`, a.type && `type ${a.type}`, a.keyword && `kw ${a.keyword}`].filter(Boolean).join(" ")
|
|
57
|
+
if (action === "delete") return a.id ? `id ${a.id} (${a.scope ?? ""})` : `batch ${a.scope ?? ""} ${a.type ? `type ${a.type} ` : ""}${a.keyword ? `kw ${a.keyword}` : ""}`.trim()
|
|
58
|
+
if (action === "clear") return `clear ${a.scope ?? ""}`
|
|
59
|
+
return action || JSON.stringify(a)
|
|
60
|
+
}
|
|
53
61
|
case "lsp": return [a.subcommand, a.uri].filter(Boolean).map(String).join(" ")
|
|
54
62
|
case "repo_outline": return a.path ? String(a.path) : ""
|
|
55
63
|
case "checkpoint": return [a.checkpointAction ?? a.action, a.checkpointId, a.path].filter(Boolean).map(String).join(" ")
|
package/src/tui/tool-events.mjs
CHANGED
|
@@ -3,22 +3,25 @@
|
|
|
3
3
|
* 满足 500 行硬限)。只做「事件 → TUI 状态/对话行」的映射:
|
|
4
4
|
*
|
|
5
5
|
* - onToken/onReasoning : 子agent 前缀分流(routeSub*)→ 主流 streaming/reasoning
|
|
6
|
-
* - onToolCall : 状态栏 + `❯ name args` 标题行 + 计时
|
|
6
|
+
* - onToolCall : 状态栏 + `❯ name args` 标题行 + 计时 + §19 action 记录
|
|
7
7
|
* - onToolResult : 子agent 完成冻结(finishSubTask + freezeDoneSubTasks)、
|
|
8
|
-
*
|
|
9
|
-
* - onToolOutput : advisor 有序块缓冲 /
|
|
8
|
+
* 工具块结果入块、advisor 评审冻结框
|
|
9
|
+
* - onToolOutput : advisor 有序块缓冲 / 工具块输出流
|
|
10
10
|
* - 其余 : usage 累计、等待提示、task 面板、回合末增量落盘
|
|
11
11
|
*
|
|
12
12
|
* flushStream 同时返回给调用方(回合循环 / onTurnEnd 共用)。纯回调装配,无终端副作用
|
|
13
|
-
* (除经 deps 注入的 pushLine/render
|
|
13
|
+
* (除经 deps 注入的 pushLine/render)。§19: subagent_check/escalate 工具退役后,
|
|
14
|
+
* subagent 家族全部调用以工具名 "subagent" + action 到达——完成路由按 onToolCall 时
|
|
15
|
+
* 记录的 action 分流(spawn 区块 / escalate 区块 / check·status 普通工具块)。
|
|
14
16
|
*/
|
|
15
17
|
import { C } from "./ansi.mjs"
|
|
16
18
|
import { formatToolSummary } from "./tool-summaries.mjs"
|
|
17
19
|
import { describeToolArgs, toolArgsLines } from "./tool-args.mjs"
|
|
18
20
|
import { ADVISOR_THINKING_PLACEHOLDER, resolveAdvisorProvider } from "../advisor/run.mjs"
|
|
19
21
|
import {
|
|
20
|
-
SUBAGENT_ROLES, routeSubToken, routeSubReasoning, routeSubToolCall,
|
|
21
|
-
routeSubToolOutput, finishSubTask, finishSubTasksByRole, finishSubTaskByModel, freezeDoneSubTasks,
|
|
22
|
+
SUB_PREFIX_RE, SUBAGENT_ROLES, routeSubToken, routeSubReasoning, routeSubToolCall,
|
|
23
|
+
routeSubToolOutput, finishSubTask, finishSubTaskKey, finishSubTasksByRole, finishSubTaskByModel, freezeDoneSubTasks,
|
|
24
|
+
ensureCompressPanel, markCompressFailed, markCompressDone, markCompressFallback,
|
|
22
25
|
} from "./subagent-blocks.mjs"
|
|
23
26
|
import { TURN_CAP_MARK } from "../agent/spawn-child.mjs"
|
|
24
27
|
|
|
@@ -37,6 +40,11 @@ const REMINDER_CAP = 3 // max pending reminders shown on turn
|
|
|
37
40
|
const REMINDER_PERSIST_TURNS = 5 // persist reminders every N turns
|
|
38
41
|
|
|
39
42
|
const _toolTicks = new Map()
|
|
43
|
+
// §19 action registry: tool_call id → subagent action (non-spawn only — spawn is
|
|
44
|
+
// the default when no record exists). onToolCall sees the args, onToolResult only
|
|
45
|
+
// the name; without the record every subagent result would route as a spawn.
|
|
46
|
+
const _subActions = new Map()
|
|
47
|
+
const _subActionQ = [] // FIFO for subagent calls without a tool id (same fallback as tick queue)
|
|
40
48
|
|
|
41
49
|
function tickStart(name, toolId) {
|
|
42
50
|
const key = toolId ?? name
|
|
@@ -73,6 +81,8 @@ export function sweepToolBlocks(state) {
|
|
|
73
81
|
}
|
|
74
82
|
}
|
|
75
83
|
_toolTicks.clear()
|
|
84
|
+
_subActions.clear()
|
|
85
|
+
_subActionQ.length = 0
|
|
76
86
|
}
|
|
77
87
|
|
|
78
88
|
/** Shared display guard for tool results — LIVE and RESTORE use the same
|
|
@@ -93,9 +103,8 @@ export function slimToolResultForDisplay(result, maxRows = 400) {
|
|
|
93
103
|
: rows
|
|
94
104
|
}
|
|
95
105
|
/** Mark the dispatch-level tool carrier done when its result is consumed by a
|
|
96
|
-
* dedicated branch (subagent/escalate/advisor blocks)
|
|
97
|
-
*
|
|
98
|
-
* "(interrupted)" (consult P1, 2026-08-30). */
|
|
106
|
+
* dedicated branch (subagent/escalate/advisor blocks) — without this the turn
|
|
107
|
+
* sweep mislabels successful calls as "(interrupted)" (consult P1, 2026-08-30). */
|
|
99
108
|
function settleToolBlock(state, name, toolId, summary) {
|
|
100
109
|
const block = findToolBlock(state, name, toolId)
|
|
101
110
|
if (block) {
|
|
@@ -106,6 +115,32 @@ function settleToolBlock(state, name, toolId, summary) {
|
|
|
106
115
|
}
|
|
107
116
|
}
|
|
108
117
|
|
|
118
|
+
/** Async spawn detection (§15 D-A1): the subagent tool's async:true result is a
|
|
119
|
+
* status JSON ({id, role, status: running|queued}), NOT a report — the child
|
|
120
|
+
* keeps running, so its activity block must not be frozen at spawn time (it
|
|
121
|
+
* freezes via the ⟦ev⟧done event at settle — §15 D-A3). */
|
|
122
|
+
function isAsyncSpawnResult(result) {
|
|
123
|
+
try {
|
|
124
|
+
const o = JSON.parse(result)
|
|
125
|
+
return Boolean(o && typeof o === "object" && typeof o.id !== "undefined" && (o.status === "running" || o.status === "queued"))
|
|
126
|
+
} catch {
|
|
127
|
+
return false
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** §7.2.3 spawn 门拒错误探测(T-F5):subagent spawn 的机械拒绝以 {status:"error"}
|
|
132
|
+
* JSON 返回(例:manual auto-turn digest spawn 门——digest 语义 organize-only)——
|
|
133
|
+
* 错误路径不得触发完成冻结(round1 #1——错误路径不冻结任何 running 块)。该形态只
|
|
134
|
+
* 出现在无 subKey 的拒绝路径(成功路径恒带 dispatch 传的 ctx._subagentKey)。 */
|
|
135
|
+
function isSpawnErrorResult(result) {
|
|
136
|
+
try {
|
|
137
|
+
const o = JSON.parse(result)
|
|
138
|
+
return Boolean(o && typeof o === "object" && o.status === "error")
|
|
139
|
+
} catch {
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
109
144
|
|
|
110
145
|
|
|
111
146
|
/** Find the live tool-block carrier for a tool event: exact id match when the
|
|
@@ -130,8 +165,7 @@ function findToolBlock(state, name, toolId) {
|
|
|
130
165
|
/** Build the agent callbacks + the shared flushStream for one turn.
|
|
131
166
|
* askPermission, askQuestion, saveSessionImpl } */
|
|
132
167
|
export function buildToolCallbacks(deps) {
|
|
133
|
-
const { agent, state, pushLine, render, scheduleRender, ensureAssistantLabel, askPermission, askQuestion, saveSessionImpl } = deps
|
|
134
|
-
|
|
168
|
+
const { agent, state, pushLine, render, scheduleRender, ensureAssistantLabel, askPermission, askBatchPermission, askQuestion, saveSessionImpl } = deps
|
|
135
169
|
// NOTE: advisor buffers (_advisorThink/advisorStreaming) are cleared here too.
|
|
136
170
|
// Timing safety: onToolResult flushes _advisorThink into history and empties
|
|
137
171
|
// the buffers BEFORE onTurnEnd can call flushStream (tool result is
|
|
@@ -141,11 +175,8 @@ export function buildToolCallbacks(deps) {
|
|
|
141
175
|
const flushStream = () => {
|
|
142
176
|
if (state.reasoning) {
|
|
143
177
|
pushLine(state.reasoning, C.reason, "thinking")
|
|
144
|
-
// Reasoning folds IMMEDIATELY on flush (user ruling 2026-08-30:
|
|
145
|
-
//
|
|
146
|
-
// rejected pre-fold plan — thinking must be DEFAULT FOLDED in the exact
|
|
147
|
-
// unified form: named header + tail 3, expand ≤60%, click back). Same
|
|
148
|
-
// shape as the restore path — zero exceptions on either path.
|
|
178
|
+
// Reasoning folds IMMEDIATELY on flush (user ruling 2026-08-30): default-folded,
|
|
179
|
+
// named header + tail 3 — same shape as the restore path, zero exceptions.
|
|
149
180
|
state.reasoning = ""
|
|
150
181
|
}
|
|
151
182
|
if (state.streaming) {
|
|
@@ -166,9 +197,8 @@ export function buildToolCallbacks(deps) {
|
|
|
166
197
|
scheduleRender()
|
|
167
198
|
},
|
|
168
199
|
onReasoning: (t) => {
|
|
169
|
-
// Subagent reasoning tokens also carry role#id/ prefix — appended into the
|
|
170
|
-
//
|
|
171
|
-
// previously the token only created the entry and the content was discarded).
|
|
200
|
+
// Subagent reasoning tokens also carry role#id/ prefix — appended into the block
|
|
201
|
+
// buffer as kind=think (F2: same treatment as the main reasoning stream).
|
|
172
202
|
if (routeSubReasoning(state, t, scheduleRender)) return
|
|
173
203
|
ensureAssistantLabel()
|
|
174
204
|
state.reasoning += t
|
|
@@ -178,9 +208,14 @@ export function buildToolCallbacks(deps) {
|
|
|
178
208
|
// Subagent tool call: prefix role#id/toolName → open a fresh tool block and
|
|
179
209
|
// set currentTool for the header summary line.
|
|
180
210
|
if (routeSubToolCall(state, name, args, scheduleRender)) return
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
211
|
+
// §19: record the action of a merged subagent-family call (spawn is the
|
|
212
|
+
// default — only non-spawn actions need a record for result-time routing).
|
|
213
|
+
if (name === "subagent" && args?.action && args.action !== "spawn") {
|
|
214
|
+
if (toolId !== undefined && toolId !== null) _subActions.set(toolId, args.action)
|
|
215
|
+
else _subActionQ.push(args.action)
|
|
216
|
+
}
|
|
217
|
+
// Redundant with flushStream() below (it clears both buffers) — defense-in-depth
|
|
218
|
+
// so a future flushStream change cannot leak advisor buffers into the next view.
|
|
184
219
|
if (name === "advisor") { state._advisorBlocks = [] }
|
|
185
220
|
flushStream()
|
|
186
221
|
ensureAssistantLabel()
|
|
@@ -208,17 +243,12 @@ export function buildToolCallbacks(deps) {
|
|
|
208
243
|
// is unreliable (it glues onto the previous line), so the round belongs here.
|
|
209
244
|
// Also show the advisor's effective model (it may differ from the main agent's).
|
|
210
245
|
const roundTag = name === "advisor" ? ` (round ${(agent._advisorRound || 0) + 1}${advModel ? " · " + advModel : ""})` : ""
|
|
211
|
-
// Readable key-args summary (vscode card-header parity, 2026-08-30) —
|
|
212
|
-
//
|
|
213
|
-
// and the crucial argument was often past the cut. Unknown/MCP tools
|
|
214
|
-
// fall back to compact JSON inside describeToolArgs.
|
|
246
|
+
// Readable key-args summary (vscode card-header parity, 2026-08-30) — replaces
|
|
247
|
+
// the raw JSON.stringify-80 slice. Unknown/MCP tools fall back to compact JSON.
|
|
215
248
|
const argSummary = describeToolArgs(name, args)
|
|
216
|
-
// ONE BLOCK PER TOOL CALL (user ruling 2026-08-30:
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
// the whole call: header = name+args+live status, body = args JSON +
|
|
220
|
-
// streaming output + result. buildConvLines renders it via the shared
|
|
221
|
-
// fold-block component; restore (historyToLines) emits the SAME carrier.
|
|
249
|
+
// ONE BLOCK PER TOOL CALL (user ruling 2026-08-30): header = name+args+live
|
|
250
|
+
// status, body = args JSON + streaming output + result. buildConvLines renders
|
|
251
|
+
// it via the shared fold-block component; restore emits the SAME carrier.
|
|
222
252
|
state.lines.push({
|
|
223
253
|
text: "", color: C.tool,
|
|
224
254
|
_lineId: (state._lineIdCounter = (state._lineIdCounter ?? 0) + 1),
|
|
@@ -235,47 +265,78 @@ export function buildToolCallbacks(deps) {
|
|
|
235
265
|
})
|
|
236
266
|
tickStart(name, toolId)
|
|
237
267
|
},
|
|
238
|
-
|
|
268
|
+
// §7.2.3(方案 e):dispatch runOne 把工具 ctx 上的 _subagentKey(sync spawn/
|
|
269
|
+
// escalate 成功路径设置——relayPrefix 去尾)作为第 4 参传来——undefined 兼容既有
|
|
270
|
+
// 签名(普通工具/老回调/错误路径不带 key)。
|
|
271
|
+
onToolResult: (name, result, toolId, subKey) => {
|
|
239
272
|
state.currentTool = null
|
|
273
|
+
// §19 merged family: route per the action recorded at onToolCall (no record = default spawn).
|
|
274
|
+
let subAction = null
|
|
275
|
+
if (name === "subagent") {
|
|
276
|
+
subAction = (toolId !== undefined && toolId !== null)
|
|
277
|
+
? _subActions.get(toolId) ?? null
|
|
278
|
+
: _subActionQ.shift() ?? null
|
|
279
|
+
if (toolId !== undefined && toolId !== null) _subActions.delete(toolId)
|
|
280
|
+
}
|
|
281
|
+
const isSubagent = name === "subagent" && subAction !== "check" && subAction !== "status" && subAction !== "escalate" && subAction !== "cancel" && subAction !== "panel"
|
|
282
|
+
const isEscalate = name === "subagent" && subAction === "escalate"
|
|
240
283
|
// Subagent complete: mark the earliest running child as done — the block
|
|
241
284
|
// persists (✓ frozen elapsed header, expandable) as the ONLY carrier of the
|
|
242
|
-
// child's activity
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
// child tool calls never enter the parent's history, so the block is the
|
|
246
|
-
// only trace of what the child did — memory bounded by the N2 line cap.
|
|
247
|
-
const isSubagent = name === "subagent"
|
|
285
|
+
// child's activity; memory bounded by the N2 line cap.
|
|
286
|
+
// §19.5: cancel 动作排除在 isSubagent 外——ack/错误 JSON 走普通工具块;区块冻结由
|
|
287
|
+
// ⟦ev⟧stopped settle 事件承担(此处 finishSubTask 会误冻最早 running 区块)。
|
|
248
288
|
if (isSubagent) {
|
|
249
289
|
// The dispatch-level tool-block carrier for this call would otherwise
|
|
250
290
|
// never be marked done (its result lands in the subagent block, not the
|
|
251
|
-
// carrier) and the turn sweep would mislabel it "(interrupted)"
|
|
252
|
-
// successful subagent call showed that banner (consult P1, 2026-08-30).
|
|
291
|
+
// carrier) and the turn sweep would mislabel it "(interrupted)".
|
|
253
292
|
settleToolBlock(state, name, toolId, "completed")
|
|
254
|
-
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
293
|
+
// Async spawn (§15 D-A1): the result is a status JSON, not a report — the
|
|
294
|
+
// child KEEPS running; skip the freeze (it would tombstone a live block and
|
|
295
|
+
// drop its relay stream). The block freezes on the ⟦ev⟧done settle event.
|
|
296
|
+
if (!isAsyncSpawnResult(result)) {
|
|
297
|
+
// §7.2.3 sync spawn 完成精确冻结:结果按 key 归属三支——
|
|
298
|
+
// ① dispatch 同步成功路径带 subKey(ctx._subagentKey = relayPrefix 去尾):
|
|
299
|
+
// finishSubTaskKey 按 key 精确冻——不再落 finishSubTask 的"最早 started"
|
|
300
|
+
// 启发式(async eng-coder 先启动时 explore 完成会误冻其块——7.2.3.1/T-F2);
|
|
301
|
+
// ② spawn 门拒错误({status:"error"} JSON——auto-turn digest spawn 拒绝):
|
|
302
|
+
// 不冻结任何块(round1 #1——错误路径不冻结 running 块——T-F5);
|
|
303
|
+
// ③ subKey undefined 非错误(老回调/测试直调——成功路径未知工具)→ 启发式
|
|
304
|
+
// 兜底(既有行为不变——面板单块时与精确冻同效——T-F1)。
|
|
305
|
+
const lastError = result.includes(TURN_CAP_MARK) ? "turn cap reached — work may be partial" : null
|
|
306
|
+
const hasSubKey = subKey !== undefined && subKey !== null && subKey !== ""
|
|
307
|
+
if (hasSubKey) {
|
|
308
|
+
finishSubTaskKey(state, String(subKey), lastError)
|
|
309
|
+
freezeDoneSubTasks(state)
|
|
310
|
+
} else if (!isSpawnErrorResult(result)) {
|
|
311
|
+
finishSubTask(state, SUBAGENT_ROLES, lastError)
|
|
312
|
+
freezeDoneSubTasks(state)
|
|
313
|
+
}
|
|
314
|
+
// Subagent report preview (max 8 lines) displayed directly in conversation
|
|
315
|
+
const lines = result.split("\n")
|
|
316
|
+
const preview = lines.slice(0, SUBAGENT_PREVIEW_LINES).map((l) => l.slice(0, PREVIEW_LINE_CHARS)).join("\n")
|
|
317
|
+
if (preview) pushLine(preview, C.dim)
|
|
318
|
+
if (lines.length > SUBAGENT_PREVIEW_LINES) pushLine(` ... (${lines.length - SUBAGENT_PREVIEW_LINES} more lines)`, C.dim)
|
|
319
|
+
}
|
|
320
|
+
} else if (isEscalate) {
|
|
321
|
+
// 飞刀 post-op report landed under the subagent tool name — freeze the
|
|
322
|
+
// escalate#N activity block (no preview; legacy surface).
|
|
267
323
|
settleToolBlock(state, name, toolId, "completed")
|
|
268
|
-
|
|
324
|
+
// §7.2.3(round1 #2):escalate 成功返回带 subKey(escalate#N)→ 精确冻;
|
|
325
|
+
// 失败/老回调无 subKey → escalate 角色启发式兜底(escalate 串行 + 角色限定——
|
|
326
|
+
// 既有行为)。
|
|
327
|
+
const lastError = result.includes(TURN_CAP_MARK) ? "turn cap reached — work may be partial" : null
|
|
328
|
+
if (subKey !== undefined && subKey !== null && subKey !== "") {
|
|
329
|
+
finishSubTaskKey(state, String(subKey), lastError)
|
|
330
|
+
} else {
|
|
331
|
+
finishSubTask(state, ["escalate"], lastError)
|
|
332
|
+
}
|
|
269
333
|
freezeDoneSubTasks(state)
|
|
270
334
|
} else if (name === "consult_check" || name === "consult_stop") {
|
|
271
|
-
// Consult session-level settle (2026-08-30 consult review): a session
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
// - individual reply (done:false): settle precisely by r.model —
|
|
277
|
-
// models settle out of order; the earliest-running heuristic froze
|
|
278
|
-
// the wrong block.
|
|
335
|
+
// Consult session-level settle (2026-08-30 consult review): a session spawns
|
|
336
|
+
// N parallel children, so completion must settle ALL of them (single-shot
|
|
337
|
+
// finishSubTask froze only the earliest — N-1 stayed "running" then got
|
|
338
|
+
// mislabeled "interrupted").
|
|
339
|
+
// - individual reply (done:false): settle precisely by r.model.
|
|
279
340
|
// - done:true / stopped: settle every remaining consult block.
|
|
280
341
|
try {
|
|
281
342
|
const r = JSON.parse(result)
|
|
@@ -293,7 +354,7 @@ export function buildToolCallbacks(deps) {
|
|
|
293
354
|
}
|
|
294
355
|
} /* non-JSON result — leave blocks as-is */
|
|
295
356
|
}
|
|
296
|
-
if (!isSubagent && name !== "advisor") {
|
|
357
|
+
if (!isSubagent && !isEscalate && name !== "advisor") {
|
|
297
358
|
// Result lands INSIDE the block (restore parity — the restored carrier
|
|
298
359
|
// carries the same fields). The done line is gone: status/elapsed live
|
|
299
360
|
// in the header now.
|
|
@@ -316,16 +377,11 @@ export function buildToolCallbacks(deps) {
|
|
|
316
377
|
// the frozen box, but the dispatch-level tool carrier must still be
|
|
317
378
|
// marked done (consult P1, 2026-08-30: sweep mislabeled it interrupted).
|
|
318
379
|
settleToolBlock(state, name, toolId, "completed")
|
|
319
|
-
// The review's thinking must survive into the conversation history like
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
// aligned with subagent blocks) instead of a flat auto-expanded line —
|
|
325
|
-
// the flat form flooded the conversation. Full text still lives in the
|
|
326
|
-
// tool result message; the box is the reviewable record. Live
|
|
327
|
-
// _advisorBlocks keep rendering the running view until cleared in the
|
|
328
|
-
// turn finally.
|
|
380
|
+
// The review's thinking must survive into the conversation history like the
|
|
381
|
+
// main agent's reasoning — flushed as a COLLAPSIBLE box before the done
|
|
382
|
+
// line (frozen-folded semantics, aligned with subagent blocks; the flat
|
|
383
|
+
// form flooded the conversation). Full text lives in the tool result;
|
|
384
|
+
// _advisorBlocks keep rendering the running view until cleared at turn end.
|
|
329
385
|
const blocks = state._advisorBlocks ?? []
|
|
330
386
|
if (blocks.length > 0) {
|
|
331
387
|
const text = blocks
|
|
@@ -347,17 +403,17 @@ export function buildToolCallbacks(deps) {
|
|
|
347
403
|
}
|
|
348
404
|
},
|
|
349
405
|
onToolOutput: (name, chunk, toolId) => {
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
406
|
+
// Subagent relays (name "role#id/tool", D1) route RAW — chunks are
|
|
407
|
+
// child-stdout/SSE fragments at arbitrary byte boundaries; trimEnd eats
|
|
408
|
+
// real trailing newlines and routeSubToolOutput's verbatim concat would
|
|
409
|
+
// glue lines (2026-09-03 修复轮; main path keeps the trimmed form below).
|
|
410
|
+
const isSubRelay = SUB_PREFIX_RE.test(name)
|
|
411
|
+
const rawText = typeof chunk === "string" ? chunk : String(chunk?.text ?? "")
|
|
412
|
+
const part = {
|
|
413
|
+
kind: typeof chunk === "string" ? "text" : (chunk?.kind ?? "text"),
|
|
414
|
+
text: isSubRelay ? rawText : rawText.trimEnd(),
|
|
415
|
+
}
|
|
356
416
|
if (!part.text) return
|
|
357
|
-
// Subagent tool output (D1: childCallbacks.onToolOutput relays under the
|
|
358
|
-
// prefixed name "role#id/toolName") → append into the CURRENT tool block of
|
|
359
|
-
// that child's activity buffer. Render throttled at 250ms (N1); the data
|
|
360
|
-
// append itself is never delayed.
|
|
361
417
|
if (routeSubToolOutput(state, name, part, scheduleRender)) return
|
|
362
418
|
if (name === "advisor") {
|
|
363
419
|
// Accumulate to buffer — formatTables + wrapText in render-conversation
|
|
@@ -365,13 +421,9 @@ export function buildToolCallbacks(deps) {
|
|
|
365
421
|
// NOTE: the advisor tool ALWAYS emits {kind, text} objects (run.mjs's
|
|
366
422
|
// emit() wrapper) — a raw string chunk is never think; if that ever
|
|
367
423
|
// changes, plain-string think would land in advisorStreaming.
|
|
368
|
-
// ORDERED block buffer — preserves the interleaved emission order
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
// regrouped ALL thinking above ALL tool progress — the alternating
|
|
372
|
-
// timeline was destroyed. Consecutive chunks of the same kind merge
|
|
373
|
-
// into one block; kind flips start a new block; render walks the
|
|
374
|
-
// blocks in order with per-kind colors.
|
|
424
|
+
// ORDERED block buffer — preserves the interleaved emission order (think →
|
|
425
|
+
// tool → think → … → final); consecutive chunks of the same kind merge
|
|
426
|
+
// into one block, kind flips start a new block, render walks them in order.
|
|
375
427
|
const isString = typeof chunk === "string"
|
|
376
428
|
const raw = isString ? chunk : String(chunk?.text ?? "")
|
|
377
429
|
const kind = isString ? "text" : (chunk?.kind ?? "text")
|
|
@@ -397,10 +449,35 @@ export function buildToolCallbacks(deps) {
|
|
|
397
449
|
}
|
|
398
450
|
scheduleRender()
|
|
399
451
|
},
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
452
|
+
// Manual-tier auto-turn digests (agent-turn.mjs suspension driver) pass null
|
|
453
|
+
// handlers — permission requests then deny WITHOUT a panel (§17 D-S7: no modal
|
|
454
|
+
// during unattended digestion) and question errors out instead of hanging.
|
|
455
|
+
...(askPermission ? { onPermissionRequest: (name, args) => askPermission(name, args) } : {}),
|
|
456
|
+
// Merged batch ask (§16 D-B1): one confirmation for N non-readonly tools in
|
|
457
|
+
// the same response — "approve all / one by one / deny" (key-handler resolves
|
|
458
|
+
// the verdict string; approveAll is batch-scope only, never the AUTO flag).
|
|
459
|
+
...(askBatchPermission ? { onBatchPermissionRequest: (req) => askBatchPermission(req) } : {}),
|
|
460
|
+
...(askQuestion ? { onQuestion: (text, options) => askQuestion(text, options) } : {}),
|
|
461
|
+
// Compression lifecycle (CONTEXT-COMPACTION.md §7 D-C2): the compression session
|
|
462
|
+
// renders as a subagent-style panel block — start → running panel ("Compressing
|
|
463
|
+
// context…" + "summarizing N messages" + elapsed ticker), fail → error text only,
|
|
464
|
+
// success → frozen "Compressed: N tokens freed → summary (Xs)" / fallback →
|
|
465
|
+
// "truncated to N messages". The summary BODY never enters the panel or stream.
|
|
466
|
+
onCompressStart: (info) => {
|
|
467
|
+
ensureCompressPanel(state, info)
|
|
468
|
+
scheduleRender()
|
|
469
|
+
},
|
|
470
|
+
onCompressFail: (error) => {
|
|
471
|
+
// Q3 (CONTEXT-COMPACTION §7 F3): failure is no longer silent — visible on the panel AND
|
|
472
|
+
// logged to stderr so the error is traceable (400/timeout/network).
|
|
473
|
+
console.error("[context] compression failed:", error?.message ?? error)
|
|
474
|
+
markCompressFailed(state, error)
|
|
475
|
+
scheduleRender()
|
|
476
|
+
},
|
|
477
|
+
onCompress: (info) => {
|
|
478
|
+
if (info?.mode === "fallback") markCompressFallback(state, info)
|
|
479
|
+
else markCompressDone(state, info)
|
|
480
|
+
scheduleRender()
|
|
404
481
|
},
|
|
405
482
|
// Async distillation landed (SEND-STALL-DISTILL §2.3): the machine line was replaced by
|
|
406
483
|
// the compressed version — persist it so the session file ends up compressed. Silent:
|