thincoder 0.12.59 → 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 +38 -3
- package/README.md +2 -2
- package/bin/thincoder.mjs +80 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +7 -4
- package/src/advisor/messages.mjs +24 -4
- package/src/advisor/run.mjs +35 -33
- package/src/advisor.mjs +25 -6
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +102 -19
- package/src/agent/helpers.mjs +36 -0
- 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 +18 -2
- package/src/agent/spawn-child.mjs +29 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +110 -108
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +121 -102
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +24 -29
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/read-history.mjs +155 -31
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +7 -17
- package/src/agent-tools/subagent-actions.mjs +168 -130
- package/src/agent-tools/subagent-async.mjs +129 -174
- 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 +45 -21
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +107 -555
- package/src/agent-tools/verify.mjs +118 -270
- package/src/agent.mjs +57 -190
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +3 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +2 -2
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +56 -8
- package/src/context.mjs +5 -147
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +11 -11
- package/src/explore-distill.mjs +155 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +6 -193
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +58 -48
- package/src/memory.mjs +3 -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 +9 -76
- package/src/prompts/advisor-round1.md +9 -68
- package/src/prompts/advisor-round2.md +7 -54
- package/src/prompts/advisor-round3.md +7 -54
- package/src/prompts/coder.md +7 -50
- package/src/prompts/consult-base.md +4 -24
- package/src/prompts/discipline.md +26 -44
- package/src/prompts/eng-coder.md +7 -32
- package/src/prompts/engineering-sub.md +3 -23
- package/src/prompts/engineering.md +53 -306
- package/src/prompts/explore.md +3 -12
- package/src/prompts/main.md +10 -32
- package/src/prompts/methodology-template.md +28 -48
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +16 -35
- package/src/provider/core.mjs +6 -67
- 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/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/edit-batch.mjs +109 -10
- package/src/tools/edit-diff.mjs +110 -27
- package/src/tools/edit.md +17 -12
- package/src/tools/execute.mjs +31 -4
- package/src/tools/file.mjs +11 -6
- 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/index.mjs +5 -6
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +3 -3
- package/src/tools/question.md +3 -0
- package/src/tools/read.md +0 -1
- package/src/tools/shared.mjs +14 -13
- package/src/tools/system.mjs +44 -9
- package/src/tools/wait_for.md +22 -0
- package/src/tui/agent-turn.mjs +17 -228
- package/src/tui/cmd-config.mjs +48 -7
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-mcp.mjs +8 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +10 -10
- 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 +3 -2
- package/src/tui/interaction.mjs +3 -3
- package/src/tui/mouse.mjs +7 -1
- package/src/tui/pickers.mjs +40 -22
- package/src/tui/render-segments.mjs +27 -10
- package/src/tui/startup.mjs +4 -0
- package/src/tui/subagent-blocks.mjs +95 -263
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +61 -23
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +3 -3
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +37 -173
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +4 -0
- package/src/tui/wizard.mjs +12 -6
- package/src/tools/pdf-parse-text.mjs +0 -497
- package/src/tools/pdf-parse-xref.mjs +0 -499
- package/src/tools/pdf.mjs +0 -155
- package/src/tools/read_pdf.md +0 -21
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* subagent-async.mjs — async subagent 机械 + 共享 post-spawn 管线 +
|
|
3
|
-
* (AGENT-LOOP.md §19:subagent
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* subagent-async.mjs — async subagent 机械 + 共享 post-spawn 管线 + cancel 动作执行器
|
|
3
|
+
* (AGENT-LOOP.md §19:subagent 单工具多动作——spawn/status/escalate/cancel/panel/
|
|
4
|
+
* consume-design + observe/send(SUBAGENT-OBSERVE-SEND)——§19.8 check 已删;spawn 路径与
|
|
5
|
+
* 工具面在 subagent.mjs;cancel 动作执行器与机械、管线在本模块——check 执行器随 §19.8 删除)。
|
|
6
|
+
* 内容:resolveChildProvider / async 池常量与域助手(ASYNC_POOL_LIMITS/poolDomainOf/
|
|
7
|
+
* resolvePoolLimits/poolLimitsFor/runningPoolCount——§24 D-24a)/ executeCancelAction +
|
|
8
|
+
* cancelAsyncSubagent(§19.5 D-M6——工具与 TUI ⏹ 共用)/ runChildPipeline /
|
|
9
|
+
* injectAsyncResult / buildChildRunOpts / mergeChildMutations。
|
|
8
10
|
* 拆分(2026-09-05——Module Split Policy §20.9——纯迁移零行为变化):§20 调度器 + 文件域
|
|
9
11
|
* 组 → ./subagent-scheduler.mjs(尾部 re-export 保测试动态 import 面——queueRunnable/
|
|
10
12
|
* describeBlockers);status/panel/escalate 动作执行器 → ./subagent-actions.mjs。
|
|
@@ -17,13 +19,92 @@ import { runWithContinue, TURN_CAP_MARK } from "../agent/spawn-child.mjs"
|
|
|
17
19
|
import { pushReal } from "../context.mjs"
|
|
18
20
|
import { offloadToolResult } from "../agent/helpers.mjs"
|
|
19
21
|
import {
|
|
20
|
-
|
|
22
|
+
dependentLabels, maybeRefillAsync, refreshQueuedTokens,
|
|
21
23
|
} from "./subagent-scheduler.mjs"
|
|
24
|
+
// §24 D-24b (R13): advisor-pool cancel fallback + mutation logging for merged
|
|
25
|
+
// code(lazy function-level cycle——advisor-async → async-settle → scheduler →
|
|
26
|
+
// 本模块——全函数级绑定无求值期依赖,环安全)。
|
|
27
|
+
import { cancelAsyncAdvisor, noteMutations } from "./advisor-async.mjs"
|
|
22
28
|
|
|
23
|
-
// Async
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
// Async pool limits per role domain (AGENT-LOOP.md §24 D-24a — R14, 2026-09-06):
|
|
30
|
+
// the old single cap (ASYNC_SUBAGENT_LIMIT = 4, §15 D-A4) evolved into two
|
|
31
|
+
// independent pools — eng-coder 4 / other roles 4 (user ruling "eng-coder 四路,
|
|
32
|
+
// 其他 4 路") — a full engCoder pool never blocks an explore spawn and vice versa
|
|
33
|
+
// (same-domain cap still 4, cross-domain total up to 8). The per-turn check
|
|
34
|
+
// budget was deleted with the check action (§19.8 — results arrive only via the
|
|
35
|
+
// auto channel; no loop guard needed).
|
|
36
|
+
// ⚠ 与 config.mjs DEFAULTS.agent.poolLimits 逐键同值(loadConfig/settings 默认源)——
|
|
37
|
+
// 耦合锚 T-24a4 断言锁住——勿单侧改默认。
|
|
38
|
+
export const ASYNC_POOL_LIMITS = { engCoder: 4, other: 4 }
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Role → pool domain (single source of truth — §24 D-24a 修正 #8): the CLI role
|
|
42
|
+
* enum/assembly table is subagent.mjs's ROLES = { explore, plan, coder, eng-coder }
|
|
43
|
+
* (mode-filtered: normal → explore/plan/coder, engineering → explore/plan/eng-coder).
|
|
44
|
+
* eng-coder → engCoder pool; every other role (including unknown roles — fail-safe)
|
|
45
|
+
* → other pool. escalate spawns its expert internally (role "coder" — other pool).
|
|
46
|
+
*/
|
|
47
|
+
export function poolDomainOf(role) {
|
|
48
|
+
return role === "eng-coder" ? "engCoder" : "other"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Per-domain limit validation (T-24a4): each key must be a positive integer ≥1 —
|
|
53
|
+
* invalid (0 / -1 / "abc") or absent keys fall back to that domain's default (4),
|
|
54
|
+
* independent per key (a partial config — e.g. settings set
|
|
55
|
+
* agent.poolLimits.engCoder — takes effect with the untouched domain at default).
|
|
56
|
+
* Non-object raw (missing / string / number / array) → both domains at default.
|
|
57
|
+
*/
|
|
58
|
+
export function resolvePoolLimits(raw) {
|
|
59
|
+
const limits = { ...ASYNC_POOL_LIMITS }
|
|
60
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return limits
|
|
61
|
+
for (const key of Object.keys(ASYNC_POOL_LIMITS)) {
|
|
62
|
+
const v = raw[key]
|
|
63
|
+
if (Number.isInteger(v) && v >= 1) limits[key] = v
|
|
64
|
+
}
|
|
65
|
+
return limits
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** One-time fallback warn dedupe (warnedModels / warnedContextProviders precedent). */
|
|
69
|
+
const warnedPoolConfigs = new Set()
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Effective per-domain limits — read at EVERY pool admission (D-24a 生效语义:
|
|
73
|
+
* 不缓存常驻——配置变更即生效下个 spawn; /config 与 settings 保存经 reloadConfig /
|
|
74
|
+
* 热应用替换 agent.config——本函数每次读最新值). Invalid present values emit ONE
|
|
75
|
+
* warning per value shape, then fall back (timeoutMs precedent).
|
|
76
|
+
*/
|
|
77
|
+
export function poolLimitsFor(agent) {
|
|
78
|
+
const raw = agent?.config?.agent?.poolLimits
|
|
79
|
+
const limits = resolvePoolLimits(raw)
|
|
80
|
+
if (raw !== undefined && raw !== null && (typeof raw !== "object" || Array.isArray(raw))) {
|
|
81
|
+
warnPoolFallback(`poolLimits has invalid shape (${JSON.stringify(raw)})`, "shape:" + JSON.stringify(raw))
|
|
82
|
+
return limits
|
|
83
|
+
}
|
|
84
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
85
|
+
for (const key of Object.keys(ASYNC_POOL_LIMITS)) {
|
|
86
|
+
const v = raw[key]
|
|
87
|
+
if (v !== undefined && !(Number.isInteger(v) && v >= 1)) {
|
|
88
|
+
warnPoolFallback(`poolLimits.${key} = ${JSON.stringify(v)}`, `${key}:` + JSON.stringify(v))
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return limits
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function warnPoolFallback(what, sig) {
|
|
96
|
+
if (warnedPoolConfigs.has(sig)) return
|
|
97
|
+
warnedPoolConfigs.add(sig)
|
|
98
|
+
console.warn(`[config] agent.poolLimits: ${what} — must be a positive integer ≥1 — falling back to default ${JSON.stringify(ASYNC_POOL_LIMITS)} (AGENT-LOOP.md §24 D-24a)`)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Running count within ONE pool domain (口径同 §15 D-A1/T6: queued 与已完成不计入;
|
|
102
|
+
* 条目带 _pool 域字段——spawn 时 poolDomainOf(role) 落位;缺字段(手工/旧条目)按
|
|
103
|
+
* other——既有 coder 域测试语义不变)。 */
|
|
104
|
+
export function runningPoolCount(parent, pool) {
|
|
105
|
+
return [...(parent?._asyncSubagents?.values() ?? [])]
|
|
106
|
+
.filter((e) => e.status === "running" && (e._pool ?? "other") === pool).length
|
|
107
|
+
}
|
|
27
108
|
|
|
28
109
|
/**
|
|
29
110
|
* Resolve the sub-agent's provider from a model override string (shared with the
|
|
@@ -60,160 +141,6 @@ export function resolveChildProvider(parent, modelArg) {
|
|
|
60
141
|
return { ...parent.provider, model: modelArg }
|
|
61
142
|
}
|
|
62
143
|
|
|
63
|
-
/** Wait for an async entry to settle (or the parent signal to abort), parked on
|
|
64
|
-
* the agent's waiter list — the entry settle finally wakes every waiter (same
|
|
65
|
-
* pattern as consult_check's session waiters). Returns "aborted" on signal. */
|
|
66
|
-
function wakeOnAsyncSettle(agent, ctx) {
|
|
67
|
-
return new Promise((resolve) => {
|
|
68
|
-
const cleanup = () => {
|
|
69
|
-
const i = (agent._asyncWaiters ?? []).indexOf(w)
|
|
70
|
-
if (i >= 0) agent._asyncWaiters.splice(i, 1)
|
|
71
|
-
ctx.signal?.removeEventListener("abort", onAbort)
|
|
72
|
-
}
|
|
73
|
-
const w = () => { cleanup(); resolve("settled") }
|
|
74
|
-
const onAbort = () => { cleanup(); resolve("aborted") }
|
|
75
|
-
;(agent._asyncWaiters ??= []).push(w)
|
|
76
|
-
if (ctx.signal) {
|
|
77
|
-
if (ctx.signal.aborted) { onAbort(); return }
|
|
78
|
-
ctx.signal.addEventListener("abort", onAbort, { once: true })
|
|
79
|
-
}
|
|
80
|
-
})
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* subagent action:"check" (§19 — the retired subagent_check's semantics verbatim,
|
|
85
|
-
* AGENT-LOOP.md §15 D-A2): fetch async subagent results.
|
|
86
|
-
* - id omitted → the next completed child in ARRIVAL order (first finished first)
|
|
87
|
-
* - id given → block until THAT child finishes (queued items wait for their start)
|
|
88
|
-
* - n (required) → 1-based read counter, strictly incrementing per run (loop
|
|
89
|
-
* guard); capped at MAX_ASYNC_CHECKS per turn — beyond that, use the turn-end
|
|
90
|
-
* auto-wait. Errors never consume results.
|
|
91
|
-
* Consumed entries are deleted from the map — a re-check of the same id is the
|
|
92
|
-
* same "unknown async subagent id" error (T12).
|
|
93
|
-
*/
|
|
94
|
-
export async function executeCheckAction(args, ctx) {
|
|
95
|
-
const agent = ctx.agent
|
|
96
|
-
const map = agent._asyncSubagents ?? new Map()
|
|
97
|
-
const { id, n } = args ?? {}
|
|
98
|
-
// Strict 1-based incrementing read counter (D-A2, review #1): out-of-order /
|
|
99
|
-
// repeated n is rejected WITHOUT consuming a result (T14).
|
|
100
|
-
const lastN = agent._asyncCheckLastN ?? 0
|
|
101
|
-
if (!Number.isInteger(n) || n !== lastN + 1) {
|
|
102
|
-
return JSON.stringify({ status: "error", error: "invalid read counter — pass n = lastN+1" })
|
|
103
|
-
}
|
|
104
|
-
if (n > MAX_ASYNC_CHECKS) {
|
|
105
|
-
return JSON.stringify({ status: "error", error: "check limit exceeded — use turn-end auto-wait for the rest" })
|
|
106
|
-
}
|
|
107
|
-
agent._asyncCheckLastN = n
|
|
108
|
-
|
|
109
|
-
let target = null
|
|
110
|
-
if (id !== undefined && id !== null && String(id) !== "") {
|
|
111
|
-
target = map.get(String(id))
|
|
112
|
-
// Unknown OR already-consumed ids (consumed entries are deleted) — same error (T12).
|
|
113
|
-
if (!target) return JSON.stringify({ id: String(id), status: "error", error: `unknown async subagent id: ${id}` })
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Block until the target settles (specific id / next completed in arrival order).
|
|
117
|
-
// §20(advisor code review 🟡 处置):不可启动的 queued 条目会让阻塞等待永久悬挂
|
|
118
|
-
// (check 是同步工具调用——模型回合被钉死——仅 Ctrl+C 可解)。补位(refill)只由
|
|
119
|
-
// settle/cancel/spawn 事件驱动:**池内无 running 条目 = 无未来 settle 事件 = queued
|
|
120
|
-
// 条目永不启动**(槽满等位的 slot 条目在 running 归零时已被最后一次 settle 的 refill
|
|
121
|
-
// 启动——此时仍 queued 者必为不可启动:depc 锁定或阻塞源本身不可启动)。
|
|
122
|
-
for (;;) {
|
|
123
|
-
if (target) {
|
|
124
|
-
if (target.done) break
|
|
125
|
-
// 守卫 ①(target 级):queued 目标在无 running 池中不可启动 → 立即返回(无论
|
|
126
|
-
// wait/depc——阻塞源链底为 depc 的 wait 条目同样无 settle 可期)。
|
|
127
|
-
if (target.status === "queued" && !target.cancelled) {
|
|
128
|
-
const blk = describeBlockers(agent, target)
|
|
129
|
-
if (blk.kind === "depc") {
|
|
130
|
-
return JSON.stringify({
|
|
131
|
-
id: String(target.id), status: "queued", waiting: "dependency-cancelled",
|
|
132
|
-
reason: blk.detail,
|
|
133
|
-
note: "check would block forever — this task is locked by a cancelled/failed dependency and will not start on its own; cancel it (action:'cancel') or run an AUTO session to release it (AGENT-LOOP.md §20 round2 #3)",
|
|
134
|
-
})
|
|
135
|
-
}
|
|
136
|
-
if (![...map.values()].some((e) => e.status === "running")) {
|
|
137
|
-
const qi = (agent._asyncQueue ?? []).indexOf(target)
|
|
138
|
-
const out = { id: String(target.id), status: "queued", position: qi >= 0 ? qi + 1 : undefined }
|
|
139
|
-
if (blk.detail) out.reason = blk.detail
|
|
140
|
-
// §21.1 P-SL2(D-SL2):停滞机械检测——目标处于不可自行解除的等待闭包(无
|
|
141
|
-
// running + 每 queued 的 blocker 均闭包内 + 无 depc)→ 明确报错列本任务
|
|
142
|
-
// 阻塞链 + 引导 cancel 破环重派(F-SL2——非静默);不满足停滞判据的形态
|
|
143
|
-
// (单 queued/depc 滞留/外逃 blocker)维持下方原守卫文本——零行为变化。
|
|
144
|
-
const stall = detectStall(agent)
|
|
145
|
-
if (stall) {
|
|
146
|
-
const chain = stall.chains.find((c) => c.task === target)?.text ?? stall.chains[0]?.text
|
|
147
|
-
out.stall = true
|
|
148
|
-
out.note = `scheduler stall — the queued tasks block each other in a closed wait loop that can never settle on its own:\n${chain}\n${STALL_NOTE}`
|
|
149
|
-
} else {
|
|
150
|
-
out.note = "check would block indefinitely — this queued task cannot start while the pool has no running task (starts are settle-driven); cancel it (action:'cancel') or make pool progress (AUTO session starts it on the next settle/refill)"
|
|
151
|
-
}
|
|
152
|
-
return JSON.stringify(out)
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
const woke = await wakeOnAsyncSettle(agent, ctx)
|
|
156
|
-
if (woke === "aborted") return JSON.stringify({ done: true, stopped: true })
|
|
157
|
-
continue
|
|
158
|
-
}
|
|
159
|
-
const completed = [...map.values()].filter((e) => e.done)
|
|
160
|
-
if (completed.length > 0) {
|
|
161
|
-
target = completed.sort((a, b) => (a._settleSeq ?? 0) - (b._settleSeq ?? 0))[0]
|
|
162
|
-
break
|
|
163
|
-
}
|
|
164
|
-
if (map.size === 0) return JSON.stringify({ done: true })
|
|
165
|
-
// 守卫 ②(arrival-order):池内无 running(completed 已空 → 无 done)且仍有条目 →
|
|
166
|
-
// 全为 queued 且永不启动 → 立即返回明确错误(防无界悬挂——补 cancel 引导)。
|
|
167
|
-
if (![...map.values()].some((e) => e.status === "running")) {
|
|
168
|
-
// §21.1 P-SL2(D-SL2):停滞机械检测——全 queued 且每 blocker 闭包内(无 depc 等
|
|
169
|
-
// 外部可解形态)→ 明确错误逐条列阻塞链(F-SL2——列链 + 引导 cancel 破环重派);
|
|
170
|
-
// 不满足停滞判据的形态(单 queued/depc 滞留/外逃 blocker)维持原守卫文本——零变化。
|
|
171
|
-
const stall = detectStall(agent)
|
|
172
|
-
if (stall) {
|
|
173
|
-
return JSON.stringify({
|
|
174
|
-
status: "error",
|
|
175
|
-
error: `scheduler stall — the pool holds only queued tasks that block each other in closed wait loops; nothing can ever settle on its own:\n${stall.chains.map((c) => c.text).join("\n")}\n${STALL_NOTE}`,
|
|
176
|
-
})
|
|
177
|
-
}
|
|
178
|
-
const stuck = [...map.values()]
|
|
179
|
-
.map((e) => `${e.role}#${e.id}(${describeBlockers(agent, e).kind === "depc" ? "dependency-cancelled" : "blocked"})`)
|
|
180
|
-
.join(", ")
|
|
181
|
-
return JSON.stringify({
|
|
182
|
-
status: "error",
|
|
183
|
-
error: `nothing will settle — the pool holds only queued task(s) that cannot start without a running task: ${stuck}; cancel them (action:'cancel') or make pool progress (AUTO session starts them on the next settle/refill)`,
|
|
184
|
-
})
|
|
185
|
-
}
|
|
186
|
-
const woke = await wakeOnAsyncSettle(agent, ctx)
|
|
187
|
-
if (woke === "aborted") return JSON.stringify({ done: true, stopped: true })
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
map.delete(String(target.id))
|
|
191
|
-
// §20(advisor code review 🟡 处置):check 消费与挂起期 settle 的竞态——settle 在挂起
|
|
192
|
-
// 分支先把条目移交 _pendingAsyncResults(本 check 在途等待期间发生——digest 回合)——
|
|
193
|
-
// 消费时若条目已被移入 pending,反向清除(两消费点互斥——防下轮 prepareRun 重复注入
|
|
194
|
-
// ——D-S3"只注入一次"不变式——同一报告双送达违例)。
|
|
195
|
-
const pend = agent._pendingAsyncResults
|
|
196
|
-
if (Array.isArray(pend)) {
|
|
197
|
-
const pendIdx = pend.findIndex((x) => String(x.id) === String(target.id))
|
|
198
|
-
if (pendIdx >= 0) pend.splice(pendIdx, 1)
|
|
199
|
-
}
|
|
200
|
-
// §20 D-SD5 终态墓碑(T-SD14):消费即终态——dependsOn 引用该 id 的条目视其终态
|
|
201
|
-
// 满足/标注(consumed-ok = 已满足;failed/cancelled = 依赖取消/失败分支)。写于
|
|
202
|
-
// 观察 cancelled/error 之后——取消/失败条目不被误记 consumed。
|
|
203
|
-
if (target.cancelled) {
|
|
204
|
-
const tombstones = (agent._asyncTombstones ??= new Map())
|
|
205
|
-
tombstones.set(String(target.id), { status: "cancelled", role: target.role })
|
|
206
|
-
// Cancelled entries are removed from the pool at their cancel — an in-flight
|
|
207
|
-
// check that held the entry object observes `cancelled` and reports the same
|
|
208
|
-
// unknown-id error a fresh check gets (nothing to consume; §19.5 T-M27).
|
|
209
|
-
return JSON.stringify({ id: String(target.id), status: "error", error: `unknown async subagent id: ${target.id}` })
|
|
210
|
-
}
|
|
211
|
-
const tombstones = (agent._asyncTombstones ??= new Map())
|
|
212
|
-
tombstones.set(String(target.id), { status: target.error ? "failed" : "consumed", role: target.role })
|
|
213
|
-
if (target.error) return JSON.stringify({ id: String(target.id), status: "error", error: target.error })
|
|
214
|
-
return JSON.stringify({ id: String(target.id), role: target.role, status: "done", report: target.report ?? "" })
|
|
215
|
-
}
|
|
216
|
-
|
|
217
144
|
/**
|
|
218
145
|
* §19.5 D-M6 cancel 核心(工具路径与 TUI ⏹ 共用):定向中止单个后台子代理,id 必填。
|
|
219
146
|
* - queued 目标(未启动无 controller):出队 + 后续 position 前移 + settle waiter
|
|
@@ -235,7 +162,7 @@ export function cancelAsyncSubagent(agent, id) {
|
|
|
235
162
|
}
|
|
236
163
|
if (entry.cancelled) return { id: key, status: "cancelled" } // abort already in flight — idempotent
|
|
237
164
|
if (entry.status === "queued") {
|
|
238
|
-
// 出队 + position
|
|
165
|
+
// 出队 + position 释放/前移(§19.5 D-M6——无 abort)
|
|
239
166
|
const queue = agent._asyncQueue ?? []
|
|
240
167
|
const qi = queue.indexOf(entry)
|
|
241
168
|
if (qi >= 0) {
|
|
@@ -251,7 +178,6 @@ export function cancelAsyncSubagent(agent, id) {
|
|
|
251
178
|
const tombstones = (agent._asyncTombstones ??= new Map())
|
|
252
179
|
tombstones.set(key, { status: "cancelled", role: entry.role })
|
|
253
180
|
entry._settle?.()
|
|
254
|
-
for (const w of agent._asyncWaiters?.splice(0) ?? []) { try { w() } catch { /* noop */ } }
|
|
255
181
|
return { id: key, status: "cancelled", was: "queued" }
|
|
256
182
|
}
|
|
257
183
|
// running:标记 + 条目 abort——settle finally 跑 cancelled 分支(移除 + stopped + 提醒)
|
|
@@ -276,6 +202,12 @@ export function executeCancelAction(args, ctx) {
|
|
|
276
202
|
const key = String(id)
|
|
277
203
|
const agent = ctx.agent
|
|
278
204
|
const entry = agent._asyncSubagents?.get(key)
|
|
205
|
+
// §24 D-24b (②-6b): an id that names no async SUBAGENT falls through to the
|
|
206
|
+
// async ADVISOR pool (the background reviews share the cancel surface — ⏹ on
|
|
207
|
+
// an advisor block / action:'cancel' with an advisor id abort that review).
|
|
208
|
+
if (!entry && agent?._asyncAdvisors?.has(key)) {
|
|
209
|
+
return JSON.stringify(cancelAsyncAdvisor(agent, key))
|
|
210
|
+
}
|
|
279
211
|
const wasQueued = entry?.status === "queued"
|
|
280
212
|
// 依赖者快照(出队前——用于 AUTO 分支判定"是否有依赖者被本次取消波及";note 组装在
|
|
281
213
|
// refill 后按**仍 queued** 的实况重算——防 AUTO 已自动启动后文案称 "stay queued")
|
|
@@ -350,22 +282,40 @@ export async function runChildPipeline(child, input, childOpts, childRunOpts, {
|
|
|
350
282
|
|
|
351
283
|
/**
|
|
352
284
|
* Inject one settled async entry into the parent history as a user-role reminder
|
|
353
|
-
* (§17 D-S3 —
|
|
354
|
-
* (collectSettledAsync, agent.mjs) and the run-start _pendingAsyncResults
|
|
355
|
-
* the message shape is identical to the §15 collector's). Consumed =
|
|
356
|
-
* removes the entry from its container; no double-inject across the
|
|
285
|
+
* (§17 D-S3 — the auto channel, sole consumption path since §19.8: turn-end
|
|
286
|
+
* collection (collectSettledAsync, agent.mjs) and the run-start _pendingAsyncResults
|
|
287
|
+
* injection; the message shape is identical to the §15 collector's). Consumed =
|
|
288
|
+
* the caller removes the entry from its container; no double-inject across the
|
|
289
|
+
* two paths (D-S3 "inject once" invariant).
|
|
357
290
|
*/
|
|
358
291
|
export async function injectAsyncResult(agent, entry) {
|
|
359
292
|
const body = entry.error ?? entry.report ?? "(no report)"
|
|
360
293
|
const preview = await offloadToolResult(String(body), `async-subagent-${entry.id}`)
|
|
294
|
+
// §24 D-24b: advisor entries label themselves (role "advisor") — the digest
|
|
295
|
+
// reminder says "async advisor review #N finished" (T-24b2 shape); subagent
|
|
296
|
+
// entries keep the legacy wording verbatim.
|
|
297
|
+
// §25 D-R17b (R17): escalate entries (role "escalate" — async 飞刀) label
|
|
298
|
+
// themselves the same way — the entry report body carries the merge/overlap
|
|
299
|
+
// notes composed at settle (done/error classification — digest 指令语义 = 已
|
|
300
|
+
// merge 报告可继续处置——动作域仍按消费回合档位——无族例外).
|
|
301
|
+
let label
|
|
302
|
+
if (entry.role === "advisor") {
|
|
303
|
+
label = `[System reminder: async advisor review #${entry.id} finished]\n${escapeXml(preview)}`
|
|
304
|
+
} else if (entry.role === "escalate") {
|
|
305
|
+
label = entry.error != null
|
|
306
|
+
? `[System reminder: async escalate #${entry.id} ended with an error]\n${escapeXml(preview)}`
|
|
307
|
+
: `[System reminder: async escalate #${entry.id} finished — post-op report (mutations merged)]\n${escapeXml(preview)}`
|
|
308
|
+
} else {
|
|
309
|
+
label = `[System reminder: async subagent #${entry.id} (${entry.role}) finished]\n${escapeXml(preview)}`
|
|
310
|
+
}
|
|
361
311
|
pushReal(agent, {
|
|
362
312
|
role: "user",
|
|
363
|
-
content:
|
|
313
|
+
content: label,
|
|
364
314
|
})
|
|
365
315
|
// §20 D-SD5 终态墓碑:本函数是全部自动注入路径的共享形态(回合尾 collect + 挂起
|
|
366
316
|
// digest 首行注入)——注入即消费(调用方随即从容器移除)——dependsOn 引用该 id 的
|
|
367
|
-
// 后续 spawn 视为已满足(T-SD14
|
|
368
|
-
//
|
|
317
|
+
// 后续 spawn 视为已满足(T-SD14 消费终态语义——§19.8 后 check 消费路径删除,本自动
|
|
318
|
+
// 通道为唯一消费方;error 条目记 failed——依赖取消/失败分支照旧,不误标成功)。
|
|
369
319
|
const tombstones = (agent._asyncTombstones ??= new Map())
|
|
370
320
|
tombstones.set(String(entry.id), { status: entry.error != null ? "failed" : "consumed", role: entry.role })
|
|
371
321
|
}
|
|
@@ -409,9 +359,14 @@ export function mergeChildMutations(parent, child) {
|
|
|
409
359
|
// to the parent's guard state.
|
|
410
360
|
if (!child._mutatedThisRun || !(child._touchedFiles?.length)) return false
|
|
411
361
|
parent._mutatedThisRun = true
|
|
362
|
+
const merged = []
|
|
412
363
|
for (const abs of child._touchedFiles ?? []) {
|
|
413
364
|
if (!parent._touchedFiles.includes(abs)) parent._touchedFiles.push(abs)
|
|
365
|
+
merged.push(abs)
|
|
414
366
|
}
|
|
367
|
+
// §24 D-24b: merged code mutates the parent's state — log it for the stale
|
|
368
|
+
// scan of in-flight reviews (a review whose code changed under it is stale).
|
|
369
|
+
noteMutations(parent, merged)
|
|
415
370
|
if (parent._calledAdvisorThisRun) parent._calledAdvisorThisRun = false
|
|
416
371
|
if (parent._verifiedThisRun) {
|
|
417
372
|
parent._verifiedThisRun = false
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subagent-panel.mjs — subagent action:"panel" 执行器(§19.6——2026-09-08 自
|
|
3
|
+
* subagent-actions.mjs 拆分——Module Split Policy:601 > 500 硬限跨档,§19.6 面板段
|
|
4
|
+
* verbatim 迁入 + ASYNC-RESULT-CONTAINER.md D1/D2 落地(池 accessor getAsyncPool +
|
|
5
|
+
* pending 单容器 _pendingAsyncResults 四族统一——escalate/consult 独立族退役))。
|
|
6
|
+
* 内容:executePanelAction(D-P2——readonly 视图面 + 门控 freeze)+ panelFreezeGate
|
|
7
|
+
* (D-P3 冻结门控)+ blockKeyIn(块归属判定随行)。
|
|
8
|
+
* subagent-actions.mjs 尾部 re-export executePanelAction 保 subagent.mjs 既有 import 面。
|
|
9
|
+
*/
|
|
10
|
+
import { getAsyncPool } from "./async-settle.mjs"
|
|
11
|
+
|
|
12
|
+
/** 面板块 key(role#N)在池(Map——条目值)/pending 单容器(数组)中的归属判定。 */
|
|
13
|
+
function blockKeyIn(container, key) {
|
|
14
|
+
const entries = container instanceof Map ? [...container.values()] : (container ?? [])
|
|
15
|
+
return entries.some((e) => `${e.role}#${e.id}` === key)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* §19.6 D-P3 冻结门控(安全):仅允许冻结 awaitingDigest 且池(_asyncSubagents/
|
|
20
|
+
* _asyncAdvisors——经 getAsyncPool accessor——D1)无对应运行条目 + pending 单容器
|
|
21
|
+
* (_pendingAsyncResults——D2)无对应条目的块(= 已消化驻留块——报告已入模型上下文
|
|
22
|
+
* ——pending 已消费——状态滞后——补发冻结不破坏任何顺序)。
|
|
23
|
+
* - pending 仍有对应(报告未达模型)→ 拒绝(提前回收破坏消化顺序——T-P3)
|
|
24
|
+
* - 不存在的 key / 仍 running / done 的块 → 拒绝(T-P4——running 块 settle 时自冻)
|
|
25
|
+
* - 无镜像 → 拒绝(headless/VS Code——freeze 不可用——T-P5)
|
|
26
|
+
* 错误信息明确(模型可解释 + 自助修正)。返回 { ok:true } 或 { err }。
|
|
27
|
+
*/
|
|
28
|
+
function panelFreezeGate(agent, key) {
|
|
29
|
+
const snap = agent._panelSnapshot
|
|
30
|
+
if (!Array.isArray(snap)) {
|
|
31
|
+
return { err: "panel unavailable — no CLI TUI panel mirror in this session (headless / VS Code / subagent contexts — freeze unavailable; panel is CLI-TUI-only, AC-P4)" }
|
|
32
|
+
}
|
|
33
|
+
const block = snap.find((b) => b.key === key)
|
|
34
|
+
if (!block) {
|
|
35
|
+
const live = snap.map((b) => `${b.key}(${b.status})`).join(", ")
|
|
36
|
+
return { err: `unknown panel block key: ${key} — the live panel holds: ${live || "(no blocks)"}` }
|
|
37
|
+
}
|
|
38
|
+
if (block.status !== "awaitingDigest") {
|
|
39
|
+
if (block.status === "done") {
|
|
40
|
+
return { err: `block ${key} is already done — nothing to freeze; it was (or is about to be) reclaimed into the conversation by the freeze sweep at the turn end / settle (only digested-stuck awaitingDigest blocks need a manual freeze)` }
|
|
41
|
+
}
|
|
42
|
+
if (block.status === "running") {
|
|
43
|
+
return { err: `block ${key} is still running — freeze only reclaims awaitingDigest blocks whose report is already digested; a running block freezes on its own settle (or stop it with action:'cancel' if it is a background async child)` }
|
|
44
|
+
}
|
|
45
|
+
return { err: `block ${key} is in state ${block.status} — freeze only reclaims awaitingDigest blocks whose report is already digested` }
|
|
46
|
+
}
|
|
47
|
+
if (blockKeyIn(getAsyncPool(agent, "subagent"), key) || blockKeyIn(getAsyncPool(agent, "advisor"), key)) {
|
|
48
|
+
return { err: `block ${key} still has a live pool entry — it is NOT a digested-stuck block (freeze refused; status action shows the pool)` }
|
|
49
|
+
}
|
|
50
|
+
// ASYNC-RESULT-CONTAINER.md D2:pending 单容器(四族统一——原 escalate/consult
|
|
51
|
+
// 独立族退役)。
|
|
52
|
+
if (blockKeyIn(agent._pendingAsyncResults, key)) {
|
|
53
|
+
return { err: `block ${key} is still genuinely awaiting digestion — its report is still pending and has NOT reached the model yet; freezing now would break the digestion order (wait for the digest run, which reclaims it automatically — §17.5.5)` }
|
|
54
|
+
}
|
|
55
|
+
return { ok: true }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* §19.6 subagent action:"panel"(D-P2——readonly 视图面 + 门控干预面——单动作双参,
|
|
60
|
+
* freeze 优先):
|
|
61
|
+
* - view(缺省——返回镜像区块列表):agent._panelSnapshot = TUI 面板镜像(块级
|
|
62
|
+
* 状态变更点由 subagent-blocks syncPanelSnapshot 同步刷新——与用户所见一致——
|
|
63
|
+
* index.mjs 装配 state._agent)。awaitingDigest 条目**读时交叉**
|
|
64
|
+
* _pendingAsyncResults/池 标注 digested(round1 #3——digested:true
|
|
65
|
+
* = 报告已消化但块仍驻留——异常块——freeze 候选;模型可定位解释 UI 怪相)。
|
|
66
|
+
* - freeze:key(D-P3 门控通过 → 发 key + "/" + ⟦ev⟧done 哨兵字面 token——
|
|
67
|
+
* onToken——TUI routeSubToken 冻结回收——落位复用 sub._freezeAt settle 锚点
|
|
68
|
+
* splice,无锚点尾推兜底——§17.5.5 同口径——round1 #2)。
|
|
69
|
+
* 无镜像(headless/VS Code——D-P2 round1 #1:webview 无 state.subTasks 对应物——
|
|
70
|
+
* 7.2.3.2 #8 先例)→ view 恒降级池视图(双池经 getAsyncPool + pending 单容器
|
|
71
|
+
* 合成)+ no panel 注;freeze 报不可用。CLI-only 完整能力(AC-P4)。
|
|
72
|
+
*/
|
|
73
|
+
export function executePanelAction(args, ctx) {
|
|
74
|
+
const agent = ctx.agent
|
|
75
|
+
const freezeKey = (args?.freeze !== undefined && args?.freeze !== null && String(args.freeze) !== "")
|
|
76
|
+
? String(args.freeze)
|
|
77
|
+
: null
|
|
78
|
+
// ── freeze 面(优先——D-P2 单动作双参互斥)──
|
|
79
|
+
if (freezeKey) {
|
|
80
|
+
if ((ctx.depth ?? 0) > 0) {
|
|
81
|
+
return JSON.stringify({ status: "error", error: "panel freeze is only available at depth 0 — a child agent has no panel of its own (AGENT-LOOP.md §19.6 D-P2)" })
|
|
82
|
+
}
|
|
83
|
+
const gate = panelFreezeGate(agent, freezeKey)
|
|
84
|
+
if (gate.err) return JSON.stringify({ status: "error", error: gate.err })
|
|
85
|
+
if (!ctx.callbacks?.onToken) {
|
|
86
|
+
return JSON.stringify({ status: "error", error: `panel mirror present but no token relay in this context — the freeze of ${freezeKey} cannot reach the TUI` })
|
|
87
|
+
}
|
|
88
|
+
// 门控通过 → 发 done 冻结事件(settle 同机制字面格式——TUI routeSubToken done
|
|
89
|
+
// 分支冻结回收——落位 _freezeAt settle 锚点 splice;无锚点(旧会话残留)时
|
|
90
|
+
// freezeSubTaskLines 尾推兜底——注明两种落位,模型不被误导(advisor 🟡1)。
|
|
91
|
+
ctx.callbacks.onToken(`${freezeKey}/⟦ev⟧done\x1e0\x1e0\x1edone\x1e`)
|
|
92
|
+
return JSON.stringify({
|
|
93
|
+
key: freezeKey,
|
|
94
|
+
status: "frozen",
|
|
95
|
+
note: "done freeze event issued — the TUI reclaimed the block into the conversation (spliced at its settle anchor when one is recorded, else appended at the current stream end — §17.5.5 same-rule position)",
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
// ── view 面(缺省——readonly)──
|
|
99
|
+
// 双参互斥(D-P2):freeze 优先;显式 view:false 且无 freeze = 无请求可执行——报错。
|
|
100
|
+
if (args?.view === false) {
|
|
101
|
+
return JSON.stringify({ status: "error", error: "panel has nothing to do — view:false with no freeze key; pass freeze:'role#N' to reclaim a digested-stuck block, or omit view (defaults to true)" })
|
|
102
|
+
}
|
|
103
|
+
const snap = agent._panelSnapshot
|
|
104
|
+
if (!Array.isArray(snap)) {
|
|
105
|
+
// F-P3 降级:无镜像(headless/VS Code/子代理上下文——CLI TUI-only 完整能力)→
|
|
106
|
+
// 池视图(双池经 getAsyncPool——运行/排队条目 + pending 单容器待消化条目)
|
|
107
|
+
const blocks = []
|
|
108
|
+
const queue = agent._asyncQueue ?? []
|
|
109
|
+
for (const e of [...(getAsyncPool(agent, "subagent")?.values() ?? []), ...(getAsyncPool(agent, "advisor")?.values() ?? [])]) {
|
|
110
|
+
const b = { key: `${e.role}#${e.id}`, role: e.role }
|
|
111
|
+
if (e.status === "running") {
|
|
112
|
+
b.status = "running"
|
|
113
|
+
b.elapsedSec = e.startedAt ? Math.max(0, Math.floor((Date.now() - e.startedAt) / 1000)) : 0
|
|
114
|
+
} else if (e.status === "queued") {
|
|
115
|
+
b.status = "queued"
|
|
116
|
+
const qi = queue.indexOf(e)
|
|
117
|
+
b.position = qi >= 0 ? qi + 1 : (e.position ?? null)
|
|
118
|
+
} else {
|
|
119
|
+
b.status = "done" // 回合内 settle 未取——自动通道(回合尾 collect/挂起 digest)送达
|
|
120
|
+
}
|
|
121
|
+
blocks.push(b)
|
|
122
|
+
}
|
|
123
|
+
// ASYNC-RESULT-CONTAINER.md D2:pending 单容器(四族统一——原 escalate/consult
|
|
124
|
+
// 独立族退役)——族注记按 role 保留。
|
|
125
|
+
for (const e of agent._pendingAsyncResults ?? []) {
|
|
126
|
+
const note = e.role === "consult"
|
|
127
|
+
? "consultation digest pending — injected at the next run start (§25 D-R17a)"
|
|
128
|
+
: e.role === "escalate"
|
|
129
|
+
? "report pending — injected at the next run start (§25 D-R17b)"
|
|
130
|
+
: "report pending — injected at the next run start (§17)"
|
|
131
|
+
blocks.push({ key: `${e.role}#${e.id}`, role: e.role, status: "awaitingDigest", note })
|
|
132
|
+
}
|
|
133
|
+
return JSON.stringify({
|
|
134
|
+
degraded: true,
|
|
135
|
+
note: "no panel — this session has no CLI TUI panel mirror (headless / VS Code / subagent context — panel view is CLI-TUI-only, AC-P4); pool-derived view below; action:'status' shows the full pool",
|
|
136
|
+
panel: blocks,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
const panel = snap.map((b) => {
|
|
140
|
+
const out = { key: b.key, role: b.role, status: b.status }
|
|
141
|
+
if (b.status === "running") {
|
|
142
|
+
out.elapsedSec = b.startedAt ? Math.max(0, Math.floor((Date.now() - b.startedAt) / 1000)) : 0
|
|
143
|
+
} else if (b.status === "awaitingDigest") {
|
|
144
|
+
// 读时交叉(round1 #3):pending/池均无对应 = 报告已消化(注入即从两者移除)——
|
|
145
|
+
// 块驻留 = 状态滞后——digested:true(freeze 候选——模型可定位异常块)。
|
|
146
|
+
// ASYNC-RESULT-CONTAINER.md D2:pending 单容器参与比对(四族统一)。
|
|
147
|
+
out.digested = !blockKeyIn(agent._pendingAsyncResults, b.key)
|
|
148
|
+
&& !blockKeyIn(getAsyncPool(agent, "subagent"), b.key) && !blockKeyIn(getAsyncPool(agent, "advisor"), b.key)
|
|
149
|
+
}
|
|
150
|
+
return out
|
|
151
|
+
})
|
|
152
|
+
return JSON.stringify({ panel })
|
|
153
|
+
}
|