thincoder 0.12.58 → 0.12.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -2
- package/README.md +3 -3
- package/bin/thincoder.mjs +88 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +135 -26
- package/src/advisor/messages.mjs +57 -4
- package/src/advisor/run.mjs +119 -79
- package/src/advisor.mjs +34 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +182 -22
- package/src/agent/helpers.mjs +71 -4
- package/src/agent/record-results.mjs +46 -10
- package/src/agent/run-stages.mjs +227 -0
- package/src/agent/setup-reminders.mjs +62 -0
- package/src/agent/setup.mjs +107 -20
- package/src/agent/spawn-child.mjs +54 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +133 -109
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +154 -104
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +26 -30
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +284 -0
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +152 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +470 -0
- package/src/agent-tools/subagent-async.mjs +382 -0
- package/src/agent-tools/subagent-panel.mjs +153 -0
- package/src/agent-tools/subagent-run.mjs +202 -0
- package/src/agent-tools/subagent-scheduler.mjs +343 -0
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +203 -377
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +198 -238
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +145 -242
- package/src/auto-think.mjs +14 -0
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +4 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +61 -8
- package/src/context.mjs +81 -163
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +30 -12
- package/src/escape.mjs +6 -4
- package/src/explore-distill.mjs +155 -0
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +11 -72
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +206 -87
- package/src/memory.mjs +3 -1
- package/src/model-specs.mjs +15 -1
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +25 -0
- package/src/prompts/advisor-design.md +18 -39
- package/src/prompts/advisor-round1.md +20 -32
- package/src/prompts/advisor-round2.md +16 -16
- package/src/prompts/advisor-round3.md +16 -16
- package/src/prompts/coder.md +7 -28
- package/src/prompts/consult-base.md +4 -11
- package/src/prompts/discipline.md +31 -44
- package/src/prompts/eng-coder.md +9 -34
- package/src/prompts/engineering-sub.md +10 -8
- package/src/prompts/engineering.md +61 -264
- package/src/prompts/explore.md +4 -14
- package/src/prompts/main.md +18 -35
- package/src/prompts/methodology-template.md +32 -38
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +18 -35
- package/src/provider/core.mjs +62 -69
- package/src/provider/errors.mjs +76 -0
- package/src/provider/retry.mjs +8 -45
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +181 -58
- package/src/session.mjs +48 -89
- package/src/token-ttl.mjs +273 -0
- package/src/tools/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +131 -44
- package/src/tools/edit-diff.mjs +348 -0
- package/src/tools/edit.md +20 -13
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +55 -24
- package/src/tools/file.mjs +25 -70
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.mjs +14 -6
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -3
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +2 -0
- package/src/tools/lsp.md +4 -1
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +84 -13
- package/src/tools/question.md +5 -1
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +18 -25
- package/src/tools/system.mjs +50 -30
- package/src/tools/tree.md +2 -1
- package/src/tools/wait_for.md +22 -0
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +179 -27
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +77 -16
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +17 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +11 -11
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +31 -96
- package/src/tui/interaction.mjs +13 -2
- package/src/tui/key-handler.mjs +105 -155
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +46 -0
- package/src/tui/pickers.mjs +51 -25
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +27 -10
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +182 -0
- package/src/tui/startup.mjs +40 -0
- package/src/tui/subagent-blocks.mjs +272 -262
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +125 -12
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +127 -231
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +76 -0
- package/src/tui/wizard.mjs +48 -12
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
|
@@ -1,106 +1,74 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
/**
|
|
2
|
+
* subagent.mjs — subagent tool(ONE tool, EIGHT actions + spawn 路径驱动器)。
|
|
3
|
+
* 2026-09-07 token 链终消费制:+action: consume-design(ENGINEERING-MODE.md §2.6 F1——
|
|
4
|
+
* 父侧链终核销消费 designId 槽——执行器 executeConsumeDesignAction 在 subagent-spawn.mjs)。
|
|
5
|
+
*
|
|
6
|
+
* 2026-09-03 拆分轮: subagent.mjs 超 500 硬顶——async 常量、共享 post-spawn 管线
|
|
7
|
+
*(runChildPipeline)与队列/注入/并账机械迁至 ./subagent-async.mjs。execute
|
|
8
|
+
*(async 分支 + 阻塞路径)原样保留于本文件;导出面由文末 re-export shim 兜住。
|
|
9
|
+
* 2026-09-03 §19 合体轮: subagent_check/escalate 工具退役——status/escalate
|
|
10
|
+
* 动作执行器并入 ./subagent-async.mjs,本文件只承载工具面(action schema)与
|
|
11
|
+
* spawn 路径 + 动作分流。
|
|
12
|
+
* 2026-09-06 §19.8 删 check 轮: check 动作删除——工具面五动作(spawn/status/
|
|
13
|
+
* escalate/cancel/panel)——async 结果仅自动通道送达。
|
|
14
|
+
* 2026-09-05 拆分轮: status/escalate/panel 动作执行器 → ./subagent-actions.mjs;§20
|
|
15
|
+
* 调度器全套 → ./subagent-scheduler.mjs——本文件 import 源随之改写。
|
|
16
|
+
* 2026-09-05 模块拆分轮(726 > 500 硬限): spawn 前置 helpers(summarizeEngTaskBook/
|
|
17
|
+
* effectiveSubagentModel/resolveDesignSlot)+ §20 准入(prepareScheduling)+ child
|
|
18
|
+
* 装配(buildSpawnChild)→ ./subagent-spawn.mjs;async 分支(executeAsyncSpawn)
|
|
19
|
+
* → ./subagent-run.mjs——execute 只保留动作分流 + 装配调用 + 阻塞路径。
|
|
20
|
+
*/
|
|
9
21
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
22
|
+
import { gateEngCoderSpawn, TURN_CAP_MARK, emitNestedChildEvent } from "../agent/spawn-child.mjs"
|
|
23
|
+
import { logEvent, errText } from "../log.mjs"
|
|
24
|
+
import {
|
|
25
|
+
runChildPipeline, executeCancelAction,
|
|
26
|
+
} from "./subagent-async.mjs"
|
|
27
|
+
import { executeStatusAction, executeEscalateAction, executePanelAction, executeObserveAction, executeSendAction } from "./subagent-actions.mjs"
|
|
28
|
+
import { prepareScheduling, buildSpawnChild, executeConsumeDesignAction } from "./subagent-spawn.mjs"
|
|
29
|
+
import { executeAsyncSpawn } from "./subagent-run.mjs"
|
|
14
30
|
|
|
15
31
|
/**
|
|
16
|
-
* subagent tool
|
|
17
|
-
* -
|
|
18
|
-
*
|
|
32
|
+
* subagent tool — ONE tool, EIGHT actions (AGENT-LOOP.md §19/§19.5/§19.6/§19.8 +
|
|
33
|
+
* SUBAGENT-OBSERVE-SEND): spawn (default) / status (non-blocking pool query) / observe
|
|
34
|
+
* (inspect a running/queued/done async child's recent activity + current tool — §7.2) /
|
|
35
|
+
* send (inject a direction into a RUNNING async child — consumed at its next turn
|
|
36
|
+
* boundary as an ordinary instruction — §7.2) / escalate (飞刀 — hand implementation to
|
|
37
|
+
* a stronger model) / cancel (stop ONE background subagent — §19.5) / panel (view + fix
|
|
38
|
+
* the live subagent panel — §19.6) / consume-design (parent-side chain-terminal token
|
|
39
|
+
* consumption — ENGINEERING-MODE.md §2.6, 2026-09-07). The check
|
|
40
|
+
* action was deleted (§19.8): async results reach the model only via the auto channel.
|
|
41
|
+
* - action:"spawn" roles: "explore" — read-only tools, search/read/analyze
|
|
42
|
+
* (suitable for codebase exploration); "coder" — full tool set, self-contained
|
|
43
|
+
* implementation tasks; "plan" — read-only planning; "eng-coder" —
|
|
44
|
+
* engineering-mode implementation (design-token gated).
|
|
19
45
|
* - no role specified — invalid by design since the 2026-08-25 fail-closed gate
|
|
20
46
|
* (role is mandatory; "no role → same tool set as parent" was removed with the
|
|
21
47
|
* coder-leak fix and the header text above predates it)
|
|
22
|
-
* - parallel subagent calls via the parallel channel (parallel: true)
|
|
23
48
|
* - non-recursive: child agents do not get the subagent tool (depth > 0 is not injected)
|
|
24
49
|
*/
|
|
25
50
|
|
|
26
|
-
/**
|
|
27
|
-
* Effective subagent model override for a role (CLI parity shared with VS Code):
|
|
28
|
-
* priority — subagent tool `model` arg > config.agent.subagentModels[role] > config.agent.subagentModel > null (inherit parent).
|
|
29
|
-
*/
|
|
30
|
-
export function effectiveSubagentModel(parent, role, modelArg) {
|
|
31
|
-
if (modelArg) return modelArg
|
|
32
|
-
const cfg = parent.config?.agent ?? {}
|
|
33
|
-
return cfg.subagentModels?.[role] ?? cfg.subagentModel ?? null
|
|
34
|
-
}
|
|
35
|
-
|
|
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
|
-
/**
|
|
61
|
-
* Resolve the design-token slot for an eng-coder spawn (2026-09-01 multi-design, FR3):
|
|
62
|
-
* - designId given → exact slot lookup (no match = explicit error, never a fuzzy guess)
|
|
63
|
-
* - designId omitted → exactly ONE slot must exist (single-design compatibility); with
|
|
64
|
-
* multiple slots we refuse rather than pick one (T16: never silently aim the wrong design)
|
|
65
|
-
* Returns { token } on success; throws with a parent-actionable message otherwise.
|
|
66
|
-
* The HMAC/TTL check itself stays in validateDesignToken (unchanged).
|
|
67
|
-
*/
|
|
68
|
-
export function resolveDesignSlot(parent, designIdArg) {
|
|
69
|
-
const slots = parent._engDesignTokens
|
|
70
|
-
const hasSlots = slots instanceof Map && slots.size > 0
|
|
71
|
-
const legacy = parent._engDesignToken
|
|
72
|
-
// eng(exit/enter) resets the single mirror to force a fresh review (eng.mjs) —
|
|
73
|
-
// a non-empty slot map surviving that reset must NOT resurrect stale tokens:
|
|
74
|
-
// mirror cleared + slots present = re-entered engineering mode → re-review.
|
|
75
|
-
if (!legacy && hasSlots) {
|
|
76
|
-
throw new Error("Design tokens were reset (engineering mode was re-entered) — run advisor with type='design' again and spawn with the fresh designId+token pair.")
|
|
77
|
-
}
|
|
78
|
-
if (designIdArg) {
|
|
79
|
-
if (!hasSlots || !slots.has(designIdArg)) {
|
|
80
|
-
throw new Error(`designId not found — no approved design review holds this id. Run advisor with type='design' again and pass the designId echoed with the token. (session holds ${hasSlots ? slots.size : 0} approved design slot(s))`)
|
|
81
|
-
}
|
|
82
|
-
return { token: slots.get(designIdArg) }
|
|
83
|
-
}
|
|
84
|
-
if (hasSlots && slots.size > 1) {
|
|
85
|
-
throw new Error(`Multiple approved designs in this session (${slots.size}) — pass the designId parameter (echoed with each token) to choose which design this eng-coder spawn belongs to.`)
|
|
86
|
-
}
|
|
87
|
-
if (hasSlots && slots.size === 1) return { token: [...slots.values()][0] }
|
|
88
|
-
if (legacy) return { token: legacy } // single-slot mirror fallback (pre-multi-slot sessions)
|
|
89
|
-
throw new Error("Invalid or missing design token — run advisor with type='design' first and pass the returned token as designToken.")
|
|
90
|
-
}
|
|
91
|
-
|
|
92
51
|
export const subagentTool = {
|
|
93
52
|
name: "subagent",
|
|
94
53
|
description:
|
|
95
|
-
"
|
|
54
|
+
"ONE tool, EIGHT actions (AGENT-LOOP.md §19/§19.5/§19.6/§19.8 + SUBAGENT-OBSERVE-SEND) — pick by what you need:\n" +
|
|
55
|
+
"- 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" +
|
|
56
|
+
"- 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 see progress — it never blocks and never consumes a result (async results are delivered to you automatically).\n" +
|
|
57
|
+
"- action:'observe': SEE what a running async subagent is DOING right now (progress vs stuck) — pass the spawn id. Returns {id, role, status, turn, maxTurns, touched…, currentTool? — array of tool name(s) currently executing (read from its dispatch state; omitted when none in flight — present when stuck in a long tool call), recentTurns: [last N one-line turn summaries, newest-first; default 5, parameterizable via recent]}. Readonly — observable on running/queued/done: running shows live activity, queued (not started) returns a placeholder, done (settled, report auto-delivered) returns the activity summary only — NOT the full report (that rides the auto channel; observe stays terse to keep your context lean). Use it to judge whether a long-running child is stuck vs progressing BEFORE deciding to cancel or steer it.\n" +
|
|
58
|
+
"- action:'send': STEER a running async subagent mid-flight — pass the spawn id + message (a direction like \"check X, don't fixate on Y\"). The message queues and the child consumes it at its next turn boundary as an ORDINARY user instruction (non-interrupting — its current tool finishes first; its convergence/audit discipline is unchanged — injection is guidance, not a deviation waiver). Returns {id, status:'delivered', queued}. Only a RUNNING async subagent is targetable — sync (you're waiting on it, no relay), queued (not started), settled or unknown ids error clearly. If the child settles before its next turn boundary, its settle report carries an 'undelivered' note so you don't assume the guidance landed.\n" +
|
|
59
|
+
"- 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). DEFAULT-ASYNC at depth 0 (AGENT-LOOP.md §25 D-R17b): the launch returns an ack {id, role:'escalate', status:'running'} and the flight runs in the background (pooled with the other role-domain spawns) — its post-op report is delivered to you automatically with its mutations merged into your bookkeeping; pass async:false to run it synchronously.\n" +
|
|
60
|
+
"- 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" +
|
|
61
|
+
"- action:'consume-design' (engineering mode, parent side — chain-terminal token consumption): after the delivery is verified and the chain closes out, consume this design's token slot — pass the designId (optional for a single-design session). The slot is consumed; a further spawn for the same designId is mechanically rejected, and any new work (including deviation fixes) requires a fresh design review and token. Idempotent: an unknown designId / already-consumed slot is a no-op notice, never an error. Do NOT call it while the chain is still open — fix rounds reuse the same slot (same designId + designToken).\n" +
|
|
62
|
+
"- 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
63
|
"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
64
|
"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...).
|
|
65
|
+
"- 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
66
|
"- 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
67
|
"- 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
68
|
"- 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
69
|
"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.
|
|
70
|
+
"Async spawn (AGENT-LOOP.md §15/§18/§24): 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. The child's report is delivered to you automatically — there is no fetch action; use action:'status' only to see progress, never to wait for the result. The DEFAULT is depth-gated (AGENT-LOOP.md §18 D-E1a): at the top level (depth 0) EVERY role spawns async by default — eng-coder's delivery protocol runs fully inside the child (implementation → audit → self-fix → advisor re-review → converged delivery); depth>0 spawns are always synchronous. Pass async:false only when you must handle the report synchronously. Use a blocking spawn when you must see the report before continuing. Async spawns are pooled per role domain (AGENT-LOOP.md §24): at most 4 concurrent eng-coders and 4 concurrent other-role spawns by default (agent.poolLimits overrides both) — a full domain queues further spawns with a position while the other domain keeps starting (domains never block each other), and top-level only. After an async spawn the turn winds down normally — nothing expects you to wait for it: the child runs in the background and its report is delivered to you automatically — before your next turn, or digested in the suspension session — so end the turn; do not poll or wait for the result. If your next step genuinely needs the report, use a synchronous spawn instead — pass `async:false` (at depth 0 every role defaults to async — async:false is the only way to block; depth>0 is always sync).\n\n" +
|
|
71
|
+
"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 (auto-delivered by the auto channel) counts as satisfied. Omit both parameters for the plain immediate spawn (no scheduler involvement).\n\n" +
|
|
104
72
|
"Writing the prompt:\n" +
|
|
105
73
|
"- 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
74
|
"- 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 +77,81 @@ export const subagentTool = {
|
|
|
109
77
|
parameters: {
|
|
110
78
|
type: "object",
|
|
111
79
|
properties: {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
80
|
+
action: { type: "string", enum: ["spawn", "status", "escalate", "cancel", "panel", "consume-design", "observe", "send"], description: "Which subagent-family action — spawn (default), status (non-blocking progress query — never consumes; async results arrive automatically — no fetch action), observe (inspect a running/queued/done async subagent's recent activity — recent turn summaries + current in-flight tool + turn/touched — §7.2), send (inject a direction into a RUNNING async subagent, consumed at its next turn boundary as an ordinary instruction — §7.2), 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), consume-design (engineering mode, parent side: chain-terminal token consumption — close out a design's token slot after the delivery is verified and the chain closes out — §2.6, 2026-09-07). See the tool description for the full action matrix." },
|
|
81
|
+
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)." },
|
|
82
|
+
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." },
|
|
83
|
+
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 status." },
|
|
84
|
+
context: { type: "string", description: "Optional background the sub-agent needs (it cannot see this conversation); action:'spawn' only." },
|
|
85
|
+
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)." },
|
|
86
|
+
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
87
|
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
|
-
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
|
|
88
|
+
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. action:'consume-design': the design whose slot to close out — optional for a single-design session; required to pick when several approved designs are active (the consume gate refuses to guess)." },
|
|
89
|
+
async: { type: "boolean", description: "action:'spawn': true = spawn without waiting — returns {id, status:\"running\"} immediately; the report is delivered to you automatically (there is no fetch action). Default: depth-0 → true (async — every role, AGENT-LOOP.md §18 D-E1a); depth>0 → sync (forced). Pass async:false to force the blocking spawn when you must process the report before continuing. action:'escalate': same semantics (AGENT-LOOP.md §25 D-R17b) — default async at depth 0; async:false keeps the legacy synchronous flight." },
|
|
90
|
+
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)." },
|
|
91
|
+
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 already consumed (auto-delivered to the model) 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." },
|
|
92
|
+
id: { type: "string", description: "action:'status'/'observe'/'send'/'cancel': the subagent id from the async spawn return. status: omit = overview of the whole pool; observe/send/cancel: REQUIRED (observe needs the child to inspect; send needs the target; never omit on cancel — a blanket cancel is unsupported)." },
|
|
93
|
+
message: { type: "string", description: "action:'send' only (REQUIRED there): the direction to inject — the running async subagent consumes it at its next turn boundary as an ordinary user instruction (non-interrupting; its convergence/audit discipline is unchanged — injection is guidance, not a deviation waiver)." },
|
|
94
|
+
recent: { type: "integer", description: "action:'observe' only (optional, default 5): how many recent-turn summaries to return (clamped 1..20 — N2 keeps observe terse)." },
|
|
119
95
|
},
|
|
120
|
-
required: [
|
|
96
|
+
required: [],
|
|
121
97
|
},
|
|
122
98
|
readonly: false,
|
|
123
99
|
sideEffectExempt: true, // child agent may write files; parent can't introspect its _mutatedThisRun
|
|
124
100
|
parallel: true,
|
|
125
101
|
async execute(args, ctx) {
|
|
102
|
+
// §19 action dispatch: default spawn keeps every legacy call unchanged
|
|
103
|
+
// (no action parameter → the spawn path below, byte-identical semantics).
|
|
104
|
+
const action = args?.action !== undefined && args?.action !== null && String(args.action) !== ""
|
|
105
|
+
? String(args.action)
|
|
106
|
+
: "spawn"
|
|
107
|
+
if (action !== "spawn") {
|
|
108
|
+
// §19 restricted-variant action gate (round2 #3): the eng-coder audit
|
|
109
|
+
// channel (depth>0, role eng-coder) is spawn-only — escalate spawns a
|
|
110
|
+
// coder+WRITE child (violates explore-only intent) and status/panel/
|
|
111
|
+
// observe/send have no async pool / panel mirror to query in a child context.
|
|
112
|
+
if ((ctx.depth ?? 0) > 0 && ctx.agent?._role === "eng-coder") {
|
|
113
|
+
throw new Error(`only action:'spawn' (sync explore audits) is available inside an eng-coder — escalate/status/cancel/panel/consume-design/observe/send are not (AGENT-LOOP.md §19 D-M3)`)
|
|
114
|
+
}
|
|
115
|
+
// §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn —
|
|
116
|
+
// async OR blocking — the digest must stay organize-only. The escalate
|
|
117
|
+
// action spawns a write child too, so the same mechanical refusal applies
|
|
118
|
+
// (AUTO tier exempt — user authorized unattended continuation).
|
|
119
|
+
if (action === "escalate" && ctx.agent?._inAutoTurn && !ctx.agent?.autoApprove) {
|
|
120
|
+
return JSON.stringify({ status: "error", error: "cannot spawn subagents from a manual auto-turn — wait for user input" })
|
|
121
|
+
}
|
|
122
|
+
if (action === "status") return executeStatusAction(args, ctx)
|
|
123
|
+
if (action === "escalate") return await executeEscalateAction(args, ctx)
|
|
124
|
+
// §19.5 控制类动作:digest 内放行(D-S7 分类——控制/自省;dispatch 控制类
|
|
125
|
+
// 豁免同批生效——19.5.2b round2 #4;escalate 的 digest 拒绝在上一分支)
|
|
126
|
+
if (action === "cancel") return executeCancelAction(args, ctx)
|
|
127
|
+
// 2026-09-07 token 链终消费制(ENGINEERING-MODE.md §2.6 F1):父侧核销消费——
|
|
128
|
+
// 非只读控制动作——depth-0 + 工程模式限定(本分流已过受限变体门;工程模式门在
|
|
129
|
+
// 执行器内)——planMode 拒绝(dispatch 不豁免)——不入批审批分组(dispatch 免审)。
|
|
130
|
+
if (action === "consume-design") return executeConsumeDesignAction(args, ctx)
|
|
131
|
+
// §19.6 panel 动作:view(readonly 面——digest 内放行——自省类)与 freeze
|
|
132
|
+
// (控制类——同 cancel——digest 内放行)。深度/门控检查在 executePanelAction 内。
|
|
133
|
+
if (action === "panel") return executePanelAction(args, ctx)
|
|
134
|
+
// SUBAGENT-OBSERVE-SEND:observe = readonly 查询(同 status——digest/planMode 放行);
|
|
135
|
+
// send = 控制类豁免(同 cancel——父回合内显式调用即授权)。深度门在各自执行器内。
|
|
136
|
+
if (action === "observe") return executeObserveAction(args, ctx)
|
|
137
|
+
if (action === "send") return executeSendAction(args, ctx)
|
|
138
|
+
throw new Error(`Unknown subagent action: ${JSON.stringify(action)}. Valid actions: spawn, status, escalate, cancel, panel, consume-design, observe, send.`)
|
|
139
|
+
}
|
|
140
|
+
|
|
126
141
|
const parent = ctx.agent
|
|
127
142
|
const role = args.role
|
|
143
|
+
// Spawn requires a task brief — schema `required` is advisory (multi-action
|
|
144
|
+
// schema), so the mechanical check lives here: an absent task would otherwise
|
|
145
|
+
// flow downstream as `content: undefined` and surface as an obscure error.
|
|
146
|
+
if (typeof args.task !== "string" || !args.task.trim()) {
|
|
147
|
+
throw new Error("subagent action:'spawn' requires a task (the self-contained task brief).")
|
|
148
|
+
}
|
|
149
|
+
// §18 D-E1a depth-gated async default (2026-09-06 需求池 R12): depth-0 spawns
|
|
150
|
+
// default to async for EVERY role (the old role-level default — eng-coder only —
|
|
151
|
+
// is superseded); depth>0 spawns default to sync (子代理内部强制同步现状保留).
|
|
152
|
+
// async:false remains the explicit escape hatch; async:true at depth>0 is
|
|
153
|
+
// refused downstream (executeAsyncSpawn top-level gate).
|
|
154
|
+
const wantAsync = args.async ?? ((ctx.depth ?? 0) === 0)
|
|
128
155
|
|
|
129
156
|
// Role normalization + whitelist (2026-08-25, coder-leak fix): exact-string gates let
|
|
130
157
|
// variant roles ("Coder", " coder") bypass BOTH mode gates and fall through to
|
|
@@ -134,6 +161,13 @@ export const subagentTool = {
|
|
|
134
161
|
if (!ROLES.has(role)) {
|
|
135
162
|
throw new Error(`Unknown subagent role: ${JSON.stringify(role)}. Valid roles: explore, plan, coder, eng-coder (exact spelling).`)
|
|
136
163
|
}
|
|
164
|
+
// §18 D-E3 internal-spawn gate: an eng-coder sub-agent may only spawn sync
|
|
165
|
+
// explore (audit) children — non-explore roles and async are refused here
|
|
166
|
+
// (mechanical), the audit budget is enforced (7th audit spawn refused), and
|
|
167
|
+
// the returned attempt number marks this spawn as an audit for the task-book
|
|
168
|
+
// augmentation below. Runs BEFORE the mode gates so the eng-coder-specific
|
|
169
|
+
// error (not the generic engineering-mode one) surfaces.
|
|
170
|
+
const engAuditAttempt = gateEngCoderSpawn(ctx.agent, ctx.depth, role, args.async)
|
|
137
171
|
// Role is mutually exclusive per mode: normal mode → "coder", engineering mode → "eng-coder"
|
|
138
172
|
if (parent.config?.agent?.engineering && role === "coder") {
|
|
139
173
|
throw new Error("Engineering mode: use role='eng-coder' for implementation tasks.")
|
|
@@ -142,105 +176,26 @@ export const subagentTool = {
|
|
|
142
176
|
throw new Error("Engineering mode is not active — use role='coder' for implementation tasks.")
|
|
143
177
|
}
|
|
144
178
|
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
let issuedToken
|
|
153
|
-
if (role === "eng-coder") {
|
|
154
|
-
issuedToken = resolveDesignSlot(parent, args.designId).token
|
|
155
|
-
if (!issuedToken || args.designToken !== issuedToken || !validateDesignToken(args.designToken)) {
|
|
156
|
-
throw new Error("Invalid or missing design token — run advisor with type='design' first and pass the returned token as designToken.")
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Filter tool set by role: explore/plan are read-only (plan is a planning agent, its deliverable is the plan itself)
|
|
161
|
-
let tools
|
|
162
|
-
if (role === "explore" || role === "plan") {
|
|
163
|
-
const allowed = readonlyToolNames(parent.tools)
|
|
164
|
-
tools = parent.tools.filter((t) => allowed.has(t.name))
|
|
165
|
-
} else {
|
|
166
|
-
tools = parent.tools
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Select prompt overlay by role
|
|
170
|
-
let overlay = ""
|
|
171
|
-
if (role === "explore") overlay = EXPLORE_OVERLAY
|
|
172
|
-
else if (role === "coder") overlay = CODER_OVERLAY
|
|
173
|
-
else if (role === "plan") overlay = PLAN_OVERLAY
|
|
174
|
-
else if (role === "eng-coder") overlay = ENG_CODER_OVERLAY
|
|
175
|
-
|
|
176
|
-
// explore/plan: force read-only permission; coder/default: AUTO passes through directly,
|
|
177
|
-
// manual mode queues permission requests for the parent agent's approval UI (human in the loop, child agent is no longer silently rejected)
|
|
178
|
-
let childPermission
|
|
179
|
-
if (role === "explore" || role === "plan") {
|
|
180
|
-
childPermission = async () => false
|
|
181
|
-
} else if (parent.autoApprove) {
|
|
182
|
-
childPermission = async () => true
|
|
183
|
-
} else {
|
|
184
|
-
childPermission = async (name, toolArgs) => {
|
|
185
|
-
if (!ctx.onPermissionRequest) return false
|
|
186
|
-
const ask = () => ctx.onPermissionRequest(`${role ?? "sub"}/${name}`, toolArgs)
|
|
187
|
-
// Queue parallel child agent permission requests to avoid two popups simultaneously overwriting each other (lesson from question tool)
|
|
188
|
-
parent._permQueue = (parent._permQueue ?? Promise.resolve()).then(ask, ask)
|
|
189
|
-
return parent._permQueue
|
|
190
|
-
}
|
|
179
|
+
// §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn — async
|
|
180
|
+
// OR blocking — the digest must stay organize-only. AUTO tier (autoApprove) is
|
|
181
|
+
// exempt (推进型 — user authorized unattended continuation). Mechanical refusal
|
|
182
|
+
// so the digest never pops a permission panel or chains new background work.
|
|
183
|
+
// (escalate 动作的同类拒绝在 action 分流处——本检查只管 spawn 路径。)
|
|
184
|
+
if (parent._inAutoTurn && !parent.autoApprove) {
|
|
185
|
+
return JSON.stringify({ status: "error", error: "cannot spawn subagents from a manual auto-turn — wait for user input" })
|
|
191
186
|
}
|
|
192
187
|
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const child = createAgent({
|
|
199
|
-
provider: childProvider,
|
|
200
|
-
tools,
|
|
201
|
-
config: childConfig,
|
|
202
|
-
cwd: parent.cwd,
|
|
203
|
-
memory: parent.memory,
|
|
204
|
-
overlay,
|
|
205
|
-
role,
|
|
206
|
-
})
|
|
188
|
+
// §20 准入(2026-09-05 module-split——prepareScheduling verbatim 迁
|
|
189
|
+
// subagent-spawn.mjs:参数形态/unknown id/依赖环/阻塞 sync 判定;files 目录声明
|
|
190
|
+
// fail-closed——检测器错误即工具结果 JSON)
|
|
191
|
+
const prep = prepareScheduling(parent, args.files, args.dependsOn, wantAsync)
|
|
192
|
+
if (prep.errorJson) return prep.errorJson
|
|
207
193
|
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
child._engDesignId = args.designId ?? null
|
|
214
|
-
child._engDesignToken = issuedToken
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// explore/plan: inject git context (branch/recent commits/working tree state) — exploration and planning both relate to current repo state (inspired by kimi-code's promptPrefix)
|
|
218
|
-
let input = args.context ? `Context:\n${args.context}\n\nTask:\n${args.task}` : args.task
|
|
219
|
-
if (role === "explore" || role === "plan") {
|
|
220
|
-
const gitCtx = collectGitContext(parent.cwd)
|
|
221
|
-
if (gitCtx) input = `<untrusted_git_context>\n${escapeXml(gitCtx)}\n</untrusted_git_context>\n\n${input}`
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Relay content/reasoning/tool/output to the parent TUI via the unified spawn-child
|
|
225
|
-
// pipeline (AGENT-LOOP.md §7.2 D3). Prefix includes a unique id: parallel child agents
|
|
226
|
-
// with the same role stay independent and don't overwrite each other.
|
|
227
|
-
// Format: role#id/ → onToken("coder#2/writing..."), onToolCall("coder#2/read", args)
|
|
228
|
-
// Async id allocation (AGENT-LOOP.md §15 D-A1): reserve the relay counter at
|
|
229
|
-
// spawn time — the returned id must be stable while the item sits in the queue.
|
|
230
|
-
// The [model] token (TUI block creation) is DEFERRED to actual start so queued
|
|
231
|
-
// children don't paint an empty panel block ("queued 态不显示").
|
|
232
|
-
let relayPrefix
|
|
233
|
-
if (args.async === true) {
|
|
234
|
-
parent._subAgentCounter = (parent._subAgentCounter ?? 0) + 1
|
|
235
|
-
relayPrefix = `${role}#${parent._subAgentCounter}/`
|
|
236
|
-
} else {
|
|
237
|
-
relayPrefix = makeRelay(parent, role ?? "sub", ctx.callbacks?.onToken, childProvider.model ?? "")
|
|
238
|
-
}
|
|
239
|
-
const childOpts = {
|
|
240
|
-
onPermissionRequest: childPermission,
|
|
241
|
-
...wrapChildCallbacks(relayPrefix, ctx.callbacks),
|
|
242
|
-
}
|
|
243
|
-
const childRunOpts = buildChildRunOpts(ctx)
|
|
194
|
+
// child 装配(2026-09-05 module-split——buildSpawnChild verbatim 迁
|
|
195
|
+
// subagent-spawn.mjs:provider/model 覆盖、角色门、design-token 门、工具集/
|
|
196
|
+
// overlay/permission 装配、审计任务书注入、relay 前缀分配、childOpts/runOpts)
|
|
197
|
+
const built = buildSpawnChild(parent, ctx, args, role, wantAsync, prep.files, prep.dependsOn, engAuditAttempt)
|
|
198
|
+
const { child, input, childOpts, childRunOpts, relayPrefix } = built
|
|
244
199
|
// Turn-cap continue loop (TURN-CAP-CONTINUE.md) via runWithContinue (§7.2 D3):
|
|
245
200
|
// hitting the cap asks the user via the SAME y/n panel the main agent uses —
|
|
246
201
|
// unlimited continues, resume:true keeps the child's history + mutation bookkeeping,
|
|
@@ -255,198 +210,69 @@ export const subagentTool = {
|
|
|
255
210
|
return parent._permQueue
|
|
256
211
|
}
|
|
257
212
|
|
|
258
|
-
// ── Async branch
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
// consumed via subagent_check or the turn-end auto-wait. Slot queue: running
|
|
263
|
-
// count < ASYNC_SUBAGENT_LIMIT → start now; ≥ limit → enqueue (status "queued",
|
|
264
|
-
// position = queue index) — never rejected, never requiring the model to batch.
|
|
265
|
-
if (args.async === true) {
|
|
266
|
-
if ((ctx.depth ?? 0) > 0) {
|
|
267
|
-
throw new Error("async spawn only available at the top level")
|
|
268
|
-
}
|
|
269
|
-
parent._asyncSubagents ??= new Map()
|
|
270
|
-
parent._asyncQueue ??= []
|
|
271
|
-
const running = [...parent._asyncSubagents.values()].filter((e) => e.status === "running").length
|
|
272
|
-
const id = parent._subAgentCounter
|
|
273
|
-
const entry = {
|
|
274
|
-
id, role, relayPrefix,
|
|
275
|
-
status: running >= ASYNC_SUBAGENT_LIMIT ? "queued" : "running",
|
|
276
|
-
position: undefined,
|
|
277
|
-
report: null, error: null, done: false,
|
|
278
|
-
promise: null, _settle: null, _settleSeq: 0,
|
|
279
|
-
}
|
|
280
|
-
// The settle signal — resolves when the run chain settles (never rejects).
|
|
281
|
-
entry.promise = new Promise((res) => { entry._settle = res })
|
|
282
|
-
entry.start = () => {
|
|
283
|
-
entry.status = "running"
|
|
284
|
-
entry.position = undefined
|
|
285
|
-
// Deferred [model] emit: the TUI block is created at ACTUAL start.
|
|
286
|
-
ctx.callbacks?.onToken?.(relayPrefix + "[model]" + (childProvider.model ?? ""))
|
|
287
|
-
// Turn-cap on background children NEVER pops the continue panel (D-A3):
|
|
288
|
-
// auto-decline, the partial-work report carries the cap reason.
|
|
289
|
-
runChildPipeline(child, input, childOpts, childRunOpts, {
|
|
290
|
-
parent, role, args,
|
|
291
|
-
askContinue: () => Promise.resolve(false),
|
|
292
|
-
})
|
|
293
|
-
.then((report) => { entry.report = report })
|
|
294
|
-
.catch((err) => { entry.error = err?.message ?? String(err) })
|
|
295
|
-
.finally(() => {
|
|
296
|
-
entry.status = "done" // running 数口径(D-A1/D-A2/T6):已完成未消费不计入
|
|
297
|
-
entry.done = true
|
|
298
|
-
// D-A3 发射时机(2026-09-02 用户实证修正):settle 同刻发射 ⟦ev⟧done——
|
|
299
|
-
// TUI routeSubToken 立即冻结区块,冻结位置 = 完成时刻的会话流位置
|
|
300
|
-
// (回合收尾统一发会把块堆在结论之后)。父会话已 abort 不发:TUI 已按
|
|
301
|
-
// interrupted 冻结,晚到 token 经 tombstone 丢弃——显式守卫更干净。
|
|
302
|
-
if (!ctx.signal?.aborted) {
|
|
303
|
-
ctx.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧done\x1e0\x1e0\x1edone\x1e`)
|
|
304
|
-
}
|
|
305
|
-
entry._settleSeq = (parent._asyncSettleSeq = (parent._asyncSettleSeq ?? 0) + 1)
|
|
306
|
-
entry._settle()
|
|
307
|
-
for (const w of parent._asyncWaiters?.splice(0) ?? []) { try { w() } catch { /* noop */ } }
|
|
308
|
-
maybeRefillAsync(parent)
|
|
309
|
-
})
|
|
310
|
-
}
|
|
311
|
-
parent._asyncSubagents.set(String(id), entry)
|
|
312
|
-
if (entry.status === "queued") {
|
|
313
|
-
parent._asyncQueue.push(entry)
|
|
314
|
-
entry.position = parent._asyncQueue.length
|
|
315
|
-
return JSON.stringify({ id: String(id), role, status: "queued", position: entry.position })
|
|
316
|
-
}
|
|
317
|
-
entry.start()
|
|
318
|
-
return JSON.stringify({ id: String(id), role, status: "running" })
|
|
213
|
+
// ── Async branch(2026-09-05 module-split——executeAsyncSpawn verbatim 迁
|
|
214
|
+
// subagent-run.mjs:条目构建/等位/启动/controller 链/turn 镜像/补位释放)──
|
|
215
|
+
if (wantAsync) {
|
|
216
|
+
return executeAsyncSpawn(parent, ctx, role, args, child, input, childOpts, childRunOpts, relayPrefix, built.childProvider, prep.files, prep.dependsOn)
|
|
319
217
|
}
|
|
320
218
|
|
|
321
219
|
// ── Blocking path (unchanged semantics): await the full pipeline ──
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
})
|
|
220
|
+
// LOGGING(LOGGING.md):child:*(阻塞 spawn——runChildPipeline 前后;declined
|
|
221
|
+
// partial 由 TURN_CAP_MARK 检出;错误原样上抛(dispatch 转 tool:error))
|
|
222
|
+
const blockT0 = Date.now()
|
|
223
|
+
logEvent("child:spawn", { role, id: child._logId, kind: "blocking" })
|
|
224
|
+
try {
|
|
225
|
+
const pipelineReport = await runChildPipeline(child, input, childOpts, childRunOpts, {
|
|
226
|
+
parent, role, args,
|
|
227
|
+
askContinue: askSubagentContinue,
|
|
228
|
+
})
|
|
229
|
+
// §27 R23 D-R23c1(评审 #1 🅰——生成侧补发射):sync spawn 同步收尾——若本 spawn
|
|
230
|
+
// 处于嵌套上下文(ctx.callbacks 已是嵌套 wrapper——eng-coder 内 explore 审计)→
|
|
231
|
+
// 发内层 ⟦ev⟧done(完整嵌套前缀——wrapper 链自动补外层)→ 主 TUI 路由子块定格
|
|
232
|
+
// (T-R23c.1)。非嵌套(depth-0)零变化——冻结仍由 dispatch subKey 精确冻承接。
|
|
233
|
+
emitNestedChildEvent(ctx, relayPrefix, "done")
|
|
234
|
+
logEvent("child:done", { role, id: child._logId, ms: Date.now() - blockT0, kind: String(pipelineReport).includes(TURN_CAP_MARK) ? "partial" : "ok" })
|
|
235
|
+
// §7.2.3 sync spawn 完成精确冻结(方案 e):execute 返回前 ctx 留子代理 key
|
|
236
|
+
// (relayPrefix 去尾 = `role#N`)——dispatch runOne 读它作 onToolResult 第 4 参 →
|
|
237
|
+
// TUI finishSubTaskKey 按 key 精确冻(async eng-coder 先启动时不再误冻其块——
|
|
238
|
+
// T-F2)。仅成功路径设置:async 分支不设(round2 #2——ack 带 status:running 由
|
|
239
|
+
// isAsyncSpawnResult 跳过冻结);错误/拒绝路径到此之前已 throw/return——ctx 未设
|
|
240
|
+
// ——错误路径不触发冻结(round1 #1——T-F5)。
|
|
241
|
+
ctx._subagentKey = relayPrefix.slice(0, -1)
|
|
242
|
+
return pipelineReport
|
|
243
|
+
} catch (e) {
|
|
244
|
+
if (ctx.signal?.aborted || e?.name === "AbortError") {
|
|
245
|
+
// §27 R23:外层 abort 传播的中断——内层开块随之外层冻结前先收尾定格
|
|
246
|
+
// (D-R23c1 stopped——T-R23c.2a 生成侧路径;TUI 冻结兜底仍在 freezeSubTaskLines)
|
|
247
|
+
emitNestedChildEvent(ctx, relayPrefix, "stopped")
|
|
248
|
+
throw e // 用户停——不落错误事件
|
|
249
|
+
}
|
|
250
|
+
// §27 R23 error-run 映射(实现批补一行):run 错误(非 abort)→ 同样发 stopped
|
|
251
|
+
// ——内层子块定格不悬空(T-R23a.3——工具错/运行错误路径)。
|
|
252
|
+
emitNestedChildEvent(ctx, relayPrefix, "stopped")
|
|
253
|
+
logEvent("child:error", { role, id: child._logId, ms: Date.now() - blockT0, err: errText(e, 200) })
|
|
254
|
+
throw e
|
|
255
|
+
}
|
|
326
256
|
},
|
|
327
257
|
}
|
|
328
258
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
}
|
|
259
|
+
// Re-export shim (2026-09-03 拆分轮 + §19 合体轮 + 2026-09-05 拆分轮): 机械与动作
|
|
260
|
+
// 执行器迁至 ./subagent-async.mjs、./subagent-actions.mjs、./subagent-scheduler.mjs
|
|
261
|
+
// ——本文件保留导出面,消费点(agent.mjs / agent-turn.mjs / consult.mjs / 测试)导入
|
|
262
|
+
// 路径零改动;池逻辑/准入见 subagent-run.mjs(executeAsyncSpawn)与 subagent-scheduler.mjs
|
|
263
|
+
// (maybeRefillAsync——execute 不再直接使用池常量)。
|
|
264
|
+
// 2026-09-05 拆分轮: maybeRefillAsync 随 §20 调度器独立(./subagent-scheduler.mjs)——
|
|
265
|
+
// 再导出源改写,消费面(agent.mjs 动态 import 等)不变。
|
|
266
|
+
// 2026-09-06 §24 拆分轮: ASYNC_SUBAGENT_LIMIT 导出 → ASYNC_POOL_LIMITS(分域常量——
|
|
267
|
+
// 定义在 subagent-async.mjs——re-export 面同步)。
|
|
268
|
+
export {
|
|
269
|
+
ASYNC_POOL_LIMITS,
|
|
270
|
+
resolveChildProvider,
|
|
271
|
+
injectAsyncResult,
|
|
272
|
+
buildChildRunOpts,
|
|
273
|
+
mergeChildMutations,
|
|
274
|
+
} from "./subagent-async.mjs"
|
|
275
|
+
export { maybeRefillAsync } from "./subagent-scheduler.mjs"
|
|
276
|
+
// 2026-09-05 module-split:spawn 装配 helpers 迁 subagent-spawn.mjs——re-export 保测试
|
|
277
|
+
// import 面(subagent-core.test.mjs 从本文件动态 import)
|
|
278
|
+
export { effectiveSubagentModel, resolveDesignSlot } from "./subagent-spawn.mjs"
|