thincoder 0.12.58 → 0.12.60
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 +78 -2
- package/README.md +3 -3
- package/bin/thincoder.mjs +88 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +135 -26
- package/src/advisor/messages.mjs +57 -4
- package/src/advisor/run.mjs +119 -79
- package/src/advisor.mjs +34 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +182 -22
- package/src/agent/helpers.mjs +71 -4
- package/src/agent/record-results.mjs +46 -10
- package/src/agent/run-stages.mjs +227 -0
- package/src/agent/setup-reminders.mjs +62 -0
- package/src/agent/setup.mjs +107 -20
- package/src/agent/spawn-child.mjs +54 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +133 -109
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +154 -104
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +26 -30
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +284 -0
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +152 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +470 -0
- package/src/agent-tools/subagent-async.mjs +382 -0
- package/src/agent-tools/subagent-panel.mjs +153 -0
- package/src/agent-tools/subagent-run.mjs +202 -0
- package/src/agent-tools/subagent-scheduler.mjs +343 -0
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +203 -377
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +198 -238
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +145 -242
- package/src/auto-think.mjs +14 -0
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +4 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +61 -8
- package/src/context.mjs +81 -163
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +30 -12
- package/src/escape.mjs +6 -4
- package/src/explore-distill.mjs +155 -0
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +11 -72
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +206 -87
- package/src/memory.mjs +3 -1
- package/src/model-specs.mjs +15 -1
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +25 -0
- package/src/prompts/advisor-design.md +18 -39
- package/src/prompts/advisor-round1.md +20 -32
- package/src/prompts/advisor-round2.md +16 -16
- package/src/prompts/advisor-round3.md +16 -16
- package/src/prompts/coder.md +7 -28
- package/src/prompts/consult-base.md +4 -11
- package/src/prompts/discipline.md +31 -44
- package/src/prompts/eng-coder.md +9 -34
- package/src/prompts/engineering-sub.md +10 -8
- package/src/prompts/engineering.md +61 -264
- package/src/prompts/explore.md +4 -14
- package/src/prompts/main.md +18 -35
- package/src/prompts/methodology-template.md +32 -38
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +18 -35
- package/src/provider/core.mjs +62 -69
- package/src/provider/errors.mjs +76 -0
- package/src/provider/retry.mjs +8 -45
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +181 -58
- package/src/session.mjs +48 -89
- package/src/token-ttl.mjs +273 -0
- package/src/tools/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +131 -44
- package/src/tools/edit-diff.mjs +348 -0
- package/src/tools/edit.md +20 -13
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +55 -24
- package/src/tools/file.mjs +25 -70
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.mjs +14 -6
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -3
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +2 -0
- package/src/tools/lsp.md +4 -1
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +84 -13
- package/src/tools/question.md +5 -1
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +18 -25
- package/src/tools/system.mjs +50 -30
- package/src/tools/tree.md +2 -1
- package/src/tools/wait_for.md +22 -0
- 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 +179 -27
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +77 -16
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +17 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +11 -11
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +31 -96
- package/src/tui/interaction.mjs +13 -2
- package/src/tui/key-handler.mjs +105 -155
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +46 -0
- package/src/tui/pickers.mjs +51 -25
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +27 -10
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +182 -0
- package/src/tui/startup.mjs +40 -0
- package/src/tui/subagent-blocks.mjs +272 -262
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +125 -12
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +127 -231
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +76 -0
- package/src/tui/wizard.mjs +48 -12
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* suspension-drive.mjs — §17 挂起会话驱动器(2026-09-05 module-split:agent-turn.mjs
|
|
3
|
+
* 535 > 500 硬限——poolCounts + §17 挂起会话段 verbatim 迁入,语义零变;agent-turn.mjs
|
|
4
|
+
* re-import(runAgentTurn ↔ suspensionSession 函数级静态环——模块求值期无顶层调用,
|
|
5
|
+
* 环安全——session-slots ↔ session.mjs 同款先例)。
|
|
6
|
+
*
|
|
7
|
+
* §17(2026-09-02,AGENT-LOOP.md §17 D-S1..S9):回合尾后台池非空 → 不阻塞等待,
|
|
8
|
+
* 进入挂起态——输入放开(Enter = 新回合 / digest 中 Enter 排队 pendingInput)、
|
|
9
|
+
* settle 事件驱动 auto-turn 消化(手动档 organize-only / AUTO 档全语义)、池空 + 无
|
|
10
|
+
* 待处理输入 → 补发 done 冻结自然退出。状态机行表见 AGENT-LOOP.md §17 D-S9。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// 函数级静态环(2026-09-05):drive 的 digestTurn/用户回合经 runAgentTurn 递归进入
|
|
14
|
+
// agent-turn;agent-turn 的回合尾经 suspensionSession 进入本文件——互相 import。
|
|
15
|
+
import { runAgentTurn } from "./agent-turn.mjs"
|
|
16
|
+
import { freezeAllSubTasks, freezeReclaimDigestedBlocks } from "./subagent-blocks.mjs"
|
|
17
|
+
import { sweepToolBlocks } from "./tool-events.mjs"
|
|
18
|
+
import { logEvent } from "../log.mjs"
|
|
19
|
+
import { C } from "./ansi.mjs"
|
|
20
|
+
// ASYNC-RESULT-CONTAINER.md D1/D2:池 accessor(双池 absorb)+ pending 单容器停靠
|
|
21
|
+
import { getAsyncPool, parkAsyncPending } from "../agent-tools/async-settle.mjs"
|
|
22
|
+
|
|
23
|
+
// ── §24 D-24c(R15——2026-09-06)排队用户指令合并常量:单批 ≤8 条且合并注入
|
|
24
|
+
// ≤2000 字符(双端逐字一致——§22 D-Q3 常量先例);超限截批先行(余下下批——不丢
|
|
25
|
+
// 不截断单条);单条 >2000 字符直发不进批;/cmd 不进合并(逐条保序即时)。──
|
|
26
|
+
export const MAX_MERGE_ITEMS = 8
|
|
27
|
+
export const MAX_MERGE_CHARS = 2000
|
|
28
|
+
|
|
29
|
+
/** 合并注入形态(编号逐条——模型一次处理全部——设计措辞锚)。 */
|
|
30
|
+
export function formatMergedMessages(items) {
|
|
31
|
+
return `你排队了 ${items.length} 条消息:\n${items.map((m, i) => `${i + 1}. ${m}`).join("\n")}\n——一次处理`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* R15 攒批计划(纯函数——消费点共享同一事实源):按队列顺序产出动作序列。
|
|
36
|
+
* - { kind:"slash", text, count:1 }——/cmd 逐条即时执行(保序——不进合并缓冲);
|
|
37
|
+
* - { kind:"turn", text, count, merged }——文本批次:连续非 / 条目攒批(单批 ≤8 条;
|
|
38
|
+
* 合并注入(格式化后)≤2000 字符;count ≥2 → merged 编号注入;count === 1 → 原文
|
|
39
|
+
* 直发(含单条 >2000 字符——不进批);超限截批先行——余下条目留待下批(不丢)。
|
|
40
|
+
* 消费者每次取动作 [0] 并按 count 从源队列移除——下轮重新计划余项(边界幂等)。
|
|
41
|
+
*/
|
|
42
|
+
export function planQueuedInput(items) {
|
|
43
|
+
const actions = []
|
|
44
|
+
const n = items.length
|
|
45
|
+
let i = 0
|
|
46
|
+
while (i < n) {
|
|
47
|
+
const cur = String(items[i])
|
|
48
|
+
if (cur.startsWith("/")) { actions.push({ kind: "slash", text: cur, count: 1 }); i++; continue }
|
|
49
|
+
let j = i
|
|
50
|
+
for (;;) {
|
|
51
|
+
if (j - i >= MAX_MERGE_ITEMS || j >= n) break
|
|
52
|
+
const it = String(items[j])
|
|
53
|
+
if (it.startsWith("/")) break
|
|
54
|
+
if (it.length > MAX_MERGE_CHARS) break // 单条超长——不进批(留作直发)
|
|
55
|
+
const cand = items.slice(i, j + 1)
|
|
56
|
+
if (cand.length > 1 && formatMergedMessages(cand).length > MAX_MERGE_CHARS) break // 批总长超限——截批
|
|
57
|
+
j++
|
|
58
|
+
}
|
|
59
|
+
// 超长条目堵头(内环一步未进)→ 该条直发(不进批——也不与前后合并)
|
|
60
|
+
if (j === i) { actions.push({ kind: "turn", text: String(items[i]), count: 1, merged: false }); i++; continue }
|
|
61
|
+
const taken = items.slice(i, j)
|
|
62
|
+
if (taken.length === 1) {
|
|
63
|
+
actions.push({ kind: "turn", text: String(taken[0]), count: 1, merged: false })
|
|
64
|
+
} else {
|
|
65
|
+
actions.push({ kind: "turn", text: formatMergedMessages(taken), count: taken.length, merged: true })
|
|
66
|
+
}
|
|
67
|
+
i = j
|
|
68
|
+
}
|
|
69
|
+
return actions
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 后台池计数(LOGGING susp/digest 事件字段——pendingN/poolN)。
|
|
73
|
+
* poolN = _asyncSubagents + _asyncAdvisors(§24 D-24b advisor 池同面板计数——queued
|
|
74
|
+
* 条目同样在 map 内——2026-09-03 code review #2:不再 +queue.length 双计)。
|
|
75
|
+
* R17(§25 D-R17a/b):pendingN = pending 单容器 `_pendingAsyncResults` 条数(
|
|
76
|
+
* ASYNC-RESULT-CONTAINER.md D2——四族统一停靠——digest 驱动判据同单容器)。 */
|
|
77
|
+
export function poolCounts(agent) {
|
|
78
|
+
const map = agent?._asyncSubagents
|
|
79
|
+
const adv = agent?._asyncAdvisors
|
|
80
|
+
const running = (map ? [...map.values()].filter((e) => e.status === "running").length : 0)
|
|
81
|
+
+ (adv ? [...adv.values()].filter((e) => e.status === "running").length : 0)
|
|
82
|
+
+ consultRunningChildren(agent)
|
|
83
|
+
return {
|
|
84
|
+
poolN: (map?.size ?? 0) + (adv?.size ?? 0),
|
|
85
|
+
pendingN: pendingFamilyCount(agent),
|
|
86
|
+
runningN: running,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 运行中 consult children(会诊会话已跨回合——挂起判据/状态行计数同源)。 */
|
|
91
|
+
function consultRunningChildren(agent) {
|
|
92
|
+
let n = 0
|
|
93
|
+
for (const s of agent?._consultSessions?.values() ?? []) {
|
|
94
|
+
if (!s.stopped) n += Math.max(0, s.pending ?? 0)
|
|
95
|
+
}
|
|
96
|
+
return n
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** pending 单容器条数(§25 D-R17a 消费驱动判据——T-R17j——ASYNC-RESULT-CONTAINER.md
|
|
100
|
+
* D2:四族统一 `_pendingAsyncResults` +role,不再分族三容器)。 */
|
|
101
|
+
export function pendingFamilyCount(agent) {
|
|
102
|
+
return agent?._pendingAsyncResults?.length ?? 0
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** pending 单容器非空(digest 触发判据——D-S2/D-S9——§25 D-R17a)。 */
|
|
106
|
+
export function pendingFamiliesNonEmpty(agent) {
|
|
107
|
+
return pendingFamilyCount(agent) > 0
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** pending 单容器条目(freezeReclaimDigestedBlocks 的归属比对表——四族同容器)。 */
|
|
111
|
+
export function allPendingEntries(agent) {
|
|
112
|
+
return [...(agent?._pendingAsyncResults ?? [])]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── §17 挂起会话(AGENT-LOOP.md §17 D-S2/D-S9 状态机行表)────────────────
|
|
116
|
+
|
|
117
|
+
/** 后台池存活判据(D-S2/F5 口径):running/queued 子代理或后台评审,或已 settle 未注入结果
|
|
118
|
+
* (pending 单容器非空 = D-S3 "未注入"),或 running consult 会话(会诊跨回合——
|
|
119
|
+
* §25 D-R17a 挂起活度钩子——consult 启动回合尾即入挂起态)。回合尾与每次轮末都用它
|
|
120
|
+
* 评估退出。§24 D-24b:_asyncAdvisors(后台评审池)与子代理池同判——评审飞行中挂起
|
|
121
|
+
* 会话必须存活。 */
|
|
122
|
+
export function poolLive(agent) {
|
|
123
|
+
const map = agent._asyncSubagents
|
|
124
|
+
const adv = agent._asyncAdvisors
|
|
125
|
+
return (map && map.size > 0) || (adv && adv.size > 0)
|
|
126
|
+
|| pendingFamilyCount(agent) > 0
|
|
127
|
+
|| consultRunningChildren(agent) > 0
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** D-S3 ③ 记账清扫:回合边界竞态落下的已 settle 项(settle 回调未及移交——发生在
|
|
131
|
+
* 回合刚结束、_suspended 尚未置位的窗口)补入 pending。幂等:回调已移交的条目已从
|
|
132
|
+
* map 删除并带 _inPending 标记,不会重复入列。
|
|
133
|
+
* ASYNC-RESULT-CONTAINER.md D2:pending 单容器(parkAsyncPending——四族统一停靠
|
|
134
|
+
* +role;escalate 不再走独立流)。 */
|
|
135
|
+
function sweepSettledToPending(agent) {
|
|
136
|
+
const maps = [getAsyncPool(agent, "subagent"), getAsyncPool(agent, "advisor")].filter((m) => m instanceof Map && m.size > 0)
|
|
137
|
+
if (maps.length === 0) return
|
|
138
|
+
for (const map of maps) {
|
|
139
|
+
for (const e of [...map.values()]) {
|
|
140
|
+
if (e.done && !e._inPending) {
|
|
141
|
+
parkAsyncPending(agent, e)
|
|
142
|
+
map.delete(String(e.id))
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 等待下一次 settle(running 子代理 promise 完成)或用户唤醒(Enter 入队 / Ctrl+C /
|
|
149
|
+
* 会话 abort)。唤醒器经 state._suspWake 单槽注入;abort 监听兜底。 */
|
|
150
|
+
function waitForSettleOrWake(agent, state) {
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
let finished = false
|
|
153
|
+
const cleanup = () => {
|
|
154
|
+
state._suspWake = null
|
|
155
|
+
const i = (agent._asyncWaiters ?? []).indexOf(w)
|
|
156
|
+
if (i >= 0) agent._asyncWaiters.splice(i, 1)
|
|
157
|
+
agent._sessionAbort?.signal.removeEventListener("abort", onAbort)
|
|
158
|
+
}
|
|
159
|
+
const finish = (why) => {
|
|
160
|
+
if (finished) return
|
|
161
|
+
finished = true
|
|
162
|
+
cleanup()
|
|
163
|
+
resolve(why)
|
|
164
|
+
}
|
|
165
|
+
const w = () => finish("settle")
|
|
166
|
+
const wake = () => finish("wake")
|
|
167
|
+
const onAbort = () => finish("aborted")
|
|
168
|
+
;(agent._asyncWaiters ??= []).push(w)
|
|
169
|
+
state._suspWake = wake
|
|
170
|
+
if (agent._sessionAbort?.signal.aborted) { onAbort(); return }
|
|
171
|
+
agent._sessionAbort?.signal.addEventListener("abort", onAbort, { once: true })
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 后台模式状态行文本(D-S8;17.5.4 #6 顺手对齐):"后台 N 子代理运行中 · M 完成待消化"
|
|
176
|
+
* ——"运行中" = running + queued(含后台评审——§24 D-24b 同面板计数)+ running consult
|
|
177
|
+
* children(R17——会诊跨回合);"完成待消化" = pending 任一族移交项 + §17.5 回合尾留池
|
|
178
|
+
* 的 settled 未消费项(挂起会话 sweep 前的可见窗口)。 */
|
|
179
|
+
function backgroundStatusText(agent) {
|
|
180
|
+
const map = agent._asyncSubagents
|
|
181
|
+
const adv = agent._asyncAdvisors
|
|
182
|
+
const running = (map ? [...map.values()].filter((e) => e.status === "running").length : 0)
|
|
183
|
+
+ (adv ? [...adv.values()].filter((e) => e.status === "running").length : 0)
|
|
184
|
+
+ consultRunningChildren(agent)
|
|
185
|
+
const queued = agent._asyncQueue?.length ?? 0
|
|
186
|
+
const pending = pendingFamilyCount(agent)
|
|
187
|
+
const doneInPool = (map ? [...map.values()].filter((e) => e.done).length : 0)
|
|
188
|
+
+ (adv ? [...adv.values()].filter((e) => e.done).length : 0) // §17.5 留池未消费
|
|
189
|
+
const awaiting = pending + doneInPool
|
|
190
|
+
const active = running + queued
|
|
191
|
+
return active > 0 || awaiting > 0
|
|
192
|
+
? `后台 ${active} 子代理运行中${awaiting ? ` · ${awaiting} 完成待消化` : ""}`
|
|
193
|
+
: "后台子代理收尾…"
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** 消化轮:系统驱动的 auto-turn(D-S6)。手动档不传权限/问答 handler(D-S7 装配
|
|
197
|
+
* 契约——denied 不弹面板、不悬挂);AUTO 档沿用普通回调(autoApprove 短路自动
|
|
198
|
+
* 执行)。_suspended 保持 true:消化中 settle 延迟冻结 + 移交 pending。R17:pending
|
|
199
|
+
* 计数/消化触发 = 任一 pending 族(T-R17j——consult/escalate 空闲 settle 也触发消化轮)。 */
|
|
200
|
+
async function digestTurn(ctx) {
|
|
201
|
+
const { agent, pushLine } = ctx
|
|
202
|
+
const manual = !agent.autoApprove
|
|
203
|
+
pushLine(manual
|
|
204
|
+
? "[auto-turn: digesting finished subagent reports…]"
|
|
205
|
+
: "[auto-turn: continuing background work…]", C.dim)
|
|
206
|
+
const digestCtx = manual
|
|
207
|
+
? { ...ctx, askPermission: null, askBatchPermission: null, askQuestion: null }
|
|
208
|
+
: ctx
|
|
209
|
+
// LOGGING:digest:* 事件(D-S9 消化轮边界——LOGGING.md F-L4 挂起态覆盖)
|
|
210
|
+
const d0 = Date.now()
|
|
211
|
+
const pend0 = pendingFamilyCount(agent)
|
|
212
|
+
logEvent("digest:start", { pendingN: pend0 })
|
|
213
|
+
await runAgentTurn(digestCtx, "", { autoTurn: true, skipSession: true })
|
|
214
|
+
logEvent("digest:end", { pendingN: pendingFamilyCount(agent), ms: Date.now() - d0 })
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* §17 挂起会话驱动(D-S9 行表;由 runAgentTurn 回合尾进入,池空自然退出):
|
|
219
|
+
* - suspension:池项 settle → 入 pending → 开 auto-turn(合并消化近邻 settle);
|
|
220
|
+
* 用户 Enter → pendingInput(digest 运行中排队,D-S5)——用户输入优先于 digest;
|
|
221
|
+
* - auto-turn:消化中 settle 不并发开新轮(单 runAgent 循环),轮末按 pending/池态
|
|
222
|
+
* 续开合并消化轮或回挂起;pendingInput 非空 → 以该消息开新回合(不触发新 digest);
|
|
223
|
+
* - §17.5.5:每次消化/会话内用户回合消费 pending 后 → freezeReclaimDigestedBlocks
|
|
224
|
+
* 逐条冻结回收(消化完成块不滞留面板——不等池空;settle 锚点 splice——digest 总览
|
|
225
|
+
* 文本之前——round1 #1 裁定);
|
|
226
|
+
* - 退出:池空 + pending 空 + 无待处理输入 → freezeAllSubTasks 补发冻结(仅兜底
|
|
227
|
+
* 未消化残项——17.5.5 块回收与池空解耦)→ idle。
|
|
228
|
+
* _suspended 翻转:会话期 true(settle 回调据此延迟冻结 + 移交 pending);会话内
|
|
229
|
+
* 用户回合执行期翻 false(普通回合语义:settle 即冻结 + 回合尾直注入 ①)。
|
|
230
|
+
*/
|
|
231
|
+
export async function suspensionSession(ctx) {
|
|
232
|
+
const { agent, state, render, pushLine } = ctx
|
|
233
|
+
state.pendingInput ??= []
|
|
234
|
+
state._suspAborted = false
|
|
235
|
+
agent._suspended = true
|
|
236
|
+
agent._sessionSignal = agent._sessionAbort.signal // 会话内 spawn 的 children 共享(subagent.mjs)
|
|
237
|
+
state.suspended = true
|
|
238
|
+
state._suspPending = false // 偏差 #1:进入真正挂起态——标志只在释放窗口期有效(此后由 state.suspended 分流)
|
|
239
|
+
const suspTick = setInterval(() => {
|
|
240
|
+
if (state.suspended && !state.processing) {
|
|
241
|
+
state.status = backgroundStatusText(agent)
|
|
242
|
+
render()
|
|
243
|
+
}
|
|
244
|
+
}, 1000)
|
|
245
|
+
state.status = backgroundStatusText(agent)
|
|
246
|
+
render()
|
|
247
|
+
// LOGGING:susp:* 事件(挂起态进入/退出——F-L4;挂起期输入事件 v1 不记——refinement #1)
|
|
248
|
+
const s0 = Date.now()
|
|
249
|
+
logEvent("susp:enter", poolCounts(agent))
|
|
250
|
+
try {
|
|
251
|
+
while (!state._suspAborted && !agent._sessionAbort.signal.aborted) {
|
|
252
|
+
sweepSettledToPending(agent)
|
|
253
|
+
// 1. 用户输入优先(D-S5):pendingInput(digest 期排队)→ queue(处理期排队)。
|
|
254
|
+
// §24 D-24c(R15):空闲消费攒批合并——同源连续文本合成单条注入(编号逐条
|
|
255
|
+
// ——单批 ≤8 条 / ≤2000 字符;单条超长直发不进批;余下留待下批——不丢);
|
|
256
|
+
// /cmd 不进合并——逐条保序即时(位置 = 源内队列序)。每次取计划动作 [0]——
|
|
257
|
+
// 单回合消费后回环续取(新到消息自然并进下一回合——不设延迟窗口)。
|
|
258
|
+
const fromPending = state.pendingInput.length > 0
|
|
259
|
+
const srcLen = fromPending ? state.pendingInput.length : state.queue.length
|
|
260
|
+
if (srcLen > 0) {
|
|
261
|
+
const src = fromPending ? state.pendingInput : state.queue.map((q) => q.text)
|
|
262
|
+
const head = planQueuedInput(src)[0]
|
|
263
|
+
if (fromPending) state.pendingInput.splice(0, head.count)
|
|
264
|
+
else state.queue.splice(0, head.count)
|
|
265
|
+
if (head.kind === "slash") {
|
|
266
|
+
await ctx.handleSlash?.(head.text)
|
|
267
|
+
render()
|
|
268
|
+
continue
|
|
269
|
+
}
|
|
270
|
+
agent._suspended = false // 用户回合 = 普通回合语义(① 直注入 + settle 即冻结)
|
|
271
|
+
await runAgentTurn(ctx, head.text, { skipSession: true })
|
|
272
|
+
agent._suspended = true
|
|
273
|
+
// §17.5.5:该回合消化完 pending(run 首行注入)→ 逐条冻结回收驻留块
|
|
274
|
+
// (不等池空——settle 锚点 splice——digest 总览文本之前;与 digest 回收同规则)
|
|
275
|
+
// R17:回收比对 = pending 单容器(四族统一——ASYNC-RESULT-CONTAINER.md D2)
|
|
276
|
+
freezeReclaimDigestedBlocks(state, allPendingEntries(agent))
|
|
277
|
+
state.status = backgroundStatusText(agent)
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
// 2. pending 任一族非空 → 合并消化轮(注入由 runAgent 首行统一完成——D-S3 单注入点;
|
|
281
|
+
// R17 判据推广——consult/escalate 族同样触发——T-R17j)
|
|
282
|
+
if (pendingFamiliesNonEmpty(agent)) {
|
|
283
|
+
await digestTurn(ctx)
|
|
284
|
+
// §17.5.5 实测修订(2026-09-03):digest 消化完成(pending 条目已注入)→ 逐条补发
|
|
285
|
+
// done 冻结回收——不等池空——块从面板移除进流(settle 锚点 splice 落位——digest
|
|
286
|
+
// 总览文本之前——round1 #1 裁定);池空 freeze-out 仅兜底未消化残项(挂起会话
|
|
287
|
+
// 结束统一清场)——块回收与池空解耦(T-H7/AC-H5)。
|
|
288
|
+
// 归属不变式:会话内任何 run 开始前 pinned 块(awaitingDigest)的条目必在 pending
|
|
289
|
+
// 任一族——run 消费后不在 pending 的 pinned 块即本 run 消化者(无需快照即精确归属)。
|
|
290
|
+
freezeReclaimDigestedBlocks(state, allPendingEntries(agent))
|
|
291
|
+
state.status = backgroundStatusText(agent)
|
|
292
|
+
continue
|
|
293
|
+
}
|
|
294
|
+
// 3. 池空(无 running/queued/未注入)→ 自然退出回 idle(补发冻结在 finally)
|
|
295
|
+
if (!poolLive(agent)) break
|
|
296
|
+
// 4. 等下一 settle / 用户唤醒(Enter 入队、Ctrl+C)
|
|
297
|
+
await waitForSettleOrWake(agent, state)
|
|
298
|
+
}
|
|
299
|
+
} finally {
|
|
300
|
+
clearInterval(suspTick)
|
|
301
|
+
const aborted = state._suspAborted || agent._sessionAbort.signal.aborted
|
|
302
|
+
if (aborted && (agent._asyncSubagents?.size ?? 0) > 0) logEvent("ev:stopped", { poolN: agent._asyncSubagents?.size ?? 0, where: "suspension-abort" })
|
|
303
|
+
logEvent("susp:exit", { ...poolCounts(agent), ms: Date.now() - s0, reason: aborted ? "aborted" : "idle" })
|
|
304
|
+
agent._suspended = false
|
|
305
|
+
agent._sessionSignal = null
|
|
306
|
+
agent._sessionAbort = null
|
|
307
|
+
agent._sessionAbortAll = null // 偏差 #3:会话期 controller 集合随句柄一并释放
|
|
308
|
+
state.suspended = false
|
|
309
|
+
state._suspWake = null
|
|
310
|
+
state.suspAbortArmed = false // round2 偏差 #4:会话退出即解除挂起中止武装(防跨会话粘滞)
|
|
311
|
+
if (aborted) {
|
|
312
|
+
// §15 abort 语义:清池不注入(用户显式停——不注入陈旧错误)
|
|
313
|
+
agent._asyncSubagents?.clear()
|
|
314
|
+
agent._asyncAdvisors?.clear()
|
|
315
|
+
agent._asyncQueue = []
|
|
316
|
+
// ASYNC-RESULT-CONTAINER.md D2:pending 单容器——中止清容器不注入陈旧结果(四族
|
|
317
|
+
// 统一一处清;consult 会话标记 stopped——settle 不入 digest 流——T-R17c)+
|
|
318
|
+
// children abort。
|
|
319
|
+
agent._pendingAsyncResults = []
|
|
320
|
+
const { cleanupConsultSessions } = await import("../agent-tools/consult.mjs")
|
|
321
|
+
cleanupConsultSessions(agent)
|
|
322
|
+
// §17 round2 偏差 #2-CLI(code review round2 #2-CLI):中止时不静默丢弃挂起期
|
|
323
|
+
// 排队的用户消息——Enter 已清空输入框并入 pendingInput(用户视为已发送),
|
|
324
|
+
// 残余转回 state.queue({text} 条目,下个普通回合的队列循环续发——零丢失)
|
|
325
|
+
// + 提示行明示去向(不静默丢)。
|
|
326
|
+
const queuedN = state.pendingInput?.length ?? 0
|
|
327
|
+
if (queuedN > 0) {
|
|
328
|
+
state.queue.push(...state.pendingInput.splice(0).map((t) => ({ text: String(t) })))
|
|
329
|
+
pushLine(`[background work stopped — ${queuedN} queued message${queuedN > 1 ? "s" : ""} will run as a normal turn]`, C.warn)
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
// D-S3 ③ 兜底:退出前残余(极端竞态)直注入再退——结果零丢失(AC-S2;
|
|
333
|
+
// ASYNC-RESULT-CONTAINER.md D2:pending 单容器一处清——注入器按 role 分发
|
|
334
|
+
// (consult → injectConsultResult;其余 → injectAsyncResult——四族同容器)。
|
|
335
|
+
const { injectAsyncResult } = await import("../agent-tools/subagent.mjs")
|
|
336
|
+
const { injectConsultResult } = await import("../agent-tools/consult.mjs")
|
|
337
|
+
const residual = agent._pendingAsyncResults
|
|
338
|
+
if (residual?.length) {
|
|
339
|
+
for (const e of residual.splice(0)) {
|
|
340
|
+
if (e.role === "consult") await injectConsultResult(agent, e)
|
|
341
|
+
else await injectAsyncResult(agent, e)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// 补发 done 冻结:驻留面板的 awaiting-digest 块随池空冻结进流(T-S14)
|
|
346
|
+
freezeAllSubTasks(state)
|
|
347
|
+
sweepToolBlocks(state)
|
|
348
|
+
state.status = "Ready"
|
|
349
|
+
render()
|
|
350
|
+
}
|
|
351
|
+
}
|
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.layer && `layer ${a.layer}`, a.type && `type ${a.type}`, a.keyword && `kw ${a.keyword}`].filter(Boolean).join(" ")
|
|
57
|
+
if (action === "delete") return a.id ? `id ${a.id}${a.layer ? ` (layer ${a.layer})` : ""}` : `batch ${a.layer ?? ""} ${a.type ? `type ${a.type} ` : ""}${a.keyword ? `kw ${a.keyword}` : ""}`.trim()
|
|
58
|
+
if (action === "clear") return `clear ${a.layer ?? ""}`
|
|
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(" ")
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-display.mjs — 工具块显示/计时/清扫 helper 族(2026-09-05 module-split:
|
|
3
|
+
* tool-events.mjs 537 > 500 硬限——28-162 区(ticks/subActions maps、sweep/slim/
|
|
4
|
+
* settle/async 探测/find)verbatim 迁入,语义零变;tool-events.mjs import 回
|
|
5
|
+
* (buildToolCallbacks 调用点零改)+ re-export sweepToolBlocks(agent-turn 消费面)。
|
|
6
|
+
* 注意:_subActions/_subActionQ/_toolTicks 为模块级**可变对象**导出——ESM 只读绑定
|
|
7
|
+
* 不禁内容变更(.set/.delete/.push),调用方行为与同模块时代一致。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Tool execution start timestamps (performance.now ms). Keyed by tool_call id
|
|
11
|
+
// when available (parallel same-name tools each get their own tick — the
|
|
12
|
+
// P0-3 fix, 2026-08-30), falling back to a per-name FIFO queue for callers
|
|
13
|
+
// without ids (subagent relay). FIFO shift assumes near-call-order completion;
|
|
14
|
+
// a parallel same-name batch finishing out of order swaps durations between
|
|
15
|
+
// siblings — same magnitude, both keep an elapsed (display-level, acceptable).
|
|
16
|
+
|
|
17
|
+
// Named caps (consult P2, 2026-08-30): inline 200/8/120/3/5 were magic numbers.
|
|
18
|
+
export const TOOL_OUTPUT_LINE_CAP = 200 // per-call streaming output ring buffer
|
|
19
|
+
export const SUBAGENT_PREVIEW_LINES = 8 // report preview rows in the conversation
|
|
20
|
+
export const PREVIEW_LINE_CHARS = 120 // per-line preview slice
|
|
21
|
+
export const REMINDER_CAP = 3 // max pending reminders shown on turn end
|
|
22
|
+
export const REMINDER_PERSIST_TURNS = 5 // persist reminders every N turns
|
|
23
|
+
|
|
24
|
+
export const _toolTicks = new Map()
|
|
25
|
+
// §19 action registry: tool_call id → subagent action (non-spawn only — spawn is
|
|
26
|
+
// the default when no record exists). onToolCall sees the args, onToolResult only
|
|
27
|
+
// the name; without the record every subagent result would route as a spawn.
|
|
28
|
+
export const _subActions = new Map()
|
|
29
|
+
export const _subActionQ = [] // FIFO for subagent calls without a tool id (same fallback as tick queue)
|
|
30
|
+
|
|
31
|
+
export function tickStart(name, toolId) {
|
|
32
|
+
const key = toolId ?? name
|
|
33
|
+
if (toolId) { _toolTicks.set(key, performance.now()); return }
|
|
34
|
+
const q = _toolTicks.get(key) ?? []
|
|
35
|
+
q.push(performance.now())
|
|
36
|
+
_toolTicks.set(key, q)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Settle one pending tick: by tool_call id, or per-name FIFO. Returns start or null. */
|
|
40
|
+
export function tickTake(name, toolId) {
|
|
41
|
+
const key = toolId ?? name
|
|
42
|
+
const v = _toolTicks.get(key)
|
|
43
|
+
if (v === undefined) return null
|
|
44
|
+
if (toolId) { _toolTicks.delete(key); return v }
|
|
45
|
+
if (v.length === 0) { _toolTicks.delete(key); return null }
|
|
46
|
+
const started = v.shift()
|
|
47
|
+
if (v.length === 0) _toolTicks.delete(key)
|
|
48
|
+
return started
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** P0-2 sweep (2026-08-30 consult): an interrupted turn (Ctrl+C / error) leaves
|
|
52
|
+
* running tool-block carriers without an onToolResult — their header would say
|
|
53
|
+
* "running" forever. runAgentTurn's finally calls this: mark them done with an
|
|
54
|
+
* "(interrupted)" status and clear the tick table so no stale start time leaks
|
|
55
|
+
* into the next turn. Mirrors freezeAllSubTasks for the tool-block family. */
|
|
56
|
+
export function sweepToolBlocks(state) {
|
|
57
|
+
for (const l of state.lines ?? []) {
|
|
58
|
+
const b = l._toolBlock
|
|
59
|
+
if (b && !b.done) {
|
|
60
|
+
b.done = true
|
|
61
|
+
b.summary = "(interrupted)"
|
|
62
|
+
b.interrupted = true
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
_toolTicks.clear()
|
|
66
|
+
_subActions.clear()
|
|
67
|
+
_subActionQ.length = 0
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Shared display guard for tool results — LIVE and RESTORE use the same
|
|
71
|
+
* function (P1, 2026-08-30 consult: restore lacked the live path's guards).
|
|
72
|
+
* 1) Multimodal results (read_image) embed FULL base64 images in the JSON —
|
|
73
|
+
* the human needs only the text part (model gets images via the multimodal
|
|
74
|
+
* channel); 2) results beyond maxRows are truncated in the block (full
|
|
75
|
+
* text always lives in history for the model). Returns row array. */
|
|
76
|
+
export function slimToolResultForDisplay(result, maxRows = 400) {
|
|
77
|
+
let displayResult = result
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(result)
|
|
80
|
+
if (parsed?.images?.length) displayResult = parsed.text ?? result
|
|
81
|
+
} catch { /* not JSON — show as-is */ }
|
|
82
|
+
const rows = String(displayResult).split("\n").filter((l) => l.trim())
|
|
83
|
+
return rows.length > maxRows
|
|
84
|
+
? [...rows.slice(0, maxRows), `… (result truncated at ${maxRows} rows — full text in history)`]
|
|
85
|
+
: rows
|
|
86
|
+
}
|
|
87
|
+
/** Mark the dispatch-level tool carrier done when its result is consumed by a
|
|
88
|
+
* dedicated branch (subagent/escalate/advisor blocks) — without this the turn
|
|
89
|
+
* sweep mislabels successful calls as "(interrupted)" (consult P1, 2026-08-30). */
|
|
90
|
+
export function settleToolBlock(state, name, toolId, summary) {
|
|
91
|
+
const block = findToolBlock(state, name, toolId)
|
|
92
|
+
if (block) {
|
|
93
|
+
block.done = true
|
|
94
|
+
block.summary = summary
|
|
95
|
+
const started = tickTake(name, toolId)
|
|
96
|
+
block.elapsed = started !== null ? Math.round(performance.now() - started) : null
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Async spawn detection (§15 D-A1): the subagent tool's async:true result is a
|
|
101
|
+
* status JSON ({id, role, status: running|queued}), NOT a report — the child
|
|
102
|
+
* keeps running, so its activity block must not be frozen at spawn time (it
|
|
103
|
+
* freezes via the ⟦ev⟧done event at settle — §15 D-A3). */
|
|
104
|
+
export function isAsyncSpawnResult(result) {
|
|
105
|
+
try {
|
|
106
|
+
const o = JSON.parse(result)
|
|
107
|
+
return Boolean(o && typeof o === "object" && typeof o.id !== "undefined" && (o.status === "running" || o.status === "queued"))
|
|
108
|
+
} catch {
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** §7.2.3 spawn 门拒错误探测(T-F5):subagent spawn 的机械拒绝以 {status:"error"}
|
|
114
|
+
* JSON 返回(例:manual auto-turn digest spawn 门——digest 语义 organize-only)——
|
|
115
|
+
* 错误路径不得触发完成冻结(round1 #1——错误路径不冻结任何 running 块)。该形态只
|
|
116
|
+
* 出现在无 subKey 的拒绝路径(成功路径恒带 dispatch 传的 ctx._subagentKey)。 */
|
|
117
|
+
export function isSpawnErrorResult(result) {
|
|
118
|
+
try {
|
|
119
|
+
const o = JSON.parse(result)
|
|
120
|
+
return Boolean(o && typeof o === "object" && o.status === "error")
|
|
121
|
+
} catch {
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Find the live tool-block carrier for a tool event: exact id match when the
|
|
127
|
+
* callback carries one (P0-3 — parallel same-name tools route to their own
|
|
128
|
+
* block); falls back to the last unfinished block of that name. */
|
|
129
|
+
export function findToolBlock(state, name, toolId) {
|
|
130
|
+
const lines = state.lines ?? []
|
|
131
|
+
if (toolId !== undefined && toolId !== null) {
|
|
132
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
133
|
+
if (lines[i]._toolBlock?.id === toolId) return lines[i]._toolBlock
|
|
134
|
+
}
|
|
135
|
+
return null
|
|
136
|
+
}
|
|
137
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
138
|
+
const b = lines[i]._toolBlock
|
|
139
|
+
if (b && b.name === name && !b.done) return b
|
|
140
|
+
}
|
|
141
|
+
return null
|
|
142
|
+
}
|