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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design-token.mjs — design-token utilities(2026-09-08 自 advisor-async.mjs 提取——
|
|
3
|
+
* advisor-async 随 DESIGN-TOKEN-SETTLEMENT D1 落盘块再越 500 行硬限;本工具组 verbatim
|
|
4
|
+
* 迁入,零语义变)。
|
|
5
|
+
*
|
|
6
|
+
* 沿革:初由 agent-tools/advisor.mjs 迁至 advisor-async.mjs(sync wrapper 与 async
|
|
7
|
+
* settle 共享一套实现——无 wrapper↔runner 环),再由 advisor-async.mjs 迁入本文件。
|
|
8
|
+
* 既有 import 面不变:advisor-async.mjs re-export 全部 7 个导出(advisor.mjs 与测试
|
|
9
|
+
* 的 import 路径原样保留——advisor.mjs 再转发 validateDesignToken 给 spawn 门禁)。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { randomUUID } from "node:crypto"
|
|
13
|
+
import { tokenExpiryMs } from "../token-ttl.mjs"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* F2c/F2e (§29.1 2026-09-07): the engine-generated Approved suffix — ONE builder
|
|
17
|
+
* shared by the sync settle and the async settle (and the prior stores, which
|
|
18
|
+
* strip it with stripApprovedSuffix — exact-suffix truncation, never a regex
|
|
19
|
+
* guess, zero collateral). The slot count is a point-in-time snapshot taken at
|
|
20
|
+
* settle time — the situation at spawn time may differ (F2d backs that up).
|
|
21
|
+
*/
|
|
22
|
+
export function buildApprovedSuffix(designToken, designId, slotCount) {
|
|
23
|
+
return `Approved. Pass this exact token to eng-coder (designToken parameter): ${designToken}\ndesignId: ${designId} (pass as the designId parameter when spawning eng-coder — optional while this session holds a single design; ${slotCount} approved design slot(s) held as of this approval, and the count may have changed since — with several designs the spawn gate refuses a missing designId and lists the held ids)`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** F2e (§29.1): strip the engine-generated Approved suffix from a report before
|
|
27
|
+
* it becomes a prior — the suffix is deterministic (buildApprovedSuffix), so the
|
|
28
|
+
* truncation is exact; a text not ending in it passes through untouched. */
|
|
29
|
+
export function stripApprovedSuffix(text, suffix) {
|
|
30
|
+
if (typeof text !== "string" || !suffix) return text
|
|
31
|
+
return text.endsWith(suffix) ? text.slice(0, text.length - suffix.length).trim() : text
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const TOKEN_TTL_DEFAULT_MS = 7 * 24 * 3600 * 1000 // 7-day ceiling (v2 2026-08-25)
|
|
35
|
+
|
|
36
|
+
/** Effective token TTL: config override with runtime validation (timeoutMs precedent). */
|
|
37
|
+
export function effectiveTokenTtlMs(agent) {
|
|
38
|
+
const cfg = agent?.config?.agent?.engTokenTtlMs
|
|
39
|
+
return (Number.isFinite(cfg) && cfg > 0) ? cfg : TOKEN_TTL_DEFAULT_MS
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Mint an unsigned design token with expiration (2026-09-06: HMAC layer removed —
|
|
43
|
+
* the token is a FLOW credential: uuid:expiresAt, exact slot match + TTL only). */
|
|
44
|
+
export function generateDesignToken(agent) {
|
|
45
|
+
const uuid = randomUUID()
|
|
46
|
+
const expiresAt = Math.floor(Date.now() + effectiveTokenTtlMs(agent))
|
|
47
|
+
return `${uuid}:${expiresAt}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Validate a design token: format + expiration — ALL fail-closed (v2 2026-08-25).
|
|
51
|
+
* R16: format/expiry semantics live in token-ttl.mjs (tokenExpiryMs — single source
|
|
52
|
+
* shared with restore filtering / enter cleanup / spawn-gate slot deletion — D-R16b). */
|
|
53
|
+
export function validateDesignToken(token) {
|
|
54
|
+
const expiry = tokenExpiryMs(token)
|
|
55
|
+
return expiry !== null && expiry >= Date.now()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Build a [DESIGN-TOKEN:...] regex matching the FULL token (uuid:expiresAt). */
|
|
59
|
+
export function makeDesignTokenRegex(token, flags = "") {
|
|
60
|
+
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
61
|
+
return new RegExp(
|
|
62
|
+
`(?:^|\\s|\`|\\*)\\[DESIGN-TOKEN:\\s*${escaped}\\s*\\](?:\\s|$|\`|\\*)`,
|
|
63
|
+
flags + "ms"
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Shared design-review settlement (sync wrapper + async settle): the token echo
|
|
69
|
+
* IS the verdict — the advisor echoes it only on approval. On echo: slot the
|
|
70
|
+
* token under designId (+ eng-coder gate flag; single-value mirror retired per
|
|
71
|
+
* DESIGN-TOKEN-SETTLEMENT D3) and return
|
|
72
|
+
* the clean output with the Approved suffix; the review instance CLOSES (a
|
|
73
|
+
* later review of the same doc-set starts a fresh full review). On non-echo:
|
|
74
|
+
* strip every dead token occurrence and return the findings text — slots stay
|
|
75
|
+
* untouched (方案 ②: a failed re-review revokes nothing).
|
|
76
|
+
* @param {Object} [opts] — 第 11 批(A/F11):`opts.incomplete` = 宿主尾族判定 kind
|
|
77
|
+
* (单谓词 `advisorIncompleteMarker`——结算方透传)。非空 ⇒ **一律不签发**(无论评审文本
|
|
78
|
+
* 是否回显 token):剥除全部 token 回显 + 追加未签发提示(原因 + 恢复指引),不写槽、
|
|
79
|
+
* 不关实例(可重评)。
|
|
80
|
+
* @returns {{passed: boolean, output: string}}
|
|
81
|
+
*/
|
|
82
|
+
export function settleDesignReview(agent, run, designToken, rawResult, opts = {}) {
|
|
83
|
+
if (!designToken || typeof rawResult !== "string") {
|
|
84
|
+
return { passed: false, output: rawResult ?? "" }
|
|
85
|
+
}
|
|
86
|
+
// A(F11/§14.3 消费点 1):未完成即不签发——判据与 code 完成守卫同源(单谓词)。
|
|
87
|
+
const incomplete = opts?.incomplete ?? null
|
|
88
|
+
if (incomplete) {
|
|
89
|
+
const stripped = rawResult.replace(makeDesignTokenRegex(designToken, "g"), "").trim()
|
|
90
|
+
return {
|
|
91
|
+
passed: false,
|
|
92
|
+
output: `${stripped}\n\n评审未完成——token 未签发 (review incomplete — no design token issued; reason: ${incomplete})\n以更小范围重跑设计评审(逐档 / 逐节拆分,或拆到两次评审),或调大 agent.advisor.timeoutMs 后重试;补充检查未完成的部分不得按已核处理。`.trim(),
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const tokenPattern = makeDesignTokenRegex(designToken)
|
|
96
|
+
if (!tokenPattern.test(rawResult)) {
|
|
97
|
+
const stripped = rawResult.replace(makeDesignTokenRegex(designToken, "g"), "").trim()
|
|
98
|
+
return { passed: false, output: stripped || "Advisor: design review did not pass." }
|
|
99
|
+
}
|
|
100
|
+
// Echoed the token → review passed. Issue it to the parent for eng-coder.
|
|
101
|
+
agent._engDesignTokens ??= new Map()
|
|
102
|
+
agent._engDesignTokens.set(run.designId, designToken)
|
|
103
|
+
// DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像 `_engDesignToken` 退役——
|
|
104
|
+
// 只写多槽 Map(AC3 零镜像写);settle 落盘由调用方(settleAdvisorRun)当场做。
|
|
105
|
+
// Unlock the dispatch design gate for eng-coder SELF-review (defense-in-depth —
|
|
106
|
+
// see the sync wrapper's note: unreachable today, kept for parity).
|
|
107
|
+
if (agent._role === "eng-coder") agent._engDesignReviewed = true
|
|
108
|
+
run.open = false // approval closes this doc-set instance — next review is fresh
|
|
109
|
+
const clean = rawResult.replace(makeDesignTokenRegex(designToken, "g"), "").trim()
|
|
110
|
+
// F2c (§29.1): id echo + omission guidance + point-in-time slot snapshot — the
|
|
111
|
+
// same suffix the F2e prior stores strip with (stored for the exact truncation).
|
|
112
|
+
run.approvedSuffix = buildApprovedSuffix(designToken, run.designId, agent._engDesignTokens.size)
|
|
113
|
+
return {
|
|
114
|
+
passed: true,
|
|
115
|
+
output: `${clean}\n\n${run.approvedSuffix}`,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* digest-budget.mjs — digest 注入批量预算单源(群 B 批 B5——AGENT-LOOP.md §22 语义源)。
|
|
3
|
+
* 原预算块居 subagent-async.mjs(只覆盖经 injectAsyncResult 的三族:subagent / advisor /
|
|
4
|
+
* escalate),consult 族注入器(injectConsultResult)绕过 ⇒ 多族条目同轮合并注入时可累计
|
|
5
|
+
* 超限(BATCH-3-STRUCTURE F-2 原始事故面 1.3MB 请求体)。本档把常量 / 判超 / 记账 / 落盘
|
|
6
|
+
* 四处合一(D2 单源)+ 四族全接线(D-DG2)。
|
|
7
|
+
*
|
|
8
|
+
* 契约(D-DG1 三导出——CLI 面):
|
|
9
|
+
* - `DIGEST_INJECT_BUDGET`(64×1024——与 offload 单条预览同量级);
|
|
10
|
+
* - `digestBudgetOver(agent, size)`——判超 + 记账:`used > 0 && used + size > BUDGET`
|
|
11
|
+
* 首条豁免保留(单条大报告 >64K 走 offload 预览照旧——预算只约束轮累计);轮界定
|
|
12
|
+
* `agent.history.length !== r.len + 1` 语义逐字迁入(相邻注入间 history 无其他落史 = 同轮;
|
|
13
|
+
* 每次注入 pushReal 恰 +1——他人落史 = 新请求窗口 → 预算复位);键 = agent(会话级复用
|
|
14
|
+
* ——Map 常驻一两项)。
|
|
15
|
+
* - `persistOverflowReport(raw, { tag })`——超限条目全量落盘(`configDir/tool-results` +
|
|
16
|
+
* `cleanupOldToolResults` 轮转;落盘名 `<ts>-<tag>.log`(tag = 溯源标签:写入族 + 条目 id
|
|
17
|
+
* ——非法字符归一 `_`))。返回清单行文本(inline = 仅此——不 inline 全文);落盘失败 →
|
|
18
|
+
* null(调用方回退常规 inline 路径——offload 同款「失败不吞报告」语义)。
|
|
19
|
+
*
|
|
20
|
+
* 计数口径(D-DG3):计入预算的 raw = 报告 / 错误正文(不含 `[System reminder: …]` 标签行
|
|
21
|
+
* ——与既有口径一致);错误条目同计同落盘。
|
|
22
|
+
*
|
|
23
|
+
* 兼容面(D-DG4):subagent-async.mjs 保留 `DIGEST_INJECT_BUDGET` +
|
|
24
|
+
* `_setDigestOffloadDirForTest` re-export(测试导入面零改);单条 offload 预览路径零改。
|
|
25
|
+
*
|
|
26
|
+
* 模块图:import node:fs/promises + node:path + ../agent/helpers.mjs(cleanupOldToolResults)+
|
|
27
|
+
* ../config.mjs(configDir)——叶子级;subagent-async / consult 单向 import 本档(无环)。
|
|
28
|
+
*/
|
|
29
|
+
import { mkdir, writeFile } from "node:fs/promises"
|
|
30
|
+
import { join } from "node:path"
|
|
31
|
+
import { cleanupOldToolResults } from "../agent/helpers.mjs"
|
|
32
|
+
import { configDir } from "../config.mjs"
|
|
33
|
+
|
|
34
|
+
export const DIGEST_INJECT_BUDGET = 64 * 1024 // 64K——与 offload 单条预览同量级
|
|
35
|
+
|
|
36
|
+
// 轮记账:agent → { len, used }(逐字自 subagent-async.mjs 迁入——语义零变)。
|
|
37
|
+
const digestRounds = new Map()
|
|
38
|
+
function digestRoundFor(agent) {
|
|
39
|
+
const h = agent.history
|
|
40
|
+
const r = digestRounds.get(agent)
|
|
41
|
+
if (!r || h.length !== r.len + 1) {
|
|
42
|
+
const fresh = { len: h.length, used: 0 }
|
|
43
|
+
digestRounds.set(agent, fresh)
|
|
44
|
+
return fresh
|
|
45
|
+
}
|
|
46
|
+
r.len = h.length
|
|
47
|
+
return r
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 判超 + 记账(size = 原始正文字符数——标签行不计;先判后记:首条恒豁免)。 */
|
|
51
|
+
export function digestBudgetOver(agent, size) {
|
|
52
|
+
const round = digestRoundFor(agent)
|
|
53
|
+
const over = round.used > 0 && round.used + size > DIGEST_INJECT_BUDGET
|
|
54
|
+
round.used += size
|
|
55
|
+
return over
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Test seam: 落盘目录重定向(单测沙箱——生产从不调用)。 */
|
|
59
|
+
let _digestOffloadDirOverride = null
|
|
60
|
+
export function _setDigestOffloadDirForTest(dir) { _digestOffloadDirOverride = dir }
|
|
61
|
+
|
|
62
|
+
/** 超限条目全量落盘(返回清单行文本;失败 null——调用方回退 inline——结果零丢失)。
|
|
63
|
+
* tag 入文件名(溯源:写入族 + 条目 id)。 */
|
|
64
|
+
export async function persistOverflowReport(raw, { tag }) {
|
|
65
|
+
try {
|
|
66
|
+
const dir = _digestOffloadDirOverride ?? join(configDir, "tool-results")
|
|
67
|
+
await cleanupOldToolResults(dir)
|
|
68
|
+
await mkdir(dir, { recursive: true })
|
|
69
|
+
const file = join(dir, `${Date.now()}-${String(tag).replace(/[^a-zA-Z0-9_-]/g, "_")}.log`)
|
|
70
|
+
await writeFile(file, raw, "utf8")
|
|
71
|
+
return `Report saved to disk (digest inject budget exceeded — full text not inlined): ${file}\nRead it with the read tool (offset/limit).`
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.warn(`[digest] report persist failed — falling back to inline (report not lost): ${e.message}`)
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/agent-tools/eng.mjs
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* eng tool: enter/exit engineering mode.
|
|
3
3
|
* In engineering mode the agent follows design-before-code methodology.
|
|
4
|
-
* Toggled here at session level;
|
|
4
|
+
* Toggled here at session level (in-memory flag; the session slot is the sole authority —
|
|
5
|
+
* saveSession round-trips it at turn end; /eng writes the slot directly). The old ctx
|
|
6
|
+
* state-persist hook was removed 2026-09-08 (ENG-SESSION-PROVIDER-CLEANUP D1.2): no
|
|
7
|
+
* provider existed and its legacy payload keys were never read by applySession.
|
|
8
|
+
* R16 (2026-09-06): design tokens are session-level flow credentials — mode toggles
|
|
9
|
+
* do NOT clear them (ON→OFF keeps, OFF→ON does not require a fresh review); only TTL
|
|
10
|
+
* expiry cleans tokens, at three points: restore filtering (session.mjs applySession) /
|
|
11
|
+
* eng enter expired cleanup (below + cmd-eng ON) / spawn-gate expired rejection
|
|
12
|
+
* (subagent-spawn.mjs). See docs/design/ENG-TOKEN-BINDING-TUNING.md §5/§5.1 (F-R16).
|
|
5
13
|
*/
|
|
6
14
|
import { ENG_ON_REMINDER, ENG_OFF_REMINDER } from "../agent.mjs"
|
|
15
|
+
import { purgeExpiredDesignTokens } from "../token-ttl.mjs"
|
|
7
16
|
|
|
8
17
|
export const engTool = {
|
|
9
18
|
name: "eng",
|
|
@@ -22,50 +31,36 @@ export const engTool = {
|
|
|
22
31
|
ctx.agent.config.agent ??= {}
|
|
23
32
|
if (args.action === "exit") {
|
|
24
33
|
ctx.agent.config.agent.engineering = false
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
ctx.agent._engDesignReviewed = false // reset gate state
|
|
34
|
+
// R16 (F-R16a): OFF 不清 token——有效 token 跨模式存活(设计评审过的产物不因
|
|
35
|
+
// 开关重复烧)。过期清理跑在另外三处(恢复过滤 / 开模式 / spawn 门禁拒)。
|
|
28
36
|
ctx.agent._advisorRound = 0 // reset convergence budget
|
|
37
|
+
ctx.agent._advisorRuns = new Map() // §11.2 D-24b: per-review instances die with the mode (fresh cycles)
|
|
29
38
|
ctx.agent._touchedFiles = [] // clear mutation tracking
|
|
30
39
|
ctx.agent._lastEngState = false
|
|
31
40
|
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
32
41
|
ctx.agent._pendingReminders.push(ENG_OFF_REMINDER)
|
|
33
|
-
// 持久化工程模式状态到会话
|
|
34
|
-
if (ctx.persistState) {
|
|
35
|
-
await ctx.persistState({
|
|
36
|
-
engineering: false,
|
|
37
|
-
engDesignToken: null,
|
|
38
|
-
engDesignReviewed: false,
|
|
39
|
-
advisorRound: 0,
|
|
40
|
-
touchedFiles: []
|
|
41
|
-
})
|
|
42
|
-
}
|
|
43
42
|
return "Engineering mode exited. Standard discipline now applies. You may edit files directly."
|
|
44
43
|
}
|
|
45
44
|
if (args.action === "enter") {
|
|
46
|
-
// Idempotent enter (v2 2026-08-25): already in engineering mode → no-op
|
|
47
|
-
//
|
|
48
|
-
//
|
|
45
|
+
// Idempotent enter (v2 2026-08-25): already in engineering mode → pure no-op
|
|
46
|
+
// (cleanup only runs on a real off→on transition — T-R16c precondition: first
|
|
47
|
+
// exit/OFF, then enter; an already-on enter must not touch tokens at all).
|
|
49
48
|
if (ctx.agent.config.agent.engineering) {
|
|
50
49
|
return "Engineering mode already active. Existing design tokens stay valid."
|
|
51
50
|
}
|
|
52
51
|
ctx.agent.config.agent.engineering = true
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
// R16 (F-R16b ②): off→on 不重评——只清过期 token(用户裁定"打开工程模式时
|
|
53
|
+
// 应该清理"),有效 token 原样保留——遍历 Map 删过期,返回文案含清理个数。
|
|
54
|
+
const cleared = purgeExpiredDesignTokens(ctx.agent)
|
|
55
|
+
ctx.agent._advisorRuns = new Map() // §11.2 D-24b: per-review instances die with the mode (fresh cycles)
|
|
55
56
|
ctx.agent._lastEngState = true
|
|
56
57
|
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
57
58
|
ctx.agent._pendingReminders.push(ENG_ON_REMINDER)
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
engineering: true,
|
|
62
|
-
engDesignToken: null,
|
|
63
|
-
engDesignReviewed: false,
|
|
64
|
-
advisorRound: 0,
|
|
65
|
-
touchedFiles: []
|
|
66
|
-
})
|
|
59
|
+
let msg = "Engineering mode activated. Design-before-code enforced: write a design document first (location per your project's document conventions), run advisor with type='design', get user approval, then implement via eng-coder subagents."
|
|
60
|
+
if (cleared > 0) {
|
|
61
|
+
msg += ` Cleared ${cleared} expired design token${cleared === 1 ? "" : "s"} — 清 ${cleared} 个过期 token,有效 token 保留(TTL 内不重评)。`
|
|
67
62
|
}
|
|
68
|
-
return
|
|
63
|
+
return msg
|
|
69
64
|
}
|
|
70
65
|
return "Invalid action: expected 'enter' or 'exit'"
|
|
71
66
|
},
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* escalate-async.mjs — async 飞刀 runner (AGENT-LOOP.md §25 D-R17b — R17, 2026-09-06).
|
|
3
|
+
*
|
|
4
|
+
* The subagent action:"escalate" is DEFAULT-async at depth 0 (decision ③: the
|
|
5
|
+
* depth-0-only escalate flips to async like every other background family;
|
|
6
|
+
* `async: false` keeps the legacy synchronous flight). An async escalate runs in
|
|
7
|
+
* the SHARED "other" pool domain (decision Q2 🅰 — the entry lives in
|
|
8
|
+
* `_asyncSubagents` with role "escalate"/_pool "other", so it shares the 4-slot
|
|
9
|
+
* other domain with explore/plan/coder spawns — capacity, queueing, refill,
|
|
10
|
+
* status and cancel all come from the generic machinery):
|
|
11
|
+
* - launch → ack {id, role:"escalate", status:"running"|"queued"[, position]};
|
|
12
|
+
* the turn ends naturally, the session suspends (poolLive counts the entry).
|
|
13
|
+
* - settle THREE-WAY classification (review #4): done → merge-all mutations back
|
|
14
|
+
* into the parent's bookkeeping + overlap warning appended to the report
|
|
15
|
+
* (report-level, not a gate — round2 #4); error (child failure / turn cap) →
|
|
16
|
+
* partial mutations merged ONLY when the parent did not touch overlapping
|
|
17
|
+
* files since launch (overlap → no merge + differences listed in the report);
|
|
18
|
+
* cancelled → never reaches the digest stream (D-M6 — no merge, stopped
|
|
19
|
+
* reminder).
|
|
20
|
+
* - the classified report lands in `_pendingAsyncResults`(pending 单容器 +role——
|
|
21
|
+
* ASYNC-RESULT-CONTAINER.md D2——挂起期 settle 移交)when the settle happens in a
|
|
22
|
+
* suspension, or stays pooled for the turn-end collection otherwise;
|
|
23
|
+
* injectAsyncResult's escalate
|
|
24
|
+
* role branch delivers the digest ("报告已 merge——可继续处置" — the action
|
|
25
|
+
* domain still follows the consuming turn's tier — no family exception).
|
|
26
|
+
*/
|
|
27
|
+
import { relative, isAbsolute } from "node:path"
|
|
28
|
+
import { runAgent, createAgent, DEFAULT_SUBAGENT_TURNS } from "../agent.mjs"
|
|
29
|
+
import { runWithContinue, TURN_CAP_MARK, wrapChildCallbacks } from "../agent/spawn-child.mjs"
|
|
30
|
+
import { logEvent } from "../log.mjs"
|
|
31
|
+
import { deathLine } from "../abort-provenance.mjs"
|
|
32
|
+
import {
|
|
33
|
+
mergeChildMutations, runningPoolCount, poolDomainOf, poolLimitsFor, ASYNC_POOL_LIMITS, enqueueAsk,
|
|
34
|
+
} from "./subagent-async.mjs"
|
|
35
|
+
import { refreshQueuedTokens, nextSubagentId } from "./subagent-scheduler.mjs"
|
|
36
|
+
import { mutationSeqOf } from "./advisor-async.mjs"
|
|
37
|
+
// ASYNC-RESULT-CONTAINER.md D3/D6:settle 公共收尾单点 + child signal 构建单点
|
|
38
|
+
import { buildChildSignal, settleAsyncEntry } from "./async-settle.mjs"
|
|
39
|
+
|
|
40
|
+
/** Parent-side mutations (absolute paths) committed AFTER the escalate launch —
|
|
41
|
+
* the overlap scan feeds the settle classification (review #4/round2 #4: the
|
|
42
|
+
* parent may have edited files while the flight ran). */
|
|
43
|
+
function parentMutationsSince(parent, launchSeq) {
|
|
44
|
+
const out = []
|
|
45
|
+
for (const m of parent?._mutLog ?? []) {
|
|
46
|
+
if (m.seq <= (launchSeq ?? -1)) continue
|
|
47
|
+
for (const p of m.paths ?? []) if (!out.includes(p)) out.push(p)
|
|
48
|
+
}
|
|
49
|
+
return out
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Files BOTH sides touched since launch (case-insensitive key on win32 —
|
|
53
|
+
* filesOverlap precedent). */
|
|
54
|
+
function overlapPaths(parent, launchSeq, childTouched) {
|
|
55
|
+
const since = parentMutationsSince(parent, launchSeq)
|
|
56
|
+
if (since.length === 0 || !childTouched?.length) return []
|
|
57
|
+
const key = (p) => (process.platform === "win32" ? String(p).toLowerCase() : String(p))
|
|
58
|
+
const sinceKeys = new Set(since.map(key))
|
|
59
|
+
const hits = childTouched.filter((p) => sinceKeys.has(key(p)))
|
|
60
|
+
// relative display form (touchedFilesNote precedent)
|
|
61
|
+
const cwd = parent?.cwd ?? process.cwd()
|
|
62
|
+
return hits.map((p) => {
|
|
63
|
+
const r = relative(cwd, p)
|
|
64
|
+
return r && !r.startsWith("..") && !isAbsolute(r) ? r : p
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Escalate-side touched files, relative display (or a placeholder). */
|
|
69
|
+
function childTouchedDisplay(child, cwd) {
|
|
70
|
+
const touched = child?._touchedFiles ?? []
|
|
71
|
+
if (touched.length === 0) return "(none recorded)"
|
|
72
|
+
const shown = touched.map((f) => {
|
|
73
|
+
const r = relative(cwd ?? process.cwd(), f)
|
|
74
|
+
return r && !r.startsWith("..") && !isAbsolute(r) ? r : f
|
|
75
|
+
})
|
|
76
|
+
return shown.join(", ")
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Relative touched-file list (mirror of the sync path's note). */
|
|
80
|
+
function touchedFilesNote(child, cwd) {
|
|
81
|
+
const touched = child?._touchedFiles ?? []
|
|
82
|
+
if (touched.length === 0) return ""
|
|
83
|
+
const shown = touched.map((f) => {
|
|
84
|
+
const r = relative(cwd ?? process.cwd(), f)
|
|
85
|
+
return r && !r.startsWith("..") && !isAbsolute(r) ? r : f
|
|
86
|
+
})
|
|
87
|
+
return `\nTouched files: ${shown.join(", ")}`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Async escalate settle — three-way classification (§25 D-R17b review #4):
|
|
92
|
+
* - done: merge ALL mutations (the escalation's changes are the parent's —
|
|
93
|
+
* verify/advisor guards must see them) + overlap warning into the report
|
|
94
|
+
* (report-level hint — not a gate — round2 #4);
|
|
95
|
+
* - error (child failure / turn cap): partial mutations merge ONLY without a
|
|
96
|
+
* parent-side overlap — overlap → NO merge + differences listed (the report
|
|
97
|
+
* carries both sides so the model can decide);
|
|
98
|
+
* - cancelled: nothing merges, never reaches pending (D-M6 — stopped reminder).
|
|
99
|
+
* Runs BEFORE the pending transfer / digest injection. Mutates entry.report /
|
|
100
|
+
* entry.error into the FINAL digest body (the family wording rides
|
|
101
|
+
* injectAsyncResult's escalate branch).
|
|
102
|
+
*/
|
|
103
|
+
export function classifyEscalateSettle(parent, entry) {
|
|
104
|
+
if (entry.cancelled) return { cancelled: true }
|
|
105
|
+
const child = entry.childAgent
|
|
106
|
+
const touched = child?._touchedFiles ?? []
|
|
107
|
+
const overlap = overlapPaths(parent, entry.launchSeq, touched)
|
|
108
|
+
const hasError = entry.error != null
|
|
109
|
+
const raw = hasError ? entry.error : entry.report
|
|
110
|
+
if (hasError) {
|
|
111
|
+
// error branch (child failure / turn cap — review #4): partial mutations
|
|
112
|
+
// merge ONLY when the parent did not touch overlapping files since launch.
|
|
113
|
+
// childAgent may be null when the flight died before child creation (advisor
|
|
114
|
+
// 复评 🟡2——防御:无 child 即无 partial mutations——不 merge 不崩)。
|
|
115
|
+
const merged = child != null && overlap.length === 0 && mergeChildMutations(parent, child)
|
|
116
|
+
let decision = ""
|
|
117
|
+
if (merged) {
|
|
118
|
+
decision = `\nPartial changes merged into the parent's bookkeeping (no parent-side overlap since launch).`
|
|
119
|
+
} else if (overlap.length > 0) {
|
|
120
|
+
decision = `\nPartial changes NOT merged — the parent changed overlapping files while this escalate ran: ${overlap.join(", ")}. Escalate-side changes: ${childTouchedDisplay(child, parent?.cwd)}. Review the conflict and decide what to keep (report-level — not a gate; AGENT-LOOP.md §25 D-R17b).`
|
|
121
|
+
}
|
|
122
|
+
// (nothing to merge + no overlap → the plain error report stands alone)
|
|
123
|
+
entry.error = `${raw}${decision}`
|
|
124
|
+
entry.report = null
|
|
125
|
+
return { cancelled: false, merged, overlap }
|
|
126
|
+
}
|
|
127
|
+
// done — merge-all + overlap warning into the report (report-level — not a gate)
|
|
128
|
+
const merged = mergeChildMutations(parent, child)
|
|
129
|
+
const overlapNote = overlap.length > 0
|
|
130
|
+
? `\n⚠ Overlapping writes: the parent changed ${overlap.join(", ")} while this escalate ran — mutations merged all the same; review those files before building on the report (report-level warning — not a gate; AGENT-LOOP.md §25 D-R17b round2 #4).`
|
|
131
|
+
: ""
|
|
132
|
+
entry.report = `${raw}${overlapNote}`
|
|
133
|
+
return { cancelled: false, merged, overlap }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Launch the async escalate (preflights already passed in executeEscalateAction):
|
|
138
|
+
* pool admission into the shared OTHER domain → entry in `_asyncSubagents`
|
|
139
|
+
* (generic queue/refill/status/cancel machinery) → ack. The flight starts on
|
|
140
|
+
* entry.start() (immediately, or later via maybeRefillAsync when a slot frees).
|
|
141
|
+
*/
|
|
142
|
+
export function launchEscalateAsync(parent, ctx, launch) {
|
|
143
|
+
const { task, provider, tag, effortNote } = launch
|
|
144
|
+
parent._asyncSubagents ??= new Map()
|
|
145
|
+
parent._asyncQueue ??= []
|
|
146
|
+
// Async id allocation (AGENT-LOOP.md §15 D-A1 precedent): reserve the relay
|
|
147
|
+
// counter at launch — the returned id stays stable while the entry sits queued.
|
|
148
|
+
// The [model] token (TUI block creation) is DEFERRED to actual start so queued
|
|
149
|
+
// flights don't paint an empty panel block (subagent-parity).
|
|
150
|
+
// SUBAGENT-ID-COUNTER-AGENT(2026-09-09):取号统一走 nextSubagentId(池活续号
|
|
151
|
+
// 兜底——counter 载体= agent 本体 _subAgentCounter——跨 run/跨压缩存活)。
|
|
152
|
+
const id = nextSubagentId(parent)
|
|
153
|
+
const relayPrefix = `escalate#${id}/`
|
|
154
|
+
const entry = {
|
|
155
|
+
id, role: "escalate", relayPrefix,
|
|
156
|
+
_pool: poolDomainOf("escalate"), // other — shares the domain with explore/plan/coder (§11.1 D-24a)
|
|
157
|
+
status: "queued",
|
|
158
|
+
position: undefined,
|
|
159
|
+
report: null, error: null, done: false, cancelled: false,
|
|
160
|
+
promise: null, _settle: null, _settleSeq: 0,
|
|
161
|
+
model: provider.model ?? null,
|
|
162
|
+
startedAt: null,
|
|
163
|
+
turn: 0, maxTurns: 0,
|
|
164
|
+
controller: null,
|
|
165
|
+
_files: undefined, _dependsOn: undefined,
|
|
166
|
+
childAgent: null,
|
|
167
|
+
tag, effortNote, launchSeq: mutationSeqOf(parent),
|
|
168
|
+
}
|
|
169
|
+
const limits = poolLimitsFor(parent)
|
|
170
|
+
entry.status = runningPoolCount(parent, entry._pool) >= (limits[entry._pool] ?? ASYNC_POOL_LIMITS[entry._pool])
|
|
171
|
+
? "queued" : "running"
|
|
172
|
+
entry.promise = new Promise((res) => { entry._settle = res })
|
|
173
|
+
const ctrl = new AbortController()
|
|
174
|
+
entry.controller = ctrl
|
|
175
|
+
// D6 buildChildSignal 单点(ASYNC-RESULT-CONTAINER.md——D5 同款:_sessionSignal 兜底)。
|
|
176
|
+
const baseSignal = buildChildSignal(parent, ctx)
|
|
177
|
+
if (baseSignal) {
|
|
178
|
+
// §20.3 站点 #10(第 24 批):hop 逐跳保 reason
|
|
179
|
+
if (baseSignal.aborted) ctrl.abort(baseSignal.reason)
|
|
180
|
+
else baseSignal.addEventListener("abort", () => ctrl.abort(baseSignal.reason), { once: true })
|
|
181
|
+
}
|
|
182
|
+
// Turn mirror (⟦ev⟧turn from the child runAgent → entry.turn/maxTurns — status parity).
|
|
183
|
+
const flight = async () => {
|
|
184
|
+
entry.status = "running"
|
|
185
|
+
entry.position = undefined
|
|
186
|
+
entry.startedAt = Date.now()
|
|
187
|
+
ctx.callbacks?.onToken?.(relayPrefix + "⟦ev⟧async\x1e")
|
|
188
|
+
ctx.callbacks?.onToken?.(relayPrefix + "[model]" + (provider.model ?? ""))
|
|
189
|
+
const child = createAgent({
|
|
190
|
+
provider,
|
|
191
|
+
tools: parent.tools,
|
|
192
|
+
config: parent.config,
|
|
193
|
+
cwd: parent.cwd,
|
|
194
|
+
memory: parent.memory,
|
|
195
|
+
// G3(施工②):overlay 摘除——coder 人格槽由 assemblePrompt 场景表承载(G3 映射)。
|
|
196
|
+
role: "coder",
|
|
197
|
+
})
|
|
198
|
+
entry.childAgent = child // settle 分类/status touched 摘要绑定(start 时刻)
|
|
199
|
+
child._logId = relayPrefix.slice(0, -1)
|
|
200
|
+
logEvent("child:spawn", { role: "escalate", id: child._logId, kind: "async", status: "running", ms: 0 })
|
|
201
|
+
const childCallbacks = wrapChildCallbacks(relayPrefix, ctx.callbacks ?? {})
|
|
202
|
+
const relayOnToken = childCallbacks.onToken
|
|
203
|
+
if (relayOnToken) {
|
|
204
|
+
childCallbacks.onToken = (t) => {
|
|
205
|
+
const ev = String(t).match(/^⟦ev⟧turn\x1e(\d+)\x1e(\d+)\x1e/)
|
|
206
|
+
if (ev) {
|
|
207
|
+
entry.turn = Number(ev[1]) || 0
|
|
208
|
+
entry.maxTurns = Number(ev[2]) || 0
|
|
209
|
+
}
|
|
210
|
+
return relayOnToken(t)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const runner = ctx.runAgent ?? runAgent
|
|
214
|
+
const runOpts = {
|
|
215
|
+
depth: 1,
|
|
216
|
+
maxTurns: parent.config?.agent?.subagentTurns ?? DEFAULT_SUBAGENT_TURNS,
|
|
217
|
+
signal: entry.controller.signal,
|
|
218
|
+
}
|
|
219
|
+
// 权限按 async 子代理同款装配:AUTO 直放行;手动档经父 _permQueue(并行子代理
|
|
220
|
+
// 审批不叠弹窗)——背景飞行撞门时无 handler → denied 不悬挂(D-S7 同规则)。
|
|
221
|
+
const childPermission = parent.autoApprove
|
|
222
|
+
? async () => true
|
|
223
|
+
: async (name, toolArgs) => {
|
|
224
|
+
if (!ctx.onPermissionRequest) return false
|
|
225
|
+
const ask = () => ctx.onPermissionRequest(`escalate/${name}`, toolArgs)
|
|
226
|
+
return enqueueAsk(parent, "_permQueue", ask)
|
|
227
|
+
}
|
|
228
|
+
const report = await runWithContinue(
|
|
229
|
+
(childAgent, input, cbs, opts) => runner(childAgent, input, cbs, opts),
|
|
230
|
+
child, task,
|
|
231
|
+
{ ...childCallbacks, onPermissionRequest: childPermission },
|
|
232
|
+
runOpts,
|
|
233
|
+
{
|
|
234
|
+
// 后台飞行不弹 continue 面板(D-A3 §15 例外同款):AUTO && engineering 自动
|
|
235
|
+
// resume——escalate 只在 normal 模式可用(engineering 拒)——恒自动拒 → partial。
|
|
236
|
+
askContinue: () => Promise.resolve(Boolean(parent.config?.agent?.engineering && parent.autoApprove)),
|
|
237
|
+
onDeclined: (e, output) => `escalate (${tag})${entry.effortNote} ${TURN_CAP_MARK} (${e.turn} turns) — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${output.slice(0, 2000)}`,
|
|
238
|
+
},
|
|
239
|
+
)
|
|
240
|
+
// done: compose the post-op body (the settle classification appends the
|
|
241
|
+
// overlap warning / merge notes afterwards)
|
|
242
|
+
return `escalate (${tag})${entry.effortNote} post-op report:\n${report || (child._capturedOutput ?? "").slice(0, 4000)}${touchedFilesNote(child, parent.cwd)}`
|
|
243
|
+
}
|
|
244
|
+
entry.start = () => {
|
|
245
|
+
flight()
|
|
246
|
+
.then((report) => {
|
|
247
|
+
// Turn-cap partial (runWithContinue auto-declined) classifies as the ERROR
|
|
248
|
+
// branch (design: 撞 turn cap = error — partial-merge decision applies):
|
|
249
|
+
// move it to entry.error so the classification + error digest wording fire.
|
|
250
|
+
if (String(report).includes(TURN_CAP_MARK)) entry.error = report
|
|
251
|
+
else entry.report = report
|
|
252
|
+
})
|
|
253
|
+
.catch((e) => {
|
|
254
|
+
// 运行失败/中止:错误文本落 entry.error(子代理同款——cancel 分支忽略它;
|
|
255
|
+
// Ctrl+I 中止的残条目由收尾消化带错误文本——不落空 "(no report)" digest)。
|
|
256
|
+
const child = entry.childAgent
|
|
257
|
+
// §20.3 第 3 条合成器(第 24 批):原 message 前缀逐字保留 + 来源后缀
|
|
258
|
+
entry.error = `escalate (${tag}) error: ${deathLine(e, entry.controller?.signal)}\nPartial output: ${(child?._capturedOutput ?? "").slice(0, 2000)}`
|
|
259
|
+
})
|
|
260
|
+
.finally(() => {
|
|
261
|
+
// settle 公共收尾单点(ASYNC-RESULT-CONTAINER.md D3——settleAsyncEntry):日志三连
|
|
262
|
+
// /cancelled 分支(出池+墓碑+⟦ev⟧stopped+提醒)/挂起分流(pending 单容器+出池——
|
|
263
|
+
// 统一守卫 !parentAborted——D4)/公共尾部(settleSeq/_settle/唤醒 waiter + 腾槽补位
|
|
264
|
+
// ——helper 尾部恒补)统一走共享 helper。族特有 hook = 三分类 merge 决策
|
|
265
|
+
// (classifyEscalateSettle——done/error 分类;cancelled 不经此——helper cancelled
|
|
266
|
+
// 分支先行)。
|
|
267
|
+
settleAsyncEntry(parent, entry, {
|
|
268
|
+
pool: parent._asyncSubagents,
|
|
269
|
+
ctx,
|
|
270
|
+
onAccounting: () => {
|
|
271
|
+
// Three-way settle classification (done/error/cancelled — review #4):
|
|
272
|
+
// done → merge-all + 重叠警告;error → 无父侧重叠才 partial merge;
|
|
273
|
+
// cancelled 不经此(helper cancelled 分支先行)。
|
|
274
|
+
void classifyEscalateSettle(parent, entry)
|
|
275
|
+
},
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
parent._asyncSubagents.set(String(id), entry)
|
|
280
|
+
logEvent("child:spawn", { role: "escalate", id: `escalate#${id}`, kind: "async", status: entry.status, ms: 0 })
|
|
281
|
+
if (entry.status === "queued") {
|
|
282
|
+
parent._asyncQueue.push(entry)
|
|
283
|
+
entry.position = parent._asyncQueue.length
|
|
284
|
+
refreshQueuedTokens(parent, ctx.callbacks?.onToken)
|
|
285
|
+
return JSON.stringify({ id: String(id), role: "escalate", status: "queued", position: entry.position })
|
|
286
|
+
}
|
|
287
|
+
entry.start()
|
|
288
|
+
return JSON.stringify({ id: String(id), role: "escalate", status: "running" })
|
|
289
|
+
}
|