thincoder 0.12.58 → 0.12.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -2
- package/README.md +3 -3
- package/bin/thincoder.mjs +88 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +135 -26
- package/src/advisor/messages.mjs +57 -4
- package/src/advisor/run.mjs +119 -79
- package/src/advisor.mjs +34 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +182 -22
- package/src/agent/helpers.mjs +71 -4
- package/src/agent/record-results.mjs +46 -10
- package/src/agent/run-stages.mjs +227 -0
- package/src/agent/setup-reminders.mjs +62 -0
- package/src/agent/setup.mjs +107 -20
- package/src/agent/spawn-child.mjs +54 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +133 -109
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +154 -104
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +26 -30
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +284 -0
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +152 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +470 -0
- package/src/agent-tools/subagent-async.mjs +382 -0
- package/src/agent-tools/subagent-panel.mjs +153 -0
- package/src/agent-tools/subagent-run.mjs +202 -0
- package/src/agent-tools/subagent-scheduler.mjs +343 -0
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +203 -377
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +198 -238
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +145 -242
- package/src/auto-think.mjs +14 -0
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +4 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +61 -8
- package/src/context.mjs +81 -163
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +30 -12
- package/src/escape.mjs +6 -4
- package/src/explore-distill.mjs +155 -0
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +11 -72
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +206 -87
- package/src/memory.mjs +3 -1
- package/src/model-specs.mjs +15 -1
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +25 -0
- package/src/prompts/advisor-design.md +18 -39
- package/src/prompts/advisor-round1.md +20 -32
- package/src/prompts/advisor-round2.md +16 -16
- package/src/prompts/advisor-round3.md +16 -16
- package/src/prompts/coder.md +7 -28
- package/src/prompts/consult-base.md +4 -11
- package/src/prompts/discipline.md +31 -44
- package/src/prompts/eng-coder.md +9 -34
- package/src/prompts/engineering-sub.md +10 -8
- package/src/prompts/engineering.md +61 -264
- package/src/prompts/explore.md +4 -14
- package/src/prompts/main.md +18 -35
- package/src/prompts/methodology-template.md +32 -38
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +18 -35
- package/src/provider/core.mjs +62 -69
- package/src/provider/errors.mjs +76 -0
- package/src/provider/retry.mjs +8 -45
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +181 -58
- package/src/session.mjs +48 -89
- package/src/token-ttl.mjs +273 -0
- package/src/tools/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +131 -44
- package/src/tools/edit-diff.mjs +348 -0
- package/src/tools/edit.md +20 -13
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +55 -24
- package/src/tools/file.mjs +25 -70
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.mjs +14 -6
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -3
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +2 -0
- package/src/tools/lsp.md +4 -1
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +84 -13
- package/src/tools/question.md +5 -1
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +18 -25
- package/src/tools/system.mjs +50 -30
- package/src/tools/tree.md +2 -1
- package/src/tools/wait_for.md +22 -0
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +179 -27
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +77 -16
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +17 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +11 -11
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +31 -96
- package/src/tui/interaction.mjs +13 -2
- package/src/tui/key-handler.mjs +105 -155
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +46 -0
- package/src/tui/pickers.mjs +51 -25
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +27 -10
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +182 -0
- package/src/tui/startup.mjs +40 -0
- package/src/tui/subagent-blocks.mjs +272 -262
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +125 -12
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +127 -231
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +76 -0
- package/src/tui/wizard.mjs +48 -12
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-stages.mjs — runAgent 主循环的阶段函数族(CLI,2026-09-05 实践轮:agent.mjs
|
|
3
|
+
* runAgent 383 ≥300 按骨干—细节两层提取——VS Code 端同构文件(双端 parity——拆分
|
|
4
|
+
* 两端同步))。本文件承载:回合压缩检查(runCompactionCheck)、回合注入组
|
|
5
|
+
* (injectTurnReminders——plan cadence + eng 状态)、回合收尾(finalizeAgentTurn——
|
|
6
|
+
* consult 清理/async 池分流/guard 继承)与其收集器(collectSettledAsync——自
|
|
7
|
+
* agent.mjs 迁入,agent.mjs 内无其他引用)。verbatim 迁移 + 签名化,语义零变。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { compressIfNeeded, compressFallback, COMPRESS_FAILURE_LIMIT } from "../context.mjs"
|
|
11
|
+
import { ensureAutoReminder, injectEngineeringReminder, ContinueError } from "./helpers.mjs"
|
|
12
|
+
import { cleanupConsultSessions } from "../agent-tools/consult.mjs"
|
|
13
|
+
import { logEvent } from "../log.mjs"
|
|
14
|
+
// ASYNC-RESULT-CONTAINER.md D1:池 accessor(absorb 双池——advisor 独立池无队列)
|
|
15
|
+
import { getAsyncPool } from "../agent-tools/async-settle.mjs"
|
|
16
|
+
// R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3a):回合末域登记 flush(写工具钩子累积 →
|
|
17
|
+
// 整写一次本实例 peers 文件——无写入跳过;失败容忍不抛)
|
|
18
|
+
import { flushPeerDomains } from "../peer-domains.mjs"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 响应后置提醒注入(2026-09-05 实践轮——自 runAgent 响应处理链提取,verbatim):
|
|
22
|
+
* 流规则 warning(warn 动作——流已完成——去重注入,模型下轮可见)+ 异常 finish
|
|
23
|
+
* reason 警告(响应可能不完整/截断——reasonMap 人类化描述注入)。
|
|
24
|
+
*/
|
|
25
|
+
export function injectResponseReminders(agent, response) {
|
|
26
|
+
// Stream rule warnings (action: "warn"): stream completed; inject de-duplicated
|
|
27
|
+
// warnings so the model sees them before its next response.
|
|
28
|
+
if (response._warnings?.length) {
|
|
29
|
+
const deDuplicated = [...new Map(response._warnings.map(w => [w.name || w.pattern, w])).values()]
|
|
30
|
+
agent.history.push({
|
|
31
|
+
role: "user",
|
|
32
|
+
content: `[System reminder — stream rule warnings from your last response:\n${deDuplicated.map(w => `- ${w.name || w.pattern}: ${w.message}`).join("\n")}]`,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Warn on abnormal finish reasons — the response may be incomplete/truncated.
|
|
37
|
+
if (response.finishReason && response.finishReason !== "stop" && response.finishReason !== "tool_calls") {
|
|
38
|
+
const reasonMap = {
|
|
39
|
+
length: "output token limit reached after exhausting continuations",
|
|
40
|
+
insufficient_system_resource: "provider inference resources exhausted — consider retrying or switching models",
|
|
41
|
+
content_filter: "response blocked by provider content filtering",
|
|
42
|
+
}
|
|
43
|
+
const detail = reasonMap[response.finishReason] || `unknown reason "${response.finishReason}"`
|
|
44
|
+
agent.history.push({
|
|
45
|
+
role: "user",
|
|
46
|
+
content: `[System reminder: the previous turn ended abnormally — ${detail}. The assistant response that follows may be incomplete.]`,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 回合压缩检查(2026-09-05 实践轮——自 runAgent 主循环提取):仅安全点(history 以
|
|
53
|
+
* 完整交换结尾——user/tool 位)。成功:失败计数清零/计划提醒 cadence 复位/停滞检测
|
|
54
|
+
* 计数复位(recentCallSigs——runAgent 回合级局部——经 ctx 传对象引用)/压缩完成事件
|
|
55
|
+
* /AUTO 提醒重注入。失败:AbortError 透传;计数 + Q3 onCompressFail;
|
|
56
|
+
* COMPRESS_FAILURE_LIMIT 连续失败降级 compressFallback(确定性截断——不发 LLM)。
|
|
57
|
+
*/
|
|
58
|
+
export async function runCompactionCheck(agent, ctx) {
|
|
59
|
+
const { threshold, callbacks, compactionOverhead, signal, recentCallSigs } = ctx
|
|
60
|
+
try {
|
|
61
|
+
if (await compressIfNeeded(agent, threshold, callbacks, compactionOverhead, signal)) {
|
|
62
|
+
agent._compressFailures = 0
|
|
63
|
+
agent._planReminderAtLen = 0 // After compression history shrinks, reset cadence so reminders resume
|
|
64
|
+
recentCallSigs.length = 0 // After compression history is rebuilt, reset stall detection counter
|
|
65
|
+
// Completion info (CONTEXT-COMPACTION §7 D-C2): { mode, tokensFreed, elapsedMs } —
|
|
66
|
+
// the TUI panel renders it; callers that ignore the arg keep prior onCompress semantics.
|
|
67
|
+
callbacks.onCompress?.(agent._lastCompressInfo ?? {})
|
|
68
|
+
ensureAutoReminder(agent)
|
|
69
|
+
}
|
|
70
|
+
} catch (compressError) {
|
|
71
|
+
// AbortError must not be swallowed: user cancellation must propagate
|
|
72
|
+
if (compressError?.name === "AbortError" || signal?.aborted) throw compressError
|
|
73
|
+
agent._compressFailures = (agent._compressFailures ?? 0) + 1
|
|
74
|
+
// Q3 (CONTEXT-COMPACTION §7 D-C1): a failed compression is surfaced to the panel;
|
|
75
|
+
// COMPRESS_FAILURE_LIMIT consecutive failures still degrade to compressFallback.
|
|
76
|
+
callbacks?.onCompressFail?.(compressError)
|
|
77
|
+
if (agent._compressFailures >= COMPRESS_FAILURE_LIMIT) {
|
|
78
|
+
agent._compressFailures = 0
|
|
79
|
+
if (compressFallback(agent)) callbacks.onCompress?.(agent._lastCompressInfo ?? {})
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 回合注入组(2026-09-05 实践轮——自 runAgent 主循环提取):plan-mode 提醒 cadence
|
|
86
|
+
* (稀疏 2 轮/满 5 轮/新用户消息重置——限制不淡化)+ 工程模式状态注入(depth-0 每新
|
|
87
|
+
* 用户消息一次——见 injectEngineeringReminder)。verbatim——语义零变。
|
|
88
|
+
*/
|
|
89
|
+
export async function injectTurnReminders(agent, ctx) {
|
|
90
|
+
const { depth } = ctx
|
|
91
|
+
// Plan-mode reminder cadence: re-inject constraint reminders while plan mode is active
|
|
92
|
+
// (sparse every 2 turns, full every 5 / on new user message) so the restriction never fades.
|
|
93
|
+
if (agent.planMode) {
|
|
94
|
+
const lastMsg = agent.history.at(-1)
|
|
95
|
+
const realUserMsg = lastMsg?.role === "user"
|
|
96
|
+
&& typeof lastMsg.content === "string"
|
|
97
|
+
&& !lastMsg.content.startsWith("[System reminder:")
|
|
98
|
+
&& !lastMsg.content.startsWith("[User interrupt:")
|
|
99
|
+
const newUserSince = realUserMsg && agent.history.length > (agent._planReminderAtLen ?? 0)
|
|
100
|
+
const { planReminderForTurn } = await import("../agent-tools/plan.mjs")
|
|
101
|
+
const reminder = planReminderForTurn(agent, newUserSince)
|
|
102
|
+
if (reminder) {
|
|
103
|
+
agent._planReminderAtLen = agent.history.length + 1
|
|
104
|
+
agent.history.push({ role: "user", content: reminder, transient: true })
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Engineering-mode status injection on every new user message (design-before-code
|
|
109
|
+
// vs standard discipline) — see injectEngineeringReminder.
|
|
110
|
+
if (depth === 0) {
|
|
111
|
+
injectEngineeringReminder(agent)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 回合收尾(2026-09-05 实践轮——自 runAgent finally 提取,verbatim + 签名化):
|
|
117
|
+
* consult 清理(无孤儿烧 token 的 consult children)→ async 池回合尾分流(Ctrl+C
|
|
118
|
+
* 清池不注入 / ContinueError 留池续跑 / 其余 collectSettledAsync)→ auto-turn guard
|
|
119
|
+
* 标记继承(对象字段清单——正常结束才继承;中止丢弃;ContinueError 由续跑快照)。
|
|
120
|
+
*/
|
|
121
|
+
export async function finalizeAgentTurn(agent, ctx) {
|
|
122
|
+
const { signal, autoTurn, suspDriven, thrownError } = ctx
|
|
123
|
+
// R10 L3(MULTI-INSTANCE-COLLAB §2a.5 D-L3a):回合末登记 flush——本回合写足迹整写
|
|
124
|
+
// 一次(首行执行:收尾链后续任何异常都不吞登记);无写入 → 跳过(hot 窗口自然衰减)。
|
|
125
|
+
flushPeerDomains(agent)
|
|
126
|
+
// R17(AGENT-LOOP.md §25 D-R17a): consultation sessions are cross-turn
|
|
127
|
+
// background work now — NO unconditional turn-end cleanup (the old rule aborted
|
|
128
|
+
// leftover consult children at every turn end because check-loop consumption was
|
|
129
|
+
// turn-scoped). Sessions stay alive across normal turn ends (and ContinueError
|
|
130
|
+
// resumes); the Ctrl+C abort branch below is the only path that aborts them
|
|
131
|
+
// (cleanupConsultSessions — marked stopped → no digest), and the suspension
|
|
132
|
+
// driver aborts them on its own abort unwind.
|
|
133
|
+
// Async subagent turn-end handling (AGENT-LOOP.md §15 D-A3 + §17 D-S1). Lifecycle:
|
|
134
|
+
// - Ctrl+C (plain abort): children were aborted with the parent signal — clear
|
|
135
|
+
// WITHOUT injecting stale errors (user explicitly stopped); consultation
|
|
136
|
+
// sessions are cross-turn background work since R17 — the abort branch is the
|
|
137
|
+
// ONLY normal-path place that aborts them (cleanupConsultSessions marks
|
|
138
|
+
// stopped → their settles never reach the digest stream).
|
|
139
|
+
// - Ctrl+I (interrupt): keeps the pool AND the consult sessions: the turn
|
|
140
|
+
// resumes with the interrupt message, children stay tracked (in a suspension
|
|
141
|
+
// session children hold agent._sessionSignal and a digest's own Ctrl+I must
|
|
142
|
+
// not orphan them).
|
|
143
|
+
// - ContinueError (turn cap): no wait, no injection — children keep running and
|
|
144
|
+
// the RESUME run's turn-end collection takes over.
|
|
145
|
+
// - anything else: inject the SETTLED entries only; running/queued stay in the
|
|
146
|
+
// pool for the suspension session (D-S1 — no allSettled turn-end wait).
|
|
147
|
+
if (signal?.aborted && !signal?.reason?.interrupt) {
|
|
148
|
+
const subPool = getAsyncPool(agent, "subagent")
|
|
149
|
+
const advPool = getAsyncPool(agent, "advisor")
|
|
150
|
+
if ((subPool?.size ?? 0) > 0 || (advPool?.size ?? 0) > 0) {
|
|
151
|
+
logEvent("ev:stopped", { poolN: (subPool?.size ?? 0) + (advPool?.size ?? 0), where: "turn-end-abort" })
|
|
152
|
+
}
|
|
153
|
+
subPool?.clear()
|
|
154
|
+
advPool?.clear()
|
|
155
|
+
agent._asyncQueue = []
|
|
156
|
+
// R17: the consult family dies with the user stop (marked stopped — no digest).
|
|
157
|
+
cleanupConsultSessions(agent)
|
|
158
|
+
// ASYNC-RESULT-CONTAINER.md D2:pending 单容器——中止丢弃 consult/escalate 族停靠
|
|
159
|
+
// (原 _pendingConsultResults/_pendingEscalateResults 同口径——VSC 同语义 filter);
|
|
160
|
+
// subagent/advisor 停靠保留(挂起期 settle 的已完成结果不随中止丢)。
|
|
161
|
+
agent._pendingAsyncResults = (agent._pendingAsyncResults ?? []).filter((e) => e.role !== "consult" && e.role !== "escalate")
|
|
162
|
+
} else if (thrownError instanceof ContinueError) {
|
|
163
|
+
// keep _asyncSubagents/_asyncAdvisors/_consultSessions — the resumed run continues them
|
|
164
|
+
} else {
|
|
165
|
+
const injectedAdvisor = await collectSettledAsync(agent, { suspDriven })
|
|
166
|
+
// §24 D-24b: a normally-ended USER run with no review/child activity closes
|
|
167
|
+
// the OPEN code instances — the converged/abandoned thread must not burn the
|
|
168
|
+
// 5-round cap of a later task (the next code review starts a fresh round 1).
|
|
169
|
+
// Digest auto-turns are exempt (their disposition precedes the fix round).
|
|
170
|
+
// A run whose turn-end collection just INJECTED a settled review report
|
|
171
|
+
// (headless, suspDriven=false) must NOT close: the model has not digested
|
|
172
|
+
// the findings yet — the fix round that follows still continues the thread.
|
|
173
|
+
if (!autoTurn && !injectedAdvisor) {
|
|
174
|
+
const { closeOpenCodeAdvisorRuns } = await import("../agent-tools/advisor-async.mjs")
|
|
175
|
+
closeOpenCodeAdvisorRuns(agent)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
agent._inAutoTurn = false
|
|
179
|
+
// §17 D-S6: auto-turn guard marks survive into the next USER run (restored at its
|
|
180
|
+
// !resume reset above). Normal ends only — abort discards; ContinueError lets the
|
|
181
|
+
// auto-resumed run snapshot at its own end.
|
|
182
|
+
if (autoTurn && !(signal?.aborted && !signal?.reason?.interrupt) && !(thrownError instanceof ContinueError)) {
|
|
183
|
+
agent._inheritedGuard = {
|
|
184
|
+
_mutatedThisRun: agent._mutatedThisRun, _verifiedThisRun: agent._verifiedThisRun,
|
|
185
|
+
_verifyPassed: agent._verifyPassed, _calledAdvisorThisRun: agent._calledAdvisorThisRun,
|
|
186
|
+
_touchedFiles: agent._touchedFiles, _verifyRetries: agent._verifyRetries,
|
|
187
|
+
_advisorRound: agent._advisorRound,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Turn-end async subagent collection (AGENT-LOOP.md §17 D-S1 + §17.5 supersede):
|
|
194
|
+
* two modes, selected by the caller's driver context (17.5.2/17.5.4 #2):
|
|
195
|
+
* - suspDriven=false (fallback — headless/direct runAgent callers without a
|
|
196
|
+
* suspension driver): inject every entry that SETTLED during this run
|
|
197
|
+
* (XML-escaped report/error — child reports may carry file/webpage content;
|
|
198
|
+
* >64K offloaded with preview + path) and remove it from the pool. Running/
|
|
199
|
+
* queued STAY — no allSettled wait. Results never lost without a session.
|
|
200
|
+
* - suspDriven=true (the interaction layer runs suspensionSession after this
|
|
201
|
+
* run): NO direct inject — settled entries STAY pooled (settled not consumed)
|
|
202
|
+
* for the session's first sweepSettledToPending → digest turn (§17.5 — done
|
|
203
|
+
* 条目留池等消化轮注入;status 在 sweep 前仍从池读——17.5.2 不变面(§19.8:check 已删——自动通道为唯一消费方))。
|
|
204
|
+
* maybeRefillAsync runs in both modes (starts queued heads whose slot freed).
|
|
205
|
+
* Single ownership: entries settled inside a suspension session were moved to
|
|
206
|
+
* _pendingAsyncResults by the settle callback, so this only sees user-turn
|
|
207
|
+
* settles (no double inject — D-S3 points ①/②). The ⟦ev⟧done freeze is NOT
|
|
208
|
+
* emitted here — each settle callback emits it (§15 D-A3).
|
|
209
|
+
* 2026-09-05 实践轮:自 agent.mjs 迁入(agent.mjs 内仅 finalize 引用——随收尾同迁)。
|
|
210
|
+
*/
|
|
211
|
+
async function collectSettledAsync(agent, { suspDriven = false } = {}) {
|
|
212
|
+
const maps = [getAsyncPool(agent, "subagent"), getAsyncPool(agent, "advisor")].filter((m) => m instanceof Map && m.size > 0)
|
|
213
|
+
if (maps.length === 0) return false
|
|
214
|
+
const { maybeRefillAsync, injectAsyncResult } = await import("../agent-tools/subagent.mjs")
|
|
215
|
+
maybeRefillAsync(agent) // start queued heads now that slots may have freed — no waiting
|
|
216
|
+
if (suspDriven) return false // §17.5: settled stays pooled — the suspension session digests it
|
|
217
|
+
let injectedAdvisor = false
|
|
218
|
+
for (const map of maps) {
|
|
219
|
+
for (const e of [...map.values()]) {
|
|
220
|
+
if (!e.done) continue // still running — stays in the pool (D-S1)
|
|
221
|
+
await injectAsyncResult(agent, e)
|
|
222
|
+
if (e.role === "advisor") injectedAdvisor = true
|
|
223
|
+
map.delete(String(e.id))
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return injectedAdvisor
|
|
227
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/setup-reminders.mjs — per-turn transient reminders
|
|
3
|
+
* (SESSION.md §11.1 — 2026-09-06 需求池 R5/R8/R9/R11 合并设计; R10 L1 peer 注入同文件).
|
|
4
|
+
*
|
|
5
|
+
* One unified transient user reminder per turn covers the whole self-awareness
|
|
6
|
+
* family: env identity (R8 — §10 D-1 END 常量先例:静态常量,不做 cmdline 判别),
|
|
7
|
+
* engineering mode (R9), active model (R11), restart awareness (R5 resumed).
|
|
8
|
+
* Change awareness needs no dedicated injection — every turn carries the
|
|
9
|
+
* CURRENT state, so a mode/model flip shows up in the next turn's line.
|
|
10
|
+
* git is NOT a field here (§11.1: CLI 不重复注入 clean|dirty 摘要) — the rich
|
|
11
|
+
* git-context injection (branch/commits/uncommitted — helpers.mjs
|
|
12
|
+
* collectGitContext, wired in setup.mjs) carries it.
|
|
13
|
+
* Peer awareness (R10 L1) rides the same per-turn transient channel:
|
|
14
|
+
* pushPeerReminder injects the live co-cwd instance list when present.
|
|
15
|
+
*/
|
|
16
|
+
import { END } from "../session-slots.mjs"
|
|
17
|
+
import { peerInstances } from "../peer-instances.mjs"
|
|
18
|
+
|
|
19
|
+
/** env-state line builder — pure, unit-testable. */
|
|
20
|
+
export function envStateLine({ mode, model, resumed }) {
|
|
21
|
+
return `[System reminder: env: ${END}, mode: ${mode}, model: ${model}, resumed: ${resumed ? "yes" : "no"}.]`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Push the per-turn env-state reminder (setup.mjs calls it for depth-0 runs —
|
|
26
|
+
* the line describes the MAIN agent's host/mode/model identity). resumed=yes
|
|
27
|
+
* exactly once: the first turn after a restored session (setup.mjs sets
|
|
28
|
+
* agent._envResumed alongside the `process restarted` reminder); consumed here
|
|
29
|
+
* so every later turn reads resumed=no (T-E5).
|
|
30
|
+
* Degrades safely: activeModel null → provider.model → "unknown" (T-E12);
|
|
31
|
+
* a missing config/history never throws (T-E13).
|
|
32
|
+
*/
|
|
33
|
+
export function pushEnvStateReminder(agent) {
|
|
34
|
+
const mode = agent.config?.agent?.engineering ? "eng" : "normal"
|
|
35
|
+
const model = agent.activeModel ?? agent.provider?.model ?? "unknown"
|
|
36
|
+
const resumed = agent._envResumed === true
|
|
37
|
+
agent._envResumed = false
|
|
38
|
+
agent.history.push({ role: "user", content: envStateLine({ mode, model, resumed }), transient: true })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* R10 L1 peer reminder(MULTI-INSTANCE-COLLAB §2a.4 D-L1a——仿 pushEnvStateReminder
|
|
43
|
+
* 形态:depth-0、transient:true——注入纪律同 env-state)。有同伴(非 self > 0)才注入
|
|
44
|
+
* ——无同伴零开销(peerInstances 惰性 mtime 缓存保证:manifest 未变零 exec)。任何
|
|
45
|
+
* 感知失败静默跳过(注入绝不打断回合)。文案(设计逐字):"本目录另有 N 个活跃
|
|
46
|
+
* thincoder({end} pid={pid}…)——文件操作注意避让"。
|
|
47
|
+
*/
|
|
48
|
+
export function pushPeerReminder(agent) {
|
|
49
|
+
let peers
|
|
50
|
+
try {
|
|
51
|
+
peers = peerInstances(agent.cwd).filter((p) => !p.self)
|
|
52
|
+
} catch {
|
|
53
|
+
return // 感知失败降级——不注入
|
|
54
|
+
}
|
|
55
|
+
if (peers.length === 0) return
|
|
56
|
+
const who = peers.map((p) => (p.end ? `${p.end} pid=${p.pid}` : `pid=${p.pid}`)).join("、")
|
|
57
|
+
agent.history.push({
|
|
58
|
+
role: "user",
|
|
59
|
+
content: `[System reminder: 本目录另有 ${peers.length} 个活跃 thincoder(${who})——文件操作注意避让]`,
|
|
60
|
+
transient: true,
|
|
61
|
+
})
|
|
62
|
+
}
|
package/src/agent/setup.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
collectGitContext, loadProjectInstructions, OUTLINE_INJECT_PREFIX,
|
|
16
16
|
DEFAULT_MAX_TURNS, ensureAutoReminder,
|
|
17
17
|
} from "./helpers.mjs"
|
|
18
|
+
import { pushEnvStateReminder, pushPeerReminder } from "./setup-reminders.mjs"
|
|
18
19
|
import { readFileSync, existsSync } from "node:fs"
|
|
19
20
|
import { resolve, dirname } from "node:path"
|
|
20
21
|
import { fileURLToPath } from "node:url"
|
|
@@ -36,7 +37,7 @@ function safeSliceUTF16(text, max) {
|
|
|
36
37
|
const MEMORY_SEARCH_LIMIT = 3
|
|
37
38
|
|
|
38
39
|
/** Build engineering-mode system prompt by reading METHODOLOGY.md and wrapping it in the engineering template */
|
|
39
|
-
async function buildEngineeringPrompt(cwd, role) {
|
|
40
|
+
export async function buildEngineeringPrompt(cwd, role) {
|
|
40
41
|
const engFile = role === "eng-coder" ? "engineering-sub.md" : "engineering.md"
|
|
41
42
|
const engTemplatePath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "prompts", engFile)
|
|
42
43
|
let engTemplate = ""
|
|
@@ -49,8 +50,17 @@ async function buildEngineeringPrompt(cwd, role) {
|
|
|
49
50
|
const methodologyPath = resolve(cwd, "METHODOLOGY.md")
|
|
50
51
|
if (!existsSync(methodologyPath)) {
|
|
51
52
|
// Template-only — engineering constraints stay active, minus project rules.
|
|
52
|
-
// The caller injects a warning into the history.
|
|
53
|
-
|
|
53
|
+
// The caller injects a warning into the history. Resolve the built-in
|
|
54
|
+
// methodology template to an absolute path (same-source join as the
|
|
55
|
+
// engineering template above — the packaged path is unreachable from the
|
|
56
|
+
// user's cwd) and carry its body so the warning can embed it verbatim
|
|
57
|
+
// (2026-09-02 D-M1/D-M2: template reachability for the model).
|
|
58
|
+
const methodologyTemplatePath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "prompts", "methodology-template.md")
|
|
59
|
+
let methodologyTemplateBody = null
|
|
60
|
+
try { methodologyTemplateBody = readFileSync(methodologyTemplatePath, "utf8") } catch {
|
|
61
|
+
// Template unreadable (packaging) — degraded: base warning only (no path/body injected), same as VS Code.
|
|
62
|
+
}
|
|
63
|
+
return { prompt: engTemplate || null, templateMissing, methodologyMissing: true, methodologyTemplatePath, methodologyTemplateBody }
|
|
54
64
|
}
|
|
55
65
|
const methodology = readFileSync(methodologyPath, "utf8")
|
|
56
66
|
const prompt = engTemplate
|
|
@@ -104,6 +114,9 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
104
114
|
}
|
|
105
115
|
if (wasRestored && !agent._restartReminderInjected) {
|
|
106
116
|
agent._restartReminderInjected = true
|
|
117
|
+
// SESSION.md §11.1 R5: the per-turn env-state reminder carries resumed:yes
|
|
118
|
+
// on THIS first turn after the restore (consumed by pushEnvStateReminder).
|
|
119
|
+
agent._envResumed = true
|
|
107
120
|
agent.history.push({ role: "user", content: `[System reminder: process restarted at ${new Date().toISOString()}.]`, transient: true })
|
|
108
121
|
}
|
|
109
122
|
if (agent.memory && !agent.history.some((m) => typeof m.content === "string" && m.content.startsWith(OUTLINE_INJECT_PREFIX))) {
|
|
@@ -158,6 +171,16 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
158
171
|
}
|
|
159
172
|
pushReal(agent, { role: "user", content: input })
|
|
160
173
|
}
|
|
174
|
+
// SESSION.md §11.1: unified per-turn env-state transient reminder (env/mode/
|
|
175
|
+
// model/resumed — R5/R8/R9/R11 one-shot coverage; changes surface next turn).
|
|
176
|
+
// R10 L1 (MULTI-INSTANCE-COLLAB §2a.4 D-L1a): peer-instance reminder right after
|
|
177
|
+
// env-state, BEFORE the time reminder — the time reminder stays LAST (prefix-cache
|
|
178
|
+
// contract). Both depth-0 only (describe the MAIN agent / its workspace peers).
|
|
179
|
+
if (depth === 0) {
|
|
180
|
+
pushEnvStateReminder(agent)
|
|
181
|
+
pushPeerReminder(agent)
|
|
182
|
+
}
|
|
183
|
+
|
|
161
184
|
// Time grounding for EVERY agent depth AND every resume, pushed LAST (after the user
|
|
162
185
|
// input): transient on the HUMAN line — dropped on persist; on the MACHINE line — kept
|
|
163
186
|
// (byte-identical resume for the provider prefix cache, 2026-08-16), fresh at every run start
|
|
@@ -178,13 +201,13 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
178
201
|
|
|
179
202
|
// task/plan tools are injected with the main loop; subagent/skill/goal/verify only at top level
|
|
180
203
|
// eng-coder subagents get advisor for mandatory design review before coding
|
|
181
|
-
const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool, engTool } = await import("../agent-tools.mjs")
|
|
182
|
-
const { consultStartTool,
|
|
183
|
-
const { subagentCheckTool } = await import("../agent-tools/subagent-check.mjs")
|
|
184
|
-
const { escalateTool } = await import("../agent-tools/escalate.mjs")
|
|
204
|
+
const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool, engTool, readHistoryTool } = await import("../agent-tools.mjs")
|
|
205
|
+
const { consultStartTool, consultStopTool } = await import("../agent-tools/consult.mjs")
|
|
185
206
|
const { CONSULT_BASE } = await import("../agent.mjs")
|
|
186
|
-
// withPool: decorate consult_start
|
|
187
|
-
// so the model knows which models it can pick (CLI parity with the plugin).
|
|
207
|
+
// withPool: decorate the consult_start description with the CURRENT candidate pool
|
|
208
|
+
// so the model knows which models it can pick (CLI parity with the plugin). The
|
|
209
|
+
// retired escalate tool surface is now the subagent action:"escalate" — its pool
|
|
210
|
+
// list is decorated onto the action property description below (same intent).
|
|
188
211
|
const withPool = (tool) => {
|
|
189
212
|
const models = agent.config?.agent?.consultModels ?? []
|
|
190
213
|
const list = models.map((m) => `${m.provider}:${m.model}${m.effort ? ` (${m.effort})` : ""}`).join(", ")
|
|
@@ -211,28 +234,84 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
211
234
|
properties: {
|
|
212
235
|
...subagentTool.parameters.properties,
|
|
213
236
|
role: { ...subagentTool.parameters.properties.role, ...subagentRoles },
|
|
237
|
+
// §19: escalate 动作的候选池 = consultModels(缺省池首 / 指定 provider:model)。
|
|
238
|
+
// 池装饰挂在 action 属性描述(原 escalate 工具注册时 withPool 同款意图——模型
|
|
239
|
+
// 需要知道可选候选人)。escalate 在工程模式禁用——装饰只对正常模式有意义。
|
|
240
|
+
action: (agent.config?.agent?.consultModels?.length && !agent.config?.agent?.engineering)
|
|
241
|
+
? {
|
|
242
|
+
...subagentTool.parameters.properties.action,
|
|
243
|
+
description: subagentTool.parameters.properties.action.description +
|
|
244
|
+
`\nCurrently configured escalate candidates (agent.consultModels pool): ${agent.config.agent.consultModels.map((m) => `${m.provider}:${m.model}${m.effort ? ` (${m.effort})` : ""}`).join(", ")}`,
|
|
245
|
+
}
|
|
246
|
+
: subagentTool.parameters.properties.action,
|
|
214
247
|
},
|
|
215
248
|
},
|
|
216
249
|
} : subagentTool
|
|
217
250
|
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
251
|
+
// §18 D-E3: eng-coder children (depth>0) get an audit-only subagent channel —
|
|
252
|
+
// role enum limited to explore, NO async parameter (sync only) and action pinned
|
|
253
|
+
// to spawn (§19 D-M3 restricted-variant action gate — escalate/check/status are
|
|
254
|
+
// refused here at the schema level too; the mechanical re-check lives in
|
|
255
|
+
// subagent.mjs execute → the §19 action gate + gateEngCoderSpawn (spawn-child.mjs)
|
|
256
|
+
// — schema enums are advisory, providers don't enforce them).
|
|
257
|
+
const engAuditSubagent = depth > 0 && agent._role === "eng-coder"
|
|
258
|
+
? (() => {
|
|
259
|
+
const props = { ...subagentTool.parameters.properties }
|
|
260
|
+
// §19 review hygiene: the audit channel is spawn-only sync explore — drop
|
|
261
|
+
// async, the check/status params (id/n) and the eng-coder token params
|
|
262
|
+
// (designToken/designId are meaningless for a read-only audit spawn; the
|
|
263
|
+
// parent spawn already carried the token). Schema noise would invite the
|
|
264
|
+
// model to pass irrelevant args.
|
|
265
|
+
delete props.async // sync only — the eng-coder blocks on the audit report
|
|
266
|
+
delete props.id
|
|
267
|
+
delete props.n
|
|
268
|
+
delete props.designToken
|
|
269
|
+
delete props.designId
|
|
270
|
+
props.role = {
|
|
271
|
+
type: "string",
|
|
272
|
+
enum: ["explore"],
|
|
273
|
+
description: "explore only — the eng-coder's internal spawn channel is reserved for read-only divergence audits (AGENT-LOOP.md §18 D-E3).",
|
|
274
|
+
}
|
|
275
|
+
props.action = {
|
|
276
|
+
type: "string",
|
|
277
|
+
enum: ["spawn"],
|
|
278
|
+
description: "spawn only — the eng-coder's internal spawn channel is reserved for read-only divergence audits (AGENT-LOOP.md §19 D-M3); escalate/check/status are refused (escalate spawns a coder+WRITE child — against explore-only intent; check/status have no async pool in a child context).",
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
...subagentTool,
|
|
282
|
+
name: "subagent",
|
|
283
|
+
description: "Spawn a read-only `explore` sub-agent to AUDIT your delivery against the design (AGENT-LOOP.md §18 D-E2 ③): it compares the delivered code with the design for divergence — partially implemented acceptance criteria, silent simplifications, doc drift, changes outside the approved file list. BLOCKING ONLY (no async — the audit report decides your next protocol step). action:'spawn' ONLY — the audit channel is a read-only spawn; escalate/check/status are not available (AGENT-LOOP.md §19). The audit task book is appended MECHANICALLY — your own spawn task (docs involved / acceptance criteria / file list) plus the files you actually touched; never hand the audit a self-written file list (a self-report could omit exactly the out-of-scope file it must catch).",
|
|
284
|
+
parameters: { ...subagentTool.parameters, properties: props },
|
|
285
|
+
}
|
|
286
|
+
})()
|
|
287
|
+
: null
|
|
288
|
+
|
|
289
|
+
// consult 工具仅在配置时注册(consultModels 空池时注册会让模型调用后吃一个错误回合)——
|
|
290
|
+
// §19: escalate 已并入常驻 subagent 的 action:"escalate"(无空池注册问题——动作在
|
|
291
|
+
// 池空时返回既有错误语义,工程模式 fail-closed 在 execute 内拒绝)。
|
|
292
|
+
// §25 D-R17a: consult_check 已退役(digest 自动注入是唯一消费通道)——consult 家族
|
|
293
|
+
// 只剩 2 工具(consult_start/consult_stop——setup 注册点与描述面同步清零)。
|
|
222
294
|
const consultModels = agent.config?.agent?.consultModels ?? []
|
|
223
|
-
const engineering = agent.config?.agent?.engineering
|
|
224
295
|
const consultTools = consultModels.length
|
|
225
|
-
? [withPool(consultStartTool),
|
|
296
|
+
? [withPool(consultStartTool), consultStopTool]
|
|
226
297
|
: []
|
|
227
|
-
const depthOnly = depth === 0 ? [filteredSubagent,
|
|
228
|
-
//
|
|
229
|
-
//
|
|
298
|
+
const depthOnly = depth === 0 ? [filteredSubagent, skillTool, goalTool, engTool, verifyTool, recentChangesTool, readHistoryTool, advisorTool, ...consultTools]
|
|
299
|
+
// SESSION.md §9 D-S2: read_history is depth-0 ONLY — a subagent querying "the session"
|
|
300
|
+
// would mix its throwaway context with the parent's record (semantic confusion).
|
|
301
|
+
// It is readonly:true, so planMode pass and no permission ask come automatically (T-S9).
|
|
302
|
+
// Write-permission coder sub-agents (subagent role="coder" + escalate action):
|
|
303
|
+
// the system prompt names verify (system.md) and advisor (discipline.md) — without them an
|
|
230
304
|
// escalate hit "unknown tool" and fell back to bash node --check / npm test to
|
|
231
305
|
// self-verify (2026-08-16 deepseek escalate diagnosis; plugin parity).
|
|
232
|
-
:
|
|
306
|
+
// eng-coder: advisor + verify + the §18 audit-only subagent channel (D-E3).
|
|
307
|
+
: agent._role === "eng-coder" ? [advisorTool, verifyTool, ...(engAuditSubagent ? [engAuditSubagent] : [])]
|
|
233
308
|
: agent._role === "coder" ? [verifyTool, advisorTool]
|
|
234
309
|
: agent._role === "consult" ? [recentChangesTool]
|
|
235
310
|
: []
|
|
311
|
+
// NOTE: every depth-0 tool schema is estimated into the compaction overhead per turn
|
|
312
|
+
// (context.mjs extras.tools) — a tool-schema change shifts the compaction fixture
|
|
313
|
+
// knife-edges (agent.test T3b: read_history's schema +~470 tokens once crossed its
|
|
314
|
+
// 11000 threshold; fixture adjusted to 12500 — rationale in the test comment).
|
|
236
315
|
const tools = [...agent.tools, taskTool, planTool, timerTool, ...depthOnly]
|
|
237
316
|
const toolSchemas = tools.map(toOpenAISchema)
|
|
238
317
|
const toolByName = new Map(tools.map((t) => [t.name, t]))
|
|
@@ -264,7 +343,15 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
264
343
|
warnings.push(`Engineering template (${agent._role === "eng-coder" ? "engineering-sub.md" : "engineering.md"}) not found — using degraded constraints.`)
|
|
265
344
|
}
|
|
266
345
|
if (engResult.methodologyMissing) {
|
|
267
|
-
|
|
346
|
+
let warning = "METHODOLOGY.md not found in the project root — no project methodology is loaded, so every 'per METHODOLOGY' reference in the engineering prompt is dangling and the three-document hard flow (requirements / design / test doc) is NOT enforced. Ask the user whether to create METHODOLOGY.md; if the user confirms, write cwd/METHODOLOGY.md before designing."
|
|
347
|
+
// 2026-09-02 D-M1/D-M2 (template accessibility): absolute path + full body — the model
|
|
348
|
+
// can read the template directly instead of hand-writing one from an unreachable source
|
|
349
|
+
// path. Body read failure → degraded warning above (path/body not injected). VS Code
|
|
350
|
+
// setup-reminders.mjs parity (两端警告文本一致,本端以 CLI 为准).
|
|
351
|
+
if (engResult.methodologyTemplateBody) {
|
|
352
|
+
warning += `\n\nbuilt-in template(可 read ${engResult.methodologyTemplatePath} 或直接参考以下内容):\n\n${engResult.methodologyTemplateBody}`
|
|
353
|
+
}
|
|
354
|
+
warnings.push(warning)
|
|
268
355
|
}
|
|
269
356
|
if (warnings.length > 0) {
|
|
270
357
|
agent.history.push({
|
|
@@ -26,6 +26,31 @@ const RS = "\x1e"
|
|
|
26
26
|
* 单源化(2026-08-30 评审):文案演进只改这里,消除文案与检测正则的漂移面。 */
|
|
27
27
|
export const TURN_CAP_MARK = "stopped: turn cap reached"
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* §18 D-E3 eng-coder 内部 spawn 机械门(AGENT-LOOP.md §18 D-E2 round5 #2 后备):
|
|
31
|
+
* eng-coder 子代理(depth>0 且 parent._role==="eng-coder")的内部 spawn 通道只做
|
|
32
|
+
* 偏差审计——role 仅 explore、async 强制同步;审计 spawn 预算 = 首审 1 + 修正轮
|
|
33
|
+
* ≤5 的再审(第 7 次审计 spawn 机械拒绝——5 轮纪律失效时不静默,错误即 stalled
|
|
34
|
+
* 信号)。返回 null = 非 eng-coder 上下文(不加限制);返回审计尝试序号 = 通过。
|
|
35
|
+
* schema 层过滤(setup.mjs 受限变体)只是给模型的参数提示——本函数是机械强制。
|
|
36
|
+
*/
|
|
37
|
+
export const ENG_AUDIT_SPAWN_LIMIT = 6 // 允许 6 次审计 spawn;第 7 次拒绝
|
|
38
|
+
export function gateEngCoderSpawn(parent, depth, role, async) {
|
|
39
|
+
if ((depth ?? 0) <= 0 || parent?._role !== "eng-coder") return null
|
|
40
|
+
if (role !== "explore") {
|
|
41
|
+
throw new Error("eng-coder subagents may only spawn role='explore' — internal spawns exist solely for the read-only divergence audit (AGENT-LOOP.md §18 D-E3)")
|
|
42
|
+
}
|
|
43
|
+
if (async === true) {
|
|
44
|
+
throw new Error("eng-coder internal spawns are sync-only — the audit report must return before the next protocol step; async spawn is only available at the top level (AGENT-LOOP.md §18 D-E3)")
|
|
45
|
+
}
|
|
46
|
+
const attempt = (parent._engAuditSpawns ?? 0) + 1
|
|
47
|
+
if (attempt > ENG_AUDIT_SPAWN_LIMIT) {
|
|
48
|
+
throw new Error("correction-round limit exceeded — deliver a stalled report (AGENT-LOOP.md §18: max 5 fix rounds; the 7th audit spawn is refused mechanically)")
|
|
49
|
+
}
|
|
50
|
+
parent._engAuditSpawns = attempt
|
|
51
|
+
return attempt
|
|
52
|
+
}
|
|
53
|
+
|
|
29
54
|
/**
|
|
30
55
|
* 构造 relay 前缀 + 发送 `[model]` 元数据 token(显示层据此更新区块头部,
|
|
31
56
|
* 不进内容流)。counter 挂在 parent agent 上,多轮/并行子代理互不冲突。
|
|
@@ -81,25 +106,45 @@ export function stripEventTokensForCapture(text) {
|
|
|
81
106
|
* - onToolOutput 带**已加前缀**的 name 走父 onToolOutput(name 形如
|
|
82
107
|
* "coder#1/bash",消费端剥前缀路由进对应区块;chunk 对象/裸串原样透传)。
|
|
83
108
|
* 父回调缺省时不包装(headless 嵌入)。
|
|
109
|
+
* §27 R23 D-R23c1:包装产物在 onToken 上留 `_relayPrefix` 标记——同步收尾段据此
|
|
110
|
+
* 判定"ctx.callbacks 已是嵌套 wrapper"(本 spawn 处于更深一层——eng-coder 内
|
|
111
|
+
* explore)→ emitNestedChildEvent 补发射内层完成事件(T-R23c.2b 断言源)。
|
|
84
112
|
*/
|
|
85
113
|
export function wrapChildCallbacks(relayPrefix, parentCallbacks = {}) {
|
|
114
|
+
const mark = (fn) => { fn._relayPrefix = relayPrefix; return fn }
|
|
86
115
|
const wrapped = {
|
|
87
116
|
onToken: parentCallbacks.onToken
|
|
88
|
-
? (t) => parentCallbacks.onToken(relayPrefix + stripEventToken(String(t)))
|
|
117
|
+
? mark((t) => parentCallbacks.onToken(relayPrefix + stripEventToken(String(t))))
|
|
89
118
|
: null,
|
|
90
119
|
onReasoning: parentCallbacks.onReasoning
|
|
91
|
-
? (t) => parentCallbacks.onReasoning(relayPrefix + t)
|
|
120
|
+
? mark((t) => parentCallbacks.onReasoning(relayPrefix + t))
|
|
92
121
|
: null,
|
|
93
122
|
onToolCall: parentCallbacks.onToolCall
|
|
94
|
-
? (name, args) => parentCallbacks.onToolCall(relayPrefix + name, args)
|
|
123
|
+
? mark((name, args) => parentCallbacks.onToolCall(relayPrefix + name, args))
|
|
95
124
|
: null,
|
|
96
125
|
onToolOutput: parentCallbacks.onToolOutput
|
|
97
|
-
? (name, chunk) => parentCallbacks.onToolOutput(relayPrefix + name, chunk)
|
|
126
|
+
? mark((name, chunk) => parentCallbacks.onToolOutput(relayPrefix + name, chunk))
|
|
98
127
|
: null,
|
|
99
128
|
}
|
|
100
129
|
return wrapped
|
|
101
130
|
}
|
|
102
131
|
|
|
132
|
+
/**
|
|
133
|
+
* §27 R23 D-R23c1(生成侧补发射——评审 #1 🅰):sync spawn 同步收尾时若父回调已是
|
|
134
|
+
* 嵌套 wrapper(onToken 带 `_relayPrefix` 标记——即本 spawn 的父本身是子代理,如
|
|
135
|
+
* eng-coder 内 explore 审计)→ 发内层 done/stopped 事件(带完整嵌套前缀——wrapper
|
|
136
|
+
* 链自动补外层前缀)→ 主 TUI routeSubToken 路由到子块定格。非嵌套(depth-0 直连主
|
|
137
|
+
* 回调——无标记)不发——完成冻结仍走既有 onToolResult/subKey 路径(零行为变化)。
|
|
138
|
+
* @returns {boolean} true = 已发射(嵌套上下文)
|
|
139
|
+
*/
|
|
140
|
+
export function emitNestedChildEvent(ctx, relayPrefix, kind) {
|
|
141
|
+
const onToken = ctx?.callbacks?.onToken
|
|
142
|
+
if (typeof onToken?._relayPrefix !== "string" || !onToken._relayPrefix) return false
|
|
143
|
+
if (kind !== "done" && kind !== "stopped") return false
|
|
144
|
+
onToken(`${relayPrefix}⟦ev⟧${kind}\x1e0\x1e0\x1e${kind}\x1e`)
|
|
145
|
+
return true
|
|
146
|
+
}
|
|
147
|
+
|
|
103
148
|
/**
|
|
104
149
|
* 子 agent provider API key 检查:trim 后非空才保留;缺失返回 null(调用方
|
|
105
150
|
* 按各自业务语汇报错——subagent 抛出 / escalate·consult 返回 Error 文本)。
|
|
@@ -148,6 +193,11 @@ export async function runWithContinue(runner, child, input, callbacks, runOpts,
|
|
|
148
193
|
const capture = callbacks?.onToken
|
|
149
194
|
? (t) => { output += stripEventTokensForCapture(String(t)); child._capturedOutput = output; callbacks.onToken(t) }
|
|
150
195
|
: (t) => { output += stripEventTokensForCapture(String(t)); child._capturedOutput = output }
|
|
196
|
+
// §27 R23 D-R23c1(评审 🔴 修复——2026-09-07):capture 是子代理 runAgent/dispatch ctx
|
|
197
|
+
// 实际收到的 onToken——嵌套 wrapper 标记(wrapChildCallbacks `_relayPrefix`)必须随
|
|
198
|
+
// capture 透传,否则 eng-coder 内 explore 同步收尾的 emitNestedChildEvent 判定恒 false
|
|
199
|
+
// (生成侧补发射结构性不可达——T-R23c.2b 真实路径回归)。
|
|
200
|
+
if (callbacks?.onToken?._relayPrefix) capture._relayPrefix = callbacks.onToken._relayPrefix
|
|
151
201
|
for (let resume = false; ; resume = true) {
|
|
152
202
|
try {
|
|
153
203
|
return await runner(child, input, { ...callbacks, onToken: capture }, { ...runOpts, resume })
|