@thincoder/core 0.9.1
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/abort-provenance.mjs +116 -0
- package/advisor/citations.mjs +139 -0
- package/advisor/compaction.mjs +174 -0
- package/advisor/convergence.mjs +80 -0
- package/advisor/history.mjs +77 -0
- package/advisor/loop.mjs +288 -0
- package/advisor/messages.mjs +299 -0
- package/advisor/notice.mjs +141 -0
- package/advisor/project-context.mjs +197 -0
- package/advisor/repos.mjs +150 -0
- package/advisor/run.mjs +190 -0
- package/advisor/truncate.mjs +57 -0
- package/advisor.mjs +281 -0
- package/agent/child-marks.mjs +24 -0
- package/agent/completion.mjs +145 -0
- package/agent/dispatch.mjs +493 -0
- package/agent/family-tools.mjs +174 -0
- package/agent/helpers.mjs +412 -0
- package/agent/post-turn.mjs +70 -0
- package/agent/record-results.mjs +174 -0
- package/agent/relay-prefix.mjs +39 -0
- package/agent/run-stages.mjs +244 -0
- package/agent/setup-reminders.mjs +199 -0
- package/agent/setup.mjs +234 -0
- package/agent/spawn-child.mjs +258 -0
- package/agent/suspension.mjs +240 -0
- package/agent/write-gate.mjs +87 -0
- package/agent-tools/advisor-async.mjs +481 -0
- package/agent-tools/advisor-settle.mjs +240 -0
- package/agent-tools/advisor.mjs +280 -0
- package/agent-tools/async-discard.mjs +143 -0
- package/agent-tools/async-settle.mjs +299 -0
- package/agent-tools/batch-segment.mjs +265 -0
- package/agent-tools/child-permission.mjs +45 -0
- package/agent-tools/consult.mjs +471 -0
- package/agent-tools/design-token.mjs +117 -0
- package/agent-tools/digest-budget.mjs +76 -0
- package/agent-tools/eng.mjs +102 -0
- package/agent-tools/escalate-async.mjs +302 -0
- package/agent-tools/goal.mjs +119 -0
- package/agent-tools/panel-blocks.mjs +24 -0
- package/agent-tools/parent-channel.mjs +231 -0
- package/agent-tools/plan.mjs +86 -0
- package/agent-tools/read-history.mjs +309 -0
- package/agent-tools/recent-changes.mjs +24 -0
- package/agent-tools/review-facts.mjs +31 -0
- package/agent-tools/settings.mjs +268 -0
- package/agent-tools/skill.mjs +63 -0
- package/agent-tools/spawn-gates.mjs +109 -0
- package/agent-tools/subagent-actions.mjs +496 -0
- package/agent-tools/subagent-async.mjs +456 -0
- package/agent-tools/subagent-panel.mjs +160 -0
- package/agent-tools/subagent-run.mjs +208 -0
- package/agent-tools/subagent-scheduler.mjs +446 -0
- package/agent-tools/subagent-spawn.mjs +478 -0
- package/agent-tools/subagent.mjs +419 -0
- package/agent-tools/task.mjs +87 -0
- package/agent-tools/timer.mjs +46 -0
- package/agent-tools/verify.mjs +295 -0
- package/agent-tools.mjs +23 -0
- package/agent.mjs +430 -0
- package/auto-think.mjs +115 -0
- package/compress-form.mjs +24 -0
- package/config-io.mjs +277 -0
- package/config-migrate.mjs +178 -0
- package/config-presets.mjs +49 -0
- package/config.mjs +419 -0
- package/context.mjs +495 -0
- package/conventions.mjs +223 -0
- package/embedding.mjs +120 -0
- package/escape.mjs +152 -0
- package/expand-home.mjs +16 -0
- package/explore-distill.mjs +152 -0
- package/generate-title.mjs +123 -0
- package/git/checkpoint.mjs +448 -0
- package/git/gitmem.mjs +100 -0
- package/history-window.mjs +179 -0
- package/hooks.mjs +108 -0
- package/i18n.mjs +106 -0
- package/index-bin.mjs +48 -0
- package/index-discover.mjs +176 -0
- package/ledger-cmd.mjs +209 -0
- package/ledger-db.mjs +86 -0
- package/ledger-surface.mjs +76 -0
- package/ledger.mjs +202 -0
- package/log.mjs +195 -0
- package/manifest.mjs +338 -0
- package/markdown.mjs +106 -0
- package/mcp/helpers.mjs +51 -0
- package/mcp/transport-http.mjs +248 -0
- package/mcp/transport-stdio.mjs +140 -0
- package/mcp/transport-ws.mjs +122 -0
- package/mcp.mjs +295 -0
- package/memory/code-index.mjs +219 -0
- package/memory/code-sync.mjs +427 -0
- package/memory/core.mjs +318 -0
- package/memory/delete.mjs +242 -0
- package/memory/docs.mjs +431 -0
- package/memory/file-walk.mjs +109 -0
- package/memory/origin.mjs +24 -0
- package/memory/scan.mjs +177 -0
- package/memory/schema.mjs +460 -0
- package/memory.mjs +21 -0
- package/model-ref.mjs +66 -0
- package/model-specs.mjs +277 -0
- package/package.json +32 -0
- package/peer-domains.mjs +265 -0
- package/peer-instances.mjs +178 -0
- package/permission.mjs +79 -0
- package/process-probe.mjs +315 -0
- package/prompt-files.mjs +113 -0
- package/prompt-overlays.mjs +78 -0
- package/prompts/advisor-design.md +43 -0
- package/prompts/advisor-round1.md +41 -0
- package/prompts/advisor-round2.md +46 -0
- package/prompts/advisor-round3.md +42 -0
- package/prompts/common.md +158 -0
- package/prompts/consult-base.md +19 -0
- package/prompts/discipline-engineering.md +123 -0
- package/prompts/discipline-normal.md +206 -0
- package/prompts/persona-coder.md +21 -0
- package/prompts/persona-eng-coder.md +41 -0
- package/prompts/persona-eng-designer.md +80 -0
- package/prompts/persona-engineering.md +160 -0
- package/prompts/persona-explore.md +15 -0
- package/prompts/persona-normal.md +35 -0
- package/prompts/persona-plan.md +27 -0
- package/provider/anthropic.mjs +225 -0
- package/provider/core.mjs +491 -0
- package/provider/errors.mjs +101 -0
- package/provider/google.mjs +257 -0
- package/provider/index.mjs +7 -0
- package/provider/list-models.mjs +163 -0
- package/provider/normalize.mjs +81 -0
- package/provider/rate.mjs +168 -0
- package/provider/responses.mjs +495 -0
- package/provider/retry.mjs +88 -0
- package/provider/sse.mjs +264 -0
- package/provider/wait-status.mjs +59 -0
- package/proxy.mjs +274 -0
- package/rules.mjs +53 -0
- package/session-gc.mjs +248 -0
- package/session-guard.mjs +59 -0
- package/session-lifecycle.mjs +305 -0
- package/session-migrate.mjs +48 -0
- package/session-rename.mjs +38 -0
- package/session-segments.mjs +99 -0
- package/session-slot-write.mjs +168 -0
- package/session-slots-manifest.mjs +264 -0
- package/session-slots.mjs +298 -0
- package/session-store.mjs +441 -0
- package/session.mjs +244 -0
- package/skills.mjs +234 -0
- package/text-budget.mjs +79 -0
- package/token-ttl.mjs +285 -0
- package/tool-docs/apply_patch.md +15 -0
- package/tool-docs/bash.md +38 -0
- package/tool-docs/delete.md +13 -0
- package/tool-docs/edit.md +30 -0
- package/tool-docs/execute.md +21 -0
- package/tool-docs/fetch.md +12 -0
- package/tool-docs/file_ops.md +17 -0
- package/tool-docs/get_current_time.md +8 -0
- package/tool-docs/git.md +54 -0
- package/tool-docs/glob.md +11 -0
- package/tool-docs/grep.md +19 -0
- package/tool-docs/hashline_edit.md +14 -0
- package/tool-docs/insert_after.md +15 -0
- package/tool-docs/lint.md +10 -0
- package/tool-docs/ls.md +12 -0
- package/tool-docs/lsp.md +10 -0
- package/tool-docs/process.md +10 -0
- package/tool-docs/question.md +16 -0
- package/tool-docs/read.md +20 -0
- package/tool-docs/read_image.md +8 -0
- package/tool-docs/tree.md +14 -0
- package/tool-docs/wait_for.md +22 -0
- package/tool-docs/websearch.md +16 -0
- package/tool-docs/write.md +11 -0
- package/tools/bash.mjs +276 -0
- package/tools/edit-batch.mjs +204 -0
- package/tools/edit-diff.mjs +388 -0
- package/tools/exec-run.mjs +43 -0
- package/tools/execute.mjs +243 -0
- package/tools/file.mjs +464 -0
- package/tools/git-checkpoint.mjs +143 -0
- package/tools/git-ext.mjs +173 -0
- package/tools/git.mjs +415 -0
- package/tools/glob-dialect.mjs +130 -0
- package/tools/index.mjs +76 -0
- package/tools/linter.mjs +120 -0
- package/tools/lsp.mjs +335 -0
- package/tools/ops.mjs +293 -0
- package/tools/patch.mjs +290 -0
- package/tools/question.mjs +26 -0
- package/tools/repomap.mjs +314 -0
- package/tools/search.mjs +248 -0
- package/tools/shared.mjs +467 -0
- package/tools/tree.mjs +81 -0
- package/tools/web.mjs +224 -0
- package/tools/write-path.mjs +191 -0
- package/traces/trace-store.mjs +303 -0
- package/undo-stack.mjs +47 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* consult.mjs — multi-model consultation ("会诊", docs/design/CONSULTATION.md). CLI port.
|
|
3
|
+
*
|
|
4
|
+
* Two tools (AGENT-LOOP.md §25 D-R17a — R17, 2026-09-06): consult_start
|
|
5
|
+
* (non-blocking spawn) / consult_stop (cancel a running session). consult_check
|
|
6
|
+
* was RETIRED with the digest auto-injection: the mechanism does ZERO judging —
|
|
7
|
+
* when every model of a session settles (pending 0), the session moves to the
|
|
8
|
+
* pending single container (`_pendingAsyncResults` +role "consult"——
|
|
9
|
+
* ASYNC-RESULT-CONTAINER.md D2——升格完整 entry) and the NEXT run start (user turn
|
|
10
|
+
* or digest auto-turn) injects the full verdict text ("[System reminder:
|
|
11
|
+
* consultation #id finished — N replies …]" — per-model status annotations on
|
|
12
|
+
* partial/full failures) for the main agent to judge and act on in the digestion
|
|
13
|
+
* round. A cancelled session (consult_stop) never reaches the stream.
|
|
14
|
+
*
|
|
15
|
+
* CLI adaptation (vs the VS Code plugin): the child runner is CLI's runAgent
|
|
16
|
+
* (runAgent(child, input, callbacks, opts) — an agent object, not provider+cwd);
|
|
17
|
+
* children are built with createAgent({ role: "consult", readonly tools,
|
|
18
|
+
* CONSULT_BASE overlay }); activity streams to the parent TUI via the relay
|
|
19
|
+
* prefix `consult#<childRelayN>/` (one per consultant child — the shared subagent
|
|
20
|
+
* relay channel; the child relay number is NOT the session id — sessions key
|
|
21
|
+
* their own `_consultIdCounter`), not onSubagent/onToolPanel.
|
|
22
|
+
* Each child settles its own TUI block with a ⟦ev⟧done event at settle (R17 —
|
|
23
|
+
* the old in-turn check consumption is gone).
|
|
24
|
+
*/
|
|
25
|
+
import { createAgent, runAgent, readonlyToolNames } from "../agent.mjs"
|
|
26
|
+
import { resolveChildProvider } from "./subagent.mjs"
|
|
27
|
+
import { pushReal } from "../context.mjs"
|
|
28
|
+
import { offloadToolResult, escapeXml } from "../agent/helpers.mjs"
|
|
29
|
+
import { logEvent, errText } from "../log.mjs"
|
|
30
|
+
import { deathLine } from "../abort-provenance.mjs"
|
|
31
|
+
import { makeRelay, wrapChildCallbacks, runWithContinue, ensureChildApiKey, clampEffort } from "../agent/spawn-child.mjs"
|
|
32
|
+
// TUI-OOM-ROOTCAUSE §23.3.1:子代理人读线窗口常量(单源——store 零依赖)。
|
|
33
|
+
import { RECORD_WINDOW_MESSAGES } from "../session-store.mjs"
|
|
34
|
+
// ASYNC-RESULT-CONTAINER.md D2/D3/D6:pending 单容器停靠 + settle 公共收尾 + child signal 单点
|
|
35
|
+
import { bindChildController, buildChildSignal, settleAsyncEntry } from "./async-settle.mjs"
|
|
36
|
+
import { digestBudgetOver, persistOverflowReport } from "./digest-budget.mjs" // B5(群 B 批 §22 D-DG2):digest 注入预算单源
|
|
37
|
+
|
|
38
|
+
// Named consult defaults (consult P2, 2026-08-30).
|
|
39
|
+
const CONSULT_TIMEOUT_MS = 600_000 // default consult lifecycle timeout
|
|
40
|
+
const CONSULT_TURNS = 40 // default per-child turn cap
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
function consultLabel(m) {
|
|
44
|
+
return `${m.provider}:${m.model}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Narrow the configured consultModels pool to a requested subset.
|
|
48
|
+
* Each selector is "provider:model", a bare provider name, or a bare model name
|
|
49
|
+
* (case-insensitive). A trailing " (effort)" suffix is tolerated (round2 复核
|
|
50
|
+
* 对齐 escalate 动作(subagent action:"escalate"):withPool 列表会带 " (high)" 后缀,模型照抄应可匹配).
|
|
51
|
+
* Returns { models, error } — error set when a selector matches
|
|
52
|
+
* nothing (surface the typo rather than silently dropping it). Absent/empty selectors
|
|
53
|
+
* → the full pool. */
|
|
54
|
+
function selectConsultModels(pool, selectors) {
|
|
55
|
+
if (selectors == null || (Array.isArray(selectors) && selectors.length === 0)) return { models: pool, error: null }
|
|
56
|
+
const list = Array.isArray(selectors) ? selectors : [selectors] // coerce a bare string → [string]
|
|
57
|
+
const selected = []
|
|
58
|
+
const seen = new Set()
|
|
59
|
+
const unknowns = []
|
|
60
|
+
for (const raw of list) {
|
|
61
|
+
const s = String(raw).replace(/\s+\([^)]*\)\s*$/, "").trim().toLowerCase()
|
|
62
|
+
const matches = pool.filter((m) =>
|
|
63
|
+
consultLabel(m).toLowerCase() === s ||
|
|
64
|
+
String(m.provider ?? "").toLowerCase() === s ||
|
|
65
|
+
String(m.model ?? "").toLowerCase() === s,
|
|
66
|
+
)
|
|
67
|
+
if (matches.length === 0) unknowns.push(String(raw))
|
|
68
|
+
else for (const m of matches) {
|
|
69
|
+
const key = consultLabel(m)
|
|
70
|
+
if (!seen.has(key)) { seen.add(key); selected.push(m) }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (unknowns.length > 0) {
|
|
74
|
+
return { models: null, error: `unknown consult model selector(s): ${unknowns.join(", ")} — choose from: ${pool.map(consultLabel).join(", ")}` }
|
|
75
|
+
}
|
|
76
|
+
return { models: selected, error: null }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Read-only tool injected into consultation children (via createAgent's tools).
|
|
80
|
+
* Lets the consultant pull the main agent's conversation history on demand —
|
|
81
|
+
* the failure trail is first-class evidence, not a retelling. */
|
|
82
|
+
export function makeMainHistoryTool(parentAgent) {
|
|
83
|
+
return {
|
|
84
|
+
name: "main_history",
|
|
85
|
+
readonly: true,
|
|
86
|
+
description:
|
|
87
|
+
"Read the main agent's conversation history — what has been tried, the exact errors, recent context. " +
|
|
88
|
+
"Use it to ground your analysis in the actual failure trail instead of guessing.\n" +
|
|
89
|
+
"Parameters:\n" +
|
|
90
|
+
"- limit: Number of recent messages to return (default 20, max 100)",
|
|
91
|
+
parameters: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: { limit: { type: "number", description: "Recent messages (default 20, max 100)" } },
|
|
94
|
+
},
|
|
95
|
+
async execute({ limit }) {
|
|
96
|
+
const n = Math.min(Math.max(limit ?? 20, 1), 100)
|
|
97
|
+
const h = parentAgent?.history ?? []
|
|
98
|
+
const slice = h.slice(-n)
|
|
99
|
+
if (slice.length === 0) return "(empty history)"
|
|
100
|
+
const render = (m) => {
|
|
101
|
+
let content
|
|
102
|
+
if (typeof m.content === "string") content = m.content
|
|
103
|
+
else if (Array.isArray(m.content)) {
|
|
104
|
+
content = m.content.map((part) => {
|
|
105
|
+
if (part?.type === "image_url" || part?.type === "image") return "[image omitted]"
|
|
106
|
+
if (part?.type === "text") return part.text ?? ""
|
|
107
|
+
return JSON.stringify(part)
|
|
108
|
+
}).join("\n")
|
|
109
|
+
} else content = m.content == null ? "" : JSON.stringify(m.content)
|
|
110
|
+
const calls = Array.isArray(m.tool_calls)
|
|
111
|
+
? m.tool_calls.map((c) => `[tool: ${c.function?.name ?? c.name}(${String(c.function?.arguments ?? c.args ?? "").slice(0, 200)})]`).join("\n")
|
|
112
|
+
: ""
|
|
113
|
+
return `--- [${m.role}] ---\n${content}${calls ? "\n" + calls : ""}`
|
|
114
|
+
}
|
|
115
|
+
const BUDGET = 60_000 // per-consult token budget (tokens)
|
|
116
|
+
let out = ""
|
|
117
|
+
for (let i = slice.length - 1; i >= 0; i--) {
|
|
118
|
+
const line = render(slice[i])
|
|
119
|
+
if (out.length + line.length > BUDGET) {
|
|
120
|
+
// A single message over the whole budget: truncate IT (it is the newest
|
|
121
|
+
// and most relevant) instead of dropping everything with a misleading
|
|
122
|
+
// "earlier messages trimmed" note. Older accumulation still trims.
|
|
123
|
+
if (out === "") { out = line.slice(0, BUDGET) + "\n(… truncated — single message exceeded budget " + BUDGET + " chars)"; break }
|
|
124
|
+
out = `(earlier messages trimmed — budget ${BUDGET} chars)\n\n` + out
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
out = out ? line + "\n\n" + out : line
|
|
128
|
+
}
|
|
129
|
+
return out
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Full-session settle routing (R17 — AGENT-LOOP.md §25 D-R17a): a session whose
|
|
136
|
+
* pending count reached 0 has no more replies coming — the session leaves
|
|
137
|
+
* `_consultSessions` and, unless it was cancelled (consult_stop / turn-end
|
|
138
|
+
* abort), moves into the pending single container (`_pendingAsyncResults` +role
|
|
139
|
+
* "consult"——ASYNC-RESULT-CONTAINER.md D2——升格完整 entry:同 subagent/advisor/
|
|
140
|
+
* escalate 的 `{id, role, report, done, ...}` 形态)whose report carries the full
|
|
141
|
+
* per-model verdict text (composed here — all settle states are known,
|
|
142
|
+
* partial/full failures annotated per model). The entry is injected at the next
|
|
143
|
+
* run start (user turn or digest auto-turn — agent.mjs); the suspension driver
|
|
144
|
+
* is woken (settle-event parity with the async pools) so an idle settle still
|
|
145
|
+
* triggers the digestion round (T-R17j).
|
|
146
|
+
* D3:公共收尾统一走 settleAsyncEntry 共享 helper(四族同机制)——consult 族参数:
|
|
147
|
+
* 无池(会话池无条目——settle 即出池)、无 ctx(无 TUI 冻结事件——子块各自 settle 时
|
|
148
|
+
* 已冻结)、无 onAccounting;helper 按 role "consult" 恒停靠 pending(非挂起期也停靠)。
|
|
149
|
+
* Cancelled sessions produce no digest (T-R17c).
|
|
150
|
+
*/
|
|
151
|
+
function sessionSettled(agent, session) {
|
|
152
|
+
agent?._consultSessions?.delete(String(session.id))
|
|
153
|
+
if (session.stopped) return // cancelled — no digest (T-R17c)
|
|
154
|
+
const entry = {
|
|
155
|
+
id: String(session.id),
|
|
156
|
+
role: "consult",
|
|
157
|
+
report: composeConsultDigest(session),
|
|
158
|
+
error: null, done: true, status: "done", cancelled: false,
|
|
159
|
+
relayPrefix: null, startedAt: null, _settle: null, _settleSeq: 0,
|
|
160
|
+
}
|
|
161
|
+
settleAsyncEntry(agent, entry, { pool: null, ctx: null })
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Digest body for a fully-settled session — title + one annotated line per
|
|
165
|
+
* reply (failed replies marked per-model — round2 #7; full text is injected
|
|
166
|
+
* verbatim and may be >64K → offloaded with a preview at injection). */
|
|
167
|
+
export function composeConsultDigest(session) {
|
|
168
|
+
const replies = session.replies ?? []
|
|
169
|
+
const parts = replies.map((r) =>
|
|
170
|
+
r.failed
|
|
171
|
+
? `- [${r.model}] (failed): ${r.reply}`
|
|
172
|
+
: `- [${r.model}]: ${r.reply}`)
|
|
173
|
+
const counts = `${replies.length} of ${session.total} models replied (${session.failed} failed)`
|
|
174
|
+
return `[System reminder: consultation #${session.id} finished — ${counts}]\n${parts.join("\n")}`
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Inject one settled consult family entry into the parent history as a
|
|
179
|
+
* user-role reminder (run-start injection — agent.mjs; same shape rules as
|
|
180
|
+
* injectAsyncResult: XML-escaped, >64K offloaded with preview + path). Consumed
|
|
181
|
+
* = the caller splices the entry out of the pending single container
|
|
182
|
+
* (_pendingAsyncResults——ASYNC-RESULT-CONTAINER.md D2——role 分发注入)。
|
|
183
|
+
*/
|
|
184
|
+
export async function injectConsultResult(agent, entry) {
|
|
185
|
+
const full = String(entry?.report ?? "(no consultation result)")
|
|
186
|
+
// §22 D-DG3(群 B 批 B5):计入预算的 raw = 报告正文(标签行不计)——首行标签拆出;超限
|
|
187
|
+
// 路径保留族标签行(与 VSC 镜像同形);落盘内容 = raw(与其余三族同口径)。
|
|
188
|
+
const at = full.indexOf("\n")
|
|
189
|
+
const label = at === -1 ? "" : full.slice(0, at + 1)
|
|
190
|
+
const raw = at === -1 ? full : full.slice(at + 1)
|
|
191
|
+
const over = digestBudgetOver(agent, raw.length)
|
|
192
|
+
const saved = over ? await persistOverflowReport(raw, { tag: `consult-${entry?.id ?? "session"}` }) : null
|
|
193
|
+
const preview = saved ? `${label}${saved}` : await offloadToolResult(full, `consult-${entry?.id ?? "session"}`)
|
|
194
|
+
pushReal(agent, {
|
|
195
|
+
role: "user",
|
|
196
|
+
content: escapeXml(preview),
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function settleChild(agent, session, id, label, ok, payload, emitDone) {
|
|
201
|
+
if (ok) {
|
|
202
|
+
session.received++
|
|
203
|
+
session.replies.push({ model: label, reply: payload })
|
|
204
|
+
} else if (session.stopped) {
|
|
205
|
+
session.terminated = (session.terminated ?? 0) + 1
|
|
206
|
+
} else {
|
|
207
|
+
session.failed++
|
|
208
|
+
session.replies.push({ model: label, reply: `(consultation failed: ${payload})`, failed: true })
|
|
209
|
+
}
|
|
210
|
+
session.pending--
|
|
211
|
+
emitDone?.() // per-child TUI block freeze at settle (R17 — child's activity card is done)
|
|
212
|
+
if (session.pending === 0) sessionSettled(agent, session)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function runConsultChild(ctx, session, id, m, problem, ctrl) {
|
|
216
|
+
const agent = ctx.agent
|
|
217
|
+
const timeoutMs = agent?.config?.agent?.consultTimeoutMs ?? CONSULT_TIMEOUT_MS
|
|
218
|
+
let timedOut = false
|
|
219
|
+
const armWatchdog = () => {
|
|
220
|
+
const t = setTimeout(() => {
|
|
221
|
+
timedOut = true
|
|
222
|
+
// §20.3 站点 #12 信号面(第 24 批):watchdog 中止带 timeout reason
|
|
223
|
+
try { ctrl.abort({ abortTrigger: "timeout", abortDetail: "consult-watchdog" }) } catch { /* already settled */ }
|
|
224
|
+
}, timeoutMs)
|
|
225
|
+
t.unref?.()
|
|
226
|
+
return t
|
|
227
|
+
}
|
|
228
|
+
let watchdog = armWatchdog()
|
|
229
|
+
const label = consultLabel(m)
|
|
230
|
+
// LOGGING(LOGGING.md):child:*(consult)——logSettle 在函数作用域声明(外层 catch
|
|
231
|
+
// 覆盖 spawn 前失败路径);spawn 事件在 relay 建立后发射(logArmed 翻转——provider/
|
|
232
|
+
// 创建失败 = 从未启动,不落子事件、错误仅经 settleChild 进会话)。
|
|
233
|
+
let childLogId = null
|
|
234
|
+
let logT0 = 0
|
|
235
|
+
let logArmed = false
|
|
236
|
+
let logDone = false
|
|
237
|
+
const logSettle = (kind, payload) => {
|
|
238
|
+
if (!logArmed || logDone || !childLogId) return
|
|
239
|
+
logDone = true
|
|
240
|
+
const ms = Date.now() - logT0
|
|
241
|
+
const base = { role: "consult", id: childLogId, ms }
|
|
242
|
+
if (kind === "ok" || kind === "partial") logEvent("child:done", { ...base, kind })
|
|
243
|
+
else logEvent("child:error", { ...base, err: errText(payload, 200) })
|
|
244
|
+
}
|
|
245
|
+
// R17: relay prefix assigned before the child runner arms — the per-child TUI
|
|
246
|
+
// block freeze emits only when a block actually exists (relay established).
|
|
247
|
+
let relayPrefix = null
|
|
248
|
+
const settle = (ok, payload) => settleChild(agent, session, id, label, ok, payload, relayPrefix
|
|
249
|
+
? () => ctx.callbacks?.onToken?.(`${relayPrefix}⟦ev⟧done\x1e0\x1e0\x1edone\x1e`)
|
|
250
|
+
: null)
|
|
251
|
+
try {
|
|
252
|
+
// Provider resolution: consultModels entries are { provider, model, effort? } — resolve
|
|
253
|
+
// via the subagent's provider resolver ("provider:model" handles cross-provider picks).
|
|
254
|
+
const provider = resolveChildProvider(agent, `${m.provider}:${m.model}`)
|
|
255
|
+
if (!ensureChildApiKey(provider)) {
|
|
256
|
+
// resolveChildProvider may still lack a key; fail loudly like the plugin precheck
|
|
257
|
+
// (settleChild turns this message into a clear failed reply instead of a raw 401)
|
|
258
|
+
throw new Error(`consult model ${label} has no API key — check providers[${m.provider}].apiKey in config.json`)
|
|
259
|
+
}
|
|
260
|
+
// Clamp the pool's effort to the model's reasoningEffortEnum — an out-of-enum
|
|
261
|
+
// value makes provider/core throw on EVERY chat call (candidate dies on takeoff).
|
|
262
|
+
// Symmetric with the escalate action (subagent action:"escalate"); 2026-08-16
|
|
263
|
+
// a real consult died on qwen3.8-max effort "high" (enum is xhigh/medium/low).
|
|
264
|
+
// Out-of-enum: DROP the effort entirely (the provider preset default may ALSO be
|
|
265
|
+
// out-of-enum for this override model).
|
|
266
|
+
clampEffort(provider, m.model, m.effort)
|
|
267
|
+
|
|
268
|
+
// Read-only consultant: filter the parent tool set down to readonly tools + main_history.
|
|
269
|
+
const allowed = readonlyToolNames(agent.tools ?? [])
|
|
270
|
+
const tools = [
|
|
271
|
+
...(agent.tools ?? []).filter((t) => allowed.has(t.name)),
|
|
272
|
+
makeMainHistoryTool(agent),
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
const child = createAgent({
|
|
276
|
+
provider,
|
|
277
|
+
tools,
|
|
278
|
+
config: agent.config,
|
|
279
|
+
cwd: agent.cwd,
|
|
280
|
+
memory: agent.memory,
|
|
281
|
+
// No overlay: setup.mjs already selects CONSULT_BASE as the base prompt for
|
|
282
|
+
// role "consult" (overlay + base would concatenate it twice).
|
|
283
|
+
role: "consult",
|
|
284
|
+
})
|
|
285
|
+
// TUI-OOM-ROOTCAUSE §23.3.1:子代理人读线窗口(四处创建点同置)——consult 会话跨回合驻留,
|
|
286
|
+
// 不置窗则 _fullHistory 仍无界(F-O1 覆盖 depth>0 全部创建点)
|
|
287
|
+
child._historyWindow = RECORD_WINDOW_MESSAGES
|
|
288
|
+
|
|
289
|
+
// Activity relay via the unified spawn-child pipeline (§7.2 D3): `consult#<subId>/`
|
|
290
|
+
// prefix (same channel subagent uses — parallel consultants stay independent) +
|
|
291
|
+
// onToolOutput passthrough so the consultant's tool output lands in its TUI block.
|
|
292
|
+
relayPrefix = makeRelay(agent, "consult", ctx.callbacks?.onToken, provider.model ?? "")
|
|
293
|
+
// LOGGING:arm(spawn 事件——relay 建立后;子内事件归属 _logId)
|
|
294
|
+
childLogId = relayPrefix.slice(0, -1)
|
|
295
|
+
child._logId = childLogId
|
|
296
|
+
logT0 = Date.now()
|
|
297
|
+
logArmed = true
|
|
298
|
+
logEvent("child:spawn", { role: "consult", id: childLogId, kind: "consult" })
|
|
299
|
+
const childCallbacks = wrapChildCallbacks(relayPrefix, ctx.callbacks ?? {})
|
|
300
|
+
let declined = false // review #1: guard against double-settle when onDeclined fired
|
|
301
|
+
|
|
302
|
+
// Turn-cap continue loop (TURN-CAP-CONTINUE.md) via runWithContinue (§7.2 D3): hitting
|
|
303
|
+
// the cap asks the user via the SAME y/n panel the main agent uses — unlimited
|
|
304
|
+
// continues, each with a fresh turn budget AND a re-armed wall-clock watchdog (a
|
|
305
|
+
// continue is a fresh budget, the clock restarts too). Parallel consultants serialize
|
|
306
|
+
// their prompts through a session-level queue. Declined / headless → failed reply
|
|
307
|
+
// (partial diagnosis).
|
|
308
|
+
const runner = ctx.runAgent ?? runAgent
|
|
309
|
+
try {
|
|
310
|
+
const result = await runWithContinue(
|
|
311
|
+
(childAgent, input, cbs, opts) => runner(childAgent, input, cbs, opts),
|
|
312
|
+
child, "# Problem\n" + problem,
|
|
313
|
+
childCallbacks,
|
|
314
|
+
{ depth: 1, maxTurns: agent?.config?.agent?.consultTurns ?? CONSULT_TURNS, signal: ctrl.signal },
|
|
315
|
+
{
|
|
316
|
+
askContinue: (e) => {
|
|
317
|
+
if (!ctx.onPermissionRequest) return Promise.resolve(false)
|
|
318
|
+
// 残环批(2026-09-16):归属键形态(`consult#<relayN>`——端侧键形解析的输入契约;
|
|
319
|
+
// 原 label = 显示名不携 id);relay 未建立(spawn 前失败路径不可达此 ask)时回落 label。
|
|
320
|
+
const ask = () => ctx.onPermissionRequest("continue", { turns: e.turn, agent: relayPrefix ? relayPrefix.slice(0, -1) : label })
|
|
321
|
+
session.continueQueue = (session.continueQueue ?? Promise.resolve()).then(ask, ask)
|
|
322
|
+
return session.continueQueue.then((go) => {
|
|
323
|
+
if (go) {
|
|
324
|
+
clearTimeout(watchdog)
|
|
325
|
+
timedOut = false // fresh budget → fresh clock
|
|
326
|
+
watchdog = armWatchdog()
|
|
327
|
+
}
|
|
328
|
+
return go
|
|
329
|
+
})
|
|
330
|
+
},
|
|
331
|
+
onDeclined: (e) => {
|
|
332
|
+
declined = true
|
|
333
|
+
settle(false, `turn cap reached (${e.turn} turns) — stopped, diagnosis may be partial`)
|
|
334
|
+
logSettle("partial", null)
|
|
335
|
+
return undefined
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
)
|
|
339
|
+
// Review #1 fix: onDeclined already settled this child as a failed reply —
|
|
340
|
+
// settling again here would push a phantom empty success reply and decrement
|
|
341
|
+
// `pending` twice (negative pending would re-enter the settle routing).
|
|
342
|
+
if (!declined) {
|
|
343
|
+
settle(true, String(result ?? ""))
|
|
344
|
+
logSettle("ok", null)
|
|
345
|
+
}
|
|
346
|
+
} catch (e) {
|
|
347
|
+
// Runner errors (incl. the watchdog's abort) settle as a failed reply — the
|
|
348
|
+
// continue/declined paths are already handled inside runWithContinue.
|
|
349
|
+
const note = timedOut ? `consultation timed out after ${Math.round(timeoutMs / 60000)}min (agent.consultTimeoutMs)` : deathLine(e, ctrl?.signal)
|
|
350
|
+
settle(false, note)
|
|
351
|
+
logSettle("error", note)
|
|
352
|
+
}
|
|
353
|
+
} catch (e) {
|
|
354
|
+
// Errors BEFORE the runner (provider resolution, createAgent) or a throwing
|
|
355
|
+
// continue-prompt settle as failed replies — the runner's own errors are already
|
|
356
|
+
// handled inside the loop above. relayPrefix is null on these paths — no TUI
|
|
357
|
+
// block was ever opened, so no freeze event is emitted.
|
|
358
|
+
const line = deathLine(e, ctrl?.signal)
|
|
359
|
+
settle(false, line)
|
|
360
|
+
logSettle("error", line)
|
|
361
|
+
} finally {
|
|
362
|
+
clearTimeout(watchdog)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Turn-end / session-end abort cleanup (R17 — call sites: the Ctrl+C abort
|
|
367
|
+
* branches of finalizeAgentTurn and the suspension driver). Consultation
|
|
368
|
+
* sessions are now cross-turn background work (like async subagents): a NORMAL
|
|
369
|
+
* turn end keeps them alive — this runs only when the user stops everything:
|
|
370
|
+
* every leftover session is marked stopped (its settles never reach the digest
|
|
371
|
+
* stream — T-R17c) and its controllers aborted. */
|
|
372
|
+
export function cleanupConsultSessions(agent) {
|
|
373
|
+
for (const s of agent._consultSessions?.values() ?? []) {
|
|
374
|
+
s.stopped = true
|
|
375
|
+
for (const c of s.controllers ?? []) { try { c.abort({ abortTrigger: "stop", abortDetail: "consult-cleanup" }) } catch { /* already settled */ } }
|
|
376
|
+
}
|
|
377
|
+
agent._consultSessions?.clear()
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export const consultStartTool = {
|
|
381
|
+
name: "consult_start",
|
|
382
|
+
readonly: false,
|
|
383
|
+
sideEffectExempt: true,
|
|
384
|
+
description:
|
|
385
|
+
"Start a parallel multi-model consultation (会诊) for a hard problem you are stuck on (repeated failures, no headway). " +
|
|
386
|
+
"Call it directly when the user asks for 会诊 / consult — an explicit user request applies even if you are not 'stuck'. " +
|
|
387
|
+
"Several configured models (agent.consultModels) analyze the same problem INDEPENDENTLY and in parallel. " +
|
|
388
|
+
"Non-blocking: returns immediately with a consult id; the consultants keep running in the background across turns. " +
|
|
389
|
+
"When EVERY model has replied (or failed), the full verdict text is delivered to you automatically — as a system " +
|
|
390
|
+
"reminder at the next run start, or digested on its own while the session is idle — judge and adopt each opinion " +
|
|
391
|
+
"yourself with your own tools (opinions are suggestions, not gates). To stop a session early (user changed their " +
|
|
392
|
+
"mind / wants the tokens back), call consult_stop(id) — a stopped session delivers no digest.\n" +
|
|
393
|
+
"Parameters:\n" +
|
|
394
|
+
"- problem (required): a brief — the symptom, what you already tried (failure trail), and entry-point files. " +
|
|
395
|
+
"Do NOT paste raw error logs; consultants pull the main session history themselves via their main_history tool.\n" +
|
|
396
|
+
"- models (optional): subset of agent.consultModels to run — an array of \"provider:model\", bare provider, or bare model names (case-insensitive). Omit to run all.",
|
|
397
|
+
parameters: {
|
|
398
|
+
type: "object",
|
|
399
|
+
properties: {
|
|
400
|
+
problem: { type: "string", description: "Problem brief (symptom + failure trail + entry files)" },
|
|
401
|
+
models: { type: "array", items: { type: "string" }, description: 'Optional subset of agent.consultModels to run (default: all). Each entry is "provider:model", a bare provider name, or a bare model name (case-insensitive).' },
|
|
402
|
+
},
|
|
403
|
+
required: ["problem"],
|
|
404
|
+
},
|
|
405
|
+
async execute({ problem, models }, ctx) {
|
|
406
|
+
if (typeof problem !== "string" || !problem.trim()) return "Error: problem is required and must be a non-empty string"
|
|
407
|
+
const agent = ctx.agent
|
|
408
|
+
if (!agent) return "Error: consult requires an agent context"
|
|
409
|
+
const pool = agent.config?.agent?.consultModels ?? []
|
|
410
|
+
if (!Array.isArray(pool) || pool.length === 0)
|
|
411
|
+
return "Consultation is not configured — add agent.consultModels ([{ provider, model }], up to 5) to ~/.thincoder/config.json"
|
|
412
|
+
if (pool.length > 5) return `Error: consultModels supports at most 5 models (got ${pool.length})`
|
|
413
|
+
|
|
414
|
+
// `models` (optional) narrows the pool to a subset; absent/empty → run the whole pool.
|
|
415
|
+
const picked = selectConsultModels(pool, models)
|
|
416
|
+
if (picked.error) return picked.error
|
|
417
|
+
const run = picked.models
|
|
418
|
+
|
|
419
|
+
agent._consultSessions ??= new Map()
|
|
420
|
+
const id = String((agent._consultIdCounter = (agent._consultIdCounter ?? 0) + 1))
|
|
421
|
+
const session = {
|
|
422
|
+
id, controllers: [], replies: [], pending: 0,
|
|
423
|
+
failed: 0, terminated: 0, stopped: false, received: 0, total: run.length,
|
|
424
|
+
models: run.map(consultLabel),
|
|
425
|
+
}
|
|
426
|
+
agent._consultSessions.set(id, session)
|
|
427
|
+
|
|
428
|
+
for (const m of run) {
|
|
429
|
+
session.pending++
|
|
430
|
+
const ctrl = new AbortController()
|
|
431
|
+
session.controllers.push(ctrl)
|
|
432
|
+
// D6 buildChildSignal 单点(ASYNC-RESULT-CONTAINER.md D5——consult 补 _sessionSignal
|
|
433
|
+
// 兜底:挂起会话内的 consult children 持会话 signal,digest 自身 Ctrl+C 不误伤)·
|
|
434
|
+
// #98 链结单点(interrupt 豁免面——Ctrl+I 不逐链中止)。
|
|
435
|
+
bindChildController(ctrl, buildChildSignal(agent, ctx))
|
|
436
|
+
// Fire and forget — each child settles itself into the session; the session
|
|
437
|
+
// routes to the pending single container when every child has settled (R17).
|
|
438
|
+
runConsultChild(ctx, session, id, m, problem, ctrl)
|
|
439
|
+
}
|
|
440
|
+
return JSON.stringify({ id, models: session.models })
|
|
441
|
+
},
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export const consultStopTool = {
|
|
445
|
+
name: "consult_stop",
|
|
446
|
+
readonly: false,
|
|
447
|
+
sideEffectExempt: true,
|
|
448
|
+
description:
|
|
449
|
+
"Cancel a still-running consultation session (会诊) — the user changed their mind, the problem resolved, or you want the tokens back. " +
|
|
450
|
+
"Aborts every consultant that is still running; a stopped session delivers NO digest (R17 — its already-collected partial replies are dropped). " +
|
|
451
|
+
"Sessions that finished on their own are no longer cancellable — their verdict text is delivered automatically.\n" +
|
|
452
|
+
"Returns JSON {abandoned: <pending count>, cancelled: true} — or {error: \"unknown consult id\"} (already finished/cancelled).\n" +
|
|
453
|
+
"Parameters:\n" +
|
|
454
|
+
"- id (required): the consult id from consult_start",
|
|
455
|
+
parameters: {
|
|
456
|
+
type: "object",
|
|
457
|
+
properties: {
|
|
458
|
+
id: { type: "string", description: "Consult id" },
|
|
459
|
+
},
|
|
460
|
+
required: ["id"],
|
|
461
|
+
},
|
|
462
|
+
async execute({ id }, ctx) {
|
|
463
|
+
const s = ctx.agent?._consultSessions?.get(String(id))
|
|
464
|
+
if (!s) return JSON.stringify({ error: "unknown consult id" })
|
|
465
|
+
const abandoned = s.pending
|
|
466
|
+
s.stopped = true
|
|
467
|
+
// §20.3 站点 #11(第 24 批):会话级停 = stop
|
|
468
|
+
for (const c of s.controllers) { try { c.abort({ abortTrigger: "stop", abortDetail: "consult-stop" }) } catch { /* already settled */ } }
|
|
469
|
+
return JSON.stringify({ abandoned, cancelled: true })
|
|
470
|
+
},
|
|
471
|
+
}
|
|
@@ -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
|
+
}
|