thincoder 0.12.66 → 0.12.67
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 +29 -0
- package/README.md +6 -0
- package/bin/thincoder.mjs +15 -0
- package/package.json +2 -2
- package/src/acp/bridge.mjs +8 -10
- package/src/cli/permission.mjs +1 -1
- package/src/prompt-injections.mjs +1 -1
- package/src/tui/agent-turn.mjs +29 -22
- package/src/tui/cmd-advisor.mjs +19 -4
- package/src/tui/cmd-config.mjs +9 -1
- package/src/tui/index.mjs +3 -4
- package/src/tui/interaction.mjs +1 -1
- package/src/tui/key-handler-busy.mjs +17 -12
- package/src/tui/key-handler-ctrlc.mjs +4 -4
- package/src/tui/key-handler-edit.mjs +12 -8
- package/src/tui/key-handler.mjs +1 -1
- package/src/tui/ledger-surface.mjs +26 -5
- package/src/tui/mouse.mjs +2 -2
- package/src/tui/queued-merge.mjs +68 -0
- package/src/tui/queued-pickup.mjs +34 -0
- package/src/tui/render-conversation.mjs +37 -1
- package/src/tui/render-frame.mjs +11 -5
- package/src/tui/render-segments.mjs +2 -2
- package/src/tui/subagent-blocks.mjs +13 -13
- package/src/tui/subagent-freeze.mjs +4 -4
- package/src/tui/subagent-panel.mjs +4 -4
- package/src/tui/suspension-drive.mjs +42 -35
- package/src/tui/tool-display.mjs +1 -1
- package/src/tui/tool-events.mjs +7 -7
- package/src/tui/tui-state.mjs +1 -1
- package/src/tui/turn-face.mjs +2 -2
package/src/tui/mouse.mjs
CHANGED
|
@@ -140,7 +140,7 @@ export function handleMouseClick(ctx, col, row) {
|
|
|
140
140
|
// 评审 #4:保底截断后可见行 ≠ 前 h 行——命中映射与 render-frame 同一几何契约
|
|
141
141
|
// (subagentLineIndex:分隔线 + 末尾区块行优先)。
|
|
142
142
|
const lineEl = layout.subagentLines[subagentLineIndex(layout.subagentLines, P.subagent.h, r - P.subagent.y)]
|
|
143
|
-
// §
|
|
143
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.2 D-M7/D-M7b ⏹ 停止标记(round1 #6 + 用户裁定 B 形态)+ SYNC-CANCEL F3
|
|
144
144
|
// (2026-09-09):列级命中——⏹ 列点击 = cancel(定向该子代理——ctx.cancelSubagent
|
|
145
145
|
// 直连池/registry abort 路径,不经模型回合),不触发折叠翻转;**async 区块与 registry
|
|
146
146
|
// live 的 sync 区块带 _stopSub 元数据**(headless 无 _agent → sync 不钉——零回归);
|
|
@@ -192,7 +192,7 @@ export function handleMouseClick(ctx, col, row) {
|
|
|
192
192
|
|
|
193
193
|
/** 鼠标装配簇(2026-09-03 D-S1a 自 index.mjs 迁入):index.mjs 只留装配调用。
|
|
194
194
|
* ctx: { agent, state, pushLine, render, popPicker };返回 { onMouseClick, mouseCtx }。
|
|
195
|
-
* cancelSubagent(⏹
|
|
195
|
+
* cancelSubagent(⏹ 停止标记,AGENT-LOOP-SUBAGENT.md §6.7.2 D-M7):UI 停止不经模型回合,直连池 abort——
|
|
196
196
|
* 与 action:"cancel" 同实现路径。block key "role#id" → 池条目 id。 */
|
|
197
197
|
export function createMouseDispatch({ agent, state, pushLine, render, popPicker }) {
|
|
198
198
|
const cancelSubagent = (key) => {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* queued-merge.mjs — busy 排队消息的合并消费纯函数族(R15 攒批恢复——queue-visible 批 2026-09-24)。
|
|
3
|
+
*
|
|
4
|
+
* 机制单源 = `docs/cli/design/TUI.md` §7.5「合并消费」+ `docs/cli/design/TUI-INPUT-BOX.md` §4.1
|
|
5
|
+
* 「送达链路」(常量 / 形态文案 / 计划语义)。恢复依据 = 旧形态逐字(R15 攒批,2026-09-06 实现 →
|
|
6
|
+
* 2026-09-09 随 INPUT-LOCK-ASYNC 废弃;废弃记录 = `thincoder-cli/docs/_archive/design/AGENT-LOOP.md`
|
|
7
|
+
* §11.3;原实现 = git `3e1234b7` 树内 `src/tui/suspension-drive.mjs:21-68`)——常量名 / 值 /
|
|
8
|
+
* 形态文案 / 批语义逐字一致。
|
|
9
|
+
*
|
|
10
|
+
* 取批四支共用同一事实源(`planQueuedInput`;队列容器仍归会话层 `state.pendingInput`——核只给缝):
|
|
11
|
+
* ① 步边界 pickup(`queued-pickup.mjs`)· ② 回合尾兜底(`agent-turn.mjs`)
|
|
12
|
+
* · ③ driver 输入优先(`suspension-drive.mjs`)· ④ 中止残余转 queue(`suspension-drive.mjs`)。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ── 合并常量(R15 逐字):单批 ≤ MAX_MERGE_ITEMS 条且合并注入 ≤ MAX_MERGE_CHARS 字符
|
|
16
|
+
// (双端逐字一致——VSC 对位 `thincoder-vscode/src/extension/queued-merge.mjs` 同值同名);
|
|
17
|
+
// 超限截批先行(余下留待下批——不丢不截断单条);单条 > MAX_MERGE_CHARS 直发不进批;
|
|
18
|
+
// /cmd 不进合并(逐条保序即时)。──
|
|
19
|
+
export const MAX_MERGE_ITEMS = 8
|
|
20
|
+
export const MAX_MERGE_CHARS = 2000
|
|
21
|
+
|
|
22
|
+
/** 队容量(条)——**常量单源**(显示面 / 输入门禁同源 import;与合并批上限 `MAX_MERGE_ITEMS`(8)
|
|
23
|
+
* **同值不同名**:常规满队 ⇒ 恰一批一次消费)。判据面 = `docs/cli/design/TUI-INPUT-BOX.md` §4.1
|
|
24
|
+
* 条件 5(第 9 条 = 拒 + 提示 + 文本保留)。 */
|
|
25
|
+
export const QUEUED_MAX_ITEMS = 8
|
|
26
|
+
|
|
27
|
+
/** 合并注入形态(编号逐条——模型一次处理全部——设计措辞锚;`docs/cli/design/TUI.md` §7.5 逐字)。 */
|
|
28
|
+
export function formatMergedMessages(items) {
|
|
29
|
+
return `你排队了 ${items.length} 条消息:\n${items.map((m, i) => `${i + 1}. ${m}`).join("\n")}\n——一次处理`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* R15 攒批计划(纯函数——消费点共享同一事实源):按队列顺序产出动作序列。
|
|
34
|
+
* - { kind:"slash", text, count:1 }——/cmd 逐条即时执行(保序——不进合并缓冲);
|
|
35
|
+
* - { kind:"turn", text, count, merged }——文本批次:连续非 / 条目攒批(单批 ≤ MAX_MERGE_ITEMS 条;
|
|
36
|
+
* 合并注入(格式化后)≤ MAX_MERGE_CHARS 字符;count ≥2 → merged 编号注入;count === 1 → 原文
|
|
37
|
+
* 直发(含单条 > MAX_MERGE_CHARS——不进批);超限截批先行——余下条目留待下批(不丢)。
|
|
38
|
+
* 消费者每次取动作 [0] 并按 count 从源队列移除——下轮重新计划余项(边界幂等)。
|
|
39
|
+
*/
|
|
40
|
+
export function planQueuedInput(items) {
|
|
41
|
+
const actions = []
|
|
42
|
+
const n = items.length
|
|
43
|
+
let i = 0
|
|
44
|
+
while (i < n) {
|
|
45
|
+
const cur = String(items[i])
|
|
46
|
+
if (cur.startsWith("/")) { actions.push({ kind: "slash", text: cur, count: 1 }); i++; continue }
|
|
47
|
+
let j = i
|
|
48
|
+
for (;;) {
|
|
49
|
+
if (j - i >= MAX_MERGE_ITEMS || j >= n) break
|
|
50
|
+
const it = String(items[j])
|
|
51
|
+
if (it.startsWith("/")) break
|
|
52
|
+
if (it.length > MAX_MERGE_CHARS) break // 单条超长——不进批(留作直发)
|
|
53
|
+
const cand = items.slice(i, j + 1)
|
|
54
|
+
if (cand.length > 1 && formatMergedMessages(cand).length > MAX_MERGE_CHARS) break // 批总长超限——截批
|
|
55
|
+
j++
|
|
56
|
+
}
|
|
57
|
+
// 超长条目堵头(内环一步未进)→ 该条直发(不进批——也不与前后合并)
|
|
58
|
+
if (j === i) { actions.push({ kind: "turn", text: String(items[i]), count: 1, merged: false }); i++; continue }
|
|
59
|
+
const taken = items.slice(i, j)
|
|
60
|
+
if (taken.length === 1) {
|
|
61
|
+
actions.push({ kind: "turn", text: String(taken[0]), count: 1, merged: false })
|
|
62
|
+
} else {
|
|
63
|
+
actions.push({ kind: "turn", text: formatMergedMessages(taken), count: taken.length, merged: true })
|
|
64
|
+
}
|
|
65
|
+
i = j
|
|
66
|
+
}
|
|
67
|
+
return actions
|
|
68
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* queued-pickup.mjs — 步边界 pickup(用户 → 主会话投送通道的主时机)。
|
|
3
|
+
*
|
|
4
|
+
* 机制单源 = `docs/core/design/AGENT-LOOP-ASYNC-POOL.md` §6.8「步边界 pickup」(核循环头投递回调
|
|
5
|
+
* `consumeQueuedInput`——一步 = 一次 LLM 调用 + 其工具执行段完结)+ `docs/cli/design/TUI.md` §7.5
|
|
6
|
+
* 「消费时机①」(queue-visible 批 fix 轮 2026-09-24 · 用户 03:06 收正)。
|
|
7
|
+
* 语义 = 不中断(在飞工具 / signal 零触碰)· 下一步生效 · 普通 user 消息落历史(参照系 = 子代理
|
|
8
|
+
* `send`——参照非改造);与 Ctrl+I 严格区分(零 abort / 零 `[User interrupt:]`)。
|
|
9
|
+
*/
|
|
10
|
+
import { pushReal } from "@thincoder/core/context.mjs"
|
|
11
|
+
import { ansi, C } from "./ansi.mjs"
|
|
12
|
+
import { planQueuedInput } from "./queued-merge.mjs"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 步边界取批(核循环头回调——空队列 no-op,headless / 直连面零开销)。
|
|
16
|
+
* 取数 = 计划首动作(`planQueuedInput`——2 条短消息 ⇒ 合并批一次消费);`/cmd` 首动作 =
|
|
17
|
+
* 入队门禁不可达的防御面(斜杠 busy 禁发——`TUI-INPUT-BOX.md` §4.1 条件 3)⇒ 不消费,
|
|
18
|
+
* 留给既有消费点(回合尾 queue / driver)。呈现 = 回执行 + `❯ You:` + 本批文本(与
|
|
19
|
+
* `agent-turn.mjs` 回合起点同形——下一步 LLM 请求即含该消息)。
|
|
20
|
+
* @param {object} ctx TUI 上下文(agent / state / pushLine / pushLabel / render)
|
|
21
|
+
*/
|
|
22
|
+
export function pickupQueuedAtStepBoundary(ctx) {
|
|
23
|
+
const { agent, state, pushLine, pushLabel, render } = ctx
|
|
24
|
+
const items = state.pendingInput
|
|
25
|
+
if (!items || items.length === 0) return
|
|
26
|
+
const action = planQueuedInput(items)[0]
|
|
27
|
+
if (action.kind !== "turn") return
|
|
28
|
+
items.splice(0, action.count)
|
|
29
|
+
pushReal(agent, { role: "user", content: action.text }) // 下一步生效(非中断通道)
|
|
30
|
+
pushLine("[sending queued message]", C.tool) // 消费回执(按批——TUI.md §7.5 消费时行)
|
|
31
|
+
pushLabel(`❯ You:`, ansi.bold + C.user)
|
|
32
|
+
pushLine(action.text, C.text)
|
|
33
|
+
render()
|
|
34
|
+
}
|
|
@@ -23,6 +23,10 @@ import { frozenSubSeg, toolSeg, frozenAdvSeg } from "./render-segments.mjs"
|
|
|
23
23
|
import { renderMarkdownPreservingWidth as _rmpw } from "./fold-block.mjs"
|
|
24
24
|
export { _rmpw as _renderMarkdownPreservingWidth }
|
|
25
25
|
|
|
26
|
+
/** 排队块形态上限(`docs/cli/design/TUI.md` §7.5——queue-visible 批 2026-09-24):每条原文行
|
|
27
|
+
* ≤ 该值,超限 ⇒ 该条尾标记行 `… [该条共 N 行——发送后完整显示]`(与 `MAX_INPUT_LINES` 同口径)。 */
|
|
28
|
+
const QUEUED_ITEM_MAX_LINES = 3
|
|
29
|
+
|
|
26
30
|
let _convCache = { key: "", cols: 0, lines: [] }
|
|
27
31
|
|
|
28
32
|
/** 2026-08-31 懒加载卡顿优化②:段级行体缓存(行对象→conv 行数组)。
|
|
@@ -164,7 +168,11 @@ export function convCacheKey(state, maxRows) {
|
|
|
164
168
|
// this the cache would serve the pre-search rows and highlight would never
|
|
165
169
|
// appear (P0-1, 2026-08-30 consult). query+index covers match navigation.
|
|
166
170
|
const searchPart = state.search?.query ? `${state.search.query}:${state.search.index ?? 0}` : ""
|
|
167
|
-
|
|
171
|
+
// queue-visible 批(TUI.md §7.5):排队签名(条数 + 各条长度 + 首 8 字——同上方 blocksSig 口径)
|
|
172
|
+
// ——待发送块是**派生插槽**(零 state.lines 写入)⇒ 不入签即缓存命中出陈旧帧(块不现 / 不消)。
|
|
173
|
+
const queuedItems = state.pendingInput ?? []
|
|
174
|
+
const queuedPart = `${queuedItems.length}|${queuedItems.map((m) => `${String(m).length}:${String(m).slice(0, 8)}`).join(",")}`
|
|
175
|
+
return `${state.lines.length}|${lastLine?.text.length ?? 0}|${state.streaming.length}|${state.reasoning.length}|${blocksSig}|${frozenSig}|${toolSig}|${colorSig}|${state.foldEnabled !== false ? "f" : "u"}|${exp}|${capPart}|${searchPart}|${foldScrollSig}|${queuedPart}`
|
|
168
176
|
}
|
|
169
177
|
|
|
170
178
|
|
|
@@ -348,6 +356,34 @@ function buildConvLines(state, cols, maxRows) {
|
|
|
348
356
|
}
|
|
349
357
|
}
|
|
350
358
|
}
|
|
359
|
+
// ── 排队期「待发送」块(queue-visible 批 2026-09-24 · TUI.md §7.5)——会话流尾派生插槽:
|
|
360
|
+
// 判据 = `state.pendingInput` 非空(渲染期现算——零 state.lines 写入 / 零生命周期簿记,
|
|
361
|
+
// 消费 / 中止随判据消失;不入搜索面、无 `_lineId` / `_foldToggle`)。渲染序在 state.streaming
|
|
362
|
+
// **之后** ⇒ 流在块上方增长、块恒居会话区底(F1 跟随语义)。体行 `_skipDimFold` = 连 dim
|
|
363
|
+
// 折叠豁免(§7.5 形态锚「永不被折走」:条数 ≥3 时 dim 连跑必过 FOLD_LINES ⇒ 显式豁免)。
|
|
364
|
+
const queuedItems = state.pendingInput ?? []
|
|
365
|
+
if (queuedItems.length > 0) {
|
|
366
|
+
if (convLines.at(-1)?.text !== "") convLines.push({ text: "", color: C.text }) // 块前空行(主输出呼吸行同约)
|
|
367
|
+
const multi = queuedItems.length >= 2
|
|
368
|
+
convLines.push({
|
|
369
|
+
text: multi
|
|
370
|
+
? `⏳ 待发送 · ${queuedItems.length} 条消息(不打断当前执行,合并发送)`
|
|
371
|
+
: `⏳ 待发送 · 不打断当前执行,自动发送`,
|
|
372
|
+
color: C.warn,
|
|
373
|
+
})
|
|
374
|
+
const indent = multi ? " " : ""
|
|
375
|
+
const wrapCols = cols - 1 // 与正文同口径(`sanitizeDisplay` + `wrapText`——TUI.md §7.5 逐字)
|
|
376
|
+
for (let qi = 0; qi < queuedItems.length; qi++) {
|
|
377
|
+
const rows = wrapText(sanitizeDisplay(String(queuedItems[qi])), wrapCols)
|
|
378
|
+
const head = multi ? `${qi + 1}. ` : ""
|
|
379
|
+
for (let ri = 0; ri < Math.min(rows.length, QUEUED_ITEM_MAX_LINES); ri++) {
|
|
380
|
+
convLines.push({ text: `${ri === 0 ? head : indent}${rows[ri]}`, color: C.dim, _skipDimFold: true })
|
|
381
|
+
}
|
|
382
|
+
if (rows.length > QUEUED_ITEM_MAX_LINES) {
|
|
383
|
+
convLines.push({ text: `${indent}… [该条共 ${rows.length} 行——发送后完整显示]`, color: C.dim, _skipDimFold: true })
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
351
387
|
// Fold long blocks (> 8 consecutive dim lines)
|
|
352
388
|
const FOLD_LINES = 8
|
|
353
389
|
const folded = []
|
package/src/tui/render-frame.mjs
CHANGED
|
@@ -396,12 +396,18 @@ function buildStatusLine(state, agent, { cols, slashCommands }) {
|
|
|
396
396
|
const ctxHint = ctxPct > 0
|
|
397
397
|
? ctxPct >= 80 ? ` │ ${ansi.reset}${C.warn}context ${ctxPct}%${ctxTokensHint}${ansi.reset}${ansi.dim}` : ` │ context ${ctxPct}%${ctxTokensHint}` : ""
|
|
398
398
|
// INPUT-LOCK-ASYNC(C'——F-3):busy(processing 含 digest)状态栏提示——取代旧排队提示
|
|
399
|
-
// (F-7 已删);F16(TUI.md §7.5
|
|
400
|
-
//
|
|
399
|
+
// (F-7 已删);F16(TUI.md §7.5)**四态分流**(queue-visible 批 2026-09-24):队列非空 ⇒
|
|
400
|
+
// 条数段(dim 段样式 · 零注意力色对——F13 豁免 · 消费即消失);队空 ⇒ 按 busy 面分排队句
|
|
401
|
+
// (普通 / 挂起内用户回合 = 步边界「当前步骤结束后自动发送」· 挂起内系统轮 = 轮末「本轮结束后
|
|
402
|
+
// 优先发送」)(判据同 TUI-INPUT-BOX.md §4.1;面感知 = `agent._inAutoTurn` 活读)。
|
|
403
|
+
const queuedN = state.pendingInput?.length ?? 0
|
|
401
404
|
const enterHint = state.processing
|
|
402
|
-
? (
|
|
403
|
-
: state.suspended || state._suspPending
|
|
404
|
-
|
|
405
|
+
? (queuedN > 0 ? `已排队 ${queuedN} 条消息`
|
|
406
|
+
: state.suspended || state._suspPending
|
|
407
|
+
? (agent._inAutoTurn === true
|
|
408
|
+
? `会话内回合处理中 — Enter 排队(本轮结束后优先发送)`
|
|
409
|
+
: `会话内回合处理中 — Enter 排队(当前步骤结束后自动发送)`)
|
|
410
|
+
: `主会话处理中 — Enter 排队(当前步骤结束后自动发送)`)
|
|
405
411
|
: "Enter: send"
|
|
406
412
|
// LEDGER-SURFACE(§2.30.3.3/§2.30.3.4):L1 常驻标记——状态段簇尾(scrollHint 后、键位组前);
|
|
407
413
|
// 空标记零注入(半态逐字节等价——同 :233-238 纪律);warn = 当前项目老化 > 0(警示色段包裹)
|
|
@@ -75,7 +75,7 @@ export function toolSeg(state, l, i, cols, maxRows) {
|
|
|
75
75
|
function frozenSubTaskLines(state, sub, cols, maxRows) {
|
|
76
76
|
const foldKey = `sub-${sub.key}`
|
|
77
77
|
const elapsed = Math.floor(((sub.doneAt ?? Date.now()) - sub.started) / 1000)
|
|
78
|
-
// §
|
|
78
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.2 D-M7b ②: 冻结头保留 sync/async 标识(done 头含历史语义——与 model 标识
|
|
79
79
|
// 同生命周期);仅真实 subagent 角色(compress 冻结等无语义)。整行 dim——
|
|
80
80
|
// 无需 ANSI 注入(running 面板头则套 dim + 恢复行色——subagent-panel.mjs)。
|
|
81
81
|
const isSubRole = SUBAGENT_ROLES.includes(sub.role)
|
|
@@ -89,7 +89,7 @@ function frozenSubTaskLines(state, sub, cols, maxRows) {
|
|
|
89
89
|
// 图标三态互斥(2026-09-20 端差·显示面消差批 M5):修前 stopped 冻结头 `✓` 与 verb 相抵
|
|
90
90
|
// ⇒ 补齐 VSC `webview/activity-view.js:46-49` 已成的 cancelled → `⏹` 形态(VSC 标尺,CLI 缺面)。
|
|
91
91
|
const icon = sub.approval ? "⏸" : sub.stopped ? "⏹" : "✓"
|
|
92
|
-
const verb = sub.stopped ? "stopped" : "done" // §
|
|
92
|
+
const verb = sub.stopped ? "stopped" : "done" // AGENT-LOOP-SUBAGENT.md §6.7.2: cancel 冻结标题 "stopped"
|
|
93
93
|
const header = `[${icon} ${sub.key}${modePart}${modelPart} · ${verb} ${elapsed}s${turnPart}${errPart}]`
|
|
94
94
|
const out = []
|
|
95
95
|
if (isExpanded(state, foldKey)) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* subagent-blocks.mjs — 子agent 活动区块缓冲(AGENT-LOOP.md §7.2 D4,消费端)。
|
|
3
|
-
* state.subTasks[key] = { key, role, model, async
|
|
3
|
+
* state.subTasks[key] = { key, role, model, async(AGENT-LOOP-SUBAGENT.md §6.7.2 D-M7b——⟦ev⟧async 标记——
|
|
4
4
|
* undefined = sync), started, done, doneAt, blocks:
|
|
5
5
|
* [{kind,text}], currentTool, toolArgs, turn, maxTurns, approval, lastError,
|
|
6
|
-
* dropped, blockEpoch, awaitingDigest
|
|
6
|
+
* dropped, blockEpoch, awaitingDigest(AGENT-LOOP-ASYNC-POOL.md §6.8), _freezeAt(冻结锚点), stopped(AGENT-LOOP-SUBAGENT.md §6.7.2),
|
|
7
7
|
* children: [](SUBAGENT-TAIL:嵌套子代理**守护载体**——内容并入本块 blocks——见
|
|
8
8
|
* subagent-children.mjs)}。
|
|
9
9
|
* 职责:前缀路由(parseRelayPath——**SUBAGENT-TAIL(D-ST1):嵌套段内容行并入外层
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
closeSubChild,
|
|
27
27
|
} from "./subagent-children.mjs"
|
|
28
28
|
export { SUB_BLOCK_LINE_LIMIT, appendSubBlock } from "./subagent-children.mjs"
|
|
29
|
-
// 2026-09-05 module-split:finish/freeze 族 + §
|
|
29
|
+
// 2026-09-05 module-split:finish/freeze 族 + AGENT-LOOP-SUBAGENT.md §6.7.2 面板现算迁 subagent-freeze.mjs——
|
|
30
30
|
// routeSub*/compression panel 内部用本地 import;re-export 保外部 import 面(index.mjs 等)
|
|
31
31
|
// CLI-ACTIVITY-DEBLOAT F-3(2026-09-10):面板手工镜像退役——re-export 面换现算导出
|
|
32
32
|
// computePanelBlocks(读时现算),本文件全部刷镜调用点删除(单账本——变更点不再手动同步)。
|
|
@@ -36,13 +36,13 @@ export { computePanelBlocks, finishSubTask, finishSubTaskKey, freezeSubTaskLines
|
|
|
36
36
|
// 前缀文法/解析已迁 src/agent/relay-prefix.mjs(第 27 批 §12.3②)——本文件不自持副本。
|
|
37
37
|
/** ⟦ev⟧ token parser:`⟦ev⟧<name>\x1e<n>\x1e<max>\x1e<phase>\x1e<detail>`。phase done
|
|
38
38
|
* = async 完成即冻结(settle 时发);settled = 挂起期完成——冻结延迟至 digest 消化
|
|
39
|
-
*
|
|
40
|
-
* stopped
|
|
39
|
+
* 完成(AGENT-LOOP-ASYNC-POOL.md §6.8 freezeReclaimDigestedBlocks 逐条回收——不等池空)或池空退出兜底补发;
|
|
40
|
+
* stopped(AGENT-LOOP-SUBAGENT.md §6.7.2 D-M6)= cancel 中止——interrupted 语义立即冻结(标题 "stopped");
|
|
41
41
|
* queued(§20 D-SD3b)= 排队 spawn 返回即建 waiting 块(round2 #2 事件通道——
|
|
42
42
|
* `⟦ev⟧queued\x1e<kind>\x1e<position>\x1equeued\x1e<detail>`——kind slot/wait/depc)——
|
|
43
43
|
* 启动后 ⟦ev⟧async 转 running(同 key 不重建);cancelled(§20)= 出队/取消——**零字段**
|
|
44
44
|
* 移除块(不冻结——无活动可冻结)——与 async 同型单独解析(不入本正则);
|
|
45
|
-
* async
|
|
45
|
+
* async(AGENT-LOOP-SUBAGENT.md §6.7.2 D-M7b + 处置 #4)= **零字段**标记(`⟦ev⟧async\x1e`——无 n/max/phase/detail 段)——
|
|
46
46
|
* routeSubToken 单独解析设 sub.async = true(不入本正则——本正则要求 4 字段);**缺失 key
|
|
47
47
|
* (块尚未创建)缓冲 `state._pendingAsyncKeys`——ensureSubTaskKey 块创建时应用(兜底)。 */
|
|
48
48
|
export const SUB_EVENT_RE = /^⟦ev⟧(turn|approval|done|settled|stopped|queued)\x1e([^\x1e]*)\x1e([^\x1e]*)\x1e([^\x1e]*)\x1e?([\s\S]*)$/
|
|
@@ -78,7 +78,7 @@ export function throttleSubRender(scheduleRender) {
|
|
|
78
78
|
_subRenderTimer.unref?.()
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/** §
|
|
81
|
+
/** AGENT-LOOP-SUBAGENT.md §6.7.2: key/role 分拆版核心(routeSub* 经 parseRelayPath;兼容 ensureSubTask(match))。 */
|
|
82
82
|
export function ensureSubTaskKey(state, key, role) {
|
|
83
83
|
// Tombstone guard:abort 子代理冻结后晚到 token 不得复活区块(2026-08-30 残项)
|
|
84
84
|
state._frozenSubKeys ??= new Set()
|
|
@@ -101,10 +101,10 @@ export function ensureSubTaskKey(state, key, role) {
|
|
|
101
101
|
blocks: [], currentTool: null, toolArgs: null, turn: 0, maxTurns: 0, approval: null,
|
|
102
102
|
lastError: null, dropped: 0,
|
|
103
103
|
_lineCount: 0, _charCount: 0, // 双维记账(TUI.md §15.3.4——行数维 + 字符维,subagent-children.mjs)
|
|
104
|
-
stopped: false, // §
|
|
104
|
+
stopped: false, // AGENT-LOOP-SUBAGENT.md §6.7.2: ⟦ev⟧stopped 冻结标记(标题 "stopped")
|
|
105
105
|
children: [], // SUBAGENT-TAIL: 嵌套子代理守护载体(内容并入本块——subagent-children.mjs)
|
|
106
106
|
}
|
|
107
|
-
// §
|
|
107
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.2 D-M7b ①(处置 #4 兜底——2026-09-03):缺失 key 的 ⟦ev⟧async 事件已缓冲
|
|
108
108
|
// pending 标志(routeSubToken——async 先于块创建到达:排队条目补位启动的时序窗口)
|
|
109
109
|
// ——块创建(此处)即应用——sub.async 区块创建即知(⏹ 门控/头标判定源保真)。
|
|
110
110
|
if (state._pendingAsyncKeys?.delete(key)) state.subTasks[key].async = true
|
|
@@ -150,7 +150,7 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
150
150
|
if (!path) return false
|
|
151
151
|
const payload = path.rest
|
|
152
152
|
const nested = path.inner.length > 0
|
|
153
|
-
// §
|
|
153
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.2 D-M7b ① + 处置 #4(2026-09-03 评审 #4——兜底):零字段 async 标记在
|
|
154
154
|
// ensureSubTaskKey **之前**单独消费(async spawn 实际启动即发——sync 不发——精确
|
|
155
155
|
// 匹配:async 后须 RS 或串尾——正文伪前缀形态不误吞;内容侧伪哨兵已由生成侧
|
|
156
156
|
// stripEventToken 先行剥除——本分支只见真事件)。live 区块直接置位 sub.async;
|
|
@@ -197,8 +197,8 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
197
197
|
const sub = ensureSubTaskKey(state, path.head, path.head.slice(0, path.head.lastIndexOf("#")))
|
|
198
198
|
if (!sub) return true // frozen tombstone — late token from an aborted child: drop
|
|
199
199
|
// ⟦ev⟧:turn/approval 进度 → 仅头部(D1——不进 blocks/主流);done(§15 D-A3)=
|
|
200
|
-
// 完成即冻结(settle 时发);settled
|
|
201
|
-
// "done · awaiting digestion",池空补发冻结;stopped
|
|
200
|
+
// 完成即冻结(settle 时发);settled(AGENT-LOOP-ASYNC-POOL.md §6.8 D-S8)= 挂起期完成——驻留面板中间态
|
|
201
|
+
// "done · awaiting digestion",池空补发冻结;stopped(AGENT-LOOP-SUBAGENT.md §6.7.2)= cancel——立即冻结;
|
|
202
202
|
// queued(§20 D-SD3b)= 排队 spawn 等待块(spawn 返回即建/状态变迁刷新——覆盖式
|
|
203
203
|
// 更新 sub.queued——块不可展开(无活动流);启动后 async 事件清标转 running)。
|
|
204
204
|
if (payload.startsWith("⟦ev⟧")) {
|
|
@@ -239,7 +239,7 @@ export function routeSubToken(state, t, scheduleRender) {
|
|
|
239
239
|
sub.done = true
|
|
240
240
|
sub.doneAt = Date.now()
|
|
241
241
|
sub.awaitingDigest = true
|
|
242
|
-
// 冻结锚点(2026-09-03 修复轮):settle
|
|
242
|
+
// 冻结锚点(2026-09-03 修复轮):settle 时刻流位置——AGENT-LOOP-ASYNC-POOL.md §6.8 digest 完成逐条回收
|
|
243
243
|
// (freezeReclaimDigestedBlocks)与未消化残项的池空退出兜底(freezeAllSubTasks)
|
|
244
244
|
// 都按它 splice 落位——digest 总览文本之前(round1 #1 裁定——T-S6/T-S14 同口径)。
|
|
245
245
|
sub._freezeAt = state.lines?.length ?? 0
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* subagent-freeze.mjs — 子agent 区块完成/冻结族(2026-09-05 module-split:
|
|
3
|
-
* subagent-blocks.mjs 625 > 500
|
|
3
|
+
* subagent-blocks.mjs 625 > 500 硬限——AGENT-LOOP-SUBAGENT.md §6.7.2 面板现算 computePanelBlocks + finish/
|
|
4
4
|
* freeze 家族迁入;subagent-blocks.mjs import 回(routeSub* 与 compression panel
|
|
5
5
|
* 调用点)+ re-export 保外部 import 面(index.mjs 等)。
|
|
6
|
-
* CLI-ACTIVITY-DEBLOAT F-3(2026-09-10
|
|
6
|
+
* CLI-ACTIVITY-DEBLOAT F-3(2026-09-10):AGENT-LOOP-SUBAGENT.md §6.7.2 面板**手工镜像退役**(镜像读写
|
|
7
7
|
* 全删)——改读时现算 computePanelBlocks(state):四字段 key/
|
|
8
8
|
* role/status/startedAt 均为 state.subTasks 活值纯推导(评审 #2 确证),读时现算
|
|
9
9
|
* 与 sync 时刻镜像等价——双账本结构性漂移根治(单账本)。完成/冻结族不再逐点刷镜
|
|
@@ -16,7 +16,7 @@ import { closeOpenSubChildren } from "./subagent-children.mjs"
|
|
|
16
16
|
// zero-block 批(§6.8.3.2):摘除路径负向出账——releaseLine(增删均须过账)。
|
|
17
17
|
import { accountLine, releaseLine } from "./display-budget.mjs"
|
|
18
18
|
|
|
19
|
-
// ─── §
|
|
19
|
+
// ─── AGENT-LOOP-SUBAGENT.md §6.7.2 D-P1 面板视图(subagent panel 检查工具)───
|
|
20
20
|
// CLI-ACTIVITY-DEBLOAT F-3:面板区块列表由消费面**读时现算**(action:"panel" 经
|
|
21
21
|
// ctx.state = agent._tuiState(index.mjs startTUI 装配)取 TUI state——subTasks
|
|
22
22
|
// 活值纯推导——视图与用户所见一致(同一数据源单账本))。未挂载(headless/VSC/
|
|
@@ -201,7 +201,7 @@ export function finishSubTasksByRole(state, roles, lastError = null) {
|
|
|
201
201
|
|
|
202
202
|
/** 回合尾/挂起退出清扫(runAgentTurn finally / suspensionSession finally):冻结全部
|
|
203
203
|
* 剩余块——中断(Ctrl+C/错误)不留下钉住输入框的 ghost;未 done 者
|
|
204
|
-
* lastError="interrupted"(Ready
|
|
204
|
+
* lastError="interrupted"(Ready 态跳过)。AGENT-LOOP-ASYNC-POOL.md §6.8:挂起自然退出时本函数只兜底
|
|
205
205
|
* **未消化残项**(已消化块由 freezeReclaimDigestedBlocks 逐条先行回收——块回收与
|
|
206
206
|
* 池空解耦)。锚点降序同 freezeDoneSubTasks(挂起期 settle 锚点交错批次各按其
|
|
207
207
|
* settle 位置落位)。
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* 运行中区块的渲染行数(F2,会话区被挤小);无运行中区块 → 返回 [](F6 空态,
|
|
7
7
|
* 无悬空分隔线)。子 agent 完成后立即冻结进会话流(subagent-blocks.mjs
|
|
8
8
|
* freezeSubTaskLines,✓ 头 + 可展开,§7.2 D4 现状不变),面板下一帧自然移除
|
|
9
|
-
* 该区块(F5)——本模块只渲染 `!done`
|
|
9
|
+
* 该区块(F5)——本模块只渲染 `!done` 条目。AGENT-LOOP-ASYNC-POOL.md §6.8 T-S14 中间态例外:挂起期已结算
|
|
10
10
|
* 区块(sub.done && sub.awaitingDigest)冻结被延迟,驻留面板显示
|
|
11
11
|
* "done · awaiting digestion",池空补发冻结后才移除。
|
|
12
12
|
*
|
|
13
|
-
* §
|
|
13
|
+
* AGENT-LOOP-SUBAGENT.md §6.7.2 D-M7 ⏹:运行中(非 done)折叠头右缘停止标记(dim,仅折叠头)——点击 =
|
|
14
14
|
* cancel(mouse.mjs 列级命中 _stopCol——不触发折叠翻转)。
|
|
15
15
|
* SUBAGENT-TAIL(supersede §27 R23 子块段方案;显示契约 docs/design/TUI.md §6):嵌套
|
|
16
16
|
* 子代理内容行并入本块 blocks(数据层——append 目标上移)——面板与冻结渲染只读
|
|
@@ -53,7 +53,7 @@ export function renderSubagentPanel(state, cols, maxRows) {
|
|
|
53
53
|
// 与冻结头 `[✓ …]` 格式统一(任务简报 UI 决策)。
|
|
54
54
|
const icon = sub.approval ? "⏸" : sub.done ? "✓" : "▶"
|
|
55
55
|
const elapsed = Math.floor(((sub.done ? (sub.doneAt ?? Date.now()) : Date.now()) - sub.started) / 1000)
|
|
56
|
-
// §
|
|
56
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.2 D-M7b ②: sync/async 显式头标(B 形态——不靠"没标推断")——async 由
|
|
57
57
|
// ⟦ev⟧async 标记置位;sync 区块(无标记)显式标 sync。真实 subagent 角色
|
|
58
58
|
// (escalate/consult/compress 等复用面板槽的条目无语义——非 spawn 角色豁免);
|
|
59
59
|
// §11.2 D-24b:role "advisor" 伪角色同面板(块/⏹/冻结复用——ruling ②-4 A)。
|
|
@@ -109,7 +109,7 @@ export function renderSubagentPanel(state, cols, maxRows) {
|
|
|
109
109
|
: ""
|
|
110
110
|
let headText = `${bracket} ${sliceByWidth(statePart + argSummary, Math.max(0, cols - 2 - bracketWidth))}`
|
|
111
111
|
const line = { color: C.tool, _foldToggle: foldKey }
|
|
112
|
-
// §
|
|
112
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.2 D-M7 ⏹ + D-M7b ③ 门控 + SYNC-CANCEL F3(2026-09-09):⏹ 只对 **live 可中止**
|
|
113
113
|
// 区块——async(running && sub.async——⟦ev⟧async 置位)∪ **sync registry live**
|
|
114
114
|
// (state._agent?._syncChildAborts?.has(sub.key)——阻塞 spawn 运行期注册——成功/折叠/
|
|
115
115
|
// 整回合停后 finally 注销——门控自动放下)——杜绝"可见但不可中止"误导。真实 subagent
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* suspension-drive.mjs — §
|
|
3
|
-
* 535 > 500 硬限——poolCounts + §
|
|
2
|
+
* suspension-drive.mjs — AGENT-LOOP-ASYNC-POOL.md §6.8 挂起会话驱动器(2026-09-05 module-split:agent-turn.mjs
|
|
3
|
+
* 535 > 500 硬限——poolCounts + AGENT-LOOP-ASYNC-POOL.md §6.8 挂起会话段 verbatim 迁入,语义零变;agent-turn.mjs
|
|
4
4
|
* re-import(runAgentTurn ↔ suspensionSession 函数级静态环——模块求值期无顶层调用,
|
|
5
5
|
* 环安全——session-slots ↔ session.mjs 同款先例)。
|
|
6
6
|
*
|
|
7
|
-
* §
|
|
8
|
-
* 进入挂起态——挂起空闲输入开放(Enter =
|
|
9
|
-
* digest
|
|
7
|
+
* AGENT-LOOP-ASYNC-POOL.md §6.8(2026-09-02,AGENT-LOOP.md §9 D-S1..S9):回合尾后台池非空 → 不阻塞等待,
|
|
8
|
+
* 进入挂起态——挂起空闲输入开放(Enter = 新回合入队列 + 唤醒)、busy(processing 含
|
|
9
|
+
* digest)提交亦入同队列(容量 8——busy-extend 批 2026-09-22 · queue-visible 批 2026-09-24;
|
|
10
|
+
* `TUI-INPUT-BOX.md` §4.1)、settle 事件驱动
|
|
10
11
|
* auto-turn 消化(手动档 organize-only / AUTO 档全语义)、池空 + 无待处理输入 → 补发
|
|
11
12
|
* done 冻结自然退出。状态机行表见 AGENT-LOOP.md §9.2。
|
|
12
13
|
* F-UC7(2026-09-19 批,AGENT-LOOP-UPSTREAM.md §6.27.12):第二开轮源 = 未 drain 的上行
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
import { runAgentTurn } from "./agent-turn.mjs"
|
|
22
23
|
import { freezeAllSubTasks, freezeReclaimDigestedBlocks } from "./subagent-blocks.mjs"
|
|
23
24
|
import { sweepToolBlocks } from "./tool-events.mjs"
|
|
25
|
+
import { planQueuedInput } from "./queued-merge.mjs"
|
|
24
26
|
import { logEvent } from "@thincoder/core/log.mjs"
|
|
25
27
|
import { C } from "./ansi.mjs"
|
|
26
28
|
// ASYNC-RESULT-CONTAINER.md D1/D2:池 accessor(双池 absorb)+ pending 单容器停靠
|
|
@@ -33,9 +35,9 @@ import { t } from "@thincoder/core/i18n.mjs"
|
|
|
33
35
|
// 批 4 CLI-ASYNC-DISCARD(AGENT-LOOP-ASYNC-POOL.md §6.20):中止分支「只清已死」收尾单点
|
|
34
36
|
import { discardAbortedPool, discardAbortedAdvisors } from "@thincoder/core/agent-tools/async-discard.mjs"
|
|
35
37
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
38
|
+
// queue-visible 批(2026-09-24):R15 排队用户指令合并**恢复**(攒批计划 / 合并文案 / 上限常量
|
|
39
|
+
// ——`queued-merge.mjs` 单源;待发送块 / 状态栏四态 = TUI.md §7.5);队列容量 8(满队 = 第 9 条
|
|
40
|
+
// 拒 + 提示 + 文本保留);busy 提交入队 = busy-extend 批 2026-09-22 扩面。
|
|
39
41
|
|
|
40
42
|
/** 后台池计数(LOGGING susp/digest 事件字段——pendingN/poolN)。
|
|
41
43
|
* poolN = _asyncSubagents + _asyncAdvisors(§11.2 D-24b advisor 池同面板计数——queued
|
|
@@ -142,7 +144,7 @@ function waitForSettleOrWake(agent, state) {
|
|
|
142
144
|
|
|
143
145
|
/** 后台模式状态行文本(D-S8;17.5.4 #6 顺手对齐):"后台 N 子代理运行中 · M 完成待消化"
|
|
144
146
|
* ——"运行中" = running + queued(含后台评审——§11.2 D-24b 同面板计数)+ running consult
|
|
145
|
-
* children(R17——会诊跨回合);"完成待消化" = pending 任一族移交项 + §
|
|
147
|
+
* children(R17——会诊跨回合);"完成待消化" = pending 任一族移交项 + AGENT-LOOP-ASYNC-POOL.md §6.8 回合尾留池
|
|
146
148
|
* 的 settled 未消费项(挂起会话 sweep 前的可见窗口)。 */
|
|
147
149
|
function backgroundStatusText(agent) {
|
|
148
150
|
const map = agent._asyncSubagents
|
|
@@ -153,7 +155,7 @@ function backgroundStatusText(agent) {
|
|
|
153
155
|
const queued = agent._asyncQueue?.length ?? 0
|
|
154
156
|
const pending = pendingFamilyCount(agent)
|
|
155
157
|
const doneInPool = (map ? [...map.values()].filter((e) => e.done).length : 0)
|
|
156
|
-
+ (adv ? [...adv.values()].filter((e) => e.done).length : 0) // §
|
|
158
|
+
+ (adv ? [...adv.values()].filter((e) => e.done).length : 0) // AGENT-LOOP-ASYNC-POOL.md §6.8 留池未消费
|
|
157
159
|
const awaiting = pending + doneInPool
|
|
158
160
|
const active = running + queued
|
|
159
161
|
return active > 0 || awaiting > 0
|
|
@@ -202,14 +204,15 @@ async function digestTurn(ctx, upstream = false) {
|
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
/**
|
|
205
|
-
* §
|
|
207
|
+
* AGENT-LOOP-ASYNC-POOL.md §6.8 挂起会话驱动(D-S9 行表;由 runAgentTurn 回合尾进入,池空自然退出):
|
|
206
208
|
* - suspension:池项 settle → 入 pending → 开 auto-turn(合并消化近邻 settle);
|
|
207
209
|
* 上行 ask 入队 → 唤醒 + 谓词 → 开唤醒轮(F-UC7——谓词先于池空退出判,见第 2 步);
|
|
208
|
-
* 挂起空闲用户 Enter、会话内 busy Enter → pendingInput
|
|
210
|
+
* 挂起空闲用户 Enter、会话内 busy Enter → pendingInput 队列(容量 8——busy-extend
|
|
211
|
+
* 2026-09-22 同判据;queue-visible 批 2026-09-24 按合并计划取批)
|
|
209
212
|
* ——用户输入优先于 digest;
|
|
210
213
|
* - auto-turn:消化中 settle 不并发开新轮(单 runAgent 循环),轮末按 pending/池态
|
|
211
|
-
* 续开合并消化轮或回挂起;pendingInput 非空 →
|
|
212
|
-
* - §
|
|
214
|
+
* 续开合并消化轮或回挂起;pendingInput 非空 → 以本批合并消息开新回合(不触发新 digest);
|
|
215
|
+
* - AGENT-LOOP-ASYNC-POOL.md §6.8:每次消化/会话内用户回合消费 pending 后 → freezeReclaimDigestedBlocks
|
|
213
216
|
* 逐条冻结回收(消化完成块不滞留面板——不等池空;settle 锚点 splice——digest 总览
|
|
214
217
|
* 文本之前——round1 #1 裁定);
|
|
215
218
|
* - 退出:池空 + pending 空 + 无待处理输入 → freezeAllSubTasks 补发冻结(仅兜底
|
|
@@ -239,22 +242,25 @@ export async function suspensionSession(ctx) {
|
|
|
239
242
|
try {
|
|
240
243
|
while (!state._suspAborted && !agent._sessionAbort.signal.aborted) {
|
|
241
244
|
sweepSettledToPending(agent)
|
|
242
|
-
// 1. 用户输入优先(D-S5):pendingInput
|
|
243
|
-
// 含 digest
|
|
244
|
-
//
|
|
245
|
-
// 单消息交接——无合并无 /cmd 分流——key-handler 只收非斜杠文本——单消息逐发)。
|
|
245
|
+
// 1. 用户输入优先(D-S5):pendingInput 队列(容量 8——INPUT-LOCK-ASYNC C';busy
|
|
246
|
+
// (processing 含 digest)提交亦入本队列——TUI-INPUT-BOX.md §4.1;queue-visible 批
|
|
247
|
+
// 2026-09-24:按合并计划取批(R15 恢复)——本批合并消息开新回合,先于 digest 合并)。
|
|
246
248
|
if ((state.pendingInput?.length ?? 0) > 0) {
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
249
|
+
const action = planQueuedInput(state.pendingInput)[0] // 按合并计划取批(首动作 = 本批)
|
|
250
|
+
// /cmd 首动作 = 入队门禁不可达的防御面(斜杠 busy 禁发——§4.1 条件 3)⇒ 不消费
|
|
251
|
+
if (action.kind === "turn") {
|
|
252
|
+
state.pendingInput.splice(0, action.count)
|
|
253
|
+
pushLine("[sending queued message]", C.tool) // F16 消费回执(TUI.md §7.5——driver 消费点)
|
|
254
|
+
agent._suspended = false // 用户回合 = 普通回合语义(① 直注入 + settle 即冻结)
|
|
255
|
+
await runAgentTurn(ctx, action.text, { skipSession: true })
|
|
256
|
+
agent._suspended = true
|
|
257
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.8:该回合消化完 pending(run 首行注入)→ 逐条冻结回收驻留块
|
|
258
|
+
// (不等池空——settle 锚点 splice——digest 总览文本之前;与 digest 回收同规则)
|
|
259
|
+
// R17:回收比对 = pending 单容器(四族统一——ASYNC-RESULT-CONTAINER.md D2)
|
|
260
|
+
freezeReclaimDigestedBlocks(state, allPendingEntries(agent))
|
|
261
|
+
state.status = backgroundStatusText(agent)
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
258
264
|
}
|
|
259
265
|
// 2. pending 任一族非空**或存在未 drain 的 ask** → 合并消化轮 / 唤醒轮(注入由 runAgent
|
|
260
266
|
// 首行 + 循环头统一完成——D-S3 单注入点;R17 判据推广——consult/escalate 族同样触发
|
|
@@ -263,7 +269,7 @@ export async function suspensionSession(ctx) {
|
|
|
263
269
|
const upstream = upstreamWaiting(agent)
|
|
264
270
|
if (pendingFamiliesNonEmpty(agent) || upstream) {
|
|
265
271
|
await digestTurn(ctx, upstream)
|
|
266
|
-
// §
|
|
272
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.8 实测修订(2026-09-03):digest 消化完成(pending 条目已注入)→ 逐条补发
|
|
267
273
|
// done 冻结回收——不等池空——块从面板移除进流(settle 锚点 splice 落位——digest
|
|
268
274
|
// 总览文本之前——round1 #1 裁定);池空 freeze-out 仅兜底未消化残项(挂起会话
|
|
269
275
|
// 结束统一清场)——块回收与池空解耦(T-H7/AC-H5)。
|
|
@@ -301,12 +307,13 @@ export async function suspensionSession(ctx) {
|
|
|
301
307
|
agent._pendingAsyncResults = []
|
|
302
308
|
const { cleanupConsultSessions } = await import("@thincoder/core/agent-tools/consult.mjs")
|
|
303
309
|
cleanupConsultSessions(agent)
|
|
304
|
-
// §
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
310
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.8 round2 偏差 #2-CLI(code review round2 #2-CLI)+ INPUT-LOCK + queue-visible 批
|
|
311
|
+
// (2026-09-24):中止时不静默丢弃队列内输入——Enter 已清空输入框并入 pendingInput
|
|
312
|
+
// (用户视为已发送)——按合并计划全量转回 state.queue({text} 逐动作条目——多批 = 多回合
|
|
313
|
+
// 续发,零丢失;不渲染待发送块——TUI.md §7.5 边界)+ 提示行明示去向(不静默丢)。
|
|
308
314
|
if ((state.pendingInput?.length ?? 0) > 0) {
|
|
309
|
-
state.queue.push({ text:
|
|
315
|
+
for (const a of planQueuedInput(state.pendingInput)) state.queue.push({ text: a.text })
|
|
316
|
+
state.pendingInput.length = 0
|
|
310
317
|
pushLine(`[background work stopped — the message you entered will run as a normal turn]`, C.warn)
|
|
311
318
|
}
|
|
312
319
|
} else {
|
package/src/tui/tool-display.mjs
CHANGED
|
@@ -26,7 +26,7 @@ export const REMINDER_CAP = 3 // max pending reminders shown on tu
|
|
|
26
26
|
export const REMINDER_PERSIST_TURNS = 5 // persist reminders every N turns
|
|
27
27
|
|
|
28
28
|
export const _toolTicks = new Map()
|
|
29
|
-
// §
|
|
29
|
+
// AGENT-LOOP-SUBAGENT.md §6.7 action registry: tool_call id → subagent action (non-spawn only — spawn is
|
|
30
30
|
// the default when no record exists). onToolCall sees the args, onToolResult only
|
|
31
31
|
// the name; without the record every subagent result would route as a spawn.
|
|
32
32
|
export const _subActions = new Map()
|
package/src/tui/tool-events.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 满足 500 行硬限)。只做「事件 → TUI 状态/对话行」的映射:
|
|
4
4
|
*
|
|
5
5
|
* - onToken/onReasoning : 子agent 前缀分流(routeSub*)→ 主流 streaming/reasoning
|
|
6
|
-
* - onToolCall : 状态栏 + `❯ name args` 标题行 + 计时 + §
|
|
6
|
+
* - onToolCall : 状态栏 + `❯ name args` 标题行 + 计时 + AGENT-LOOP-SUBAGENT.md §6.7 action 记录
|
|
7
7
|
* - onToolResult : 子agent 完成冻结(finishSubTaskKey 精确冻 + freezeDoneSubTasks——
|
|
8
8
|
* F-2 后 finishSubTask 为 no-op 兼容保留)、
|
|
9
9
|
* 工具块结果入块、advisor 评审冻结框
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* - 其余 : usage 累计、等待提示、task 面板、回合末增量落盘
|
|
12
12
|
*
|
|
13
13
|
* flushStream 同时返回给调用方(回合循环 / onTurnEnd 共用)。纯回调装配,无终端副作用
|
|
14
|
-
* (除经 deps 注入的 pushLine/render
|
|
15
|
-
*
|
|
14
|
+
* (除经 deps 注入的 pushLine/render)。AGENT-LOOP-SUBAGENT.md §6.7: subagent_check/escalate 工具退役后
|
|
15
|
+
* (AGENT-LOOP-SUBAGENT.md §6.7.5——check 动作已删),subagent 家族全部调用以工具名 "subagent" +
|
|
16
16
|
* action 到达——完成路由按 onToolCall 时记录的 action 分流(spawn 区块 / escalate
|
|
17
17
|
* 区块 / status/observe/send 普通工具块——observe/send 不建子代理区块)。
|
|
18
18
|
*/
|
|
@@ -115,7 +115,7 @@ export function buildToolCallbacks(deps) {
|
|
|
115
115
|
// Subagent tool call: prefix role#id/toolName → open a fresh tool block and
|
|
116
116
|
// set currentTool for the header summary line.
|
|
117
117
|
if (routeSubToolCall(state, name, args, scheduleRender)) return
|
|
118
|
-
// §
|
|
118
|
+
// AGENT-LOOP-SUBAGENT.md §6.7: record the action of a merged subagent-family call (spawn is the
|
|
119
119
|
// default — only non-spawn actions need a record for result-time routing).
|
|
120
120
|
if (name === "subagent" && args?.action && args.action !== "spawn") {
|
|
121
121
|
if (toolId !== undefined && toolId !== null) _subActions.set(toolId, args.action)
|
|
@@ -179,7 +179,7 @@ export function buildToolCallbacks(deps) {
|
|
|
179
179
|
// 签名(普通工具/老回调/错误路径不带 key)。
|
|
180
180
|
onToolResult: (name, result, toolId, subKey) => {
|
|
181
181
|
state.currentTool = null
|
|
182
|
-
// §
|
|
182
|
+
// AGENT-LOOP-SUBAGENT.md §6.7 merged family: route per the action recorded at onToolCall (no record = default spawn).
|
|
183
183
|
let subAction = null
|
|
184
184
|
if (name === "subagent") {
|
|
185
185
|
subAction = (toolId !== undefined && toolId !== null)
|
|
@@ -192,7 +192,7 @@ export function buildToolCallbacks(deps) {
|
|
|
192
192
|
// Subagent complete: mark the earliest running child as done — the block
|
|
193
193
|
// persists (✓ frozen elapsed header, expandable) as the ONLY carrier of the
|
|
194
194
|
// child's activity; memory bounded by the N2 line cap.
|
|
195
|
-
// §
|
|
195
|
+
// AGENT-LOOP-SUBAGENT.md §6.7.2: cancel 动作排除在 isSubagent 外——ack/错误 JSON 走普通工具块;区块冻结由
|
|
196
196
|
// ⟦ev⟧stopped settle 事件承担(此处 finishSubTask 会误冻最早 running 区块)。
|
|
197
197
|
if (isSubagent) {
|
|
198
198
|
// The dispatch-level tool-block carrier for this call would otherwise
|
|
@@ -362,7 +362,7 @@ export function buildToolCallbacks(deps) {
|
|
|
362
362
|
scheduleRender()
|
|
363
363
|
},
|
|
364
364
|
// Manual-tier auto-turn digests (agent-turn.mjs suspension driver) pass null
|
|
365
|
-
// handlers — permission requests then deny WITHOUT a panel (§
|
|
365
|
+
// handlers — permission requests then deny WITHOUT a panel (AGENT-LOOP-ASYNC-POOL.md §6.8 D-S7: no modal
|
|
366
366
|
// during unattended digestion) and question errors out instead of hanging.
|
|
367
367
|
...(askPermission ? { onPermissionRequest: (name, args) => askPermission(name, args) } : {}),
|
|
368
368
|
// Merged batch ask (§16 D-B1): one confirmation for N non-readonly tools in
|
package/src/tui/tui-state.mjs
CHANGED
|
@@ -37,7 +37,7 @@ export function createTuiState({ cols, rows, agent }) {
|
|
|
37
37
|
reasoning: "", // thinking stream buffer (dimmed display)
|
|
38
38
|
completion: null, // Tab completion state { candidates, index }
|
|
39
39
|
|
|
40
|
-
subTasks: {}, // sub-agent activity blocks (§7.2 D4): { "coder#1": { key, role, model, started, done, doneAt, blocks: [{kind,text}], currentTool, toolArgs, turn, maxTurns, approval, lastError, dropped, blockEpoch, awaitingDigest
|
|
40
|
+
subTasks: {}, // sub-agent activity blocks (§7.2 D4): { "coder#1": { key, role, model, started, done, doneAt, blocks: [{kind,text}], currentTool, toolArgs, turn, maxTurns, approval, lastError, dropped, blockEpoch, awaitingDigest(AGENT-LOOP-ASYNC-POOL.md §6.8 挂起中间态), _freezeAt(冻结锚点), stopped, children: [](SUBAGENT-TAIL:嵌套子代理**守护载体**——内容行并入本块 blocks——subagent-children.mjs) } } — rendered as collapsible in-conversation blocks; persists across turns (blocks are the child activity's ONLY carrier — child tool calls never enter the parent history); bounded by the N2 500-line per-child ring buffer(SUBAGENT-TAIL 单环:内层行同环计数、单载体最旧先行——TUI.md §6)
|
|
41
41
|
currentTool: null, // currently executing tool name (shown in status bar)
|
|
42
42
|
processingStarted: 0, // current turn start time (status bar timer)
|
|
43
43
|
status: "Ready",
|