thincoder 0.12.59 → 0.12.61
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 +62 -4
- package/README.md +10 -8
- package/bin/thincoder.mjs +99 -133
- package/package.json +6 -4
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +45 -21
- package/src/acp.mjs +6 -1
- package/src/advisor/citations.mjs +83 -21
- package/src/advisor/compaction.mjs +174 -0
- package/src/advisor/loop.mjs +293 -0
- package/src/advisor/messages.mjs +59 -137
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +156 -359
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +27 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +145 -27
- package/src/agent/helpers.mjs +107 -13
- package/src/agent/record-results.mjs +55 -11
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +242 -0
- package/src/agent/setup-reminders.mjs +69 -0
- package/src/agent/setup.mjs +107 -127
- package/src/agent/spawn-child.mjs +55 -13
- package/src/agent-tools/advisor-async.mjs +346 -0
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +167 -116
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +139 -107
- package/src/agent-tools/design-token.mjs +117 -0
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +24 -29
- package/src/agent-tools/escalate-async.mjs +289 -0
- package/src/agent-tools/read-history.mjs +166 -32
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +137 -34
- package/src/agent-tools/subagent-actions.mjs +180 -133
- package/src/agent-tools/subagent-async.mjs +184 -177
- package/src/agent-tools/subagent-panel.mjs +160 -0
- package/src/agent-tools/subagent-run.mjs +205 -0
- package/src/agent-tools/subagent-scheduler.mjs +100 -27
- package/src/agent-tools/subagent-spawn.mjs +453 -0
- package/src/agent-tools/subagent.mjs +256 -578
- package/src/agent-tools/verify.mjs +119 -292
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +89 -205
- package/src/cli/distill-command.mjs +12 -6
- package/src/cli/make-agent.mjs +26 -8
- package/src/cli/memory-command.mjs +4 -3
- package/src/cli/permission.mjs +2 -2
- package/src/cli/setup-wizard.mjs +42 -17
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +180 -63
- package/src/context.mjs +5 -147
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +128 -0
- package/src/distill.mjs +11 -11
- package/src/expand-home.mjs +16 -0
- package/src/explore-distill.mjs +155 -0
- package/src/generate-title.mjs +1 -1
- package/src/hooks.mjs +7 -3
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +72 -32
- package/src/memory/core.mjs +6 -193
- package/src/memory/delete.mjs +236 -0
- package/src/memory/docs.mjs +68 -54
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/memory.mjs +3 -1
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +82 -0
- package/src/prompts/advisor-design.md +18 -75
- package/src/prompts/advisor-round1.md +14 -67
- package/src/prompts/advisor-round2.md +15 -51
- package/src/prompts/advisor-round3.md +15 -51
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +5 -23
- package/src/prompts/discipline-engineering.md +217 -0
- package/src/prompts/discipline-normal.md +179 -0
- package/src/prompts/persona-coder.md +21 -0
- package/src/prompts/persona-eng-coder.md +37 -0
- package/src/prompts/persona-eng-designer.md +55 -0
- package/src/prompts/persona-engineering.md +54 -0
- package/src/prompts/persona-explore.md +15 -0
- package/src/prompts/persona-normal.md +27 -0
- package/src/prompts/persona-plan.md +26 -0
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +18 -98
- package/src/provider/errors.mjs +101 -0
- package/src/provider/google.mjs +5 -6
- package/src/provider/index.mjs +2 -1
- package/src/provider/list-models.mjs +93 -0
- package/src/provider/rate.mjs +2 -1
- package/src/provider/responses.mjs +5 -3
- package/src/provider/retry.mjs +8 -45
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- 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 +188 -60
- package/src/session.mjs +104 -124
- package/src/token-ttl.mjs +274 -0
- package/src/tools/{system.mjs → bash.mjs} +19 -221
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/edit-batch.mjs +109 -10
- package/src/tools/edit-diff.mjs +110 -27
- package/src/tools/edit.md +17 -12
- package/src/tools/execute.mjs +31 -4
- package/src/tools/file.mjs +41 -16
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +23 -34
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/index.mjs +9 -8
- package/src/tools/ops.mjs +188 -3
- package/src/tools/patch.mjs +3 -3
- package/src/tools/question.md +4 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -2
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tools/shared.mjs +14 -13
- package/src/tools/wait_for.md +22 -0
- package/src/tui/agent-turn.mjs +36 -228
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +7 -1
- package/src/tui/cmd-advisor.mjs +3 -2
- package/src/tui/cmd-config.mjs +142 -30
- package/src/tui/cmd-eng.mjs +28 -40
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-mcp.mjs +8 -2
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-think.mjs +10 -10
- package/src/tui/cmd-undo.mjs +4 -3
- 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 +40 -38
- package/src/tui/interaction.mjs +3 -3
- package/src/tui/key-handler.mjs +61 -17
- package/src/tui/key-modes.mjs +86 -8
- package/src/tui/layout.mjs +18 -10
- package/src/tui/model-catalog.mjs +89 -0
- package/src/tui/model-picker.mjs +498 -0
- package/src/tui/mouse.mjs +52 -9
- package/src/tui/pickers.mjs +28 -392
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +12 -9
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/startup.mjs +4 -0
- package/src/tui/subagent-blocks.mjs +106 -295
- package/src/tui/subagent-children.mjs +162 -0
- package/src/tui/subagent-freeze.mjs +169 -0
- package/src/tui/subagent-panel.mjs +24 -31
- package/src/tui/suspension-drive.mjs +297 -0
- package/src/tui/tool-args.mjs +7 -5
- package/src/tui/tool-display.mjs +143 -0
- package/src/tui/tool-events.mjs +56 -185
- package/src/tui/tui-lifecycle.mjs +46 -4
- package/src/tui/update-notice.mjs +4 -0
- package/src/tui/wizard.mjs +61 -21
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -56
- package/src/prompts/discipline.md +0 -102
- package/src/prompts/eng-coder.md +0 -44
- package/src/prompts/engineering-sub.md +0 -34
- package/src/prompts/engineering.md +0 -340
- package/src/prompts/explore.md +0 -21
- package/src/prompts/main.md +0 -56
- package/src/prompts/methodology-template.md +0 -58
- package/src/prompts/plan.md +0 -16
- package/src/prompts/system.md +0 -63
- package/src/tools/pdf-parse-text.mjs +0 -497
- package/src/tools/pdf-parse-xref.mjs +0 -499
- package/src/tools/pdf.mjs +0 -155
- package/src/tools/read_pdf.md +0 -21
|
@@ -1,171 +1,138 @@
|
|
|
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
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { gateEngCoderSpawn, TURN_CAP_MARK, STOPPED_MARK, emitNestedChildEvent } from "../agent/spawn-child.mjs"
|
|
9
23
|
import { logEvent, errText } from "../log.mjs"
|
|
24
|
+
import { abortError, deathLine } from "../abort-provenance.mjs"
|
|
10
25
|
import {
|
|
11
|
-
runChildPipeline,
|
|
12
|
-
buildChildRunOpts, executeCheckAction, executeCancelAction,
|
|
26
|
+
runChildPipeline, executeCancelAction, enqueueAsk, mergeChildMutations,
|
|
13
27
|
} from "./subagent-async.mjs"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from "./subagent-
|
|
28
|
+
// SYNC-CANCEL(L52——2026-09-09):阻塞路径自属 AbortController 链到会话/回合基信号
|
|
29
|
+
// 的单点(async-settle.mjs D6——_sessionSignal ?? ctx.signal——与 async 条目 controller
|
|
30
|
+
// 链同一语义——挂起场景 base 命中而 ctx.signal 未 abort——R2)。
|
|
31
|
+
import { buildChildSignal } from "./async-settle.mjs"
|
|
32
|
+
import { executeStatusAction, executeEscalateAction, executePanelAction, executeObserveAction, executeSendAction } from "./subagent-actions.mjs"
|
|
33
|
+
import { prepareScheduling, buildSpawnChild, executeConsumeDesignAction } from "./subagent-spawn.mjs"
|
|
34
|
+
import { executeAsyncSpawn } from "./subagent-run.mjs"
|
|
21
35
|
|
|
22
|
-
//
|
|
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。
|
|
36
|
+
// ─── SYNC-CANCEL 纯函数(可测——无 io)──────────────────────────────────────────
|
|
32
37
|
|
|
33
38
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* of the SAME OR HIGHER level ("## 文件清单" survives a "### 修改" sub-header).
|
|
44
|
-
* Marker not found → the section is reported as missing (never fabricate).
|
|
39
|
+
* SYNC-CANCEL F2 catch 三分支分类(可测纯函数——R3 收紧):
|
|
40
|
+
* ① "base" 整回合停:ctx.signal 或 baseSignal(= parent._sessionSignal ?? ctx.signal——
|
|
41
|
+
* buildChildSignal——挂起会话场景 base 命中而 ctx.signal 未 abort——R2)aborted →
|
|
42
|
+
* 现状保留(emitNestedChildEvent stopped + rethrow);
|
|
43
|
+
* ② "targeted" 定向中止:err 是 AbortError 且自属 ctrl aborted(且非整回合停)→
|
|
44
|
+
* 折叠 stopped partial 报告(父回合继续——merge/STOPPED_MARK/警示);
|
|
45
|
+
* ③ "error" 其他错误 → 现状保留(child:error + rethrow)。
|
|
46
|
+
* ⚠ 查 baseSignal 非仅 ctx.signal——挂起 digest 场景 child 链 _sessionSignal(R2——
|
|
47
|
+
* digest 自身 Ctrl+I/Ctrl+C 不误伤;会话 Stop 逐链中止必须归 ①)。
|
|
45
48
|
*/
|
|
46
|
-
function
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
49
|
+
export function classifySyncAbort(ctxSignal, baseSignal, ctrlSignal, err) {
|
|
50
|
+
if (ctxSignal?.aborted || baseSignal?.aborted) return "base"
|
|
51
|
+
if (err?.name === "AbortError" && ctrlSignal?.aborted) return "targeted"
|
|
52
|
+
return "error"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* SYNC-CANCEL F1/F5 中止控制器装配(可测):sync 阻塞 spawn 建**自属** AbortController
|
|
57
|
+
* (childRunOpts.signal 覆写为 ctrl.signal——照抄 async 分支 subagent-run.mjs 覆写模式)
|
|
58
|
+
* ——ctrl 链到基信号:baseSignal aborted → ctrl.abort();否则 addEventListener("abort",
|
|
59
|
+
* → ctrl.abort(), { once:true })——Ctrl+C/I 整回合停语义不变(base abort 逐链传播——
|
|
60
|
+
* AC2);嵌套 sync spawn 递归可中止(内层链外层 ctrl.signal——逐层自属——AC4)。
|
|
61
|
+
* 注册 `parent._syncChildAborts`(key = relayPrefix 去尾——{ ctrl, stopped:false }——
|
|
62
|
+
* TUI ⏹ 门控 live 判据 + cancelSyncChild 定向中止目标——与 async 条目 controller 存池
|
|
63
|
+
* 分层一致)。返回 { ctrl, disarm }——disarm 注销 registry(调用方 try/finally 三路径
|
|
64
|
+
* 共用——R7 防跨回合残留)。
|
|
65
|
+
*/
|
|
66
|
+
export function armSyncChildAbort(parent, key, baseSignal) {
|
|
67
|
+
const ctrl = new AbortController()
|
|
68
|
+
if (baseSignal) {
|
|
69
|
+
// §20.3 站点 #10(第 24 批):hop 逐跳保 reason(下游可判定「谁杀的」)
|
|
70
|
+
if (baseSignal.aborted) ctrl.abort(baseSignal.reason)
|
|
71
|
+
else baseSignal.addEventListener("abort", () => ctrl.abort(baseSignal.reason), { once: true })
|
|
64
72
|
}
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
const registry = (parent._syncChildAborts ??= new Map())
|
|
74
|
+
registry.set(key, { ctrl, stopped: false })
|
|
75
|
+
const disarm = () => { registry.delete(key) }
|
|
76
|
+
return { ctrl, disarm }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* SYNC-CANCEL ② 折叠报告构建(可测纯函数——仿 runChildPipeline onDeclined partial 形态,
|
|
81
|
+
* subagent-async.mjs onDeclined:STOPPED_MARK + partial 警示 + 捕获输出 + eng-coder
|
|
82
|
+
* designId 后缀——AC3)。capturedOutput = child._capturedOutput(spawn-child.mjs
|
|
83
|
+
* runWithContinue capture 累积——子代理已流式输出的剥哨兵文本)。
|
|
84
|
+
*/
|
|
85
|
+
export function buildSyncStoppedReport(role, capturedOutput, designId) {
|
|
86
|
+
let report = `Subagent (${role}) ${STOPPED_MARK} — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${capturedOutput || ""}`
|
|
87
|
+
if (role === "eng-coder") {
|
|
88
|
+
report += `\ndesignId: ${designId ?? "(single-design session — designId optional)"} — reuse it (with the same designToken) when re-spawning this eng-coder.`
|
|
80
89
|
}
|
|
81
|
-
return
|
|
90
|
+
return report
|
|
82
91
|
}
|
|
83
92
|
|
|
84
93
|
/**
|
|
85
|
-
* subagent tool — ONE tool,
|
|
86
|
-
* (default) /
|
|
87
|
-
* (
|
|
88
|
-
*
|
|
89
|
-
*
|
|
94
|
+
* subagent tool — ONE tool, EIGHT actions (AGENT-LOOP.md §19/§19.5/§19.6/§19.8 +
|
|
95
|
+
* SUBAGENT-OBSERVE-SEND): spawn (default) / status (non-blocking pool query) / observe
|
|
96
|
+
* (inspect a running/queued/done async child's recent activity + current tool — §7.2) /
|
|
97
|
+
* send (inject a direction into a RUNNING async child — consumed at its next turn
|
|
98
|
+
* boundary as an ordinary instruction — §7.2) / escalate (飞刀 — hand implementation to
|
|
99
|
+
* a stronger model) / cancel (stop ONE background subagent — §19.5) / panel (view + fix
|
|
100
|
+
* the live subagent panel — §19.6) / consume-design (parent-side chain-terminal token
|
|
101
|
+
* consumption — ENGINEERING-MODE.md §2.6, 2026-09-07). The check
|
|
102
|
+
* action was deleted (§19.8): async results reach the model only via the auto channel.
|
|
90
103
|
* - action:"spawn" roles: "explore" — read-only tools, search/read/analyze
|
|
91
104
|
* (suitable for codebase exploration); "coder" — full tool set, self-contained
|
|
92
105
|
* implementation tasks; "plan" — read-only planning; "eng-coder" —
|
|
93
|
-
* engineering-mode implementation (design-token gated)
|
|
106
|
+
* engineering-mode implementation (design-token gated); "eng-designer" —
|
|
107
|
+
* engineering-mode design writing (requirements + design docs, batchDoc gated).
|
|
94
108
|
* - no role specified — invalid by design since the 2026-08-25 fail-closed gate
|
|
95
109
|
* (role is mandatory; "no role → same tool set as parent" was removed with the
|
|
96
110
|
* coder-leak fix and the header text above predates it)
|
|
97
111
|
* - non-recursive: child agents do not get the subagent tool (depth > 0 is not injected)
|
|
98
112
|
*/
|
|
99
113
|
|
|
100
|
-
/**
|
|
101
|
-
* Effective subagent model override for a role (CLI parity shared with VS Code):
|
|
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).
|
|
105
|
-
*/
|
|
106
|
-
export function effectiveSubagentModel(parent, role, modelArg) {
|
|
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
|
|
114
|
-
const cfg = parent.config?.agent ?? {}
|
|
115
|
-
return cfg.subagentModels?.[role] ?? cfg.subagentModel ?? null
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Resolve the design-token slot for an eng-coder spawn (2026-09-01 multi-design, FR3):
|
|
120
|
-
* - designId given → exact slot lookup (no match = explicit error, never a fuzzy guess)
|
|
121
|
-
* - designId omitted → exactly ONE slot must exist (single-design compatibility); with
|
|
122
|
-
* multiple slots we refuse rather than pick one (T16: never silently aim the wrong design)
|
|
123
|
-
* Returns { token } on success; throws with a parent-actionable message otherwise.
|
|
124
|
-
* The HMAC/TTL check itself stays in validateDesignToken (unchanged).
|
|
125
|
-
*/
|
|
126
|
-
export function resolveDesignSlot(parent, designIdArg) {
|
|
127
|
-
const slots = parent._engDesignTokens
|
|
128
|
-
const hasSlots = slots instanceof Map && slots.size > 0
|
|
129
|
-
const legacy = parent._engDesignToken
|
|
130
|
-
// eng(exit/enter) resets the single mirror to force a fresh review (eng.mjs) —
|
|
131
|
-
// a non-empty slot map surviving that reset must NOT resurrect stale tokens:
|
|
132
|
-
// mirror cleared + slots present = re-entered engineering mode → re-review.
|
|
133
|
-
if (!legacy && hasSlots) {
|
|
134
|
-
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.")
|
|
135
|
-
}
|
|
136
|
-
if (designIdArg) {
|
|
137
|
-
if (!hasSlots || !slots.has(designIdArg)) {
|
|
138
|
-
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))`)
|
|
139
|
-
}
|
|
140
|
-
return { token: slots.get(designIdArg) }
|
|
141
|
-
}
|
|
142
|
-
if (hasSlots && slots.size > 1) {
|
|
143
|
-
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.`)
|
|
144
|
-
}
|
|
145
|
-
if (hasSlots && slots.size === 1) return { token: [...slots.values()][0] }
|
|
146
|
-
if (legacy) return { token: legacy } // single-slot mirror fallback (pre-multi-slot sessions)
|
|
147
|
-
throw new Error("Invalid or missing design token — run advisor with type='design' first and pass the returned token as designToken.")
|
|
148
|
-
}
|
|
149
|
-
|
|
150
114
|
export const subagentTool = {
|
|
151
115
|
name: "subagent",
|
|
152
116
|
description:
|
|
153
|
-
"ONE tool,
|
|
117
|
+
"ONE tool, EIGHT actions (AGENT-LOOP.md §19/§19.5/§19.6/§19.8 + SUBAGENT-OBSERVE-SEND) — pick by what you need:\n" +
|
|
154
118
|
"- 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:'
|
|
156
|
-
"- action:'
|
|
157
|
-
"- action:'
|
|
158
|
-
"- action:'
|
|
119
|
+
"- 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). Background advisor reviews share this status surface (AGENT-LOOP.md §18): pass a review id and it answers in the same shape with role:'advisor' plus reviewType (design|code) / round / elapsedSec — and the overview lists reviews alongside subagents.\n" +
|
|
120
|
+
"- 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" +
|
|
121
|
+
"- 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" +
|
|
122
|
+
"- 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.\n" +
|
|
123
|
+
"- 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. Background advisor reviews are cancelable on this same action (AGENT-LOOP.md §18): pass the review id — its controller aborts, no token is issued for a cancelled review.\n" +
|
|
124
|
+
"- 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" +
|
|
159
125
|
"- 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" +
|
|
160
126
|
"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" +
|
|
161
127
|
"Available roles (which roles are exposed depends on the active mode — see Mode filtering below):\n" +
|
|
162
128
|
"- 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" +
|
|
163
129
|
"- plan — read-only implementation planning. Same read/search toolset; NEVER edits files. Returns a step-by-step plan for the parent to execute.\n" +
|
|
164
130
|
"- 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" +
|
|
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" +
|
|
166
|
-
"
|
|
167
|
-
"
|
|
168
|
-
"
|
|
131
|
+
"- 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. ALSO REQUIRES a batchDoc arg — the batch record path (docs/batches/<batch>-<topic>.md), the batch §2 task book this spawn implements: a spawn without it, or with a path that does not resolve to a readable file, is mechanically refused.\n" +
|
|
132
|
+
"- eng-designer — engineering-mode design writer (available only in engineering mode): the SOLE author of the requirements + design documents and of the batch record §2 (the batch task book) — revisions included. Writes no implementation code, does not edit prompt files, does not fire reviews, and needs NO designToken (its authorization is the confirmed requirements). It surveys on its own, but may only spawn read-only 'explore' children (sync, ≤6 per batch). ALSO REQUIRES a batchDoc arg — the batch record path (docs/batches/<batch>-<topic>.md); the same mechanical gate as eng-coder: a spawn without it, or with a path that does not resolve to a readable file, is mechanically refused.\n" +
|
|
133
|
+
"Mode filtering: normal mode exposes explore/plan/coder; engineering mode exposes explore/plan/eng-designer/eng-coder. The schema enum reflects the active mode.\n\n" +
|
|
134
|
+
"Async spawn (AGENT-LOOP.md §15/§18/§11.1): 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. Top-level spawns are ALWAYS async — never pass `async:false` at depth-0 (the report arrives automatically; if your next step needs it, end the turn and let the digest deliver it). Inside subagents (depth>0) spawns are always synchronous (platform rule). Eng-coder's delivery protocol runs fully inside the child (implementation → audit → self-fix → advisor re-review → converged delivery). Async spawns are pooled per role domain (AGENT-LOOP.md §11.1): 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.\n\n" +
|
|
135
|
+
"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" +
|
|
169
136
|
"Writing the prompt:\n" +
|
|
170
137
|
"- 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" +
|
|
171
138
|
"- Put exact paths and commands in the prompt when you know them. The sub-agent should not search for things you already know.\n" +
|
|
@@ -174,20 +141,22 @@ export const subagentTool = {
|
|
|
174
141
|
parameters: {
|
|
175
142
|
type: "object",
|
|
176
143
|
properties: {
|
|
177
|
-
action: { type: "string", enum: ["spawn", "
|
|
144
|
+
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." },
|
|
178
145
|
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
146
|
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
|
|
147
|
+
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." },
|
|
181
148
|
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)." },
|
|
149
|
+
role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder", "eng-designer"], 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
150
|
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)." },
|
|
184
151
|
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." },
|
|
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." },
|
|
186
|
-
|
|
152
|
+
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)." },
|
|
153
|
+
batchDoc: { type: "string", description: "REQUIRED for role='eng-coder' and role='eng-designer': the batch record path (docs/batches/<batch>-<topic>.md) — the batch §2 task book this spawn implements (or writes). The spawn is mechanically refused without it, and also when the path does not resolve (cwd-relative or absolute) to a readable file; the CONTENT is never validated (the batch record owns that). explore/plan/coder spawns ignore it." },
|
|
154
|
+
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). action:'escalate': same semantics (AGENT-LOOP.md §25 D-R17b) — default async at depth 0; async:false keeps the legacy synchronous flight (mechanism parameter — see the Async spawn section for top-level guidance)." },
|
|
187
155
|
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
|
|
189
|
-
id: { type: "string", description: "action:'
|
|
190
|
-
|
|
156
|
+
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." },
|
|
157
|
+
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)." },
|
|
158
|
+
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)." },
|
|
159
|
+
recent: { type: "integer", description: "action:'observe' only (optional, default 5): how many recent-turn summaries to return (clamped 1..20 — N2 keeps observe terse)." },
|
|
191
160
|
},
|
|
192
161
|
required: [],
|
|
193
162
|
},
|
|
@@ -201,12 +170,12 @@ export const subagentTool = {
|
|
|
201
170
|
? String(args.action)
|
|
202
171
|
: "spawn"
|
|
203
172
|
if (action !== "spawn") {
|
|
204
|
-
// §19 restricted-variant action gate (round2 #3): the
|
|
205
|
-
//
|
|
206
|
-
// coder+WRITE child (violates explore-only intent) and
|
|
173
|
+
// §19 restricted-variant action gate (round2 #3): the engineering-child channel
|
|
174
|
+
// (depth>0, role eng-coder or eng-designer) is spawn-only — escalate spawns a
|
|
175
|
+
// coder+WRITE child (violates explore-only intent) and status/panel/observe/send
|
|
207
176
|
// 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
|
|
177
|
+
if ((ctx.depth ?? 0) > 0 && (ctx.agent?._role === "eng-coder" || ctx.agent?._role === "eng-designer")) {
|
|
178
|
+
throw new Error(`only action:'spawn' (sync explore children) is available inside an ${ctx.agent._role} — escalate/status/cancel/panel/consume-design/observe/send are not (AGENT-LOOP.md §19 D-M3)`)
|
|
210
179
|
}
|
|
211
180
|
// §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn —
|
|
212
181
|
// async OR blocking — the digest must stay organize-only. The escalate
|
|
@@ -215,16 +184,26 @@ export const subagentTool = {
|
|
|
215
184
|
if (action === "escalate" && ctx.agent?._inAutoTurn && !ctx.agent?.autoApprove) {
|
|
216
185
|
return JSON.stringify({ status: "error", error: "cannot spawn subagents from a manual auto-turn — wait for user input" })
|
|
217
186
|
}
|
|
218
|
-
if (action === "check") return await executeCheckAction(args, ctx)
|
|
219
187
|
if (action === "status") return executeStatusAction(args, ctx)
|
|
220
188
|
if (action === "escalate") return await executeEscalateAction(args, ctx)
|
|
221
189
|
// §19.5 控制类动作:digest 内放行(D-S7 分类——控制/自省;dispatch 控制类
|
|
222
190
|
// 豁免同批生效——19.5.2b round2 #4;escalate 的 digest 拒绝在上一分支)
|
|
223
191
|
if (action === "cancel") return executeCancelAction(args, ctx)
|
|
192
|
+
// 2026-09-07 token 链终消费制(ENGINEERING-MODE.md §2.6 F1):父侧核销消费——
|
|
193
|
+
// 非只读控制动作——depth-0 + 工程模式限定(本分流已过受限变体门;工程模式门在
|
|
194
|
+
// 执行器内)——planMode 拒绝(dispatch 不豁免)——不入批审批分组(dispatch 免审)。
|
|
195
|
+
if (action === "consume-design") return executeConsumeDesignAction(args, ctx)
|
|
224
196
|
// §19.6 panel 动作:view(readonly 面——digest 内放行——自省类)与 freeze
|
|
225
197
|
// (控制类——同 cancel——digest 内放行)。深度/门控检查在 executePanelAction 内。
|
|
226
|
-
|
|
227
|
-
|
|
198
|
+
// CLI-ACTIVITY-DEBLOAT F-3(2026-09-10)接线:executePanelAction 经 ctx.state
|
|
199
|
+
// (= agent._tuiState——startTUI 反向挂载)读时现算面板块(computePanelBlocks)——
|
|
200
|
+
// 手工面板镜像已退役。headless/VSC 无挂载 → 现算返 null → 降级照旧。
|
|
201
|
+
if (action === "panel") return executePanelAction(args, { ...ctx, state: ctx.agent?._tuiState })
|
|
202
|
+
// SUBAGENT-OBSERVE-SEND:observe = readonly 查询(同 status——digest/planMode 放行);
|
|
203
|
+
// send = 控制类豁免(同 cancel——父回合内显式调用即授权)。深度门在各自执行器内。
|
|
204
|
+
if (action === "observe") return executeObserveAction(args, ctx)
|
|
205
|
+
if (action === "send") return executeSendAction(args, ctx)
|
|
206
|
+
throw new Error(`Unknown subagent action: ${JSON.stringify(action)}. Valid actions: spawn, status, escalate, cancel, panel, consume-design, observe, send.`)
|
|
228
207
|
}
|
|
229
208
|
|
|
230
209
|
const parent = ctx.agent
|
|
@@ -235,17 +214,20 @@ export const subagentTool = {
|
|
|
235
214
|
if (typeof args.task !== "string" || !args.task.trim()) {
|
|
236
215
|
throw new Error("subagent action:'spawn' requires a task (the self-contained task brief).")
|
|
237
216
|
}
|
|
238
|
-
// §18
|
|
239
|
-
//
|
|
240
|
-
|
|
217
|
+
// §18 D-E1a depth-gated async default (2026-09-06 需求池 R12): depth-0 spawns
|
|
218
|
+
// default to async for EVERY role (the old role-level default — eng-coder only —
|
|
219
|
+
// is superseded); depth>0 spawns default to sync (子代理内部强制同步现状保留).
|
|
220
|
+
// async:false remains the explicit escape hatch; async:true at depth>0 is
|
|
221
|
+
// refused downstream (executeAsyncSpawn top-level gate).
|
|
222
|
+
const wantAsync = args.async ?? ((ctx.depth ?? 0) === 0)
|
|
241
223
|
|
|
242
224
|
// Role normalization + whitelist (2026-08-25, coder-leak fix): exact-string gates let
|
|
243
225
|
// variant roles ("Coder", " coder") bypass BOTH mode gates and fall through to
|
|
244
226
|
// full tools / no overlay — a full-write coder without design review. Schema enums are
|
|
245
227
|
// advisory; providers don't enforce them. Fail closed on unknown roles.
|
|
246
|
-
const ROLES = new Set(["explore", "plan", "coder", "eng-coder"])
|
|
228
|
+
const ROLES = new Set(["explore", "plan", "coder", "eng-coder", "eng-designer"])
|
|
247
229
|
if (!ROLES.has(role)) {
|
|
248
|
-
throw new Error(`Unknown subagent role: ${JSON.stringify(role)}. Valid roles: explore, plan, coder, eng-coder (exact spelling).`)
|
|
230
|
+
throw new Error(`Unknown subagent role: ${JSON.stringify(role)}. Valid roles: explore, plan, coder, eng-coder, eng-designer (exact spelling).`)
|
|
249
231
|
}
|
|
250
232
|
// §18 D-E3 internal-spawn gate: an eng-coder sub-agent may only spawn sync
|
|
251
233
|
// explore (audit) children — non-explore roles and async are refused here
|
|
@@ -254,13 +236,19 @@ export const subagentTool = {
|
|
|
254
236
|
// augmentation below. Runs BEFORE the mode gates so the eng-coder-specific
|
|
255
237
|
// error (not the generic engineering-mode one) surfaces.
|
|
256
238
|
const engAuditAttempt = gateEngCoderSpawn(ctx.agent, ctx.depth, role, args.async)
|
|
257
|
-
// Role is mutually exclusive per mode: normal mode → "coder", engineering mode → "eng-coder"
|
|
239
|
+
// Role is mutually exclusive per mode: normal mode → "coder", engineering mode → "eng-coder"/"eng-designer"
|
|
258
240
|
if (parent.config?.agent?.engineering && role === "coder") {
|
|
259
|
-
throw new Error("Engineering mode: use role='eng-coder' for implementation tasks.")
|
|
241
|
+
throw new Error("Engineering mode: role='coder' is disabled — use role='eng-coder' for implementation tasks (or role='eng-designer' for design writing).")
|
|
260
242
|
}
|
|
261
243
|
if (!parent.config?.agent?.engineering && role === "eng-coder") {
|
|
262
244
|
throw new Error("Engineering mode is not active — use role='coder' for implementation tasks.")
|
|
263
245
|
}
|
|
246
|
+
// Third mode gate (ENGINEERING-MODE.md §2.15 A—— symmetric completion):
|
|
247
|
+
// eng-designer is engineering-mode-only, same family as eng-coder (both carry
|
|
248
|
+
// the engineering discipline overlay + the batchDoc gate).
|
|
249
|
+
if (!parent.config?.agent?.engineering && role === "eng-designer") {
|
|
250
|
+
throw new Error("Engineering mode is not active — role='eng-designer' is engineering-mode only (it writes the requirements/design documents inside the engineering workflow); use role='explore' or role='plan' for read-only work.")
|
|
251
|
+
}
|
|
264
252
|
|
|
265
253
|
// §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn — async
|
|
266
254
|
// OR blocking — the digest must stay organize-only. AUTO tier (autoApprove) is
|
|
@@ -271,208 +259,17 @@ export const subagentTool = {
|
|
|
271
259
|
return JSON.stringify({ status: "error", error: "cannot spawn subagents from a manual auto-turn — wait for user input" })
|
|
272
260
|
}
|
|
273
261
|
|
|
274
|
-
//
|
|
275
|
-
// files
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
319
|
-
// Provider/model override: tool `model` arg > subagentModels[role] > subagentModel > parent provider
|
|
320
|
-
const childProvider = resolveChildProvider(parent, effectiveSubagentModel(parent, role, args.model))
|
|
321
|
-
|
|
322
|
-
// eng-coder token gate: the design review must have passed and the caller must
|
|
323
|
-
// present the exact token advisor issued — otherwise the child is not authorized to code.
|
|
324
|
-
// 2026-09-01: multi-design slots — the token is located by designId (exact slot,
|
|
325
|
-
// single-slot fallthrough); HMAC/TTL validation itself is unchanged.
|
|
326
|
-
let issuedToken
|
|
327
|
-
if (role === "eng-coder") {
|
|
328
|
-
issuedToken = resolveDesignSlot(parent, args.designId).token
|
|
329
|
-
if (!issuedToken || args.designToken !== issuedToken || !validateDesignToken(args.designToken)) {
|
|
330
|
-
throw new Error("Invalid or missing design token — run advisor with type='design' first and pass the returned token as designToken.")
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Filter tool set by role: explore/plan are read-only (plan is a planning agent, its deliverable is the plan itself)
|
|
335
|
-
let tools
|
|
336
|
-
if (role === "explore" || role === "plan") {
|
|
337
|
-
const allowed = readonlyToolNames(parent.tools)
|
|
338
|
-
tools = parent.tools.filter((t) => allowed.has(t.name))
|
|
339
|
-
} else {
|
|
340
|
-
tools = parent.tools
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Select prompt overlay by role
|
|
344
|
-
let overlay = ""
|
|
345
|
-
if (role === "explore") overlay = EXPLORE_OVERLAY
|
|
346
|
-
else if (role === "coder") overlay = CODER_OVERLAY
|
|
347
|
-
else if (role === "plan") overlay = PLAN_OVERLAY
|
|
348
|
-
else if (role === "eng-coder") overlay = ENG_CODER_OVERLAY
|
|
349
|
-
|
|
350
|
-
// explore/plan: force read-only permission; coder/default: AUTO passes through directly,
|
|
351
|
-
// manual mode queues permission requests for the parent agent's approval UI (human in the loop, child agent is no longer silently rejected)
|
|
352
|
-
let childPermission
|
|
353
|
-
if (role === "explore" || role === "plan") {
|
|
354
|
-
childPermission = async () => false
|
|
355
|
-
} else if (parent.autoApprove) {
|
|
356
|
-
childPermission = async () => true
|
|
357
|
-
} else {
|
|
358
|
-
childPermission = async (name, toolArgs) => {
|
|
359
|
-
if (!ctx.onPermissionRequest) return false
|
|
360
|
-
const ask = () => ctx.onPermissionRequest(`${role ?? "sub"}/${name}`, toolArgs)
|
|
361
|
-
// Queue parallel child agent permission requests to avoid two popups simultaneously overwriting each other (lesson from question tool)
|
|
362
|
-
parent._permQueue = (parent._permQueue ?? Promise.resolve()).then(ask, ask)
|
|
363
|
-
return parent._permQueue
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// eng-coder: force engineering=true on child config so setup.mjs applies engineering prompt
|
|
368
|
-
const childConfig = role === "eng-coder"
|
|
369
|
-
? { ...parent.config, agent: { ...parent.config.agent, engineering: true } }
|
|
370
|
-
: parent.config
|
|
371
|
-
|
|
372
|
-
const child = createAgent({
|
|
373
|
-
provider: childProvider,
|
|
374
|
-
tools,
|
|
375
|
-
config: childConfig,
|
|
376
|
-
cwd: parent.cwd,
|
|
377
|
-
memory: parent.memory,
|
|
378
|
-
overlay,
|
|
379
|
-
role,
|
|
380
|
-
})
|
|
262
|
+
// §20 准入(2026-09-05 module-split——prepareScheduling verbatim 迁
|
|
263
|
+
// subagent-spawn.mjs:参数形态/unknown id/依赖环/阻塞 sync 判定;files 目录声明
|
|
264
|
+
// fail-closed——检测器错误即工具结果 JSON)
|
|
265
|
+
const prep = prepareScheduling(parent, args.files, args.dependsOn, wantAsync)
|
|
266
|
+
if (prep.errorJson) return prep.errorJson
|
|
381
267
|
|
|
382
|
-
//
|
|
383
|
-
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
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
|
|
390
|
-
// designId+token ride the child bookkeeping: the delivery report carries the designId
|
|
391
|
-
// so the divergence-audit fix round re-spawns with the SAME slot (2026-09-01 FR3).
|
|
392
|
-
if (role === "eng-coder" && issuedToken) {
|
|
393
|
-
child._engDesignId = args.designId ?? null
|
|
394
|
-
child._engDesignToken = issuedToken
|
|
395
|
-
}
|
|
396
|
-
|
|
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 范围边界)。
|
|
403
|
-
let input = args.context ? `Context:\n${args.context}\n\nTask:\n${args.task}` : args.task
|
|
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`
|
|
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
|
|
452
|
-
|
|
453
|
-
// Relay content/reasoning/tool/output to the parent TUI via the unified spawn-child
|
|
454
|
-
// pipeline (AGENT-LOOP.md §7.2 D3). Prefix includes a unique id: parallel child agents
|
|
455
|
-
// with the same role stay independent and don't overwrite each other.
|
|
456
|
-
// Format: role#id/ → onToken("coder#2/writing..."), onToolCall("coder#2/read", args)
|
|
457
|
-
// Async id allocation (AGENT-LOOP.md §15 D-A1): reserve the relay counter at
|
|
458
|
-
// spawn time — the returned id must be stable while the item sits in the queue.
|
|
459
|
-
// The [model] token (TUI block creation) is DEFERRED to actual start so queued
|
|
460
|
-
// children don't paint an empty panel block ("queued 态不显示").
|
|
461
|
-
let relayPrefix
|
|
462
|
-
if (wantAsync) {
|
|
463
|
-
parent._subAgentCounter = (parent._subAgentCounter ?? 0) + 1
|
|
464
|
-
relayPrefix = `${role}#${parent._subAgentCounter}/`
|
|
465
|
-
} else {
|
|
466
|
-
relayPrefix = makeRelay(parent, role ?? "sub", ctx.callbacks?.onToken, childProvider.model ?? "")
|
|
467
|
-
}
|
|
468
|
-
// LOGGING(LOGGING.md):子代理内部事件(子内 llm:*/tool:*)以 childId 归属——
|
|
469
|
-
// agent._logId 随 runAgent 的 logCtx 透出(主文件单文件全记、按 childId grep)。
|
|
470
|
-
child._logId = relayPrefix.slice(0, -1)
|
|
471
|
-
const childOpts = {
|
|
472
|
-
onPermissionRequest: childPermission,
|
|
473
|
-
...wrapChildCallbacks(relayPrefix, ctx.callbacks),
|
|
474
|
-
}
|
|
475
|
-
const childRunOpts = buildChildRunOpts(ctx)
|
|
268
|
+
// child 装配(2026-09-05 module-split——buildSpawnChild verbatim 迁
|
|
269
|
+
// subagent-spawn.mjs:provider/model 覆盖、角色门、design-token 门、工具集/
|
|
270
|
+
// overlay/permission 装配、审计任务书注入、relay 前缀分配、childOpts/runOpts)
|
|
271
|
+
const built = buildSpawnChild(parent, ctx, args, role, wantAsync, prep.files, prep.dependsOn, engAuditAttempt)
|
|
272
|
+
const { child, input, childOpts, childRunOpts, relayPrefix } = built
|
|
476
273
|
// Turn-cap continue loop (TURN-CAP-CONTINUE.md) via runWithContinue (§7.2 D3):
|
|
477
274
|
// hitting the cap asks the user via the SAME y/n panel the main agent uses —
|
|
478
275
|
// unlimited continues, resume:true keeps the child's history + mutation bookkeeping,
|
|
@@ -482,245 +279,126 @@ export const subagentTool = {
|
|
|
482
279
|
// turns them into Error tool results — unchanged behavior).
|
|
483
280
|
const askSubagentContinue = (e) => {
|
|
484
281
|
if (!ctx.onPermissionRequest) return Promise.resolve(false)
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
282
|
+
const key = relayPrefix.slice(0, -1)
|
|
283
|
+
const ask = async () => {
|
|
284
|
+
// SYNC-CANCEL v2(模态 deny——用户裁):⏹ 后 entry.stopped——不再弹模态。
|
|
285
|
+
// ⚠ 不能直接 resolve(false) 走 onDeclined 降级(TURN_CAP partial——child 已撞
|
|
286
|
+
// cap——runWithContinue 的 decline 是正常 return——永远到不了 abort 检出点——
|
|
287
|
+
// stopped 折叠语义丢失:无 ⟦ev⟧stopped/无 STOPPED_MARK——块冻结标 done 而非
|
|
288
|
+
// stopped——评审 🟡#2)。stopped 分支改抛 AbortError——runWithContinue 只捕
|
|
289
|
+
// ContinueError——原样上抛 → 阻塞 catch 三分支②折叠("child 随即在 abort 检出点
|
|
290
|
+
// 解绕折叠"——AGENT-LOOP §7.2 机制文)。abort 恒已在途(stopped 只由
|
|
291
|
+
// cancelSyncChild 与 ctrl.abort 同时置位)——信号语义真实。
|
|
292
|
+
if (parent._syncChildAborts?.get(key)?.stopped) throw abortError(ctrl.signal, "settle", "sync-stopped")
|
|
293
|
+
const go = await ctx.onPermissionRequest("continue", { turns: e.turn, agent: key })
|
|
294
|
+
// ⏹ deny(denyModalForOwner resolve(false))与用户按 n 同形——旗标区分:
|
|
295
|
+
// stopped → 同上抛(折叠——abort 先于 deny 已在途);普通 n → false 走 decline
|
|
296
|
+
// (现状——cap partial 报告)。
|
|
297
|
+
if (parent._syncChildAborts?.get(key)?.stopped) throw abortError(ctrl.signal, "settle", "sync-stopped")
|
|
298
|
+
return go
|
|
299
|
+
}
|
|
300
|
+
return enqueueAsk(parent, "_permQueue", ask)
|
|
488
301
|
}
|
|
489
302
|
|
|
490
|
-
// ── Async branch
|
|
491
|
-
//
|
|
492
|
-
// turn-cap / permission / MIN_REPORT_CHARS / mergeChildMutations all unchanged),
|
|
493
|
-
// but the parent does not await it: the promise is parked in _asyncSubagents and
|
|
494
|
-
// consumed via action:'check' or the turn-end auto-wait. Slot queue: running
|
|
495
|
-
// count < ASYNC_SUBAGENT_LIMIT → start now; ≥ limit → enqueue (status "queued",
|
|
496
|
-
// position = queue index) — never rejected, never requiring the model to batch.
|
|
303
|
+
// ── Async branch(2026-09-05 module-split——executeAsyncSpawn verbatim 迁
|
|
304
|
+
// subagent-run.mjs:条目构建/等位/启动/controller 链/turn 镜像/补位释放)──
|
|
497
305
|
if (wantAsync) {
|
|
498
|
-
|
|
499
|
-
throw new Error("async spawn only available at the top level")
|
|
500
|
-
}
|
|
501
|
-
parent._asyncSubagents ??= new Map()
|
|
502
|
-
parent._asyncQueue ??= []
|
|
503
|
-
const running = [...parent._asyncSubagents.values()].filter((e) => e.status === "running").length
|
|
504
|
-
const id = parent._subAgentCounter
|
|
505
|
-
const entry = {
|
|
506
|
-
id, role, relayPrefix,
|
|
507
|
-
status: "queued", // 下面按等待态/槽位重定(避免两处判断漂移)
|
|
508
|
-
position: undefined,
|
|
509
|
-
report: null, error: null, done: false, cancelled: false,
|
|
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 空也不启动
|
|
535
|
-
}
|
|
536
|
-
// The settle signal — resolves when the run chain settles (never rejects).
|
|
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
|
-
}
|
|
561
|
-
entry.start = () => {
|
|
562
|
-
entry.status = "running"
|
|
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")
|
|
576
|
-
// Deferred [model] emit: the TUI block is created at ACTUAL start.
|
|
577
|
-
ctx.callbacks?.onToken?.(relayPrefix + "[model]" + (childProvider.model ?? ""))
|
|
578
|
-
// Turn-cap on background children NEVER pops the continue panel (D-A3):
|
|
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 }, {
|
|
586
|
-
parent, role, args,
|
|
587
|
-
askContinue: () => Promise.resolve(Boolean(parent.config?.agent?.engineering && parent.autoApprove)),
|
|
588
|
-
})
|
|
589
|
-
.then((report) => { entry.report = report })
|
|
590
|
-
.catch((err) => { entry.error = err?.message ?? String(err) })
|
|
591
|
-
.finally(() => {
|
|
592
|
-
entry.status = "done" // running 数口径(D-A1/D-A2/T6):已完成未消费不计入
|
|
593
|
-
entry.done = true
|
|
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
|
-
}
|
|
651
|
-
}
|
|
652
|
-
entry._settleSeq = (parent._asyncSettleSeq = (parent._asyncSettleSeq ?? 0) + 1)
|
|
653
|
-
entry._settle()
|
|
654
|
-
for (const w of parent._asyncWaiters?.splice(0) ?? []) { try { w() } catch { /* noop */ } }
|
|
655
|
-
// §20 D-SD4 释放点:settle 腾槽 + 依赖终态转移 → 补位(依赖满足者/域冲突
|
|
656
|
-
// 解除者自动启动——槽 ≤4)→ 排队态面板刷新(等待块头标注随终态更新——
|
|
657
|
-
// dependency cancelled / 位置前移)。
|
|
658
|
-
maybeRefillAsync(parent)
|
|
659
|
-
refreshQueuedTokens(parent, ctx.callbacks?.onToken)
|
|
660
|
-
})
|
|
661
|
-
}
|
|
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 })
|
|
666
|
-
if (entry.status === "queued") {
|
|
667
|
-
parent._asyncQueue.push(entry)
|
|
668
|
-
entry.position = parent._asyncQueue.length
|
|
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)
|
|
680
|
-
}
|
|
681
|
-
entry.start()
|
|
682
|
-
return JSON.stringify({ id: String(id), role, status: "running" })
|
|
306
|
+
return executeAsyncSpawn(parent, ctx, role, args, child, input, childOpts, childRunOpts, relayPrefix, built.childProvider, prep.files, prep.dependsOn)
|
|
683
307
|
}
|
|
684
308
|
|
|
685
|
-
// ── Blocking path (unchanged semantics
|
|
686
|
-
//
|
|
687
|
-
//
|
|
309
|
+
// ── Blocking path (unchanged semantics + SYNC-CANCEL targeted stop) ──
|
|
310
|
+
// SYNC-CANCEL F1/F5(2026-09-09):自属 AbortController(armSyncChildAbort——
|
|
311
|
+
// childRunOpts.signal 覆写 ctrl.signal——照抄 async 分支 subagent-run.mjs 的
|
|
312
|
+
// 覆写模式——buildChildRunOpts 不改——escalate/consult 零触碰)——⏹ 定向中止
|
|
313
|
+
// (cancelSyncChild → ctrl.abort)与整回合停(base abort 逐链传播)解耦;registry
|
|
314
|
+
// 注册/注销(try/finally 三路径——R7 防跨回合残留)。LOGGING(LOGGING.md):
|
|
315
|
+
// child:*(阻塞 spawn——runChildPipeline 前后;declined partial 由 TURN_CAP_MARK
|
|
316
|
+
// 检出;⏹ 折叠由 STOPPED_MARK 检出——kind partial;错误原样上抛(dispatch 转
|
|
317
|
+
// tool:error))
|
|
688
318
|
const blockT0 = Date.now()
|
|
689
319
|
logEvent("child:spawn", { role, id: child._logId, kind: "blocking" })
|
|
320
|
+
const syncKey = relayPrefix.slice(0, -1)
|
|
321
|
+
// baseSignal 一次性快照(spawn 时刻)——catch 分类复用同一信号对象(会话收尾把
|
|
322
|
+
// _sessionSignal 置 null 的窗口内重读会漂移——快照防误判)
|
|
323
|
+
const baseSignal = buildChildSignal(parent, ctx)
|
|
324
|
+
const { ctrl, disarm } = armSyncChildAbort(parent, syncKey, baseSignal)
|
|
325
|
+
let pipelineReport
|
|
690
326
|
try {
|
|
691
|
-
|
|
327
|
+
pipelineReport = await runChildPipeline(child, input, childOpts, { ...childRunOpts, signal: ctrl.signal }, {
|
|
692
328
|
parent, role, args,
|
|
693
329
|
askContinue: askSubagentContinue,
|
|
694
330
|
})
|
|
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
331
|
} catch (e) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
332
|
+
// SYNC-CANCEL F2 三分支(classifySyncAbort 纯函数):
|
|
333
|
+
const cls = classifySyncAbort(ctx.signal, baseSignal, ctrl.signal, e)
|
|
334
|
+
if (cls === "base") {
|
|
335
|
+
// ① 整回合停(现状逐字保留——挂起场景 base 命中而 ctx.signal 未 abort——R2):
|
|
336
|
+
// §27 R23:外层 abort 传播的中断——内层开块随之外层冻结前先收尾定格
|
|
337
|
+
// (D-R23c1 stopped——T-R23c.2a 生成侧路径;TUI 冻结兜底仍在 freezeSubTaskLines)
|
|
338
|
+
emitNestedChildEvent(ctx, relayPrefix, "stopped")
|
|
339
|
+
throw e // 用户停——不落错误事件
|
|
340
|
+
}
|
|
341
|
+
if (cls === "error") {
|
|
342
|
+
// ③ 其他错误(现状逐字保留——:249-253):
|
|
343
|
+
// §27 R23 error-run 映射(实现批补一行):run 错误(非 abort)→ 同样发 stopped
|
|
344
|
+
// ——内层子块定格不悬空(T-R23a.3——工具错/运行错误路径)。
|
|
345
|
+
emitNestedChildEvent(ctx, relayPrefix, "stopped")
|
|
346
|
+
logEvent("child:error", { role, id: child._logId, ms: Date.now() - blockT0, err: errText(deathLine(e, ctrl?.signal), 200) })
|
|
347
|
+
throw e
|
|
348
|
+
}
|
|
349
|
+
// ② targeted 折叠(err AbortError && 自属 ctrl aborted && 非整回合停):merge +
|
|
350
|
+
// stopped partial 报告(父回合继续拿报告——AC1/AC3)。merge 镜像 escalate sync
|
|
351
|
+
// runner 先例(subagent-actions.mjs runner 包装层——guard 在 mergeChildMutations
|
|
352
|
+
// 内——见子代理已写文件才传播)。
|
|
353
|
+
if (role === "eng-coder" && child._mutatedThisRun) mergeChildMutations(parent, child)
|
|
354
|
+
pipelineReport = buildSyncStoppedReport(role, child._capturedOutput ?? "", args?.designId)
|
|
355
|
+
// 块冻结标 stopped(R6——非 done):⏹ 定向中止的 TUI 顶层块立即定格 stopped
|
|
356
|
+
// (async settle cancelled 分支同款直发——async-settle.mjs settleAsyncEntry);
|
|
357
|
+
// 嵌套(eng-coder 内 explore 审计)经 emitNestedChildEvent 定格子块——stopped
|
|
358
|
+
// 幂等无害(重复/迟到 done 由 §27.1 F2 done 子块定格丢弃兜底)。
|
|
359
|
+
ctx.callbacks?.onToken?.(`${relayPrefix}⟦ev⟧stopped\x1e0\x1e0\x1estopped\x1e`)
|
|
360
|
+
emitNestedChildEvent(ctx, relayPrefix, "stopped")
|
|
361
|
+
} finally {
|
|
362
|
+
// R7 防跨回合残留:成功/折叠②/整回合停①/错误③ 四出口统一注销(设计"三路径"
|
|
363
|
+
// 口径 = 成功/折叠/整回合停——错误③ 同样 rethrow 经 finally——同归本注销)
|
|
364
|
+
disarm()
|
|
708
365
|
}
|
|
366
|
+
// §27 R23 D-R23c1(评审 #1 🅰——生成侧补发射):sync spawn 同步收尾——若本 spawn
|
|
367
|
+
// 处于嵌套上下文(ctx.callbacks 已是嵌套 wrapper——eng-coder 内 explore 审计)→
|
|
368
|
+
// 发内层 ⟦ev⟧done(完整嵌套前缀——wrapper 链自动补外层)→ 主 TUI 路由子块定格
|
|
369
|
+
// (T-R23c.1)。非嵌套(depth-0)零变化——冻结仍由 dispatch subKey 精确冻承接。
|
|
370
|
+
// ② 折叠 = 正常 return(本共用出口:done 补发照设 + ctx._subagentKey 照设——成功
|
|
371
|
+
// 冻结管线复用——迟到 done 对已定格 stopped 块被丢弃——幂等无害)。
|
|
372
|
+
emitNestedChildEvent(ctx, relayPrefix, "done")
|
|
373
|
+
logEvent("child:done", { role, id: child._logId, ms: Date.now() - blockT0, kind: String(pipelineReport).includes(TURN_CAP_MARK) || String(pipelineReport).includes(STOPPED_MARK) ? "partial" : "ok" })
|
|
374
|
+
// §7.2.3 sync spawn 完成精确冻结(方案 e):execute 返回前 ctx 留子代理 key
|
|
375
|
+
// (relayPrefix 去尾 = `role#N`)——dispatch runOne 读它作 onToolResult 第 4 参 →
|
|
376
|
+
// TUI finishSubTaskKey 按 key 精确冻(async eng-coder 先启动时不再误冻其块——
|
|
377
|
+
// T-F2)。仅成功/折叠路径设置:async 分支不设(round2 #2——ack 带 status:running 由
|
|
378
|
+
// isAsyncSpawnResult 跳过冻结);base/error 路径到此之前已 throw——ctx 未设
|
|
379
|
+
// ——中止/错误路径不触发冻结(round1 #1——T-F5)。
|
|
380
|
+
ctx._subagentKey = syncKey
|
|
381
|
+
return pipelineReport
|
|
709
382
|
},
|
|
710
383
|
}
|
|
711
384
|
|
|
712
|
-
// Re-export shim (2026-09-03 拆分轮 + §19 合体轮):
|
|
713
|
-
// ./subagent-async.mjs
|
|
714
|
-
//
|
|
715
|
-
//
|
|
385
|
+
// Re-export shim (2026-09-03 拆分轮 + §19 合体轮 + 2026-09-05 拆分轮): 机械与动作
|
|
386
|
+
// 执行器迁至 ./subagent-async.mjs、./subagent-actions.mjs、./subagent-scheduler.mjs
|
|
387
|
+
// ——本文件保留导出面,消费点(agent.mjs / agent-turn.mjs / consult.mjs / 测试)导入
|
|
388
|
+
// 路径零改动;池逻辑/准入见 subagent-run.mjs(executeAsyncSpawn)与 subagent-scheduler.mjs
|
|
389
|
+
// (maybeRefillAsync——execute 不再直接使用池常量)。
|
|
716
390
|
// 2026-09-05 拆分轮: maybeRefillAsync 随 §20 调度器独立(./subagent-scheduler.mjs)——
|
|
717
391
|
// 再导出源改写,消费面(agent.mjs 动态 import 等)不变。
|
|
392
|
+
// 2026-09-06 §11.1 拆分轮: ASYNC_SUBAGENT_LIMIT 导出 → ASYNC_POOL_LIMITS(分域常量——
|
|
393
|
+
// 定义在 subagent-async.mjs——re-export 面同步)。
|
|
718
394
|
export {
|
|
719
|
-
|
|
720
|
-
MAX_ASYNC_CHECKS,
|
|
395
|
+
ASYNC_POOL_LIMITS,
|
|
721
396
|
resolveChildProvider,
|
|
722
397
|
injectAsyncResult,
|
|
723
398
|
buildChildRunOpts,
|
|
724
399
|
mergeChildMutations,
|
|
725
400
|
} from "./subagent-async.mjs"
|
|
726
401
|
export { maybeRefillAsync } from "./subagent-scheduler.mjs"
|
|
402
|
+
// 2026-09-05 module-split:spawn 装配 helpers 迁 subagent-spawn.mjs——re-export 保测试
|
|
403
|
+
// import 面(subagent-core.test.mjs 从本文件动态 import)
|
|
404
|
+
export { effectiveSubagentModel, resolveDesignSlot } from "./subagent-spawn.mjs"
|