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
|
@@ -1,62 +1,120 @@
|
|
|
1
1
|
import {
|
|
2
|
-
createAgent,
|
|
3
|
-
readonlyToolNames,
|
|
2
|
+
createAgent,
|
|
3
|
+
readonlyToolNames, escapeXml,
|
|
4
4
|
EXPLORE_OVERLAY, CODER_OVERLAY, PLAN_OVERLAY, ENG_CODER_OVERLAY,
|
|
5
|
-
MIN_REPORT_CHARS, REPORT_CONTINUATION, DEFAULT_SUBAGENT_TURNS,
|
|
6
5
|
} from "../agent.mjs"
|
|
7
|
-
import { makeRelay, wrapChildCallbacks,
|
|
6
|
+
import { makeRelay, wrapChildCallbacks, gateEngCoderSpawn, TURN_CAP_MARK } from "../agent/spawn-child.mjs"
|
|
8
7
|
import { validateDesignToken } from "./advisor.mjs"
|
|
8
|
+
import { pushReal } from "../context.mjs"
|
|
9
|
+
import { logEvent, errText } from "../log.mjs"
|
|
10
|
+
import {
|
|
11
|
+
runChildPipeline, resolveChildProvider, ASYNC_SUBAGENT_LIMIT,
|
|
12
|
+
buildChildRunOpts, executeCheckAction, executeCancelAction,
|
|
13
|
+
} from "./subagent-async.mjs"
|
|
14
|
+
import { executeStatusAction, executeEscalateAction, executePanelAction } from "./subagent-actions.mjs"
|
|
15
|
+
import {
|
|
16
|
+
// §20 调度器(AGENT-LOOP.md §20——D-SD1..SD5):文件域归一化/等待态派生/环防御/
|
|
17
|
+
// 排队态面板刷新/依赖者标注/补位——2026-09-05 拆分轮后独立模块。
|
|
18
|
+
normalizeFileList, describeBlockers, queueRunnable, refreshQueuedTokens,
|
|
19
|
+
assertNoDepCycle, depInfo, dependentLabels, maybeRefillAsync,
|
|
20
|
+
} from "./subagent-scheduler.mjs"
|
|
9
21
|
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
22
|
+
// 2026-09-03 拆分轮: subagent.mjs 超 500 硬顶——async 常量、共享 post-spawn 管线
|
|
23
|
+
//(runChildPipeline)与队列/注入/并账机械迁至 ./subagent-async.mjs。execute
|
|
24
|
+
//(async 分支 + 阻塞路径)原样保留于本文件;导出面由文末 re-export shim 兜住。
|
|
25
|
+
// 2026-09-03 §19 合体轮: subagent_check/escalate 工具退役——check/status/escalate
|
|
26
|
+
// 动作执行器并入 ./subagent-async.mjs,本文件只承载工具面(action schema)与
|
|
27
|
+
// spawn 路径 + 动作分流。
|
|
28
|
+
// 2026-09-05 拆分轮: status/escalate/panel 动作执行器 → ./subagent-actions.mjs;§20
|
|
29
|
+
// 调度器全套(normalizeFileList/describeBlockers/queueRunnable/refreshQueuedTokens/
|
|
30
|
+
// assertNoDepCycle/depInfo/dependentLabels/maybeRefillAsync)→ ./subagent-scheduler.mjs
|
|
31
|
+
// ——本文件 import 源随之改写;check/cancel 与机械/管线仍来自 ./subagent-async.mjs。
|
|
14
32
|
|
|
15
33
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* -
|
|
34
|
+
* §18.7 D-TS5 (A2): mechanically summarize the parent spawn task book for the
|
|
35
|
+
* audit spawn — the three audit-relevant elements VERBATIM (design doc paths /
|
|
36
|
+
* affected-file list / acceptance criteria); verbose context/background is
|
|
37
|
+
* dropped (the auditor can read the design docs themselves — they stay
|
|
38
|
+
* available outside this input). Independence preserved: the input is
|
|
39
|
+
* _engTaskInput (mechanically kept by the parent spawn) — never the
|
|
40
|
+
* eng-coder's self-report. Sections are located by header marker, prioritizing
|
|
41
|
+
* header lines (structured task books: "## 文件清单 …") and falling back to
|
|
42
|
+
* inline markers (flat one-line task books); a section runs to the next header
|
|
43
|
+
* of the SAME OR HIGHER level ("## 文件清单" survives a "### 修改" sub-header).
|
|
44
|
+
* Marker not found → the section is reported as missing (never fabricate).
|
|
45
|
+
*/
|
|
46
|
+
function summarizeEngTaskBook(taskInput) {
|
|
47
|
+
if (!taskInput) return "(unavailable)"
|
|
48
|
+
const SECTIONS = [
|
|
49
|
+
{ name: "Design docs involved", markers: [/Docs? involved/i, /涉及文档/] },
|
|
50
|
+
{ name: "Affected-file list", markers: [/Files? (?:list|to (?:modify|change)|modified)/i, /受影响文件/, /文件清单/, /涉及文件/] },
|
|
51
|
+
{ name: "Acceptance criteria", markers: [/Acceptance(?: criteria)?/i, /验收标准/] },
|
|
52
|
+
]
|
|
53
|
+
const lines = taskInput.split("\n")
|
|
54
|
+
const headerLevel = (l) => {
|
|
55
|
+
const m = l.match(/^\s*(#{1,6})\s/)
|
|
56
|
+
return m ? m[1].length : 0
|
|
57
|
+
}
|
|
58
|
+
const headerIdx = lines.map((l, i) => (headerLevel(l) > 0 ? i : -1)).filter((i) => i >= 0)
|
|
59
|
+
const boundsFor = (from, level) => {
|
|
60
|
+
for (const j of headerIdx) {
|
|
61
|
+
if (j > from && (level === 0 || headerLevel(lines[j]) <= level)) return j
|
|
62
|
+
}
|
|
63
|
+
return lines.length
|
|
64
|
+
}
|
|
65
|
+
const out = []
|
|
66
|
+
for (const { name, markers } of SECTIONS) {
|
|
67
|
+
let from = -1
|
|
68
|
+
let level = 0
|
|
69
|
+
for (const i of headerIdx) {
|
|
70
|
+
if (markers.some((m) => m.test(lines[i]))) { from = i; level = headerLevel(lines[i]); break }
|
|
71
|
+
}
|
|
72
|
+
if (from === -1) {
|
|
73
|
+
for (let i = 0; i < lines.length; i++) {
|
|
74
|
+
if (markers.some((m) => m.test(lines[i]))) { from = i; level = 0; break }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (from === -1) { out.push(`${name}: (not found in the parent task book)`); continue }
|
|
78
|
+
const body = lines.slice(from, boundsFor(from, level)).join("\n").trim()
|
|
79
|
+
out.push(body || `${name}: (empty section)`)
|
|
80
|
+
}
|
|
81
|
+
return out.join("\n\n")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* subagent tool — ONE tool, SIX actions (AGENT-LOOP.md §19/§19.5/§19.6): spawn
|
|
86
|
+
* (default) / check (fetch async results, blocking + consuming) / status
|
|
87
|
+
* (non-blocking pool query) / escalate (飞刀 — hand implementation to a
|
|
88
|
+
* stronger model) / cancel (stop ONE background subagent — §19.5) / panel
|
|
89
|
+
* (view + fix the live subagent panel — §19.6).
|
|
90
|
+
* - action:"spawn" roles: "explore" — read-only tools, search/read/analyze
|
|
91
|
+
* (suitable for codebase exploration); "coder" — full tool set, self-contained
|
|
92
|
+
* implementation tasks; "plan" — read-only planning; "eng-coder" —
|
|
93
|
+
* engineering-mode implementation (design-token gated).
|
|
19
94
|
* - no role specified — invalid by design since the 2026-08-25 fail-closed gate
|
|
20
95
|
* (role is mandatory; "no role → same tool set as parent" was removed with the
|
|
21
96
|
* coder-leak fix and the header text above predates it)
|
|
22
|
-
* - parallel subagent calls via the parallel channel (parallel: true)
|
|
23
97
|
* - non-recursive: child agents do not get the subagent tool (depth > 0 is not injected)
|
|
24
98
|
*/
|
|
25
99
|
|
|
26
100
|
/**
|
|
27
101
|
* Effective subagent model override for a role (CLI parity shared with VS Code):
|
|
28
102
|
* priority — subagent tool `model` arg > config.agent.subagentModels[role] > config.agent.subagentModel > null (inherit parent).
|
|
103
|
+
* The literal "default" (case-insensitive) means "no tool-arg override → run the
|
|
104
|
+
* chain" — same as omitting the parameter (2026-09-05 user ruling).
|
|
29
105
|
*/
|
|
30
106
|
export function effectiveSubagentModel(parent, role, modelArg) {
|
|
31
|
-
|
|
107
|
+
// "default" alias (2026-09-05 user ruling — ARCHITECTURE.md 子 agent 模型指定):
|
|
108
|
+
// the literal "default", matched case-insensitively, explicitly declares "no
|
|
109
|
+
// override at the tool-arg level → run the default priority chain" (type-level
|
|
110
|
+
// subagentModels[role] → global subagentModel → null = inherit parent). It is
|
|
111
|
+
// equivalent to omitting the parameter / passing ""/null/undefined (which fall
|
|
112
|
+
// through below). Any other value still overrides.
|
|
113
|
+
if (modelArg && String(modelArg).toLowerCase() !== "default") return modelArg
|
|
32
114
|
const cfg = parent.config?.agent ?? {}
|
|
33
115
|
return cfg.subagentModels?.[role] ?? cfg.subagentModel ?? null
|
|
34
116
|
}
|
|
35
117
|
|
|
36
|
-
/**
|
|
37
|
-
* Resolve the sub-agent's provider from a model override string (shared with the
|
|
38
|
-
* VS Code port). Forms accepted:
|
|
39
|
-
* "provider:model" → the named provider with the named model
|
|
40
|
-
* "provider" → the named provider's configured model
|
|
41
|
-
* "model" → same provider as the parent, different model
|
|
42
|
-
* null → parent's provider unchanged.
|
|
43
|
-
* API keys come from config.json only (env vars are not a key source).
|
|
44
|
-
*/
|
|
45
|
-
export function resolveChildProvider(parent, modelArg) {
|
|
46
|
-
if (!modelArg) return { ...parent.provider }
|
|
47
|
-
const providers = parent.config?.providersList ?? []
|
|
48
|
-
const withKey = (p) => (p.apiKey?.trim() ? { ...p, apiKey: p.apiKey.trim() } : { ...p })
|
|
49
|
-
if (modelArg.includes(":")) {
|
|
50
|
-
const [pname, mname] = modelArg.split(":")
|
|
51
|
-
const p = providers.find((x) => x.name === pname)
|
|
52
|
-
if (!p) throw new Error(`subagent model: unknown provider "${pname}" (available: ${providers.map((x) => x.name).join(", ") || "none"})`)
|
|
53
|
-
return { ...withKey(p), model: mname || p.model }
|
|
54
|
-
}
|
|
55
|
-
const byName = providers.find((x) => x.name === modelArg)
|
|
56
|
-
if (byName) return withKey(byName)
|
|
57
|
-
return { ...parent.provider, model: modelArg }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
118
|
/**
|
|
61
119
|
* Resolve the design-token slot for an eng-coder spawn (2026-09-01 multi-design, FR3):
|
|
62
120
|
* - designId given → exact slot lookup (no match = explicit error, never a fuzzy guess)
|
|
@@ -92,15 +150,22 @@ export function resolveDesignSlot(parent, designIdArg) {
|
|
|
92
150
|
export const subagentTool = {
|
|
93
151
|
name: "subagent",
|
|
94
152
|
description:
|
|
95
|
-
"
|
|
153
|
+
"ONE tool, SIX actions (AGENT-LOOP.md §19/§19.5/§19.6) — pick by what you need:\n" +
|
|
154
|
+
"- action:'spawn' (DEFAULT): spawn a sub-agent to handle an independent subtask in an isolated context; the sub-agent returns only its final report. Spawn MULTIPLE subagents in the SAME response for parallel work—they run concurrently.\n" +
|
|
155
|
+
"- action:'check': fetch the report of an async spawn (async:true). BLOCKS until the target finishes — this is the explicit consuming fetch, NOT a progress query. Multiple async children return in completion (arrival) order, first finished first. Pass n = 1 on the first check of the turn, 2 on the next... (loop guard; at most 3 per turn — the rest arrive at turn end).\n" +
|
|
156
|
+
"- action:'status': NON-BLOCKING progress query — returns immediately and consumes nothing. Give the spawn's id for one child ({id, role, status: running|queued|done, model, elapsedSec, turn, maxTurns, position?), or omit it for an overview of the whole pool ({overview: {running: [{id, role, model, elapsedSec, turn, maxTurns}], queued: [{id, role, position}], done: [{id, role}]}}). §19.5.6 touched-files summary: running entries also carry touchedFiles (first 5, relative to your cwd), touchedMore (count beyond 5) and, when nothing was touched yet, the placeholder touched (\"—(尚无改动)\"); queued (not yet started) entries carry the placeholder touched (\"—(未启动)\") — see what a running child has changed BEFORE deciding to cancel it. Use THIS to check progress — action:'check' blocks until the target finishes (checking progress with check is what hangs a parallel turn).\n" +
|
|
157
|
+
"- action:'escalate' (飞刀 — a flown-in expert): hand an implementation task to a STRONGER model from your consult models (agent.consultModels). It gets WRITE access and does the work itself — reads, edits, runs tests — then returns a post-op report (what changed, why, verification). Use it when YOU judge the task calls for stronger hands (complex multi-file refactoring, an intractable bug, intricate algorithm work — or work beyond your comfortable ability); escalate EARLY, not after burning attempts. model: pick a candidate as 'provider:model' (default = the first consult model). Not available in engineering mode (implementation goes through eng-coder spawns there).\n" +
|
|
158
|
+
"- action:'cancel': STOP one background subagent — pass the id from the async spawn return (REQUIRED — omitting it errors; a blanket cancel is unsupported, Ctrl+C stops everything). Running target aborts immediately ({id, status:'cancelled'}); a queued target is removed from the queue ({id, status:'cancelled', was:'queued'} and later queue positions shift forward). Other children and the session keep running — cancellation is targeted. Use it when a background child is going the wrong way (e.g. burning turns) and you must stop it before its report arrives. Cancel is a last resort: verify alarming signals with reliable checks (git/node — not guesses) first; prefer scoped recovery (restore a single affected file) over killing the child — a running child's in-flight work dies with it, partial changes stay unmerged and unaudited.\n" +
|
|
159
|
+
"- action:'panel': DIAGNOSE + fix the subagent panel — the collapsible blocks under the conversation the user sees (CLI TUI panel mirror; headless/VS Code degrade to a 'no panel' pool view). view (default — call it with no params or view:true): returns the live panel blocks [{key, role, status: running|done|awaitingDigest} — running entries also carry elapsedSec; awaitingDigest entries whose report is ALREADY digested carry digested:true (stuck blocks — the freezable ones — explain odd panel states here)] exactly as the user sees them. freeze: pass the block key of a digested-stuck block ({action:'panel', freeze:'role#N'}) to reclaim it into the conversation — the freeze ONLY passes for awaitingDigest blocks with no live pool entry and no pending report (gated); freezing a block whose report is still pending would break the digestion order and is refused with a clear error.\n\n" +
|
|
96
160
|
"Why delegate? A sub-agent runs in its own isolated context — its reads, searches, tool calls and edits never enter your history or pollute your window; only its final report comes back. Delegation keeps your working context lean (you see the whole session, not the child's noise) and the child single-mindedly focused on one task. Parallel children run concurrently, saving wall-clock time. Every coder/eng-coder child carries its own verify + advisor self-review discipline — handed-off work is already verified before you read a word of it.\n\n" +
|
|
97
161
|
"Available roles (which roles are exposed depends on the active mode — see Mode filtering below):\n" +
|
|
98
|
-
"- explore — read-only search & analysis. Toolset: the read/search family (grep, read, glob, code_search, doc_search, repo_outline, lsp, tree...).
|
|
162
|
+
"- explore — read-only search & analysis. Toolset: the read/search family (grep, read, glob, code_search, doc_search, repo_outline, lsp, tree...). No git context injected—evidence from read/glob/grep and the task book. Its report must list what it searched and what it did NOT find. Fast — specify thoroughness in the task: quick / medium / thorough (default medium).\n" +
|
|
99
163
|
"- plan — read-only implementation planning. Same read/search toolset; NEVER edits files. Returns a step-by-step plan for the parent to execute.\n" +
|
|
100
164
|
"- coder — full implementation. The parent's complete read/write/execute toolset plus verify and advisor for self-review. Its final report must include a delivery transparency table with one row per task requirement (Done / Simplified / Not done — no deferred column).\n" +
|
|
101
165
|
"- eng-coder — engineering-mode coder (available only in engineering mode, replacing coder). Same full toolset as coder plus the design-driven methodology overlay; REQUIRES a valid designToken arg obtained from a passed advisor(type='design') review. The advisor's Approved reply also echoes a designId — pass it as the designId arg: required to pick between designs when several approved reviews are active, optional for a single design. The delivery report echoes the designId back for the audit fix round.\n" +
|
|
102
166
|
"Mode filtering: normal mode exposes explore/plan/coder; engineering mode exposes explore/plan/eng-coder. The schema enum reflects the active mode.\n\n" +
|
|
103
|
-
"Async spawn (AGENT-LOOP.md §15): pass async:true to spawn WITHOUT waiting — returns {id, role, status:\"running\"} immediately so you can keep working in your own turn (read/check files, run other tools) while the child runs in the background.
|
|
167
|
+
"Async spawn (AGENT-LOOP.md §15/§18): pass async:true to spawn WITHOUT waiting — returns {id, role, status:\"running\"} immediately so you can keep working in your own turn (read/check files, run other tools) while the child runs in the background. Fetch the report later with action:'check' — multiple async children return in completion (arrival) order, first finished first, so fast results are handled immediately. Query progress with action:'status' (non-blocking) — action:'check' BLOCKS until the target finishes. The DEFAULT is role-level: role='eng-coder' spawns async (its delivery protocol runs fully inside the child — implementation → audit → self-fix → advisor re-review → converged delivery; pass async:false only when you must handle the report synchronously); every other role defaults to blocking. Use async when your own turn must keep moving; use a blocking spawn when you must see the report before continuing. Async spawns are capped at 4 concurrent (further spawns queue with a position), and top-level only.\n\n" +
|
|
168
|
+
"Task scheduling (AGENT-LOOP.md §20): declare the scheduling metadata to let the SCHEDULER order your spawns — files: the file paths this task will modify, dependsOn: ids from prior async spawn returns whose outcome this task needs. Overlapping-file tasks are serialized and dependent tasks are started in order automatically: a spawn that would conflict, or whose dependencies have not settled, queues instead of running ({id, status:\"queued\", position, reason} — the waiting task auto-starts when the conflict clears / its dependency settles; cancel a queued task to drop it). A spawn whose dependency was cancelled or failed stays queued and marked \"dependency cancelled\" until you decide (cancel it) — in an AUTO session it starts by itself. Referencing an unknown id errors; an id already consumed by check counts as satisfied. Omit both parameters for the plain immediate spawn (no scheduler involvement).\n\n" +
|
|
104
169
|
"Writing the prompt:\n" +
|
|
105
170
|
"- The sub-agent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics.\n" +
|
|
106
171
|
"- Put exact paths and commands in the prompt when you know them. The sub-agent should not search for things you already know.\n" +
|
|
@@ -109,22 +174,70 @@ export const subagentTool = {
|
|
|
109
174
|
parameters: {
|
|
110
175
|
type: "object",
|
|
111
176
|
properties: {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
177
|
+
action: { type: "string", enum: ["spawn", "check", "status", "escalate", "cancel", "panel"], description: "Which subagent-family action — spawn (default), check (fetch async results — BLOCKS until the target finishes and consumes the report), status (non-blocking progress query — never consumes), escalate (飞刀 — hand implementation to a stronger consult model), cancel (stop ONE background subagent — pass its id; never omit), panel (view the live subagent panel / freeze a digested-stuck block — §19.6). See the tool description for the full action matrix." },
|
|
178
|
+
view: { type: "boolean", description: "action:'panel' only: true (default) = return the live panel blocks (the mirror of what the user sees). false with no freeze = nothing to do — error. Mutually exclusive with freeze (freeze wins)." },
|
|
179
|
+
freeze: { type: "string", description: "action:'panel' only: block key of a digested-stuck awaitingDigest block (e.g. \"eng-coder#9\") to reclaim into the conversation via the gated done-freeze event. Refused when the block is running/done/unknown or its report is still pending digestion (would break the digestion order). Requires the CLI TUI panel mirror — headless/VS Code report the freeze unavailable." },
|
|
180
|
+
task: { type: "string", description: "Required for action:'spawn' (the self-contained task brief) and action:'escalate' (goal, constraints, entry files, acceptance criteria). Not used by check/status." },
|
|
181
|
+
context: { type: "string", description: "Optional background the sub-agent needs (it cannot see this conversation); action:'spawn' only." },
|
|
182
|
+
role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder"], description: "The sub-agent role — see the tool description for the role capability matrix. Exact spelling required. action:'spawn' only (escalate spawns its own expert internally)." },
|
|
183
|
+
model: { type: "string", description: "action:'spawn': provider/model override for this sub-agent ('provider:model', a provider name, or a model name on the parent's provider — defaults to config.agent.subagentModels[role], then config.agent.subagentModel, then the parent's provider). pass \"default\" to explicitly inherit the default model — equivalent to omitting the parameter. action:'escalate': pick a consult candidate as 'provider:model' (default = the first consult model)." },
|
|
116
184
|
designToken: { type: "string", description: "Required when role='eng-coder': the token returned by advisor(type='design') after the design review passed. Without a valid token, eng-coder cannot modify files." },
|
|
117
185
|
designId: { type: "string", description: "Optional when role='eng-coder': the designId echoed with the approved token by advisor(type='design'). Required to pick between designs when several approved reviews are active in the session — each eng-coder carries its own designId+token pair so parallel implementations never overwrite each other. Optional for a single design." },
|
|
118
|
-
async: { type: "boolean", description: "true = spawn without waiting — returns {id} immediately, fetch results later via
|
|
186
|
+
async: { type: "boolean", description: "true = spawn without waiting — returns {id, status:\"running\"} immediately, fetch results later via action:'check'. Default is role-level: role='eng-coder' → true (async; its internal delivery protocol runs in the background — pass async:false to force the blocking spawn when you must process the report before continuing); all other roles → false (blocking)." },
|
|
187
|
+
files: { type: "array", items: { type: "string" }, description: "action:'spawn' only: the file write-domain this task declares (cwd-relative or absolute paths). files must be file-level paths (one per file you will modify). Directory declarations are NOT supported — they bypass the conflict detector and are rejected with an error. Tasks with overlapping files are serialized automatically — a conflicting spawn queues ({id, status:\"queued\", position, reason}) instead of running concurrently and starts when the conflict clears. Omit to skip conflict detection (plain immediate spawn)." },
|
|
188
|
+
dependsOn: { type: "array", items: { type: "string" }, description: "action:'spawn' only: ids from prior async spawn returns whose outcome this task needs — the task queues ({id, status:\"queued\", position, reason}) until every dependency settles, then starts automatically. Ids consumed by action:'check' count as satisfied; a dependency cancelled or failed leaves the task queued marked 'dependency cancelled' until you decide (cancel it — AUTO sessions auto-start). Unknown ids error." },
|
|
189
|
+
id: { type: "string", description: "action:'check'/'status'/'cancel': the subagent id from the async spawn return. check: omit = the next completed child (arrival order); status: omit = overview of the whole pool; cancel: REQUIRED (never omit — a blanket cancel is unsupported)." },
|
|
190
|
+
n: { type: "number", description: "action:'check' (required): 1-based read counter — 1 for the first check of the turn, incrementing with each subsequent check (loop detector — consecutive checks must be distinct tool calls)." },
|
|
119
191
|
},
|
|
120
|
-
required: [
|
|
192
|
+
required: [],
|
|
121
193
|
},
|
|
122
194
|
readonly: false,
|
|
123
195
|
sideEffectExempt: true, // child agent may write files; parent can't introspect its _mutatedThisRun
|
|
124
196
|
parallel: true,
|
|
125
197
|
async execute(args, ctx) {
|
|
198
|
+
// §19 action dispatch: default spawn keeps every legacy call unchanged
|
|
199
|
+
// (no action parameter → the spawn path below, byte-identical semantics).
|
|
200
|
+
const action = args?.action !== undefined && args?.action !== null && String(args.action) !== ""
|
|
201
|
+
? String(args.action)
|
|
202
|
+
: "spawn"
|
|
203
|
+
if (action !== "spawn") {
|
|
204
|
+
// §19 restricted-variant action gate (round2 #3): the eng-coder audit
|
|
205
|
+
// channel (depth>0, role eng-coder) is spawn-only — escalate spawns a
|
|
206
|
+
// coder+WRITE child (violates explore-only intent) and check/status/panel
|
|
207
|
+
// have no async pool / panel mirror to query in a child context.
|
|
208
|
+
if ((ctx.depth ?? 0) > 0 && ctx.agent?._role === "eng-coder") {
|
|
209
|
+
throw new Error(`only action:'spawn' (sync explore audits) is available inside an eng-coder — escalate/check/status/cancel/panel are not (AGENT-LOOP.md §19 D-M3)`)
|
|
210
|
+
}
|
|
211
|
+
// §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn —
|
|
212
|
+
// async OR blocking — the digest must stay organize-only. The escalate
|
|
213
|
+
// action spawns a write child too, so the same mechanical refusal applies
|
|
214
|
+
// (AUTO tier exempt — user authorized unattended continuation).
|
|
215
|
+
if (action === "escalate" && ctx.agent?._inAutoTurn && !ctx.agent?.autoApprove) {
|
|
216
|
+
return JSON.stringify({ status: "error", error: "cannot spawn subagents from a manual auto-turn — wait for user input" })
|
|
217
|
+
}
|
|
218
|
+
if (action === "check") return await executeCheckAction(args, ctx)
|
|
219
|
+
if (action === "status") return executeStatusAction(args, ctx)
|
|
220
|
+
if (action === "escalate") return await executeEscalateAction(args, ctx)
|
|
221
|
+
// §19.5 控制类动作:digest 内放行(D-S7 分类——控制/自省;dispatch 控制类
|
|
222
|
+
// 豁免同批生效——19.5.2b round2 #4;escalate 的 digest 拒绝在上一分支)
|
|
223
|
+
if (action === "cancel") return executeCancelAction(args, ctx)
|
|
224
|
+
// §19.6 panel 动作:view(readonly 面——digest 内放行——自省类)与 freeze
|
|
225
|
+
// (控制类——同 cancel——digest 内放行)。深度/门控检查在 executePanelAction 内。
|
|
226
|
+
if (action === "panel") return executePanelAction(args, ctx)
|
|
227
|
+
throw new Error(`Unknown subagent action: ${JSON.stringify(action)}. Valid actions: spawn, check, status, escalate, cancel, panel.`)
|
|
228
|
+
}
|
|
229
|
+
|
|
126
230
|
const parent = ctx.agent
|
|
127
231
|
const role = args.role
|
|
232
|
+
// Spawn requires a task brief — schema `required` is advisory (multi-action
|
|
233
|
+
// schema), so the mechanical check lives here: an absent task would otherwise
|
|
234
|
+
// flow downstream as `content: undefined` and surface as an obscure error.
|
|
235
|
+
if (typeof args.task !== "string" || !args.task.trim()) {
|
|
236
|
+
throw new Error("subagent action:'spawn' requires a task (the self-contained task brief).")
|
|
237
|
+
}
|
|
238
|
+
// §18 F1/D-E1 role-level async default: eng-coder spawns async unless the
|
|
239
|
+
// caller explicitly passes async:false; every other role stays blocking.
|
|
240
|
+
const wantAsync = args.async ?? role === "eng-coder"
|
|
128
241
|
|
|
129
242
|
// Role normalization + whitelist (2026-08-25, coder-leak fix): exact-string gates let
|
|
130
243
|
// variant roles ("Coder", " coder") bypass BOTH mode gates and fall through to
|
|
@@ -134,6 +247,13 @@ export const subagentTool = {
|
|
|
134
247
|
if (!ROLES.has(role)) {
|
|
135
248
|
throw new Error(`Unknown subagent role: ${JSON.stringify(role)}. Valid roles: explore, plan, coder, eng-coder (exact spelling).`)
|
|
136
249
|
}
|
|
250
|
+
// §18 D-E3 internal-spawn gate: an eng-coder sub-agent may only spawn sync
|
|
251
|
+
// explore (audit) children — non-explore roles and async are refused here
|
|
252
|
+
// (mechanical), the audit budget is enforced (7th audit spawn refused), and
|
|
253
|
+
// the returned attempt number marks this spawn as an audit for the task-book
|
|
254
|
+
// augmentation below. Runs BEFORE the mode gates so the eng-coder-specific
|
|
255
|
+
// error (not the generic engineering-mode one) surfaces.
|
|
256
|
+
const engAuditAttempt = gateEngCoderSpawn(ctx.agent, ctx.depth, role, args.async)
|
|
137
257
|
// Role is mutually exclusive per mode: normal mode → "coder", engineering mode → "eng-coder"
|
|
138
258
|
if (parent.config?.agent?.engineering && role === "coder") {
|
|
139
259
|
throw new Error("Engineering mode: use role='eng-coder' for implementation tasks.")
|
|
@@ -142,6 +262,60 @@ export const subagentTool = {
|
|
|
142
262
|
throw new Error("Engineering mode is not active — use role='coder' for implementation tasks.")
|
|
143
263
|
}
|
|
144
264
|
|
|
265
|
+
// §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn — async
|
|
266
|
+
// OR blocking — the digest must stay organize-only. AUTO tier (autoApprove) is
|
|
267
|
+
// exempt (推进型 — user authorized unattended continuation). Mechanical refusal
|
|
268
|
+
// so the digest never pops a permission panel or chains new background work.
|
|
269
|
+
// (escalate 动作的同类拒绝在 action 分流处——本检查只管 spawn 路径。)
|
|
270
|
+
if (parent._inAutoTurn && !parent.autoApprove) {
|
|
271
|
+
return JSON.stringify({ status: "error", error: "cannot spawn subagents from a manual auto-turn — wait for user input" })
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── §20 spawn 调度参数准入(AGENT-LOOP.md §20 D-SD1/D-SD3 + 20.4 round2 #5/#7)──
|
|
275
|
+
// files/dependsOn 声明即契约(v1:不做任务书文本自动解析——不可靠)。缺省(两者皆
|
|
276
|
+
// 缺)= 既有语义零改动(不参与冲突检测/无校验——legacy spawn 零开销直通)。
|
|
277
|
+
// 校验序:参数形态 → 依赖 unknown id(非 consumed 墓碑——T-SD10)→ 依赖环可达
|
|
278
|
+
// (T-SD5——防御断言:自然流程不可达)→ 等待态判定。判定结果:wait/depc 阻塞 →
|
|
279
|
+
// async 入 queued 等位(spawn 返回带 reason——D-SD3b);**sync spawn(async:false)
|
|
280
|
+
// 命中阻塞 → 明确错误——不队列化 sync——sync 语义零变更(round2 #7——T-SD13)**。
|
|
281
|
+
const filesRaw = args.files
|
|
282
|
+
const dependsRaw = args.dependsOn
|
|
283
|
+
// §20.8 D-F1.1:目录声明 fail-closed——检测器 throw → catch → 错误即工具结果
|
|
284
|
+
// (模型可见"文件级明细"提示——不加静默——目录绕过冲突检测的通道闭合)。
|
|
285
|
+
let files = []
|
|
286
|
+
if (filesRaw !== undefined && filesRaw !== null) {
|
|
287
|
+
try {
|
|
288
|
+
files = normalizeFileList(filesRaw, parent.cwd)
|
|
289
|
+
} catch (e) {
|
|
290
|
+
return JSON.stringify({ status: "error", error: e.message })
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (filesRaw !== undefined && filesRaw !== null && !Array.isArray(filesRaw)) {
|
|
294
|
+
throw new Error("subagent files must be an array of file paths (the write domain this task declares)")
|
|
295
|
+
}
|
|
296
|
+
const dependsOn = []
|
|
297
|
+
if (dependsRaw !== undefined && dependsRaw !== null) {
|
|
298
|
+
if (!Array.isArray(dependsRaw)) throw new Error("subagent dependsOn must be an array of async subagent ids (from prior spawn returns)")
|
|
299
|
+
for (const d of dependsRaw) {
|
|
300
|
+
if (typeof d !== "string" && typeof d !== "number") {
|
|
301
|
+
throw new Error(`subagent dependsOn entries must be async subagent ids — got ${JSON.stringify(d)}`)
|
|
302
|
+
}
|
|
303
|
+
dependsOn.push(String(d))
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (files.length > 0 || dependsOn.length > 0) {
|
|
307
|
+
for (const d of dependsOn) {
|
|
308
|
+
if (depInfo(parent, d).state === "unknown") {
|
|
309
|
+
throw new Error(`subagent dependsOn: unknown async subagent id: ${d} — dependsOn references ids from prior async spawn returns; an id already consumed by action:'check' (or auto-injected) counts as satisfied, anything else is a mistake (AGENT-LOOP.md §20 D-SD5)`)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
assertNoDepCycle(parent, dependsOn)
|
|
313
|
+
const block = describeBlockers(parent, { _files: files, _dependsOn: dependsOn })
|
|
314
|
+
if (!wantAsync && block.kind !== "slot") {
|
|
315
|
+
throw new Error(`sync spawn (async:false) cannot queue behind a scheduling conflict: ${block.detail} — pass async:true to queue the task (the scheduler starts it when the blockers clear), or wait for them to finish first (AGENT-LOOP.md §20 round2 #7)`)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
145
319
|
// Provider/model override: tool `model` arg > subagentModels[role] > subagentModel > parent provider
|
|
146
320
|
const childProvider = resolveChildProvider(parent, effectiveSubagentModel(parent, role, args.model))
|
|
147
321
|
|
|
@@ -207,6 +381,12 @@ export const subagentTool = {
|
|
|
207
381
|
|
|
208
382
|
// Token-verified design review → child is authorized to modify files without re-reviewing
|
|
209
383
|
if (role === "eng-coder") child._engDesignReviewed = true
|
|
384
|
+
// §18 D-E3 task-domain authorization: approved design + spawn task = authorization.
|
|
385
|
+
// The child's OWN tools skip ONLY the onPermissionRequest ask (autoApprove
|
|
386
|
+
// equivalent — dispatch.mjs permission stage); every other gate (JSON parse /
|
|
387
|
+
// unknown tool / planMode / design-token) still applies (T-E14). Non-eng-coder
|
|
388
|
+
// children keep the manual per-write parent approval (human in the loop).
|
|
389
|
+
if (role === "eng-coder") child._engTaskAuthorized = true
|
|
210
390
|
// designId+token ride the child bookkeeping: the delivery report carries the designId
|
|
211
391
|
// so the divergence-audit fix round re-spawns with the SAME slot (2026-09-01 FR3).
|
|
212
392
|
if (role === "eng-coder" && issuedToken) {
|
|
@@ -214,12 +394,61 @@ export const subagentTool = {
|
|
|
214
394
|
child._engDesignToken = issuedToken
|
|
215
395
|
}
|
|
216
396
|
|
|
217
|
-
//
|
|
397
|
+
// §18.5 子代理零 git(D-AG1——2026-09-04 用户裁定):explore/plan 一律不注入
|
|
398
|
+
// git 上下文——子代理证据链 = 任务书 ∪ 磁盘当前状态(read/glob/grep)∪(审计时)
|
|
399
|
+
// _touchedFiles,无一项来自 git;注入的全工作区脏状态快照与任务域无关,会误导
|
|
400
|
+
// 审计/探索("status 里这个文件算不算超清单?")。注入分支整体删除(B 方案
|
|
401
|
+
// git 只读变体亦随裁定废弃——D-AG5)。顶层主 agent 注入保留(§3 prepareRun——
|
|
402
|
+
// setup.mjs depth===0——D-AG7 范围边界)。
|
|
218
403
|
let input = args.context ? `Context:\n${args.context}\n\nTask:\n${args.task}` : args.task
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
404
|
+
// §18 D-E2 ③ (round4 #4, T-E13/T-E15): an eng-coder audit spawn's task book is
|
|
405
|
+
// the eng-coder's OWN spawn task — mechanically kept as _engTaskInput by the
|
|
406
|
+
// parent spawn and injected as the D-TS5 A2 mechanical summary (design docs /
|
|
407
|
+
// affected-file list / acceptance criteria verbatim, verbose context dropped)
|
|
408
|
+
// — ∪ the mechanically tracked _touchedFiles — NEVER the eng-coder's
|
|
409
|
+
// self-written list: a self-report could omit exactly the out-of-scope file
|
|
410
|
+
// the audit must catch.
|
|
411
|
+
if (engAuditAttempt !== null) {
|
|
412
|
+
const touched = (ctx.agent._touchedFiles ?? []).map((f) => `- ${f}`).join("\n") || "- (none yet)"
|
|
413
|
+
input += `\n\n[Audit scope — mechanical context, independent of the eng-coder's self-report:]\n` +
|
|
414
|
+
// §18.7 D-TS4 A1:审计指令模板(四类偏差 + 范围限制 + 校验清单格式)——审计语义
|
|
415
|
+
// 不再靠模型自悟;范围限制是 §18.5 D-AG3 声明(下方 Zero-git scope authority)
|
|
416
|
+
// 的同源一句指注,不重复声明。
|
|
417
|
+
`[Audit instructions — mechanical template (AGENT-LOOP.md §18.7 D-TS4 A1):]\n` +
|
|
418
|
+
`You are auditing an eng-coder delivery against its approved design — audit for EXACTLY these four deviation categories:\n` +
|
|
419
|
+
`- PARTIAL: an acceptance criterion implemented partially or not at all;\n` +
|
|
420
|
+
`- SILENT-SIMPLIFICATION: a "simpler approximation" of a specified behavior substituted for the spec;\n` +
|
|
421
|
+
`- DOC-DRIFT: code changed without the owning design-doc section (module map / affected-files table) updated in the same delivery;\n` +
|
|
422
|
+
`- OUT-OF-LIST: changes outside the approved file list.\n` +
|
|
423
|
+
`Audit scope = _touchedFiles above UNION the files confirmed by the parent task book (single source — the Zero-git scope authority note below, AGENT-LOOP.md §18.5 D-AG3; NOT a second copy): ` +
|
|
424
|
+
`workspace changes not listed there are unrelated to this delivery and are NOT grounds for an out-of-list finding.\n` +
|
|
425
|
+
`Scope discipline (F-TS6 A1): read ONLY the audited files and the design-doc sections relevant to this delivery — do NOT re-read whole documents.\n` +
|
|
426
|
+
`Every deviation item MUST be fieldized: file:line + design reference (doc path + section/AC id) + severity + evidence (quoted code or doc text).\n` +
|
|
427
|
+
// §18.7 D-TS5 A2:任务书从全量 verbatim 改机械摘要块(三要素逐字——排除冗长上下文)。
|
|
428
|
+
`[Parent spawn task book — mechanical summary (AGENT-LOOP.md §18.7 D-TS5 A2): design docs + affected-file list + acceptance criteria verbatim; verbose context/background dropped — the design docs are still available for reading outside this input:]\n` +
|
|
429
|
+
`${summarizeEngTaskBook(ctx.agent._engTaskInput)}\n` +
|
|
430
|
+
`Files actually touched by the eng-coder (mechanical union — audit these against the file list):\n${touched}\n` +
|
|
431
|
+
// §18.5 D-AG3(2026-09-04):审计零 git 范围权威声明——本审计任务零 git(不注入
|
|
432
|
+
// git 上下文——§18.5 全角色零 git);_touchedFiles 为审计范围;工作区未列于
|
|
433
|
+
// _touchedFiles 的改动与本任务无关,不作超清单依据(VS Code auditTaskBook 同款措辞)。
|
|
434
|
+
"Zero-git scope authority (AGENT-LOOP.md §18.5 D-AG3): this audit task receives NO git context — nothing is injected. " +
|
|
435
|
+
"The evidence base is the design documents, the current disk state (read/glob/grep), and the _touchedFiles list above. " +
|
|
436
|
+
"Workspace changes NOT listed in _touchedFiles are unrelated to this delivery — they are NOT grounds for an out-of-file-list finding." +
|
|
437
|
+
// §18.13 D-A1.2:审计预算句——A1 指令模板 + A2 摘要块之后、A3 报告模板之前(定序——评审 #7)。
|
|
438
|
+
// 逐字设计锚(D-A1.2 代码块):只读该读的——10 轮机械预算——超时报 PROBLEM 下结论。
|
|
439
|
+
// 前导 \n 与 A3 同款块分隔约定(上一句 Zero-git 句末无换行——不触碰既有句)。
|
|
440
|
+
`\n[Audit budget — mechanical]: read ONLY the touched files listed above and the design-doc sections the parent task book names (affected-files table, acceptance criteria, status line). Do NOT read whole documents. Budget = 10 tool rounds max — if you cannot conclude within it, report PROBLEM (inconclusive) rather than continuing to explore.\n` +
|
|
441
|
+
// §18.7 D-TS6 A3:审计输出报告格式模板(三态——字段化行——不让模型自由发挥)。
|
|
442
|
+
`\n[Audit report format — mechanical template (AGENT-LOOP.md §18.7 D-TS6 A3):]\n` +
|
|
443
|
+
`Report EXACTLY one of three states:\n` +
|
|
444
|
+
`- CLEAN — no deviation across the four categories: reply the line "Four deviation categories: none found." (四类偏差均未发现);\n` +
|
|
445
|
+
`- DEVIATIONS — one row per deviation, every row fieldized: | category | file:line | design reference | severity | evidence |;\n` +
|
|
446
|
+
`- PROBLEM — the audit itself could not run / inconclusive: state what blocked it.\n`
|
|
222
447
|
}
|
|
448
|
+
// The child's own task input rides the child object: an eng-coder's audit
|
|
449
|
+
// spawns reuse it as the task-book SOURCE — injected as the D-TS5 A2
|
|
450
|
+
// mechanical summary, not verbatim (see above).
|
|
451
|
+
if (role === "eng-coder") child._engTaskInput = input
|
|
223
452
|
|
|
224
453
|
// Relay content/reasoning/tool/output to the parent TUI via the unified spawn-child
|
|
225
454
|
// pipeline (AGENT-LOOP.md §7.2 D3). Prefix includes a unique id: parallel child agents
|
|
@@ -230,12 +459,15 @@ export const subagentTool = {
|
|
|
230
459
|
// The [model] token (TUI block creation) is DEFERRED to actual start so queued
|
|
231
460
|
// children don't paint an empty panel block ("queued 态不显示").
|
|
232
461
|
let relayPrefix
|
|
233
|
-
if (
|
|
462
|
+
if (wantAsync) {
|
|
234
463
|
parent._subAgentCounter = (parent._subAgentCounter ?? 0) + 1
|
|
235
464
|
relayPrefix = `${role}#${parent._subAgentCounter}/`
|
|
236
465
|
} else {
|
|
237
466
|
relayPrefix = makeRelay(parent, role ?? "sub", ctx.callbacks?.onToken, childProvider.model ?? "")
|
|
238
467
|
}
|
|
468
|
+
// LOGGING(LOGGING.md):子代理内部事件(子内 llm:*/tool:*)以 childId 归属——
|
|
469
|
+
// agent._logId 随 runAgent 的 logCtx 透出(主文件单文件全记、按 childId grep)。
|
|
470
|
+
child._logId = relayPrefix.slice(0, -1)
|
|
239
471
|
const childOpts = {
|
|
240
472
|
onPermissionRequest: childPermission,
|
|
241
473
|
...wrapChildCallbacks(relayPrefix, ctx.callbacks),
|
|
@@ -259,10 +491,10 @@ export const subagentTool = {
|
|
|
259
491
|
// The child runs the EXACT blocking pipeline (runChildPipeline below — relay /
|
|
260
492
|
// turn-cap / permission / MIN_REPORT_CHARS / mergeChildMutations all unchanged),
|
|
261
493
|
// but the parent does not await it: the promise is parked in _asyncSubagents and
|
|
262
|
-
// consumed via
|
|
494
|
+
// consumed via action:'check' or the turn-end auto-wait. Slot queue: running
|
|
263
495
|
// count < ASYNC_SUBAGENT_LIMIT → start now; ≥ limit → enqueue (status "queued",
|
|
264
496
|
// position = queue index) — never rejected, never requiring the model to batch.
|
|
265
|
-
if (
|
|
497
|
+
if (wantAsync) {
|
|
266
498
|
if ((ctx.depth ?? 0) > 0) {
|
|
267
499
|
throw new Error("async spawn only available at the top level")
|
|
268
500
|
}
|
|
@@ -272,181 +504,223 @@ export const subagentTool = {
|
|
|
272
504
|
const id = parent._subAgentCounter
|
|
273
505
|
const entry = {
|
|
274
506
|
id, role, relayPrefix,
|
|
275
|
-
status:
|
|
507
|
+
status: "queued", // 下面按等待态/槽位重定(避免两处判断漂移)
|
|
276
508
|
position: undefined,
|
|
277
|
-
report: null, error: null, done: false,
|
|
509
|
+
report: null, error: null, done: false, cancelled: false,
|
|
278
510
|
promise: null, _settle: null, _settleSeq: 0,
|
|
511
|
+
// §19.5 D-M5 可决策字段(status 数据装配锚点):model 在 spawn 时记录;
|
|
512
|
+
// startedAt 在 ACTUAL start(queued 等待不计 elapsed);turn/maxTurns 由
|
|
513
|
+
// 下方 onToken 拦截层从子代理 ⟦ev⟧turn 事件镜像(T-M18 正确性断言)。
|
|
514
|
+
model: childProvider?.model ?? null,
|
|
515
|
+
startedAt: null,
|
|
516
|
+
turn: 0, maxTurns: 0,
|
|
517
|
+
// §19.5 D-M6 (round2 #2):条目级 AbortController——cancel 定向 abort 本
|
|
518
|
+
// 条目(runAgent signal 链);Ctrl+C 全停语义不变(基信号 abort 逐链传播)。
|
|
519
|
+
controller: null,
|
|
520
|
+
// §20 D-SD2 域元数据(AGENT-LOOP.md §20):running ∪ queued 条目全带——
|
|
521
|
+
// _files(归一化绝对路径)/ _dependsOn(字符串 id)——冲突检测与补位判据
|
|
522
|
+
// 的事实源;无调度参数 spawn 两字段皆空(legacy——不参与冲突检测——零改动)。
|
|
523
|
+
_files: files,
|
|
524
|
+
_dependsOn: dependsOn,
|
|
525
|
+
_lastQueuedSig: null, // ⟦ev⟧queued 去重 sig(refreshQueuedTokens)
|
|
526
|
+
}
|
|
527
|
+
// §20 D-SD3 准入落点:等待态(依赖未满足/域冲突/depc)→ queued(waiting-deps——
|
|
528
|
+
// 不占槽不启动——即使槽空);纯槽满(kind slot)→ queued(等位);否则立即启动。
|
|
529
|
+
// 派生实时计算(describeBlockers——池状态在 spawn 同步段内不变——与前面准入一致)。
|
|
530
|
+
const blockers = describeBlockers(parent, entry)
|
|
531
|
+
if (blockers.kind === "slot") {
|
|
532
|
+
entry.status = running >= ASYNC_SUBAGENT_LIMIT ? "queued" : "running"
|
|
533
|
+
} else {
|
|
534
|
+
entry.status = "queued" // waiting-deps / dependency-cancelled——slot 空也不启动
|
|
279
535
|
}
|
|
280
536
|
// The settle signal — resolves when the run chain settles (never rejects).
|
|
281
537
|
entry.promise = new Promise((res) => { entry._settle = res })
|
|
538
|
+
// §19.5 D-M6:条目 controller 链到会话/回合基信号(_sessionSignal 优先——
|
|
539
|
+
// §17 挂起会话内 children 持会话 signal,digest 自身 Ctrl+C 不误伤)。
|
|
540
|
+
const ctrl = new AbortController()
|
|
541
|
+
entry.controller = ctrl
|
|
542
|
+
const baseSignal = parent._sessionSignal ?? ctx.signal ?? null
|
|
543
|
+
if (baseSignal) {
|
|
544
|
+
if (baseSignal.aborted) ctrl.abort()
|
|
545
|
+
else baseSignal.addEventListener("abort", () => ctrl.abort(), { once: true })
|
|
546
|
+
}
|
|
547
|
+
// §19.5 D-M5:turn 镜像拦截层(callbacks 包装层——选改动最小方案:在既有
|
|
548
|
+
// wrapChildCallbacks 之外再包一层,只解析 ⟦ev⟧turn 更新条目,其余原样转发)。
|
|
549
|
+
const trackOpts = { ...childOpts }
|
|
550
|
+
const parentOnToken = trackOpts.onToken
|
|
551
|
+
if (parentOnToken) {
|
|
552
|
+
trackOpts.onToken = (t) => {
|
|
553
|
+
const ev = String(t).match(/^⟦ev⟧turn\x1e(\d+)\x1e(\d+)\x1e/)
|
|
554
|
+
if (ev) {
|
|
555
|
+
entry.turn = Number(ev[1]) || 0
|
|
556
|
+
entry.maxTurns = Number(ev[2]) || 0
|
|
557
|
+
}
|
|
558
|
+
return parentOnToken(t)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
282
561
|
entry.start = () => {
|
|
283
562
|
entry.status = "running"
|
|
284
563
|
entry.position = undefined
|
|
564
|
+
entry.startedAt = Date.now()
|
|
565
|
+
// §19.5.6 D-SF1(round3 #1):绑定子代理对象引用——绑定时刻 = 实际启动时
|
|
566
|
+
// (queued 条目 spawn-ack 时刻尚无子代理对象——§20 D-SD3b);绑定对象 = 子代理
|
|
567
|
+
// 对象(不是 _touchedFiles 数组引用——per-run 记账在 prepareRun 重置——数组
|
|
568
|
+
// 引用会陈旧——对象引用保证 status 查询时实时读——杀前一刻最新)。
|
|
569
|
+
entry.childAgent = child
|
|
570
|
+
// §19.5 D-M7b ①: async 标记事件——零字段 ⟦ev⟧async token(sync 不发)。
|
|
571
|
+
// 锚点 = 实际启动(与 [model] 同步——queued 入队不 paint,补位启动才发);
|
|
572
|
+
// 先于 [model] 发出——区块创建即知 sub.async(routeSubToken 解析——
|
|
573
|
+
// ⏹ 门控与头标 async 的判定源)。父级直接 emit(depth-0 专属路径——
|
|
574
|
+
// 不经子代理文本 strip 白名单——与 ⟦ev⟧stopped/settled 同族)。
|
|
575
|
+
ctx.callbacks?.onToken?.(relayPrefix + "⟦ev⟧async\x1e")
|
|
285
576
|
// Deferred [model] emit: the TUI block is created at ACTUAL start.
|
|
286
577
|
ctx.callbacks?.onToken?.(relayPrefix + "[model]" + (childProvider.model ?? ""))
|
|
287
578
|
// Turn-cap on background children NEVER pops the continue panel (D-A3):
|
|
288
|
-
//
|
|
289
|
-
|
|
579
|
+
// §15 D-A3 exception (2026-09-02 unified rule, AGENT-LOOP.md §2): in an
|
|
580
|
+
// engineering && AUTO session the child auto-resumes — the user authorized
|
|
581
|
+
// unattended runs, no one is at the panel. Every other tier auto-declines
|
|
582
|
+
// and the partial-work report carries the cap reason. §18 D-E2 relies on
|
|
583
|
+
// this exception as the turn-cap fallback for the default-async eng-coder
|
|
584
|
+
// delivery (the internal protocol does not raise the 100-turn cap).
|
|
585
|
+
runChildPipeline(child, input, trackOpts, { ...childRunOpts, signal: entry.controller.signal }, {
|
|
290
586
|
parent, role, args,
|
|
291
|
-
askContinue: () => Promise.resolve(
|
|
587
|
+
askContinue: () => Promise.resolve(Boolean(parent.config?.agent?.engineering && parent.autoApprove)),
|
|
292
588
|
})
|
|
293
589
|
.then((report) => { entry.report = report })
|
|
294
590
|
.catch((err) => { entry.error = err?.message ?? String(err) })
|
|
295
591
|
.finally(() => {
|
|
296
592
|
entry.status = "done" // running 数口径(D-A1/D-A2/T6):已完成未消费不计入
|
|
297
593
|
entry.done = true
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
594
|
+
// LOGGING(LOGGING.md):settle 分流事件——child:done/child:error(结果)+
|
|
595
|
+
// ev:cancelled/ev:settled(settle 回调分流——取消/挂起移交;正常回合内 settle
|
|
596
|
+
// 由 child:done 覆盖不另发 ev——ev:stopped 见中止清池点)
|
|
597
|
+
const childLogId = `${entry.role}#${entry.id}`
|
|
598
|
+
const childMs = entry.startedAt ? Date.now() - entry.startedAt : 0
|
|
599
|
+
// 中止守卫(2026-09-03 code review #5):Ctrl+C/会话中止时子代理以 error 形态
|
|
600
|
+
// settle——不落 child:error/done/ev:settled(ev:stopped 已在中止清池点表达;
|
|
601
|
+
// 同文件阻塞路径同款抑制——"用户停——不落错误事件")。定向 cancel 走 ev:cancelled。
|
|
602
|
+
const parentAborted = ctx.signal?.aborted || entry.controller?.signal?.aborted
|
|
603
|
+
if (entry.cancelled) {
|
|
604
|
+
logEvent("ev:cancelled", { id: childLogId })
|
|
605
|
+
} else if (!parentAborted) {
|
|
606
|
+
if (entry.error != null) logEvent("child:error", { role: entry.role, id: childLogId, ms: childMs, err: errText(entry.error, 200) })
|
|
607
|
+
else logEvent("child:done", { role: entry.role, id: childLogId, ms: childMs, kind: String(entry.report ?? "").includes(TURN_CAP_MARK) ? "partial" : "ok" })
|
|
608
|
+
if (parent._suspended) logEvent("ev:settled", { id: childLogId, kind: "suspended" })
|
|
609
|
+
}
|
|
610
|
+
// §19.5 cancelled settle 分支(D-M6 round1 #1 + round2 #3):cancel 定向
|
|
611
|
+
// 中止的条目——不入 _pendingAsyncResults、不参与 collectSettledAsync 直注入
|
|
612
|
+
// (清池规则同 Ctrl+C 全停但只清该条目——陈旧错误零注入);发 ⟦ev⟧stopped
|
|
613
|
+
// 冻结事件(TUI 区块 interrupted 语义冻结——标题 "stopped");取消事实与半成品
|
|
614
|
+
// 警示对模型可见(user-role 提醒——形态仿 injectAsyncResult、XML 转义——防基于
|
|
615
|
+
// 半成品树继续:mergeChildMutations 不覆盖 abort 路径)。
|
|
616
|
+
// §20 D-SD5:running 依赖取消的 settle 终态点——写终态墓碑(running 取消无
|
|
617
|
+
// 出队事件——出池在 settle);queued 依赖者随之标注 dependency cancelled
|
|
618
|
+
// (refreshQueuedTokens——settle 后统一段);提醒文本列出依赖者(供模型决策)。
|
|
619
|
+
if (entry.cancelled) {
|
|
620
|
+
parent._asyncSubagents?.delete(String(entry.id))
|
|
621
|
+
const tombstones = (parent._asyncTombstones ??= new Map())
|
|
622
|
+
tombstones.set(String(entry.id), { status: "cancelled", role: entry.role })
|
|
623
|
+
ctx.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧stopped\x1e0\x1e0\x1estopped\x1e`)
|
|
624
|
+
const dependents = dependentLabels(parent, String(entry.id))
|
|
625
|
+
const autoNote = parent.autoApprove
|
|
626
|
+
? " — AUTO session: they auto-start on slot availability (round2 #3)"
|
|
627
|
+
: " — they stay queued until you cancel them or an AUTO session starts them"
|
|
628
|
+
pushReal(parent, {
|
|
629
|
+
role: "user",
|
|
630
|
+
content: `[System reminder: subagent ${escapeXml(entry.role)}#${entry.id} cancelled by user — partial changes not merged/audited${dependents.length > 0 ? `; queued dependents ${dependents.join(", ")} marked "dependency cancelled"${autoNote}` : ""}]`,
|
|
631
|
+
})
|
|
632
|
+
} else if (!ctx.signal?.aborted) {
|
|
633
|
+
// 完成信号按会话态分流(§17 D-S8 冻结门控 + D-S3 记账——以读取时刻为准,确定性):
|
|
634
|
+
// - 非挂起态(普通回合内 settle):照发 ⟦ev⟧done —— TUI 立即冻结区块,冻结位置 =
|
|
635
|
+
// 完成时刻的流位置(§15 D-A3 2026-09-02 用户实证修正:收尾统一发会把块堆在结论之后)。
|
|
636
|
+
// §17.5 supersede:回合尾 collectSettledAsync(suspDriven)不再直注入——条目留池
|
|
637
|
+
// 由挂起会话首轮 sweep → digest 消化(17.5.2 方案 B——块已冻结不受影响)。
|
|
638
|
+
// - 挂起态(_suspended:回合已结束或 auto-turn 消化中):冻结延迟——改发 ⟦ev⟧settled
|
|
639
|
+
// (区块显示 "done · awaiting digestion" 驻留面板),条目移交 _pendingAsyncResults
|
|
640
|
+
// 由下个回合 prepareRun 前注入(D-S3 ②;注入即从池/pending 移除,无重复);
|
|
641
|
+
// §17.5.5:注入完成后 freezeReclaimDigestedBlocks 逐条回收冻结(不等池空)。
|
|
642
|
+
// 父会话 abort 两种都不发:TUI 已按 interrupted 冻结,晚到 token 经 tombstone 丢弃。
|
|
643
|
+
if (parent._suspended) {
|
|
644
|
+
parent._pendingAsyncResults ??= []
|
|
645
|
+
parent._pendingAsyncResults.push(entry)
|
|
646
|
+
parent._asyncSubagents?.delete(String(entry.id))
|
|
647
|
+
ctx.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧settled\x1e0\x1e0\x1esettled\x1e`)
|
|
648
|
+
} else {
|
|
649
|
+
ctx.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧done\x1e0\x1e0\x1edone\x1e`)
|
|
650
|
+
}
|
|
304
651
|
}
|
|
305
652
|
entry._settleSeq = (parent._asyncSettleSeq = (parent._asyncSettleSeq ?? 0) + 1)
|
|
306
653
|
entry._settle()
|
|
307
654
|
for (const w of parent._asyncWaiters?.splice(0) ?? []) { try { w() } catch { /* noop */ } }
|
|
655
|
+
// §20 D-SD4 释放点:settle 腾槽 + 依赖终态转移 → 补位(依赖满足者/域冲突
|
|
656
|
+
// 解除者自动启动——槽 ≤4)→ 排队态面板刷新(等待块头标注随终态更新——
|
|
657
|
+
// dependency cancelled / 位置前移)。
|
|
308
658
|
maybeRefillAsync(parent)
|
|
659
|
+
refreshQueuedTokens(parent, ctx.callbacks?.onToken)
|
|
309
660
|
})
|
|
310
661
|
}
|
|
311
662
|
parent._asyncSubagents.set(String(id), entry)
|
|
663
|
+
// LOGGING(LOGGING.md):child:spawn(async——注册即事件;status 记 queued/running 分流;
|
|
664
|
+
// 实际启动由补位 start() 触发——运行中由子内 llm/tool 事件可见)
|
|
665
|
+
logEvent("child:spawn", { role, id: `${role}#${id}`, kind: "async", status: entry.status, ms: 0 })
|
|
312
666
|
if (entry.status === "queued") {
|
|
313
667
|
parent._asyncQueue.push(entry)
|
|
314
668
|
entry.position = parent._asyncQueue.length
|
|
315
|
-
|
|
669
|
+
// §20 D-SD3b:排队 spawn 返回即建面板 waiting 块(⟦ev⟧queued 事件——spawn 侧
|
|
670
|
+
// 发——TUI routeSubToken 消费建块/更新头;启动后 ⟦ev⟧async 转 running——同 key
|
|
671
|
+
// 不重建)。refreshQueuedTokens 同时校正既有排队条目的位置/等待态头。
|
|
672
|
+
refreshQueuedTokens(parent, ctx.callbacks?.onToken)
|
|
673
|
+
const blk = describeBlockers(parent, entry)
|
|
674
|
+
const out = { id: String(id), role, status: "queued", position: entry.position }
|
|
675
|
+
if (blk.kind !== "slot") {
|
|
676
|
+
out.waiting = blk.kind === "depc" ? "dependency-cancelled" : "waiting-deps"
|
|
677
|
+
out.reason = blk.detail
|
|
678
|
+
}
|
|
679
|
+
return JSON.stringify(out)
|
|
316
680
|
}
|
|
317
681
|
entry.start()
|
|
318
682
|
return JSON.stringify({ id: String(id), role, status: "running" })
|
|
319
683
|
}
|
|
320
684
|
|
|
321
685
|
// ── Blocking path (unchanged semantics): await the full pipeline ──
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
})
|
|
686
|
+
// LOGGING(LOGGING.md):child:*(阻塞 spawn——runChildPipeline 前后;declined
|
|
687
|
+
// partial 由 TURN_CAP_MARK 检出;错误原样上抛(dispatch 转 tool:error))
|
|
688
|
+
const blockT0 = Date.now()
|
|
689
|
+
logEvent("child:spawn", { role, id: child._logId, kind: "blocking" })
|
|
690
|
+
try {
|
|
691
|
+
const pipelineReport = await runChildPipeline(child, input, childOpts, childRunOpts, {
|
|
692
|
+
parent, role, args,
|
|
693
|
+
askContinue: askSubagentContinue,
|
|
694
|
+
})
|
|
695
|
+
logEvent("child:done", { role, id: child._logId, ms: Date.now() - blockT0, kind: String(pipelineReport).includes(TURN_CAP_MARK) ? "partial" : "ok" })
|
|
696
|
+
// §7.2.3 sync spawn 完成精确冻结(方案 e):execute 返回前 ctx 留子代理 key
|
|
697
|
+
// (relayPrefix 去尾 = `role#N`)——dispatch runOne 读它作 onToolResult 第 4 参 →
|
|
698
|
+
// TUI finishSubTaskKey 按 key 精确冻(async eng-coder 先启动时不再误冻其块——
|
|
699
|
+
// T-F2)。仅成功路径设置:async 分支不设(round2 #2——ack 带 status:running 由
|
|
700
|
+
// isAsyncSpawnResult 跳过冻结);错误/拒绝路径到此之前已 throw/return——ctx 未设
|
|
701
|
+
// ——错误路径不触发冻结(round1 #1——T-F5)。
|
|
702
|
+
ctx._subagentKey = relayPrefix.slice(0, -1)
|
|
703
|
+
return pipelineReport
|
|
704
|
+
} catch (e) {
|
|
705
|
+
if (ctx.signal?.aborted || e?.name === "AbortError") throw e // 用户停——不落错误事件
|
|
706
|
+
logEvent("child:error", { role, id: child._logId, ms: Date.now() - blockT0, err: errText(e, 200) })
|
|
707
|
+
throw e
|
|
708
|
+
}
|
|
326
709
|
},
|
|
327
710
|
}
|
|
328
711
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
onDeclined: (e, output) => {
|
|
345
|
-
if (role === "eng-coder" && child._mutatedThisRun) mergeChildMutations(parent, child)
|
|
346
|
-
// Early return semantics (unchanged from the inline loop): the declined
|
|
347
|
-
// partial-work message is returned WITHOUT the MIN_REPORT_CHARS expansion —
|
|
348
|
-
// re-prompting a capped child for a longer report is wrong.
|
|
349
|
-
// Review #2 fix: use the pipeline-captured output (the `report` variable is
|
|
350
|
-
// still "" at this point — runWithContinue hasn't returned yet).
|
|
351
|
-
declined.partial = `Subagent (${role}) ${TURN_CAP_MARK} (${e.turn} turns) — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${output || ""}`
|
|
352
|
-
},
|
|
353
|
-
},
|
|
354
|
-
)
|
|
355
|
-
if (declined.partial !== null) {
|
|
356
|
-
// declined eng-coder delivery still carries its designId — the fix round
|
|
357
|
-
// re-spawns with the same slot (2026-09-01).
|
|
358
|
-
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.`
|
|
359
|
-
return declined.partial
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Report too short = incomplete handoff: send back for expansion once (inspired by kimi-code's summaryPolicy: min 200 chars, retry 1 time).
|
|
363
|
-
// The child agent's history is still intact; the continuation instruction is appended as new input so it can see its own earlier work.
|
|
364
|
-
if (report.length < MIN_REPORT_CHARS) {
|
|
365
|
-
report = await runAgent(child, REPORT_CONTINUATION, childOpts, childRunOpts)
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Engineering mode mechanical code gate: delegated file changes must not
|
|
369
|
-
// bypass the parent's advisor/verify guards. Merge the child's mutations
|
|
370
|
-
// into the parent so "advisor mandatory at both gates" is enforced, not just
|
|
371
|
-
// promised in the engineering prompt.
|
|
372
|
-
// CRITICAL: Only merge if child actually mutated files (defense-in-depth against
|
|
373
|
-
// runAgent throwing before any writes occurred).
|
|
374
|
-
// Review #8 clarification: eng-coder ONLY is intentional — the mechanical
|
|
375
|
-
// two-gate merge exists for engineering mode; plain `coder` children carry
|
|
376
|
-
// their own verify/advisor self-review discipline (per tool description), and
|
|
377
|
-
// normal mode has no parent advisor/verify gate to feed.
|
|
378
|
-
if (role === "eng-coder" && child._mutatedThisRun) {
|
|
379
|
-
mergeChildMutations(parent, child)
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// designId rides the delivery report (2026-09-01): the divergence-audit fix round
|
|
383
|
-
// re-spawns with the SAME designId+token — the parent copies it from here, and the
|
|
384
|
-
// prompt tells the model exactly where the matching token came from.
|
|
385
|
-
if (role === "eng-coder") {
|
|
386
|
-
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.`
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return report
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/** Slot-queue refill (AGENT-LOOP.md §15 D-A1/D-A6): start queue heads while a
|
|
393
|
-
* running slot is free — called from every settle (completion frees a slot) and
|
|
394
|
-
* from the turn-end collection's refill loop. Serial by construction: one slot
|
|
395
|
-
* frees per settle, one head starts per call. */
|
|
396
|
-
export function maybeRefillAsync(parent) {
|
|
397
|
-
const queue = parent._asyncQueue ?? []
|
|
398
|
-
while (queue.length > 0) {
|
|
399
|
-
const running = [...(parent._asyncSubagents?.values() ?? [])].filter((e) => e.status === "running").length
|
|
400
|
-
if (running >= ASYNC_SUBAGENT_LIMIT) return
|
|
401
|
-
queue.shift().start()
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Child agent run options — the parent's abort signal MUST propagate to the
|
|
407
|
-
* child: without it, Ctrl+C aborts the parent's controller but the child keeps
|
|
408
|
-
* running its full turn budget (up to subagentTurns) while the parent awaits —
|
|
409
|
-
* the interrupt appears to do nothing.
|
|
410
|
-
*/
|
|
411
|
-
export function buildChildRunOpts(ctx) {
|
|
412
|
-
return {
|
|
413
|
-
depth: (ctx.depth ?? 0) + 1,
|
|
414
|
-
maxTurns: ctx.agent?.config?.agent?.subagentTurns ?? DEFAULT_SUBAGENT_TURNS,
|
|
415
|
-
signal: ctx.signal ?? null,
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Merge an eng-coder child's mutations into the parent agent's bookkeeping.
|
|
421
|
-
* The parent must stay aware of delegated file changes: `_touchedFiles` enables
|
|
422
|
-
* the advisor guard (completion.mjs) to detect that code was modified and
|
|
423
|
-
* pushback for review. Prior verify/advisor state is invalidated because it
|
|
424
|
-
* judged an older state.
|
|
425
|
-
*
|
|
426
|
-
* `_advisorRound` is NOT reset: merged code enters the CURRENT convergence
|
|
427
|
-
* cycle. Resetting here would break the review→fix→re-review loop (the parent
|
|
428
|
-
* reviews, spawns an eng-coder to fix, merges, reviews again — every merge
|
|
429
|
-
* would restart at round 1 and the 5-round cap could never be reached).
|
|
430
|
-
* `_calledAdvisorThisRun` IS cleared so the merged code triggers a fresh
|
|
431
|
-
* advisor call (the guard demands review of new mutations).
|
|
432
|
-
*
|
|
433
|
-
* Returns true when mutations were merged (kept for future caller checks).
|
|
434
|
-
*/
|
|
435
|
-
export function mergeChildMutations(parent, child) {
|
|
436
|
-
// A child claiming mutations without any touched file is a misbehaving
|
|
437
|
-
// child (or a bookkeeping bug) — do not propagate an empty mutation claim
|
|
438
|
-
// to the parent's guard state.
|
|
439
|
-
if (!child._mutatedThisRun || !(child._touchedFiles?.length)) return false
|
|
440
|
-
parent._mutatedThisRun = true
|
|
441
|
-
for (const abs of child._touchedFiles ?? []) {
|
|
442
|
-
if (!parent._touchedFiles.includes(abs)) parent._touchedFiles.push(abs)
|
|
443
|
-
}
|
|
444
|
-
if (parent._calledAdvisorThisRun) parent._calledAdvisorThisRun = false
|
|
445
|
-
if (parent._verifiedThisRun) {
|
|
446
|
-
parent._verifiedThisRun = false
|
|
447
|
-
parent._verifyPassed = undefined
|
|
448
|
-
}
|
|
449
|
-
// Stale session cleanup only — the round counter survives (see above).
|
|
450
|
-
parent._advisorSession = null
|
|
451
|
-
return true
|
|
452
|
-
}
|
|
712
|
+
// Re-export shim (2026-09-03 拆分轮 + §19 合体轮): 机械与合体动作执行器迁至
|
|
713
|
+
// ./subagent-async.mjs——保留本文件导出面,消费点(agent.mjs / agent-turn.mjs /
|
|
714
|
+
// consult.mjs / 测试)导入路径零改动。execute 仍直接使用 ASYNC_SUBAGENT_LIMIT /
|
|
715
|
+
// maybeRefillAsync / buildChildRunOpts / runChildPipeline(文件头部 import)。
|
|
716
|
+
// 2026-09-05 拆分轮: maybeRefillAsync 随 §20 调度器独立(./subagent-scheduler.mjs)——
|
|
717
|
+
// 再导出源改写,消费面(agent.mjs 动态 import 等)不变。
|
|
718
|
+
export {
|
|
719
|
+
ASYNC_SUBAGENT_LIMIT,
|
|
720
|
+
MAX_ASYNC_CHECKS,
|
|
721
|
+
resolveChildProvider,
|
|
722
|
+
injectAsyncResult,
|
|
723
|
+
buildChildRunOpts,
|
|
724
|
+
mergeChildMutations,
|
|
725
|
+
} from "./subagent-async.mjs"
|
|
726
|
+
export { maybeRefillAsync } from "./subagent-scheduler.mjs"
|