thincoder 0.12.60 → 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 +24 -1
- package/README.md +8 -6
- package/bin/thincoder.mjs +27 -122
- package/package.json +3 -2
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +38 -17
- 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 +36 -134
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +124 -329
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +3 -2
- package/src/agent/completion.mjs +1 -1
- package/src/agent/dispatch.mjs +47 -12
- package/src/agent/helpers.mjs +71 -13
- package/src/agent/record-results.mjs +13 -5
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +21 -6
- package/src/agent/setup-reminders.mjs +16 -9
- package/src/agent/setup.mjs +92 -128
- package/src/agent/spawn-child.mjs +26 -9
- package/src/agent-tools/advisor-async.mjs +70 -180
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +69 -20
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +23 -10
- package/src/agent-tools/design-token.mjs +14 -1
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +3 -3
- package/src/agent-tools/escalate-async.mjs +16 -13
- package/src/agent-tools/read-history.mjs +13 -3
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +130 -17
- package/src/agent-tools/subagent-actions.mjs +15 -6
- package/src/agent-tools/subagent-async.mjs +66 -14
- package/src/agent-tools/subagent-panel.mjs +22 -15
- package/src/agent-tools/subagent-run.mjs +9 -6
- package/src/agent-tools/subagent-scheduler.mjs +57 -8
- package/src/agent-tools/subagent-spawn.mjs +63 -16
- package/src/agent-tools/subagent.mjs +175 -49
- package/src/agent-tools/verify.mjs +13 -34
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +38 -21
- package/src/cli/distill-command.mjs +2 -2
- package/src/cli/make-agent.mjs +23 -7
- package/src/cli/memory-command.mjs +2 -2
- package/src/cli/setup-wizard.mjs +29 -9
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +132 -63
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +7 -2
- package/src/expand-home.mjs +16 -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 +70 -31
- package/src/memory/delete.mjs +2 -0
- package/src/memory/docs.mjs +10 -6
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/prompt-overlays.mjs +73 -16
- package/src/prompts/advisor-design.md +18 -8
- package/src/prompts/advisor-round1.md +7 -1
- package/src/prompts/advisor-round2.md +13 -2
- package/src/prompts/advisor-round3.md +13 -2
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +2 -0
- 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 +13 -32
- package/src/provider/errors.mjs +26 -1
- 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/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- package/src/session-slots.mjs +7 -2
- package/src/session.mjs +59 -38
- package/src/token-ttl.mjs +2 -1
- package/src/tools/{system.mjs → bash.mjs} +6 -243
- package/src/tools/file.mjs +30 -10
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +15 -34
- package/src/tools/index.mjs +4 -2
- package/src/tools/ops.mjs +20 -7
- package/src/tools/question.md +1 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -1
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tui/agent-turn.mjs +32 -13
- 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 +108 -37
- package/src/tui/cmd-eng.mjs +11 -27
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-undo.mjs +4 -3
- package/src/tui/index.mjs +38 -37
- 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 +47 -10
- package/src/tui/pickers.mjs +28 -410
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +5 -19
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/subagent-blocks.mjs +69 -90
- package/src/tui/subagent-children.mjs +50 -64
- package/src/tui/subagent-freeze.mjs +40 -43
- package/src/tui/subagent-panel.mjs +21 -66
- package/src/tui/suspension-drive.mjs +28 -82
- package/src/tui/tool-args.mjs +4 -2
- package/src/tui/tool-display.mjs +4 -3
- package/src/tui/tool-events.mjs +23 -16
- package/src/tui/tui-lifecycle.mjs +18 -5
- package/src/tui/wizard.mjs +52 -18
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -13
- package/src/prompts/discipline.md +0 -84
- package/src/prompts/eng-coder.md +0 -19
- package/src/prompts/engineering-sub.md +0 -14
- package/src/prompts/engineering.md +0 -87
- package/src/prompts/explore.md +0 -12
- package/src/prompts/main.md +0 -34
- package/src/prompts/methodology-template.md +0 -38
- package/src/prompts/plan.md +0 -9
- package/src/prompts/system.md +0 -44
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* consume-design + observe/send(SUBAGENT-OBSERVE-SEND)——§19.8 check 已删;spawn 路径与
|
|
5
5
|
* 工具面在 subagent.mjs;cancel 动作执行器与机械、管线在本模块——check 执行器随 §19.8 删除)。
|
|
6
6
|
* 内容:resolveChildProvider / async 池常量与域助手(ASYNC_POOL_LIMITS/poolDomainOf/
|
|
7
|
-
* resolvePoolLimits/poolLimitsFor/runningPoolCount——§
|
|
7
|
+
* resolvePoolLimits/poolLimitsFor/runningPoolCount——§11.1)/ executeCancelAction +
|
|
8
8
|
* cancelAsyncSubagent(§19.5 D-M6——工具与 TUI ⏹ 共用)/ runChildPipeline /
|
|
9
|
-
* injectAsyncResult / buildChildRunOpts / mergeChildMutations
|
|
9
|
+
* injectAsyncResult / buildChildRunOpts / mergeChildMutations / enqueueAsk(批 6——
|
|
10
|
+
* _permQueue 审批/继续弹窗串行收 helper——消费位 subagent.mjs/subagent-spawn.mjs/
|
|
11
|
+
* escalate-async.mjs)。
|
|
10
12
|
* 拆分(2026-09-05——Module Split Policy §20.9——纯迁移零行为变化):§20 调度器 + 文件域
|
|
11
13
|
* 组 → ./subagent-scheduler.mjs(尾部 re-export 保测试动态 import 面——queueRunnable/
|
|
12
14
|
* describeBlockers);status/panel/escalate 动作执行器 → ./subagent-actions.mjs。
|
|
@@ -18,27 +20,40 @@ import {
|
|
|
18
20
|
import { runWithContinue, TURN_CAP_MARK } from "../agent/spawn-child.mjs"
|
|
19
21
|
import { pushReal } from "../context.mjs"
|
|
20
22
|
import { offloadToolResult } from "../agent/helpers.mjs"
|
|
23
|
+
import { digestBudgetOver, persistOverflowReport } from "./digest-budget.mjs"
|
|
24
|
+
// 群 B 批 B5(§22 D-DG4):预算单源迁出(digest-budget.mjs)——原处 re-export 保测试
|
|
25
|
+
// 导入面零改(DIGEST_INJECT_BUDGET / _setDigestOffloadDirForTest)。
|
|
26
|
+
export { DIGEST_INJECT_BUDGET, _setDigestOffloadDirForTest } from "./digest-budget.mjs"
|
|
21
27
|
import {
|
|
22
28
|
dependentLabels, maybeRefillAsync, refreshQueuedTokens,
|
|
23
29
|
} from "./subagent-scheduler.mjs"
|
|
24
|
-
// §
|
|
30
|
+
// §11.2 (R13): advisor-pool cancel fallback + mutation logging for merged
|
|
25
31
|
// code(lazy function-level cycle——advisor-async → async-settle → scheduler →
|
|
26
32
|
// 本模块——全函数级绑定无求值期依赖,环安全)。
|
|
27
33
|
import { cancelAsyncAdvisor, noteMutations } from "./advisor-async.mjs"
|
|
28
34
|
|
|
29
|
-
//
|
|
35
|
+
// agent-tools 共享:并行子代理的审批/继续弹窗经 owner 上命名 promise 链串行——
|
|
36
|
+
// 永不叠弹窗(返回链供调用方 .then 续接)。
|
|
37
|
+
export function enqueueAsk(owner, key, ask) {
|
|
38
|
+
const chain = (owner[key] ?? Promise.resolve()).then(ask, ask)
|
|
39
|
+
owner[key] = chain
|
|
40
|
+
return chain
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Async pool limits per role domain (AGENT-LOOP.md §11.1 — R14, 2026-09-06):
|
|
30
44
|
// the old single cap (ASYNC_SUBAGENT_LIMIT = 4, §15 D-A4) evolved into two
|
|
31
45
|
// independent pools — eng-coder 4 / other roles 4 (user ruling "eng-coder 四路,
|
|
32
46
|
// 其他 4 路") — a full engCoder pool never blocks an explore spawn and vice versa
|
|
33
47
|
// (same-domain cap still 4, cross-domain total up to 8). The per-turn check
|
|
34
48
|
// budget was deleted with the check action (§19.8 — results arrive only via the
|
|
35
49
|
// auto channel; no loop guard needed).
|
|
36
|
-
// ⚠ 与 config.mjs DEFAULTS.agent.poolLimits
|
|
37
|
-
//
|
|
50
|
+
// ⚠ 与 config.mjs DEFAULTS.agent.poolLimits 的 SUBAGENT 两键(engCoder/other)逐键同值
|
|
51
|
+
// (loadConfig/settings 默认源)——耦合锚 T-24a4 断言锁住——勿单侧改默认。advisor 第三键
|
|
52
|
+
// 不属本域(POOL-CONFIG-UNIFIED 2026-09-09——由 advisor-async 读取器消费——见该文件)。
|
|
38
53
|
export const ASYNC_POOL_LIMITS = { engCoder: 4, other: 4 }
|
|
39
54
|
|
|
40
55
|
/**
|
|
41
|
-
* Role → pool domain (single source of truth — §
|
|
56
|
+
* Role → pool domain (single source of truth — §11.1 修正 #8): the CLI role
|
|
42
57
|
* enum/assembly table is subagent.mjs's ROLES = { explore, plan, coder, eng-coder }
|
|
43
58
|
* (mode-filtered: normal → explore/plan/coder, engineering → explore/plan/eng-coder).
|
|
44
59
|
* eng-coder → engCoder pool; every other role (including unknown roles — fail-safe)
|
|
@@ -95,7 +110,7 @@ export function poolLimitsFor(agent) {
|
|
|
95
110
|
function warnPoolFallback(what, sig) {
|
|
96
111
|
if (warnedPoolConfigs.has(sig)) return
|
|
97
112
|
warnedPoolConfigs.add(sig)
|
|
98
|
-
console.warn(`[config] agent.poolLimits: ${what} — must be a positive integer ≥1 — falling back to default ${JSON.stringify(ASYNC_POOL_LIMITS)} (AGENT-LOOP.md §
|
|
113
|
+
console.warn(`[config] agent.poolLimits: ${what} — must be a positive integer ≥1 — falling back to default ${JSON.stringify(ASYNC_POOL_LIMITS)} (AGENT-LOOP.md §11.1)`)
|
|
99
114
|
}
|
|
100
115
|
|
|
101
116
|
/** Running count within ONE pool domain (口径同 §15 D-A1/T6: queued 与已完成不计入;
|
|
@@ -137,7 +152,10 @@ export function resolveChildProvider(parent, modelArg) {
|
|
|
137
152
|
return { ...withKey(p), model: mname || p.model }
|
|
138
153
|
}
|
|
139
154
|
const byName = providers.find((x) => x.name === modelArg)
|
|
140
|
-
|
|
155
|
+
// F-2c (MODEL-400-FIX):裸渠道名克隆须重派生 model——渠道裸克隆会丢 model 键 → 无 model 请求
|
|
156
|
+
// → serde 400。MODEL-SELECTION v2(M3④):取渠道默认模型(`provider.model` 单值——2026-09-10
|
|
157
|
+
// 起;老 models[] 首候选形态已退场);无默认模型 → 主 provider 的 model 兜底(T28)。
|
|
158
|
+
if (byName) return { ...withKey(byName), model: byName.model ?? parent.provider?.model }
|
|
141
159
|
return { ...parent.provider, model: modelArg }
|
|
142
160
|
}
|
|
143
161
|
|
|
@@ -182,10 +200,35 @@ export function cancelAsyncSubagent(agent, id) {
|
|
|
182
200
|
}
|
|
183
201
|
// running:标记 + 条目 abort——settle finally 跑 cancelled 分支(移除 + stopped + 提醒)
|
|
184
202
|
entry.cancelled = true
|
|
185
|
-
|
|
203
|
+
// §20.3 站点 #11(第 24 批):定向中止 = cancel(reason 载荷——下游可判「谁杀的」)
|
|
204
|
+
entry.controller?.abort?.({ abortTrigger: "cancel", abortDetail: "subagent-cancel" })
|
|
186
205
|
return { id: key, status: "cancelled" }
|
|
187
206
|
}
|
|
188
207
|
|
|
208
|
+
/**
|
|
209
|
+
* SYNC-CANCEL(L52——2026-09-09):sync spawn(深度>0 或阻塞调用——阻塞路径)定向中止
|
|
210
|
+
* 核心(TUI ⏹ 直连——不经模型回合)。registry `agent._syncChildAborts`(key =
|
|
211
|
+
* relayPrefix 去尾 `role#N`——subagent.mjs 阻塞路径注册 { ctrl, stopped:false },
|
|
212
|
+
* try/finally 三路径注销——R7)——与 async 的池条目 controller 存 agent 分层一致
|
|
213
|
+
* (subagent.mjs 装配注):
|
|
214
|
+
* - 有 live ctrl(未 stopped)→ 置 stopped 旗标 + ctrl.abort()(child runAgent 的
|
|
215
|
+
* childRunOpts.signal = ctrl.signal——N4 无轮询直连 abort——AbortError 解绕)→
|
|
216
|
+
* { id, status:"cancelled" }——catch 三分支②折叠 stopped partial 报告(父回合继续)
|
|
217
|
+
* - 无 live ctrl(registry miss——未注册/已注销——或已 stopped——stop 在途/已完成)→
|
|
218
|
+
* error 文案(mouse :199 同款——"finished or stop no longer applies")
|
|
219
|
+
* 幂等边界:stopped 后再调恒 error(不重复 abort——无副作用);重复 ⏹ 由调用方文案兜底。
|
|
220
|
+
*/
|
|
221
|
+
export function cancelSyncChild(agent, key) {
|
|
222
|
+
const k = String(key)
|
|
223
|
+
const entry = agent?._syncChildAborts?.get(k)
|
|
224
|
+
if (!entry || entry.stopped) {
|
|
225
|
+
return { id: k, status: "error", error: `sync child ${k} has no live stop control — it has finished or stop no longer applies` }
|
|
226
|
+
}
|
|
227
|
+
entry.stopped = true
|
|
228
|
+
entry.ctrl.abort({ abortTrigger: "cancel", abortDetail: "sync-child-cancel" })
|
|
229
|
+
return { id: k, status: "cancelled" }
|
|
230
|
+
}
|
|
231
|
+
|
|
189
232
|
/** subagent action:"cancel" (§19.5 D-M6): depth-0 main-session control only.
|
|
190
233
|
* §20 D-SD5/round1 #4(queued 依赖取消——无 settle 事件):出队后**返回即**重估
|
|
191
234
|
* 依赖者——依赖者留 queued 标 dependency cancelled(refreshQueuedTokens 发更新块头
|
|
@@ -202,7 +245,7 @@ export function executeCancelAction(args, ctx) {
|
|
|
202
245
|
const key = String(id)
|
|
203
246
|
const agent = ctx.agent
|
|
204
247
|
const entry = agent._asyncSubagents?.get(key)
|
|
205
|
-
// §
|
|
248
|
+
// §11.2 (②-6b): an id that names no async SUBAGENT falls through to the
|
|
206
249
|
// async ADVISOR pool (the background reviews share the cancel surface — ⏹ on
|
|
207
250
|
// an advisor block / action:'cancel' with an advisor id abort that review).
|
|
208
251
|
if (!entry && agent?._asyncAdvisors?.has(key)) {
|
|
@@ -280,6 +323,10 @@ export async function runChildPipeline(child, input, childOpts, childRunOpts, {
|
|
|
280
323
|
return report
|
|
281
324
|
}
|
|
282
325
|
|
|
326
|
+
// ─── BATCH-3-STRUCTURE F-2:digest 注入批量预算(2026-09-09)───
|
|
327
|
+
// §22 D-DG1(群 B 批 B5):常量 / 判超 / 记账 / 落盘四处合一入 digest-budget.mjs 叶子模块
|
|
328
|
+
// (四族共享单源——D2);本文件保留 re-export(顶部)与经本入口的三族注入器接线。
|
|
329
|
+
|
|
283
330
|
/**
|
|
284
331
|
* Inject one settled async entry into the parent history as a user-role reminder
|
|
285
332
|
* (§17 D-S3 — the auto channel, sole consumption path since §19.8: turn-end
|
|
@@ -290,8 +337,13 @@ export async function runChildPipeline(child, input, childOpts, childRunOpts, {
|
|
|
290
337
|
*/
|
|
291
338
|
export async function injectAsyncResult(agent, entry) {
|
|
292
339
|
const body = entry.error ?? entry.report ?? "(no report)"
|
|
293
|
-
|
|
294
|
-
//
|
|
340
|
+
// F-2(BATCH-3-STRUCTURE)+ §22 D-DG2(群 B 批 B5):注入前查轮累计(单源)——超限条目不
|
|
341
|
+
// inline 全文,改清单行(全文经 persistOverflowReport 落盘——path 随行)。首条豁免
|
|
342
|
+
// (used===0 不判超):单条大报告 >64K offload 预览照旧——轮预算只约束累计。
|
|
343
|
+
const raw = String(body)
|
|
344
|
+
const over = digestBudgetOver(agent, raw.length)
|
|
345
|
+
const preview = (over && await persistOverflowReport(raw, { tag: `async-subagent-${entry.id}` })) || await offloadToolResult(raw, `async-subagent-${entry.id}`)
|
|
346
|
+
// §11.2: advisor entries label themselves (role "advisor") — the digest
|
|
295
347
|
// reminder says "async advisor review #N finished" (T-24b2 shape); subagent
|
|
296
348
|
// entries keep the legacy wording verbatim.
|
|
297
349
|
// §25 D-R17b (R17): escalate entries (role "escalate" — async 飞刀) label
|
|
@@ -364,7 +416,7 @@ export function mergeChildMutations(parent, child) {
|
|
|
364
416
|
if (!parent._touchedFiles.includes(abs)) parent._touchedFiles.push(abs)
|
|
365
417
|
merged.push(abs)
|
|
366
418
|
}
|
|
367
|
-
// §
|
|
419
|
+
// §11.2: merged code mutates the parent's state — log it for the stale
|
|
368
420
|
// scan of in-flight reviews (a review whose code changed under it is stale).
|
|
369
421
|
noteMutations(parent, merged)
|
|
370
422
|
if (parent._calledAdvisorThisRun) parent._calledAdvisorThisRun = false
|
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
* pending 单容器 _pendingAsyncResults 四族统一——escalate/consult 独立族退役))。
|
|
6
6
|
* 内容:executePanelAction(D-P2——readonly 视图面 + 门控 freeze)+ panelFreezeGate
|
|
7
7
|
* (D-P3 冻结门控)+ blockKeyIn(块归属判定随行)。
|
|
8
|
+
* CLI-ACTIVITY-DEBLOAT F-3(2026-09-10):手工面板镜像退役——视图面与门控改读时现算
|
|
9
|
+
* computePanelBlocks(ctx.state)(ctx.state = agent._tuiState——TUI 装配处接线——单账本);
|
|
10
|
+
* 未挂载(headless/VSC/子代理)→ 现算返 null → 降级/报不可用照旧
|
|
11
|
+
* (T-P5 语义零变)。门控语义零动(F-4)——仅数据源从镜像换现算。
|
|
8
12
|
* subagent-actions.mjs 尾部 re-export executePanelAction 保 subagent.mjs 既有 import 面。
|
|
9
13
|
*/
|
|
10
14
|
import { getAsyncPool } from "./async-settle.mjs"
|
|
15
|
+
import { computePanelBlocks } from "../tui/subagent-freeze.mjs" // F-3:面板视图读时现算(单账本——tui→core 单向依赖,无环)
|
|
11
16
|
|
|
12
17
|
/** 面板块 key(role#N)在池(Map——条目值)/pending 单容器(数组)中的归属判定。 */
|
|
13
18
|
function blockKeyIn(container, key) {
|
|
@@ -22,13 +27,15 @@ function blockKeyIn(container, key) {
|
|
|
22
27
|
* ——pending 已消费——状态滞后——补发冻结不破坏任何顺序)。
|
|
23
28
|
* - pending 仍有对应(报告未达模型)→ 拒绝(提前回收破坏消化顺序——T-P3)
|
|
24
29
|
* - 不存在的 key / 仍 running / done 的块 → 拒绝(T-P4——running 块 settle 时自冻)
|
|
25
|
-
* -
|
|
30
|
+
* - 无面板(现算返 null)→ 拒绝(headless/VS Code——freeze 不可用——T-P5)
|
|
26
31
|
* 错误信息明确(模型可解释 + 自助修正)。返回 { ok:true } 或 { err }。
|
|
27
32
|
*/
|
|
28
|
-
function panelFreezeGate(
|
|
29
|
-
|
|
33
|
+
function panelFreezeGate(ctx, key) {
|
|
34
|
+
// F-3(2026-09-10):镜像退役——经 ctx.state 读时现算(CLI-ACTIVITY-DEBLOAT)。
|
|
35
|
+
// 门控语义零动(F-4):awaitingDigest 限定/池归属查/pending 查逐字保留——仅数据源换现算。
|
|
36
|
+
const snap = computePanelBlocks(ctx.state)
|
|
30
37
|
if (!Array.isArray(snap)) {
|
|
31
|
-
return { err: "panel unavailable — no CLI TUI panel
|
|
38
|
+
return { err: "panel unavailable — no CLI TUI panel in this session (headless / VS Code / subagent contexts — freeze unavailable; panel is CLI-TUI-only, AC-P4)" }
|
|
32
39
|
}
|
|
33
40
|
const block = snap.find((b) => b.key === key)
|
|
34
41
|
if (!block) {
|
|
@@ -44,12 +51,12 @@ function panelFreezeGate(agent, key) {
|
|
|
44
51
|
}
|
|
45
52
|
return { err: `block ${key} is in state ${block.status} — freeze only reclaims awaitingDigest blocks whose report is already digested` }
|
|
46
53
|
}
|
|
47
|
-
if (blockKeyIn(getAsyncPool(agent, "subagent"), key) || blockKeyIn(getAsyncPool(agent, "advisor"), key)) {
|
|
54
|
+
if (blockKeyIn(getAsyncPool(ctx.agent, "subagent"), key) || blockKeyIn(getAsyncPool(ctx.agent, "advisor"), key)) {
|
|
48
55
|
return { err: `block ${key} still has a live pool entry — it is NOT a digested-stuck block (freeze refused; status action shows the pool)` }
|
|
49
56
|
}
|
|
50
57
|
// ASYNC-RESULT-CONTAINER.md D2:pending 单容器(四族统一——原 escalate/consult
|
|
51
58
|
// 独立族退役)。
|
|
52
|
-
if (blockKeyIn(agent._pendingAsyncResults, key)) {
|
|
59
|
+
if (blockKeyIn(ctx.agent._pendingAsyncResults, key)) {
|
|
53
60
|
return { err: `block ${key} is still genuinely awaiting digestion — its report is still pending and has NOT reached the model yet; freezing now would break the digestion order (wait for the digest run, which reclaims it automatically — §17.5.5)` }
|
|
54
61
|
}
|
|
55
62
|
return { ok: true }
|
|
@@ -58,15 +65,15 @@ function panelFreezeGate(agent, key) {
|
|
|
58
65
|
/**
|
|
59
66
|
* §19.6 subagent action:"panel"(D-P2——readonly 视图面 + 门控干预面——单动作双参,
|
|
60
67
|
* freeze 优先):
|
|
61
|
-
* - view
|
|
62
|
-
*
|
|
63
|
-
*
|
|
68
|
+
* - view(缺省——返回面板块列表):ctx.state(= agent._tuiState——CLI TUI 装配)经
|
|
69
|
+
* computePanelBlocks **读时现算**(F-3 单账本——与用户所见一致)。awaitingDigest 条目
|
|
70
|
+
* **读时交叉**
|
|
64
71
|
* _pendingAsyncResults/池 标注 digested(round1 #3——digested:true
|
|
65
72
|
* = 报告已消化但块仍驻留——异常块——freeze 候选;模型可定位解释 UI 怪相)。
|
|
66
73
|
* - freeze:key(D-P3 门控通过 → 发 key + "/" + ⟦ev⟧done 哨兵字面 token——
|
|
67
74
|
* onToken——TUI routeSubToken 冻结回收——落位复用 sub._freezeAt settle 锚点
|
|
68
75
|
* splice,无锚点尾推兜底——§17.5.5 同口径——round1 #2)。
|
|
69
|
-
*
|
|
76
|
+
* 无 TUI state(headless/VS Code——D-P2 round1 #1:webview 无 state.subTasks 对应物——
|
|
70
77
|
* 7.2.3.2 #8 先例)→ view 恒降级池视图(双池经 getAsyncPool + pending 单容器
|
|
71
78
|
* 合成)+ no panel 注;freeze 报不可用。CLI-only 完整能力(AC-P4)。
|
|
72
79
|
*/
|
|
@@ -80,10 +87,10 @@ export function executePanelAction(args, ctx) {
|
|
|
80
87
|
if ((ctx.depth ?? 0) > 0) {
|
|
81
88
|
return JSON.stringify({ status: "error", error: "panel freeze is only available at depth 0 — a child agent has no panel of its own (AGENT-LOOP.md §19.6 D-P2)" })
|
|
82
89
|
}
|
|
83
|
-
const gate = panelFreezeGate(
|
|
90
|
+
const gate = panelFreezeGate(ctx, freezeKey)
|
|
84
91
|
if (gate.err) return JSON.stringify({ status: "error", error: gate.err })
|
|
85
92
|
if (!ctx.callbacks?.onToken) {
|
|
86
|
-
return JSON.stringify({ status: "error", error: `panel
|
|
93
|
+
return JSON.stringify({ status: "error", error: `panel present but no token relay in this context — the freeze of ${freezeKey} cannot reach the TUI` })
|
|
87
94
|
}
|
|
88
95
|
// 门控通过 → 发 done 冻结事件(settle 同机制字面格式——TUI routeSubToken done
|
|
89
96
|
// 分支冻结回收——落位 _freezeAt settle 锚点 splice;无锚点(旧会话残留)时
|
|
@@ -100,9 +107,9 @@ export function executePanelAction(args, ctx) {
|
|
|
100
107
|
if (args?.view === false) {
|
|
101
108
|
return JSON.stringify({ status: "error", error: "panel has nothing to do — view:false with no freeze key; pass freeze:'role#N' to reclaim a digested-stuck block, or omit view (defaults to true)" })
|
|
102
109
|
}
|
|
103
|
-
const snap =
|
|
110
|
+
const snap = computePanelBlocks(ctx.state)
|
|
104
111
|
if (!Array.isArray(snap)) {
|
|
105
|
-
// F-P3
|
|
112
|
+
// F-P3 降级:无 TUI state(headless/VS Code/子代理上下文——CLI TUI-only 完整能力)→
|
|
106
113
|
// 池视图(双池经 getAsyncPool——运行/排队条目 + pending 单容器待消化条目)
|
|
107
114
|
const blocks = []
|
|
108
115
|
const queue = agent._asyncQueue ?? []
|
|
@@ -132,7 +139,7 @@ export function executePanelAction(args, ctx) {
|
|
|
132
139
|
}
|
|
133
140
|
return JSON.stringify({
|
|
134
141
|
degraded: true,
|
|
135
|
-
note: "no panel — this session has no CLI TUI panel
|
|
142
|
+
note: "no panel — this session has no CLI TUI panel (headless / VS Code / subagent context — panel view is CLI-TUI-only, AC-P4); pool-derived view below; action:'status' shows the full pool",
|
|
136
143
|
panel: blocks,
|
|
137
144
|
})
|
|
138
145
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { ASYNC_POOL_LIMITS, poolDomainOf, poolLimitsFor, runningPoolCount } from "./subagent-async.mjs"
|
|
9
9
|
import { runChildPipeline } from "./subagent-async.mjs"
|
|
10
10
|
import { logEvent } from "../log.mjs"
|
|
11
|
+
import { deathLine } from "../abort-provenance.mjs"
|
|
11
12
|
import { pushReal } from "../context.mjs"
|
|
12
13
|
import {
|
|
13
14
|
describeBlockers, refreshQueuedTokens,
|
|
@@ -36,7 +37,7 @@ export function drainInjectedQueue(entry, agent) {
|
|
|
36
37
|
* turn-cap / permission / MIN_REPORT_CHARS / mergeChildMutations all unchanged),
|
|
37
38
|
* but the parent does not await it: the promise is parked in _asyncSubagents and
|
|
38
39
|
* consumed by the auto channel (§19.8 — turn-end collection / suspension digest;
|
|
39
|
-
* the check action is gone). Slot queue (§
|
|
40
|
+
* the check action is gone). Slot queue (§11.1 D-24a/R14 — 分域): the entry carries a
|
|
40
41
|
* pool domain (_pool = poolDomainOf(role)); running count < limit[its domain]
|
|
41
42
|
* (agent.poolLimits — default engCoder 4 / other 4) → start now; ≥ → enqueue
|
|
42
43
|
* (status "queued", position = queue index) — never rejected, never requiring the
|
|
@@ -52,7 +53,7 @@ export function executeAsyncSpawn(parent, ctx, role, args, child, input, childOp
|
|
|
52
53
|
const id = parent._subAgentCounter
|
|
53
54
|
const entry = {
|
|
54
55
|
id, role, relayPrefix,
|
|
55
|
-
// §
|
|
56
|
+
// §11.1 D-24a/R14:池域字段(域判定单一事实源——poolDomainOf——role 枚举见
|
|
56
57
|
// subagent.mjs ROLES;未知角色归 other)——running 计数/补位按域过滤。
|
|
57
58
|
_pool: poolDomainOf(role),
|
|
58
59
|
status: "queued", // 下面按等待态/槽位重定(避免两处判断漂移)
|
|
@@ -80,7 +81,7 @@ export function executeAsyncSpawn(parent, ctx, role, args, child, input, childOp
|
|
|
80
81
|
_injected: [],
|
|
81
82
|
}
|
|
82
83
|
// §20 D-SD3 准入落点:等待态(依赖未满足/域冲突/depc)→ queued(waiting-deps——
|
|
83
|
-
// 不占槽不启动——即使槽空);纯槽满(kind slot)→ 按域计数判定(§
|
|
84
|
+
// 不占槽不启动——即使槽空);纯槽满(kind slot)→ 按域计数判定(§11.1 D-24a:
|
|
84
85
|
// runningIn(domain) < limit(domain)——跨域互不阻塞——每次入池判定时读配置)。
|
|
85
86
|
const blockers = describeBlockers(parent, entry)
|
|
86
87
|
if (blockers.kind === "slot") {
|
|
@@ -99,8 +100,9 @@ export function executeAsyncSpawn(parent, ctx, role, args, child, input, childOp
|
|
|
99
100
|
entry.controller = ctrl
|
|
100
101
|
const baseSignal = buildChildSignal(parent, ctx)
|
|
101
102
|
if (baseSignal) {
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
// §20.3 站点 #10(第 24 批):hop 逐跳保 reason
|
|
104
|
+
if (baseSignal.aborted) ctrl.abort(baseSignal.reason)
|
|
105
|
+
else baseSignal.addEventListener("abort", () => ctrl.abort(baseSignal.reason), { once: true })
|
|
104
106
|
}
|
|
105
107
|
// §19.5 D-M5:turn 镜像拦截层(callbacks 包装层——选改动最小方案:在既有
|
|
106
108
|
// wrapChildCallbacks 之外再包一层,只解析 ⟦ev⟧turn 更新条目,其余原样转发)。
|
|
@@ -156,7 +158,8 @@ export function executeAsyncSpawn(parent, ctx, role, args, child, input, childOp
|
|
|
156
158
|
askContinue: () => Promise.resolve(Boolean(parent.config?.agent?.engineering && parent.autoApprove)),
|
|
157
159
|
})
|
|
158
160
|
.then((report) => { entry.report = report })
|
|
159
|
-
.
|
|
161
|
+
// §20.3 第 3 条合成器(第 24 批):原 message 前缀逐字保留 + 来源后缀
|
|
162
|
+
.catch((err) => { entry.error = deathLine(err, entry.controller?.signal) })
|
|
160
163
|
.finally(() => {
|
|
161
164
|
// SUBAGENT-OBSERVE-SEND D3(send→settle 竞态):settle 收尾时 _injected 仍残留
|
|
162
165
|
// = 消息入队后子代理在下一回合边界前 settle——未投递——附 settle 报告/错误提示
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* subagent-scheduler.mjs — §20 子 agent 任务调度器 + 文件域组(2026-09-05 自
|
|
3
3
|
* subagent-async.mjs 拆分——Module Split Policy §20.9——纯迁移零行为变化——
|
|
4
4
|
* AGENT-LOOP.md §20 D-SD1..SD5 + §21.1 D-SL1 环形死锁修正)。
|
|
5
|
-
* 内容:normalizeFileList / filesOverlap /
|
|
5
|
+
* 内容:normalizeFileList / filesOverlap / effectiveFiles(SCHEDULER-DYNAMIC-DOMAIN
|
|
6
|
+
* 动态域——声明 ∪ running touched)/ depInfo / describeBlockers / detectStall(§21.1
|
|
6
7
|
* P-SL2 停滞机械检测)/ queueRunnable / assertNoDepCycle / dependentLabels /
|
|
7
8
|
* refreshQueuedTokens / maybeRefillAsync。
|
|
8
9
|
* ASYNC_POOL_LIMITS/poolLimitsFor/runningPoolCount 回引自 subagent-async.mjs(主体保有——
|
|
@@ -40,7 +41,7 @@ export function normalizeFileList(files, cwd) {
|
|
|
40
41
|
const violations = [] // §28 R26 F-R26b:父侧维护文件命中(保留声明原样——提示可读)
|
|
41
42
|
for (const f of Array.isArray(files) ? files : []) {
|
|
42
43
|
if (typeof f !== "string" || !f.trim()) continue
|
|
43
|
-
if (f.endsWith("/") || f.endsWith("\\")) {
|
|
44
|
+
if (f.trimEnd().endsWith("/") || f.trimEnd().endsWith("\\")) { // 2.7 批:尾随空格目录声明("test/ ")同拒
|
|
44
45
|
throw new Error(`files must be file-level paths — directory declarations are not supported: ${f}`)
|
|
45
46
|
}
|
|
46
47
|
const abs = resolve(cwd ?? process.cwd(), f)
|
|
@@ -80,6 +81,23 @@ function showFile(parent, key) {
|
|
|
80
81
|
return rel && !rel.startsWith("..") && !isAbsolute(rel) ? rel : key
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
/** 有效文件域(SCHEDULER-DYNAMIC-DOMAIN 2026-09-09——动态域 = 声明 ∪ 运行中实际写入):
|
|
85
|
+
* running 且已绑 childAgent → 声明域 ∪ childAgent._touchedFiles(写工具批提交实时记录
|
|
86
|
+
* ——绝对路径与 normalizeFileList 同源——fileKey/filesOverlap 键空间零改动);去重按
|
|
87
|
+
* fileKey。queued 无 childAgent(start 才绑——VSC 首 onAgentTurn 才绑)——天然只声明域
|
|
88
|
+
* (`?.` null 安全——既有无 childAgent 用例零行为变化)。 */
|
|
89
|
+
export function effectiveFiles(e) {
|
|
90
|
+
const declared = e._files ?? []
|
|
91
|
+
if (e.status !== "running" || !e.childAgent) return declared
|
|
92
|
+
const out = [...declared]
|
|
93
|
+
const seen = new Set(declared.map(fileKey))
|
|
94
|
+
for (const f of e.childAgent._touchedFiles ?? []) {
|
|
95
|
+
const k = fileKey(f)
|
|
96
|
+
if (!seen.has(k)) { seen.add(k); out.push(f) }
|
|
97
|
+
}
|
|
98
|
+
return out
|
|
99
|
+
}
|
|
100
|
+
|
|
83
101
|
/**
|
|
84
102
|
* §20 依赖终态查询(单点事实——池条目 / pending(挂起期 settle 移交——注入前)/
|
|
85
103
|
* 终态墓碑(自动通道注入消费——consumed;取消/失败——D-SD5 分支)):
|
|
@@ -127,13 +145,17 @@ export function describeBlockers(parent, entry) {
|
|
|
127
145
|
for (const e of parent._asyncSubagents?.values() ?? []) {
|
|
128
146
|
if (e === entry) continue
|
|
129
147
|
if (e.status !== "running" && e.status !== "queued") continue
|
|
130
|
-
|
|
148
|
+
// SCHEDULER-DYNAMIC-DOMAIN(2026-09-09):他条目域 = effectiveFiles(e)(声明 ∪
|
|
149
|
+
// running 实际 touched);命中仅来自 touched(∉ 声明域)→ 加注"运行中实际写入"
|
|
150
|
+
// ——纯声明命中文案不回归(评审 #7 重叠优先级:声明∩touched 同文件不加注)。
|
|
151
|
+
const hitDeclared = filesOverlap(myFiles, e._files ?? [])
|
|
152
|
+
const hit = hitDeclared ?? filesOverlap(myFiles, effectiveFiles(e))
|
|
131
153
|
if (!hit) continue
|
|
132
154
|
// §21.1 D-SL1.2(环形死锁修正——与 queueRunnable 同界——展示一致):后入
|
|
133
155
|
// 者(spawn 序晚于我——数字 id 比较)不列——只列"会真正阻断我的"(running
|
|
134
156
|
// 任意序 + 先入 queued);列后入者 = 误导"等一个其实等不到的人"。
|
|
135
157
|
if (e.status === "queued" && Number(e.id) > Number(entry.id)) continue
|
|
136
|
-
wait.push(`${e.role}#${e.id}(域冲突 ${showFile(parent, hit)})`)
|
|
158
|
+
wait.push(`${e.role}#${e.id}(域冲突 ${showFile(parent, hit)}${hitDeclared ? "" : "(运行中实际写入)"})`)
|
|
137
159
|
}
|
|
138
160
|
}
|
|
139
161
|
// 长列表裁剪(块头宽度预算——细节 status 可查全量)
|
|
@@ -192,7 +214,7 @@ export function detectStall(parent) {
|
|
|
192
214
|
for (const e of all) {
|
|
193
215
|
if (e === q) continue
|
|
194
216
|
if (e.status !== "running" && e.status !== "queued") continue
|
|
195
|
-
const hit = filesOverlap(myFiles, e
|
|
217
|
+
const hit = filesOverlap(myFiles, effectiveFiles(e)) // 动态域(running 锚点守卫下零增量——读法同界防御一致)
|
|
196
218
|
if (!hit) continue
|
|
197
219
|
if (e.status === "queued" && Number(e.id) > Number(q.id)) continue // 后入不阻断
|
|
198
220
|
list.push({ to: e, reason: `files ${showFile(parent, hit)}——先入者` })
|
|
@@ -250,7 +272,7 @@ export function queueRunnable(parent, entry) {
|
|
|
250
272
|
for (const e of parent._asyncSubagents?.values() ?? []) {
|
|
251
273
|
if (e === entry) continue
|
|
252
274
|
if (e.status !== "running" && e.status !== "queued") continue
|
|
253
|
-
if (!filesOverlap(myFiles, e
|
|
275
|
+
if (!filesOverlap(myFiles, effectiveFiles(e))) continue // 动态域:声明 ∪ running touched
|
|
254
276
|
// §21.1 D-SL1.1 序判定:queued 仅"先入者"(spawn 序早于我——数字 id 比较)阻断;
|
|
255
277
|
// 后入者不阻断——先入者先启动——两个 queued 同文件不再互等(环形死锁修正)。
|
|
256
278
|
// 防御(评审 #3——id 形态):池条目 id 为数字递增(_subAgentCounter——已核实);
|
|
@@ -316,12 +338,12 @@ export function refreshQueuedTokens(parent, onToken) {
|
|
|
316
338
|
}
|
|
317
339
|
|
|
318
340
|
/**
|
|
319
|
-
* Slot-queue refill (AGENT-LOOP.md §15 D-A1/D-A6 + §20 D-SD4 + §
|
|
341
|
+
* Slot-queue refill (AGENT-LOOP.md §15 D-A1/D-A6 + §20 D-SD4 + §11.1 D-24a/R14 分域):
|
|
320
342
|
* start queue heads while a running slot is free — called from every settle
|
|
321
343
|
* (completion frees a slot) and from the turn-end collection's refill loop. §20:
|
|
322
344
|
* 队列现可混合 waiting-deps 与 slot-queued——扫描选"依赖全满足 + 域无冲突"的最早
|
|
323
345
|
* 条目启动(waiting 越行不阻塞槽位;多任务同时解除按 queued 序逐个启动)。
|
|
324
|
-
* §
|
|
346
|
+
* §11.1 D-24a(R14):槽位判定按域——条目 _pool 域内 running 数 < 该域上限才启动
|
|
325
347
|
* (同域仍 4——纯单域队列行为与旧 shift 完全一致;跨域总量 8——各域独立腾槽,
|
|
326
348
|
* 互不阻塞)。配置每次补位时读(poolLimitsFor——变更即生效下个补位)。
|
|
327
349
|
*/
|
|
@@ -340,4 +362,31 @@ export function maybeRefillAsync(parent) {
|
|
|
340
362
|
if (pick < 0) return
|
|
341
363
|
queue.splice(pick, 1)[0].start()
|
|
342
364
|
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* 分配下一子代理 id——池活续号兜底(SUBAGENT-ID-COUNTER-AGENT,2026-09-09——双端同构
|
|
369
|
+
* 机制:VSC 端载体自 history expando 移本体并接本函数;CLI 端计数器一直挂 agent 本体
|
|
370
|
+
* (`_subAgentCounter`——per-run reset 清单不含——跨 run/跨压缩存活),本函数补齐
|
|
371
|
+
* poolMax 兜底:计数器丢失面(AUTO 撞上限续跑/挂起驱动的重建形态)不再复用仍在
|
|
372
|
+
* 跑/排队的条目 id——map.set 覆盖旧条目 = 静默丢报告 + status/cancel 错址)。优先级
|
|
373
|
+
* next = max(counter ?? 0, poolMax) + 1——取号后 counter 同步(首取号初始化)。id 作用
|
|
374
|
+
* 域 = 进程内(不做槽持久化——reload 后池清块消失,新进程从 1 无冲突——设计范围边界)。
|
|
375
|
+
* spawn(subagent-spawn.mjs async 分支)、escalate(escalate-async.mjs)、async-advisor
|
|
376
|
+
* 池(advisor-async.mjs——§11.2 跨池共号)共用;executeAsyncSpawn 直读 counter(分配与
|
|
377
|
+
* 消费同步——无 await 间隙)。CLI 两池键均为字符串(set(String(id))——解析分支防御保留)。
|
|
378
|
+
* @returns {number} 全池唯一的下一 id(单调——进程内)
|
|
379
|
+
*/
|
|
380
|
+
export function nextSubagentId(parent) {
|
|
381
|
+
let poolMax = 0
|
|
382
|
+
for (const pool of [parent?._asyncSubagents, parent?._asyncAdvisors]) {
|
|
383
|
+
if (!pool || pool.size === 0) continue
|
|
384
|
+
for (const k of pool.keys()) {
|
|
385
|
+
const n = typeof k === "number" ? k : Number.parseInt(k, 10)
|
|
386
|
+
if (Number.isFinite(n) && n > poolMax) poolMax = n
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const next = Math.max(parent?._subAgentCounter ?? 0, poolMax) + 1
|
|
390
|
+
parent._subAgentCounter = next
|
|
391
|
+
return next
|
|
343
392
|
}
|
|
@@ -8,15 +8,17 @@
|
|
|
8
8
|
* slot 族同域——ENGINEERING-MODE.md §2.6 F1——removeDesignTokenSlot 自 token-ttl.mjs)。
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { resolve } from "node:path"
|
|
12
|
+
import { existsSync, statSync } from "node:fs"
|
|
11
13
|
import {
|
|
12
14
|
createAgent,
|
|
13
15
|
readonlyToolNames, escapeXml,
|
|
14
|
-
EXPLORE_OVERLAY, CODER_OVERLAY, PLAN_OVERLAY, ENG_CODER_OVERLAY,
|
|
15
16
|
} from "../agent.mjs"
|
|
16
|
-
import { makeRelay, wrapChildCallbacks } from "../agent/spawn-child.mjs"
|
|
17
|
+
import { makeRelay, wrapChildCallbacks, relayPrefixOf } from "../agent/spawn-child.mjs"
|
|
17
18
|
import { validateDesignToken } from "./advisor.mjs"
|
|
18
19
|
import { tokenExpired, removeDesignTokenSlot, reconcileEngTokensFromSlot, persistEngTokens } from "../token-ttl.mjs"
|
|
19
|
-
import { resolveChildProvider, buildChildRunOpts } from "./subagent-async.mjs"
|
|
20
|
+
import { resolveChildProvider, buildChildRunOpts, enqueueAsk } from "./subagent-async.mjs"
|
|
21
|
+
import { nextSubagentId } from "./subagent-scheduler.mjs"
|
|
20
22
|
import {
|
|
21
23
|
normalizeFileList, describeBlockers, assertNoDepCycle, depInfo,
|
|
22
24
|
} from "./subagent-scheduler.mjs"
|
|
@@ -238,6 +240,27 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
|
|
|
238
240
|
// Provider/model override: tool `model` arg > subagentModels[role] > subagentModel > parent provider
|
|
239
241
|
const childProvider = resolveChildProvider(parent, effectiveSubagentModel(parent, role, args.model))
|
|
240
242
|
|
|
243
|
+
// ── batchDoc gate(ENGINEERING-MODE.md §2.12 + §2.15 D1——FR20 #9 机械面)────────
|
|
244
|
+
// 工程模式下 spawn 工程角色(**eng-coder / eng-designer**——第 2 批扩为角色集合)必传
|
|
245
|
+
// 批次档路径(需求 §1.11 铁律 #5 随件传递的机械面)。落点 = 本装配点(token 门之前):
|
|
246
|
+
// sync 与 async 两条 spawn 路径都经过 buildSpawnChild——一处校验双路生效,错误出口与
|
|
247
|
+
// token 门一致(§2.13.1 选型 A)。
|
|
248
|
+
// 判据只到"参数在 + 路径可读"——**不校验内容/措辞**(不做"已收口"正则、不匹配模板、
|
|
249
|
+
// 不生成;内容够不够由执行者拒收兜底——需求 §1.14 #9 行为面)。路径语义照
|
|
250
|
+
// `files` 先例:cwd 相对或绝对均可,`\` 归一为 `/`。错误文案带**实际角色名**(§2.15
|
|
251
|
+
// 越界文案参数化——designer 撞门时不误导)。
|
|
252
|
+
let batchDocAbs = null
|
|
253
|
+
if (role === "eng-coder" || role === "eng-designer") {
|
|
254
|
+
const given = typeof args.batchDoc === "string" ? args.batchDoc.trim() : ""
|
|
255
|
+
const refusal = (suffix = "") => new Error(
|
|
256
|
+
`batchDoc is required for role='${role}' — pass the batch record path (docs/batches/<batch>-<topic>.md); spawn refused without it.` + suffix)
|
|
257
|
+
if (!given) throw refusal()
|
|
258
|
+
batchDocAbs = resolve(parent.cwd ?? process.cwd(), given.replace(/\\/g, "/"))
|
|
259
|
+
let readable = false
|
|
260
|
+
try { readable = existsSync(batchDocAbs) && statSync(batchDocAbs).isFile() } catch { readable = false }
|
|
261
|
+
if (!readable) throw refusal(" (given path is not a readable file)")
|
|
262
|
+
}
|
|
263
|
+
|
|
241
264
|
// eng-coder token gate: the design review must have passed and the caller must
|
|
242
265
|
// present the exact token advisor issued — otherwise the child is not authorized to code.
|
|
243
266
|
// 2026-09-01: multi-design slots — the token is located by designId (exact slot,
|
|
@@ -267,12 +290,10 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
|
|
|
267
290
|
tools = parent.tools
|
|
268
291
|
}
|
|
269
292
|
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
else if (role === "plan") overlay = PLAN_OVERLAY
|
|
275
|
-
else if (role === "eng-coder") overlay = ENG_CODER_OVERLAY
|
|
293
|
+
// PROMPT-SYSTEM 施工② G3(2026-09-10):overlay(人格)装载随装配改造退役——人格槽
|
|
294
|
+
// 由 assemblePrompt 的 D1 场景表按 role 承载(persona-{role},explore/coder/plan =
|
|
295
|
+
// PERSONA_NORMAL 同源);child.overlay 恒空(setup 不再前缀叠加)。
|
|
296
|
+
const overlay = ""
|
|
276
297
|
|
|
277
298
|
// explore/plan: force read-only permission; coder/default: AUTO passes through directly,
|
|
278
299
|
// manual mode queues permission requests for the parent agent's approval UI (human in the loop, child agent is no longer silently rejected)
|
|
@@ -284,15 +305,27 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
|
|
|
284
305
|
} else {
|
|
285
306
|
childPermission = async (name, toolArgs) => {
|
|
286
307
|
if (!ctx.onPermissionRequest) return false
|
|
287
|
-
|
|
308
|
+
// SYNC-CANCEL v2(用户裁——2026-09-09):工具权限 ask 加 owner key 标识——
|
|
309
|
+
// name `${key}/${tool}`(原 `${role}/${tool}` 无 key——模态 deny 归属判定 +
|
|
310
|
+
// 显示可辨 child);relayPrefix 在 buildSpawnChild 尾部定值——闭包运行于 child
|
|
311
|
+
// 实际 runAgent(buildSpawnChild 返回后)——无 TDZ 风险
|
|
312
|
+
const ownerKey = relayPrefix.slice(0, -1)
|
|
313
|
+
const ask = () => {
|
|
314
|
+
// ⏹ 后(entry.stopped)不再弹模态——直接拒绝(_permQueue 排队 ask 到达时查
|
|
315
|
+
// stopped 旗标——子代理随即在 abort 检出点解绕——v2 模态 deny)
|
|
316
|
+
if (parent._syncChildAborts?.get(ownerKey)?.stopped) return Promise.resolve(false)
|
|
317
|
+
return ctx.onPermissionRequest(`${ownerKey}/${name}`, toolArgs)
|
|
318
|
+
}
|
|
288
319
|
// Queue parallel child agent permission requests to avoid two popups simultaneously overwriting each other (lesson from question tool)
|
|
289
|
-
|
|
290
|
-
return parent._permQueue
|
|
320
|
+
return enqueueAsk(parent, "_permQueue", ask)
|
|
291
321
|
}
|
|
292
322
|
}
|
|
293
323
|
|
|
294
|
-
//
|
|
295
|
-
|
|
324
|
+
// G6(施工②)+ §2.15 B4(第 2 批):工程角色场景即工程纪律——engineering=true 使 setup
|
|
325
|
+
// 场景映射落到 assemblePrompt("eng-coder"/"eng-designer")(persona-{role} + common +
|
|
326
|
+
// discipline-engineering);designer 也必须 engineering=true 才能装配工程纪律槽。
|
|
327
|
+
const engineeringRole = role === "eng-coder" || role === "eng-designer"
|
|
328
|
+
const childConfig = engineeringRole
|
|
296
329
|
? { ...parent.config, agent: { ...parent.config.agent, engineering: true } }
|
|
297
330
|
: parent.config
|
|
298
331
|
|
|
@@ -306,6 +339,11 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
|
|
|
306
339
|
role,
|
|
307
340
|
})
|
|
308
341
|
|
|
342
|
+
// §2.20.2 批次档段写入通道绑定(第 4 批):工程角色(eng-coder/eng-designer)把批次档
|
|
343
|
+
// 绝对路径记在 child 上——setup 挂载 batch_segment 时读它。无路径参数的工具靠这条
|
|
344
|
+
// 绑定决定目标档(spawn 门已保证「参数在 + 路径可读」)。
|
|
345
|
+
if (engineeringRole) child._batchDoc = batchDocAbs
|
|
346
|
+
|
|
309
347
|
// Token-verified design review → child is authorized to modify files without re-reviewing
|
|
310
348
|
if (role === "eng-coder") child._engDesignReviewed = true
|
|
311
349
|
// §18 D-E3 task-domain authorization: approved design + spawn task = authorization.
|
|
@@ -330,6 +368,12 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
|
|
|
330
368
|
// git 只读变体亦随裁定废弃——D-AG5)。顶层主 agent 注入保留(§3 prepareRun——
|
|
331
369
|
// setup.mjs depth===0——D-AG7 范围边界)。
|
|
332
370
|
let input = args.context ? `Context:\n${args.context}\n\nTask:\n${args.task}` : args.task
|
|
371
|
+
// §2.11 第 1 点(FR16 载体 + FR17 铁律 #5 的机械面):批次档**绝对路径**随任务输入
|
|
372
|
+
// 下发给工程角色(eng-coder 实现 / eng-designer 写稿)——追加一行,任务文本本身不动
|
|
373
|
+
// (批次档 §2 才是任务书本体;本行让子代理"拿到本档路径")。行文不含
|
|
374
|
+
// summarizeEngTaskBook 的三组段 marker(Docs involved / Files list / Acceptance
|
|
375
|
+
// criteria)——段匹配不受影响。仅工程角色注入。
|
|
376
|
+
if (engineeringRole) input += `\n\nBatch record (batchDoc): ${batchDocAbs}`
|
|
333
377
|
// §18 D-E2 ③ (round4 #4, T-E13/T-E15): an eng-coder audit spawn's task book is
|
|
334
378
|
// the eng-coder's OWN spawn task — mechanically kept as _engTaskInput by the
|
|
335
379
|
// parent spawn and injected as the D-TS5 A2 mechanical summary (design docs /
|
|
@@ -389,8 +433,11 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
|
|
|
389
433
|
// children don't paint an empty panel block ("queued 态不显示").
|
|
390
434
|
let relayPrefix
|
|
391
435
|
if (wantAsync) {
|
|
392
|
-
|
|
393
|
-
|
|
436
|
+
// SUBAGENT-ID-COUNTER-AGENT(2026-09-09):async id 取号统一走 nextSubagentId
|
|
437
|
+
// (池活续号兜底——counter 载体= agent 本体 _subAgentCounter——跨 run/跨压缩
|
|
438
|
+
// 存活——per-run reset 清单不含它)。sync 分支 makeRelay 不进池——照旧。
|
|
439
|
+
const id = nextSubagentId(parent)
|
|
440
|
+
relayPrefix = relayPrefixOf(role, id)
|
|
394
441
|
} else {
|
|
395
442
|
relayPrefix = makeRelay(parent, role ?? "sub", ctx.callbacks?.onToken, childProvider.model ?? "")
|
|
396
443
|
}
|