thincoder 0.12.58 → 0.12.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -1
- package/README.md +1 -1
- package/bin/thincoder.mjs +8 -0
- package/package.json +1 -1
- package/src/acp/bridge.mjs +132 -26
- package/src/advisor/messages.mjs +34 -1
- package/src/advisor/run.mjs +89 -51
- package/src/advisor.mjs +15 -7
- package/src/agent/dispatch.mjs +91 -14
- package/src/agent/helpers.mjs +35 -4
- package/src/agent/setup.mjs +90 -19
- package/src/agent/spawn-child.mjs +25 -0
- package/src/agent-tools/advisor.mjs +24 -2
- package/src/agent-tools/consult.mjs +37 -6
- package/src/agent-tools/eng.mjs +2 -1
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +160 -0
- package/src/agent-tools/settings.mjs +162 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +432 -0
- package/src/agent-tools/subagent-async.mjs +427 -0
- package/src/agent-tools/subagent-scheduler.mjs +319 -0
- package/src/agent-tools/subagent.mjs +467 -193
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +161 -49
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +161 -125
- package/src/auto-think.mjs +14 -0
- package/src/cli/make-agent.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/config.mjs +5 -0
- package/src/context.mjs +87 -27
- package/src/distill.mjs +19 -1
- package/src/escape.mjs +6 -4
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +1 -1
- package/src/memory/core.mjs +126 -0
- package/src/memory/docs.mjs +196 -87
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +15 -1
- package/src/prompts/advisor-design.md +46 -0
- package/src/prompts/advisor-round1.md +49 -2
- package/src/prompts/advisor-round2.md +47 -0
- package/src/prompts/advisor-round3.md +47 -0
- package/src/prompts/coder.md +22 -0
- package/src/prompts/consult-base.md +13 -0
- package/src/prompts/discipline.md +10 -5
- package/src/prompts/eng-coder.md +2 -2
- package/src/prompts/engineering-sub.md +23 -1
- package/src/prompts/engineering.md +106 -56
- package/src/prompts/explore.md +1 -2
- package/src/prompts/main.md +11 -6
- package/src/prompts/methodology-template.md +14 -0
- package/src/prompts/system.md +4 -2
- package/src/provider/core.mjs +56 -2
- package/src/tools/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +31 -43
- package/src/tools/edit-diff.mjs +265 -0
- package/src/tools/edit.md +10 -8
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +24 -20
- package/src/tools/file.mjs +18 -68
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -2
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +2 -0
- package/src/tools/lsp.md +4 -1
- package/src/tools/patch.mjs +84 -13
- package/src/tools/pdf-parse-text.mjs +497 -0
- package/src/tools/pdf-parse-xref.mjs +499 -0
- package/src/tools/pdf.mjs +155 -0
- package/src/tools/question.md +2 -1
- package/src/tools/read.md +1 -0
- package/src/tools/read_pdf.md +21 -0
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +4 -12
- package/src/tools/system.mjs +6 -21
- package/src/tools/tree.md +2 -1
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +385 -22
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +29 -9
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +9 -0
- package/src/tui/cmd-think.mjs +1 -1
- package/src/tui/index.mjs +29 -95
- 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 +40 -0
- package/src/tui/pickers.mjs +11 -3
- 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 +165 -0
- package/src/tui/startup.mjs +36 -0
- package/src/tui/subagent-blocks.mjs +322 -144
- package/src/tui/subagent-panel.mjs +88 -13
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-events.mjs +132 -100
- package/src/tui/update-notice.mjs +72 -0
- package/src/tui/wizard.mjs +36 -6
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subagent-async.mjs — async subagent 机械 + 共享 post-spawn 管线 + check/cancel 动作执行器
|
|
3
|
+
* (AGENT-LOOP.md §19:subagent 单工具六动作 spawn/check/status/escalate/cancel/panel——
|
|
4
|
+
* spawn 路径与工具面在 subagent.mjs;check/cancel 动作执行器与机械、管线在本模块)。
|
|
5
|
+
* 内容:resolveChildProvider / async 常量(ASYNC_SUBAGENT_LIMIT/MAX_ASYNC_CHECKS)/
|
|
6
|
+
* executeCheckAction / executeCancelAction + cancelAsyncSubagent(§19.5 D-M6——工具与
|
|
7
|
+
* TUI ⏹ 共用)/ runChildPipeline / injectAsyncResult / buildChildRunOpts / mergeChildMutations。
|
|
8
|
+
* 拆分(2026-09-05——Module Split Policy §20.9——纯迁移零行为变化):§20 调度器 + 文件域
|
|
9
|
+
* 组 → ./subagent-scheduler.mjs(尾部 re-export 保测试动态 import 面——queueRunnable/
|
|
10
|
+
* describeBlockers);status/panel/escalate 动作执行器 → ./subagent-actions.mjs。
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
runAgent, escapeXml,
|
|
14
|
+
MIN_REPORT_CHARS, REPORT_CONTINUATION, DEFAULT_SUBAGENT_TURNS,
|
|
15
|
+
} from "../agent.mjs"
|
|
16
|
+
import { runWithContinue, TURN_CAP_MARK } from "../agent/spawn-child.mjs"
|
|
17
|
+
import { pushReal } from "../context.mjs"
|
|
18
|
+
import { offloadToolResult } from "../agent/helpers.mjs"
|
|
19
|
+
import {
|
|
20
|
+
describeBlockers, dependentLabels, detectStall, maybeRefillAsync, refreshQueuedTokens, STALL_NOTE,
|
|
21
|
+
} from "./subagent-scheduler.mjs"
|
|
22
|
+
|
|
23
|
+
// Async subagent limits (AGENT-LOOP.md §15 D-A4): mechanical concurrency cap for
|
|
24
|
+
// background spawns + the per-turn check budget (consult-style loop guard).
|
|
25
|
+
export const ASYNC_SUBAGENT_LIMIT = 4
|
|
26
|
+
export const MAX_ASYNC_CHECKS = 3
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the sub-agent's provider from a model override string (shared with the
|
|
30
|
+
* VS Code port). Forms accepted:
|
|
31
|
+
* "provider:model" → the named provider with the named model
|
|
32
|
+
* "provider" → the named provider's configured model
|
|
33
|
+
* "model" → same provider as the parent, different model
|
|
34
|
+
* "default" → same as null: the parent's provider unchanged (explicit
|
|
35
|
+
* "use the default model" — 2026-09-05 user ruling; matched
|
|
36
|
+
* case-insensitively — parser-layer alias guard)
|
|
37
|
+
* null → parent's provider unchanged.
|
|
38
|
+
* API keys come from config.json only (env vars are not a key source).
|
|
39
|
+
*/
|
|
40
|
+
export function resolveChildProvider(parent, modelArg) {
|
|
41
|
+
if (!modelArg) return { ...parent.provider }
|
|
42
|
+
// "default" alias (2026-09-05 user ruling — ARCHITECTURE.md 子 agent 模型指定):
|
|
43
|
+
// the literal "default", matched case-insensitively, declares "no override →
|
|
44
|
+
// inherit the default model" — equivalent to null/omission (parent provider
|
|
45
|
+
// unchanged). Checked BEFORE the provider-name lookup so the alias is reserved
|
|
46
|
+
// (≡ omission — providers are not consulted) and never falls through to the
|
|
47
|
+
// model-name swap branch. Any other single-segment value keeps the legacy
|
|
48
|
+
// semantics below.
|
|
49
|
+
if (String(modelArg).toLowerCase() === "default") return { ...parent.provider }
|
|
50
|
+
const providers = parent.config?.providersList ?? []
|
|
51
|
+
const withKey = (p) => (p.apiKey?.trim() ? { ...p, apiKey: p.apiKey.trim() } : { ...p })
|
|
52
|
+
if (modelArg.includes(":")) {
|
|
53
|
+
const [pname, mname] = modelArg.split(":")
|
|
54
|
+
const p = providers.find((x) => x.name === pname)
|
|
55
|
+
if (!p) throw new Error(`subagent model: unknown provider "${pname}" (available: ${providers.map((x) => x.name).join(", ") || "none"})`)
|
|
56
|
+
return { ...withKey(p), model: mname || p.model }
|
|
57
|
+
}
|
|
58
|
+
const byName = providers.find((x) => x.name === modelArg)
|
|
59
|
+
if (byName) return withKey(byName)
|
|
60
|
+
return { ...parent.provider, model: modelArg }
|
|
61
|
+
}
|
|
62
|
+
|
|
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
|
+
/**
|
|
218
|
+
* §19.5 D-M6 cancel 核心(工具路径与 TUI ⏹ 共用):定向中止单个后台子代理,id 必填。
|
|
219
|
+
* - queued 目标(未启动无 controller):出队 + 后续 position 前移 + settle waiter
|
|
220
|
+
* → { id, status:"cancelled", was:"queued" }——无 abort(T-M27)
|
|
221
|
+
* - running 目标:置 entry.cancelled + 条目 controller abort(child runAgent signal)
|
|
222
|
+
* → settle finally 跑 cancelled 分支(⟦ev⟧stopped 冻结 + 模型提醒;不入 pending/
|
|
223
|
+
* 不直注入)→ { id, status:"cancelled" }(T-M19)
|
|
224
|
+
* - 未知/已完成 id → error(T-M20);重复 cancel 幂等(abort 已在途)
|
|
225
|
+
*/
|
|
226
|
+
export function cancelAsyncSubagent(agent, id) {
|
|
227
|
+
const key = String(id)
|
|
228
|
+
const map = agent._asyncSubagents ?? new Map()
|
|
229
|
+
const entry = map.get(key)
|
|
230
|
+
if (!entry) {
|
|
231
|
+
return { id: key, status: "error", error: `unknown async subagent id: ${key}` }
|
|
232
|
+
}
|
|
233
|
+
if (entry.done) {
|
|
234
|
+
return { id: key, status: "error", error: `async subagent #${key} has already finished — nothing to cancel` }
|
|
235
|
+
}
|
|
236
|
+
if (entry.cancelled) return { id: key, status: "cancelled" } // abort already in flight — idempotent
|
|
237
|
+
if (entry.status === "queued") {
|
|
238
|
+
// 出队 + position 释放/前移;settle 使在途 check waiter 终止(观察 cancelled)
|
|
239
|
+
const queue = agent._asyncQueue ?? []
|
|
240
|
+
const qi = queue.indexOf(entry)
|
|
241
|
+
if (qi >= 0) {
|
|
242
|
+
queue.splice(qi, 1)
|
|
243
|
+
for (let i = 0; i < queue.length; i++) queue[i].position = i + 1
|
|
244
|
+
}
|
|
245
|
+
entry.cancelled = true
|
|
246
|
+
entry.done = true
|
|
247
|
+
entry.status = "done"
|
|
248
|
+
map.delete(key)
|
|
249
|
+
// §20 D-SD5 终态墓碑:queued 取消(无 settle 事件——出队即终态)——依赖者经
|
|
250
|
+
// 墓碑查得 cancelled 分支(round1 #4——cancel 返回时即重估标注)。
|
|
251
|
+
const tombstones = (agent._asyncTombstones ??= new Map())
|
|
252
|
+
tombstones.set(key, { status: "cancelled", role: entry.role })
|
|
253
|
+
entry._settle?.()
|
|
254
|
+
for (const w of agent._asyncWaiters?.splice(0) ?? []) { try { w() } catch { /* noop */ } }
|
|
255
|
+
return { id: key, status: "cancelled", was: "queued" }
|
|
256
|
+
}
|
|
257
|
+
// running:标记 + 条目 abort——settle finally 跑 cancelled 分支(移除 + stopped + 提醒)
|
|
258
|
+
entry.cancelled = true
|
|
259
|
+
entry.controller?.abort?.()
|
|
260
|
+
return { id: key, status: "cancelled" }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** subagent action:"cancel" (§19.5 D-M6): depth-0 main-session control only.
|
|
264
|
+
* §20 D-SD5/round1 #4(queued 依赖取消——无 settle 事件):出队后**返回即**重估
|
|
265
|
+
* 依赖者——依赖者留 queued 标 dependency cancelled(refreshQueuedTokens 发更新块头
|
|
266
|
+
* token)+ 工具结果内注依赖者(模型可见——工具结果内——round1 #4 明示通道)+ 补位
|
|
267
|
+
* (AUTO 档依赖者自动启动/槽位竞态释放)。被取消条目自身发 ⟦ev⟧cancelled 移除等待块。 */
|
|
268
|
+
export function executeCancelAction(args, ctx) {
|
|
269
|
+
if ((ctx.depth ?? 0) > 0) {
|
|
270
|
+
return JSON.stringify({ status: "error", error: "cancel is only available at depth 0 — a child agent has no async pool of its own (AGENT-LOOP.md §19.5 D-M6)" })
|
|
271
|
+
}
|
|
272
|
+
const id = args?.id
|
|
273
|
+
if (id === undefined || id === null || String(id) === "") {
|
|
274
|
+
return JSON.stringify({ status: "error", error: "cancel requires the id of the async subagent to stop — omitting it would mean a blanket cancel (Ctrl+C stops everything; AGENT-LOOP.md §19.5 D-M6)" })
|
|
275
|
+
}
|
|
276
|
+
const key = String(id)
|
|
277
|
+
const agent = ctx.agent
|
|
278
|
+
const entry = agent._asyncSubagents?.get(key)
|
|
279
|
+
const wasQueued = entry?.status === "queued"
|
|
280
|
+
// 依赖者快照(出队前——用于 AUTO 分支判定"是否有依赖者被本次取消波及";note 组装在
|
|
281
|
+
// refill 后按**仍 queued** 的实况重算——防 AUTO 已自动启动后文案称 "stay queued")
|
|
282
|
+
const hadDependents = entry ? dependentLabels(agent, key).length > 0 : false
|
|
283
|
+
const result = cancelAsyncSubagent(agent, key)
|
|
284
|
+
if (wasQueued && result.status === "cancelled" && result.was === "queued" && entry) {
|
|
285
|
+
// §20 D-SD3b:取消/出队 → 移除等待块(不冻结——TUI routeSubToken cancelled 分支)
|
|
286
|
+
ctx.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧cancelled\x1e`)
|
|
287
|
+
// 依赖者标注 + 位置前移 + AUTO 自动启动(round2 #3:AUTO 档才启动——手动留 queued)
|
|
288
|
+
maybeRefillAsync(agent)
|
|
289
|
+
refreshQueuedTokens(agent, ctx.callbacks?.onToken)
|
|
290
|
+
if (hadDependents) {
|
|
291
|
+
// refill 后重算——AUTO 下依赖者已启动者不再列(文案与实际状态一致——code review 🔵)
|
|
292
|
+
const dependents = dependentLabels(agent, key)
|
|
293
|
+
if (dependents.length > 0) {
|
|
294
|
+
result.dependents = dependents
|
|
295
|
+
result.note = `queued dependents ${dependents.join(", ")} marked "dependency cancelled" — they stay queued until you cancel them (this action again with their id) or an AUTO session starts them (AGENT-LOOP.md §20 D-SD5)`
|
|
296
|
+
} else if (agent.autoApprove) {
|
|
297
|
+
result.note = `dependents of the cancelled task auto-started (AUTO session — round2 #3: an AUTO session starts dependency-cancelled dependents on slot availability)`
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return JSON.stringify(result)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* 共享 post-spawn 管线(阻塞与 async 同一条——§15 D-A1 "全不变"):turn-cap continue
|
|
306
|
+
* 循环 → 拒绝降级 partial 返回 → MIN_REPORT_CHARS 扩写 → eng-coder mutation merge →
|
|
307
|
+
* designId 后缀。onDeclined 在此(两路同一形态)——只有 askContinue 不同:阻塞经权限
|
|
308
|
+
* 面板询问;async 永不弹面板(自动拒绝;engineering && AUTO 自动 resume——§15 D-A3)。
|
|
309
|
+
*/
|
|
310
|
+
export async function runChildPipeline(child, input, childOpts, childRunOpts, { parent, role, args, askContinue }) {
|
|
311
|
+
const declined = { partial: null }
|
|
312
|
+
let report = await runWithContinue(
|
|
313
|
+
(child, input, cbs, opts) => runAgent(child, input, cbs, opts), // opts = childRunOpts + resume (由管线管理)
|
|
314
|
+
child, input, childOpts, childRunOpts,
|
|
315
|
+
{
|
|
316
|
+
askContinue,
|
|
317
|
+
onDeclined: (e, output) => {
|
|
318
|
+
if (role === "eng-coder" && child._mutatedThisRun) mergeChildMutations(parent, child)
|
|
319
|
+
// 拒绝 = 早退语义:不带 MIN_REPORT_CHARS 扩写(对已 cap 的 child 追问长报告是错的);
|
|
320
|
+
// review #2:用管线捕获的 output(此时 report 仍 ""——runWithContinue 未返回)
|
|
321
|
+
declined.partial = `Subagent (${role}) ${TURN_CAP_MARK} (${e.turn} turns) — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${output || ""}`
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
)
|
|
325
|
+
if (declined.partial !== null) {
|
|
326
|
+
// 被拒的 eng-coder 交付仍带 designId(fix round 以同 slot 重 spawn——2026-09-01)
|
|
327
|
+
if (role === "eng-coder") declined.partial += `\ndesignId: ${args.designId ?? "(single-design session — designId optional)"} — reuse it (with the same designToken) when re-spawning this eng-coder.`
|
|
328
|
+
return declined.partial
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 报告过短 = 交接不完整:送回扩写一次(kimi-code summaryPolicy: min 200 chars 同源)
|
|
332
|
+
if (report.length < MIN_REPORT_CHARS) {
|
|
333
|
+
report = await runAgent(child, REPORT_CONTINUATION, childOpts, childRunOpts)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// 工程模式机械码门:委托的文件改动不得绕过父侧 advisor/verify guard——eng-coder ONLY
|
|
337
|
+
// 有意为之(review #8):普通 coder 自带自评纪律;normal 模式无父侧 gate 可喂。
|
|
338
|
+
// 只在实际 mutated 时 merge(防 runAgent 写前抛错时传播空 mutation 声明——纵深防御)
|
|
339
|
+
if (role === "eng-coder" && child._mutatedThisRun) {
|
|
340
|
+
mergeChildMutations(parent, child)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// designId 随交付报告(2026-09-01):divergence audit fix round 用同一 designId+token 重 spawn
|
|
344
|
+
if (role === "eng-coder") {
|
|
345
|
+
report += `\ndesignId: ${args.designId ?? "(single-design session — designId optional)"} — reuse this designId with the same designToken (from the approved advisor type='design' review) when re-spawning this eng-coder for an audit fix round.`
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return report
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Inject one settled async entry into the parent history as a user-role reminder
|
|
353
|
+
* (§17 D-S3 — single shared form for BOTH consumption points: turn-end collection
|
|
354
|
+
* (collectSettledAsync, agent.mjs) and the run-start _pendingAsyncResults injection;
|
|
355
|
+
* the message shape is identical to the §15 collector's). Consumed = the caller
|
|
356
|
+
* removes the entry from its container; no double-inject across the two paths.
|
|
357
|
+
*/
|
|
358
|
+
export async function injectAsyncResult(agent, entry) {
|
|
359
|
+
const body = entry.error ?? entry.report ?? "(no report)"
|
|
360
|
+
const preview = await offloadToolResult(String(body), `async-subagent-${entry.id}`)
|
|
361
|
+
pushReal(agent, {
|
|
362
|
+
role: "user",
|
|
363
|
+
content: `[System reminder: async subagent #${entry.id} (${entry.role}) finished]\n${escapeXml(preview)}`,
|
|
364
|
+
})
|
|
365
|
+
// §20 D-SD5 终态墓碑:本函数是全部自动注入路径的共享形态(回合尾 collect + 挂起
|
|
366
|
+
// digest 首行注入)——注入即消费(调用方随即从容器移除)——dependsOn 引用该 id 的
|
|
367
|
+
// 后续 spawn 视为已满足(T-SD14 同 check 消费语义;error 条目记 failed——依赖取消/
|
|
368
|
+
// 失败分支照旧,不误标成功)。
|
|
369
|
+
const tombstones = (agent._asyncTombstones ??= new Map())
|
|
370
|
+
tombstones.set(String(entry.id), { status: entry.error != null ? "failed" : "consumed", role: entry.role })
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Child agent run options — the parent's abort signal MUST propagate to the
|
|
375
|
+
* child: without it, Ctrl+C aborts the parent's controller but the child keeps
|
|
376
|
+
* running its full turn budget (up to subagentTurns) while the parent awaits —
|
|
377
|
+
* the interrupt appears to do nothing.
|
|
378
|
+
* §17 D-S9: during a suspension session children share the SESSION signal instead
|
|
379
|
+
* (agent._sessionSignal) — a digest's own Ctrl+I/Ctrl+C must not abort the whole
|
|
380
|
+
* pool; the session driver aborts the session controller to stop everything.
|
|
381
|
+
*/
|
|
382
|
+
export function buildChildRunOpts(ctx) {
|
|
383
|
+
return {
|
|
384
|
+
depth: (ctx.depth ?? 0) + 1,
|
|
385
|
+
maxTurns: ctx.agent?.config?.agent?.subagentTurns ?? DEFAULT_SUBAGENT_TURNS,
|
|
386
|
+
signal: ctx.agent?._sessionSignal ?? ctx.signal ?? null,
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Merge an eng-coder child's mutations into the parent agent's bookkeeping.
|
|
392
|
+
* The parent must stay aware of delegated file changes: `_touchedFiles` enables
|
|
393
|
+
* the advisor guard (completion.mjs) to detect that code was modified and
|
|
394
|
+
* pushback for review. Prior verify/advisor state is invalidated because it
|
|
395
|
+
* judged an older state.
|
|
396
|
+
*
|
|
397
|
+
* `_advisorRound` is NOT reset: merged code enters the CURRENT convergence
|
|
398
|
+
* cycle. Resetting here would break the review→fix→re-review loop (the parent
|
|
399
|
+
* reviews, spawns an eng-coder to fix, merges, reviews again — every merge
|
|
400
|
+
* would restart at round 1 and the 5-round cap could never be reached).
|
|
401
|
+
* `_calledAdvisorThisRun` IS cleared so the merged code triggers a fresh
|
|
402
|
+
* advisor call (the guard demands review of new mutations).
|
|
403
|
+
*
|
|
404
|
+
* Returns true when mutations were merged (kept for future caller checks).
|
|
405
|
+
*/
|
|
406
|
+
export function mergeChildMutations(parent, child) {
|
|
407
|
+
// A child claiming mutations without any touched file is a misbehaving
|
|
408
|
+
// child (or a bookkeeping bug) — do not propagate an empty mutation claim
|
|
409
|
+
// to the parent's guard state.
|
|
410
|
+
if (!child._mutatedThisRun || !(child._touchedFiles?.length)) return false
|
|
411
|
+
parent._mutatedThisRun = true
|
|
412
|
+
for (const abs of child._touchedFiles ?? []) {
|
|
413
|
+
if (!parent._touchedFiles.includes(abs)) parent._touchedFiles.push(abs)
|
|
414
|
+
}
|
|
415
|
+
if (parent._calledAdvisorThisRun) parent._calledAdvisorThisRun = false
|
|
416
|
+
if (parent._verifiedThisRun) {
|
|
417
|
+
parent._verifiedThisRun = false
|
|
418
|
+
parent._verifyPassed = undefined
|
|
419
|
+
}
|
|
420
|
+
// Stale session cleanup only — the round counter survives (see above).
|
|
421
|
+
parent._advisorSession = null
|
|
422
|
+
return true
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Re-export shim (2026-09-05 拆分轮): §20 调度符号迁至 ./subagent-scheduler.mjs——本面
|
|
426
|
+
// 保留供 test/subagent-scheduler.test.mjs 动态 import(测试零改动——freeze.mjs 同款转发链)。
|
|
427
|
+
export { queueRunnable, describeBlockers } from "./subagent-scheduler.mjs"
|