thincoder 0.12.54 → 0.12.59
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 +98 -0
- package/README.md +1 -1
- package/bin/thincoder.mjs +25 -3
- package/package.json +3 -7
- package/src/acp/bridge.mjs +132 -26
- package/src/advisor/messages.mjs +38 -3
- package/src/advisor/run.mjs +91 -53
- package/src/advisor.mjs +15 -7
- package/src/agent/dispatch.mjs +156 -39
- package/src/agent/helpers.mjs +46 -4
- package/src/agent/setup.mjs +102 -19
- package/src/agent/spawn-child.mjs +28 -1
- package/src/agent-tools/advisor.mjs +43 -11
- package/src/agent-tools/consult.mjs +37 -6
- package/src/agent-tools/eng.mjs +4 -1
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +160 -0
- package/src/agent-tools/settings.mjs +162 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +432 -0
- package/src/agent-tools/subagent-async.mjs +427 -0
- package/src/agent-tools/subagent-scheduler.mjs +319 -0
- package/src/agent-tools/subagent.mjs +565 -128
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +161 -49
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +182 -81
- package/src/auto-think.mjs +14 -0
- package/src/cli/make-agent.mjs +27 -1
- package/src/cli/memory-command.mjs +28 -7
- package/src/cli/permission.mjs +8 -1
- package/src/config.mjs +125 -8
- package/src/context.mjs +115 -34
- package/src/distill.mjs +19 -1
- package/src/escape.mjs +82 -27
- package/src/log.mjs +195 -0
- package/src/mcp/transport-http.mjs +13 -1
- package/src/mcp.mjs +52 -7
- package/src/memory/code-sync.mjs +1 -1
- package/src/memory/core.mjs +204 -10
- package/src/memory/docs.mjs +197 -62
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +38 -1
- package/src/prompts/advisor-design.md +46 -0
- package/src/prompts/advisor-round1.md +49 -2
- package/src/prompts/advisor-round2.md +47 -0
- package/src/prompts/advisor-round3.md +47 -0
- package/src/prompts/coder.md +22 -0
- package/src/prompts/consult-base.md +13 -0
- package/src/prompts/discipline.md +25 -6
- package/src/prompts/eng-coder.md +2 -2
- package/src/prompts/engineering-sub.md +23 -1
- package/src/prompts/engineering.md +157 -50
- package/src/prompts/explore.md +1 -2
- package/src/prompts/main.md +11 -5
- package/src/prompts/methodology-template.md +14 -0
- package/src/prompts/system.md +5 -2
- package/src/provider/anthropic.mjs +7 -5
- package/src/provider/core.mjs +104 -28
- package/src/provider/google.mjs +57 -24
- package/src/provider/normalize.mjs +1 -1
- package/src/provider/rate.mjs +0 -2
- package/src/provider/responses.mjs +8 -13
- package/src/provider/sse.mjs +20 -0
- package/src/session.mjs +15 -0
- package/src/tools/apply_patch.md +5 -1
- package/src/tools/bash.md +3 -3
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +92 -0
- package/src/tools/edit-diff.mjs +265 -0
- package/src/tools/edit.md +11 -6
- package/src/tools/execute.md +8 -8
- package/src/tools/execute.mjs +31 -35
- package/src/tools/file.mjs +26 -114
- package/src/tools/file_ops.md +3 -2
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +8 -16
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -2
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +3 -1
- package/src/tools/linter.mjs +9 -37
- package/src/tools/lsp.md +4 -1
- package/src/tools/patch.mjs +84 -13
- package/src/tools/pdf-parse-text.mjs +497 -0
- package/src/tools/pdf-parse-xref.mjs +499 -0
- package/src/tools/pdf.mjs +155 -0
- package/src/tools/question.md +2 -1
- package/src/tools/read.md +1 -0
- package/src/tools/read_pdf.md +21 -0
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +11 -32
- package/src/tools/system.mjs +6 -21
- package/src/tools/tree.md +2 -1
- 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 +387 -24
- package/src/tui/clipboard.mjs +17 -6
- package/src/tui/cmd-config.mjs +29 -9
- package/src/tui/cmd-eng.mjs +1 -0
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp-form.mjs +197 -0
- package/src/tui/cmd-mcp.mjs +264 -114
- package/src/tui/cmd-think.mjs +1 -1
- package/src/tui/index.mjs +49 -95
- package/src/tui/interaction.mjs +41 -3
- package/src/tui/key-handler.mjs +105 -143
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +41 -1
- package/src/tui/pickers.mjs +73 -7
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +45 -20
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +165 -0
- package/src/tui/render.mjs +4 -4
- package/src/tui/startup.mjs +40 -2
- package/src/tui/subagent-blocks.mjs +404 -111
- package/src/tui/subagent-panel.mjs +88 -13
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-events.mjs +172 -95
- package/src/tui/update-notice.mjs +72 -0
- package/src/tui/wizard.mjs +36 -6
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/tools/exec-prelude.mjs +0 -84
package/src/agent/helpers.mjs
CHANGED
|
@@ -33,7 +33,47 @@ export const REPORT_CONTINUATION =
|
|
|
33
33
|
"4. Anything left undone or worth follow-up"
|
|
34
34
|
|
|
35
35
|
const TOOL_RESULT_OFFLOAD_LIMIT = 64 * 1024 // 65536 chars — offload only above 64K (2026-08-24)
|
|
36
|
-
const TOOL_RESULT_PREVIEW = 64 * 1024 //
|
|
36
|
+
const TOOL_RESULT_PREVIEW = 64 * 1024 // total preview budget: head + middle note + tail ≤ 65536 (aligns with CLI/VS Code webview)
|
|
37
|
+
const TOOL_RESULT_PREVIEW_HEAD = 16 * 1024 // head slice preserved (2026-09-04 §5 — dual-end preview)
|
|
38
|
+
const TOOL_RESULT_PREVIEW_TAIL = 48 * 1024 // nominal tail slice (results/errors/stats live here — actual tail = budget remainder, see buildDualEndPreview)
|
|
39
|
+
|
|
40
|
+
/** UTF-16 安全截断(2026-09-02 deepseek 400 根因):slice(0, N) 按码元切会把 emoji 代理对切成孤立
|
|
41
|
+
* 高代理(如 🔴=U+D83D+DD34 只剩 D83D)——deepseek 解析器严格 UTF-16 报 400
|
|
42
|
+
* "unexpected end of hex escape"。截断点落在高代理上时向前收一个码元。
|
|
43
|
+
* 与 setup.mjs 的 safeSliceUTF16 同语义(两处独立实现——escape.mjs 的 sanitizeLoneSurrogates 是发送兜底,此处是源头)。 */
|
|
44
|
+
function safeSliceUTF16(text, max) {
|
|
45
|
+
if (text.length <= max) return text
|
|
46
|
+
const cp = text.charCodeAt(max - 1)
|
|
47
|
+
if (cp >= 0xd800 && cp <= 0xdbff) return text.slice(0, max - 1)
|
|
48
|
+
return text.slice(0, max)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** UTF-16 safe END slice (2026-09-04 §5 dual-end preview — review #5: both boundaries must not split
|
|
52
|
+
* a surrogate pair). Same rule as safeSliceUTF16, mirrored: if the slice START lands on a LOW
|
|
53
|
+
* surrogate (DC00-DFFF — the second half of a pair whose high half sits just before the boundary),
|
|
54
|
+
* advance one code unit so the slice never begins with an orphan low surrogate. */
|
|
55
|
+
function safeSliceUTF16End(text, max) {
|
|
56
|
+
if (text.length <= max) return text
|
|
57
|
+
const start = text.length - max
|
|
58
|
+
const cp = text.charCodeAt(start)
|
|
59
|
+
if (cp >= 0xdc00 && cp <= 0xdfff) return text.slice(start + 1)
|
|
60
|
+
return text.slice(start)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Dual-end preview (design §5 D-4.1): head + middle-omitted note + tail — the tail carries
|
|
64
|
+
* results/errors/stats that a pure-head truncation would cut off.
|
|
65
|
+
* Budget (round1 review #2, fixed): head + note + tail ≤ TOOL_RESULT_PREVIEW (65536) — tail is
|
|
66
|
+
* computed from constants (tail = TOOL_RESULT_PREVIEW − head − noteLen), never hardcoded.
|
|
67
|
+
* The note length depends on the omitted digit count; text.length's digit count is an upper bound
|
|
68
|
+
* for omitted (< text.length), so budgeting with it keeps the total ≤ 65536 while the printed
|
|
69
|
+
* note reports the actual omitted count. Both boundaries run surrogate-safe slices (review #5). */
|
|
70
|
+
function buildDualEndPreview(text) {
|
|
71
|
+
const head = safeSliceUTF16(text, TOOL_RESULT_PREVIEW_HEAD)
|
|
72
|
+
const noteFn = (omitted) => `\n\n… [middle omitted: ${omitted} chars] …\n\n`
|
|
73
|
+
const tailLen = Math.min(TOOL_RESULT_PREVIEW_TAIL, TOOL_RESULT_PREVIEW - TOOL_RESULT_PREVIEW_HEAD - noteFn(text.length).length)
|
|
74
|
+
const tail = safeSliceUTF16End(text, tailLen)
|
|
75
|
+
return head + noteFn(Math.max(0, text.length - head.length - tail.length)) + tail
|
|
76
|
+
}
|
|
37
77
|
|
|
38
78
|
/** Offload-dir write-time self-cleanup retention window (2026-08-21): files older than 3 days are deleted on the next offload. */
|
|
39
79
|
export const TMP_RETENTION_MS = 3 * 24 * 3600 * 1000
|
|
@@ -77,7 +117,8 @@ export async function cleanupOldToolResults(dir) {
|
|
|
77
117
|
}
|
|
78
118
|
}
|
|
79
119
|
|
|
80
|
-
/** Offload oversized tool results (>64K chars) to disk, returning a preview + file path
|
|
120
|
+
/** Offload oversized tool results (>64K chars) to disk, returning a head+tail preview + file path
|
|
121
|
+
* (2026-09-04 §5 — dual-end preview; the failed-offload fallback uses the same dual-end slice).
|
|
81
122
|
* Writes trigger write-time self-cleanup of the offload dir first (dir param overridable for tests). */
|
|
82
123
|
export async function offloadToolResult(text, callId, dir = join(configDir, "tool-results")) {
|
|
83
124
|
if (text.length <= TOOL_RESULT_OFFLOAD_LIMIT) return text
|
|
@@ -87,12 +128,13 @@ export async function offloadToolResult(text, callId, dir = join(configDir, "too
|
|
|
87
128
|
const file = join(dir, `${Date.now()}-${String(callId).replace(/[^a-zA-Z0-9_-]/g, "_")}.log`)
|
|
88
129
|
await writeFile(file, text, "utf8")
|
|
89
130
|
return (
|
|
90
|
-
text
|
|
131
|
+
buildDualEndPreview(text) +
|
|
91
132
|
`\n\n[... output too large (${text.length} chars total), full content saved to: ${file}\n` +
|
|
92
133
|
`Page through it with the read tool (offset/limit) or sed -n 'START,ENDp' — do NOT re-run the tool blindly.]`
|
|
93
134
|
)
|
|
94
135
|
} catch {
|
|
95
|
-
|
|
136
|
+
// review #3: fallback uses the same dual-end slice (head + omitted note + tail, no path hint)
|
|
137
|
+
return buildDualEndPreview(text) + `\n\n[... truncated: ${text.length} chars total, offload to disk failed]`
|
|
96
138
|
}
|
|
97
139
|
}
|
|
98
140
|
|
package/src/agent/setup.mjs
CHANGED
|
@@ -22,10 +22,21 @@ import { fileURLToPath } from "node:url"
|
|
|
22
22
|
const DEFAULT_COMPACT_THRESHOLD = 100_000
|
|
23
23
|
const DOC_SEARCH_LIMIT = 5
|
|
24
24
|
const DOC_CHUNK_PREVIEW_LEN = 300
|
|
25
|
+
/** UTF-16 安全截断(2026-09-02 deepseek 400 根因):slice(0, N) 按码元切会把 emoji 代理对切成孤立
|
|
26
|
+
* 高代理(如 🔴=U+D83D+DD34 只剩 D83D)——deepseek 解析器严格 UTF-16 报 400
|
|
27
|
+
* "unexpected end of hex escape"。截断点落在高代理上时向前收一个码元。 */
|
|
28
|
+
function safeSliceUTF16(text, max) {
|
|
29
|
+
if (text.length <= max) return text
|
|
30
|
+
const end = max
|
|
31
|
+
// 截断点恰在高代理(D800-DBFF)上 → 收到高代理之前(不带它)
|
|
32
|
+
const cp = text.charCodeAt(end - 1)
|
|
33
|
+
if (cp >= 0xd800 && cp <= 0xdbff) return text.slice(0, end - 1)
|
|
34
|
+
return text.slice(0, end)
|
|
35
|
+
}
|
|
25
36
|
const MEMORY_SEARCH_LIMIT = 3
|
|
26
37
|
|
|
27
38
|
/** Build engineering-mode system prompt by reading METHODOLOGY.md and wrapping it in the engineering template */
|
|
28
|
-
async function buildEngineeringPrompt(cwd, role) {
|
|
39
|
+
export async function buildEngineeringPrompt(cwd, role) {
|
|
29
40
|
const engFile = role === "eng-coder" ? "engineering-sub.md" : "engineering.md"
|
|
30
41
|
const engTemplatePath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "prompts", engFile)
|
|
31
42
|
let engTemplate = ""
|
|
@@ -38,8 +49,17 @@ async function buildEngineeringPrompt(cwd, role) {
|
|
|
38
49
|
const methodologyPath = resolve(cwd, "METHODOLOGY.md")
|
|
39
50
|
if (!existsSync(methodologyPath)) {
|
|
40
51
|
// Template-only — engineering constraints stay active, minus project rules.
|
|
41
|
-
// The caller injects a warning into the history.
|
|
42
|
-
|
|
52
|
+
// The caller injects a warning into the history. Resolve the built-in
|
|
53
|
+
// methodology template to an absolute path (same-source join as the
|
|
54
|
+
// engineering template above — the packaged path is unreachable from the
|
|
55
|
+
// user's cwd) and carry its body so the warning can embed it verbatim
|
|
56
|
+
// (2026-09-02 D-M1/D-M2: template reachability for the model).
|
|
57
|
+
const methodologyTemplatePath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "prompts", "methodology-template.md")
|
|
58
|
+
let methodologyTemplateBody = null
|
|
59
|
+
try { methodologyTemplateBody = readFileSync(methodologyTemplatePath, "utf8") } catch {
|
|
60
|
+
// Template unreadable (packaging) — degraded: base warning only (no path/body injected), same as VS Code.
|
|
61
|
+
}
|
|
62
|
+
return { prompt: engTemplate || null, templateMissing, methodologyMissing: true, methodologyTemplatePath, methodologyTemplateBody }
|
|
43
63
|
}
|
|
44
64
|
const methodology = readFileSync(methodologyPath, "utf8")
|
|
45
65
|
const prompt = engTemplate
|
|
@@ -114,7 +134,7 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
114
134
|
role: "user",
|
|
115
135
|
content:
|
|
116
136
|
`[Relevant documentation${more}:\n` +
|
|
117
|
-
docs.map((d) => `- ${d.path}${d.heading ? " > " + d.heading : ""}: <untrusted_doc_chunk>${escapeXml(d.content
|
|
137
|
+
docs.map((d) => `- ${d.path}${d.heading ? " > " + d.heading : ""}: <untrusted_doc_chunk>${escapeXml(safeSliceUTF16(d.content, DOC_CHUNK_PREVIEW_LEN))}</untrusted_doc_chunk>`).join("\n") +
|
|
118
138
|
"]",
|
|
119
139
|
transient: true,
|
|
120
140
|
})
|
|
@@ -167,12 +187,13 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
167
187
|
|
|
168
188
|
// task/plan tools are injected with the main loop; subagent/skill/goal/verify only at top level
|
|
169
189
|
// eng-coder subagents get advisor for mandatory design review before coding
|
|
170
|
-
const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool, engTool } = await import("../agent-tools.mjs")
|
|
190
|
+
const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool, engTool, readHistoryTool } = await import("../agent-tools.mjs")
|
|
171
191
|
const { consultStartTool, consultCheckTool, consultStopTool } = await import("../agent-tools/consult.mjs")
|
|
172
|
-
const { escalateTool } = await import("../agent-tools/escalate.mjs")
|
|
173
192
|
const { CONSULT_BASE } = await import("../agent.mjs")
|
|
174
|
-
// withPool: decorate consult_start
|
|
175
|
-
// so the model knows which models it can pick (CLI parity with the plugin).
|
|
193
|
+
// withPool: decorate the consult_start description with the CURRENT candidate pool
|
|
194
|
+
// so the model knows which models it can pick (CLI parity with the plugin). The
|
|
195
|
+
// retired escalate tool surface is now the subagent action:"escalate" — its pool
|
|
196
|
+
// list is decorated onto the action property description below (same intent).
|
|
176
197
|
const withPool = (tool) => {
|
|
177
198
|
const models = agent.config?.agent?.consultModels ?? []
|
|
178
199
|
const list = models.map((m) => `${m.provider}:${m.model}${m.effort ? ` (${m.effort})` : ""}`).join(", ")
|
|
@@ -199,28 +220,82 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
199
220
|
properties: {
|
|
200
221
|
...subagentTool.parameters.properties,
|
|
201
222
|
role: { ...subagentTool.parameters.properties.role, ...subagentRoles },
|
|
223
|
+
// §19: escalate 动作的候选池 = consultModels(缺省池首 / 指定 provider:model)。
|
|
224
|
+
// 池装饰挂在 action 属性描述(原 escalate 工具注册时 withPool 同款意图——模型
|
|
225
|
+
// 需要知道可选候选人)。escalate 在工程模式禁用——装饰只对正常模式有意义。
|
|
226
|
+
action: (agent.config?.agent?.consultModels?.length && !agent.config?.agent?.engineering)
|
|
227
|
+
? {
|
|
228
|
+
...subagentTool.parameters.properties.action,
|
|
229
|
+
description: subagentTool.parameters.properties.action.description +
|
|
230
|
+
`\nCurrently configured escalate candidates (agent.consultModels pool): ${agent.config.agent.consultModels.map((m) => `${m.provider}:${m.model}${m.effort ? ` (${m.effort})` : ""}`).join(", ")}`,
|
|
231
|
+
}
|
|
232
|
+
: subagentTool.parameters.properties.action,
|
|
202
233
|
},
|
|
203
234
|
},
|
|
204
235
|
} : subagentTool
|
|
205
236
|
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
237
|
+
// §18 D-E3: eng-coder children (depth>0) get an audit-only subagent channel —
|
|
238
|
+
// role enum limited to explore, NO async parameter (sync only) and action pinned
|
|
239
|
+
// to spawn (§19 D-M3 restricted-variant action gate — escalate/check/status are
|
|
240
|
+
// refused here at the schema level too; the mechanical re-check lives in
|
|
241
|
+
// subagent.mjs execute → the §19 action gate + gateEngCoderSpawn (spawn-child.mjs)
|
|
242
|
+
// — schema enums are advisory, providers don't enforce them).
|
|
243
|
+
const engAuditSubagent = depth > 0 && agent._role === "eng-coder"
|
|
244
|
+
? (() => {
|
|
245
|
+
const props = { ...subagentTool.parameters.properties }
|
|
246
|
+
// §19 review hygiene: the audit channel is spawn-only sync explore — drop
|
|
247
|
+
// async, the check/status params (id/n) and the eng-coder token params
|
|
248
|
+
// (designToken/designId are meaningless for a read-only audit spawn; the
|
|
249
|
+
// parent spawn already carried the token). Schema noise would invite the
|
|
250
|
+
// model to pass irrelevant args.
|
|
251
|
+
delete props.async // sync only — the eng-coder blocks on the audit report
|
|
252
|
+
delete props.id
|
|
253
|
+
delete props.n
|
|
254
|
+
delete props.designToken
|
|
255
|
+
delete props.designId
|
|
256
|
+
props.role = {
|
|
257
|
+
type: "string",
|
|
258
|
+
enum: ["explore"],
|
|
259
|
+
description: "explore only — the eng-coder's internal spawn channel is reserved for read-only divergence audits (AGENT-LOOP.md §18 D-E3).",
|
|
260
|
+
}
|
|
261
|
+
props.action = {
|
|
262
|
+
type: "string",
|
|
263
|
+
enum: ["spawn"],
|
|
264
|
+
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).",
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
...subagentTool,
|
|
268
|
+
name: "subagent",
|
|
269
|
+
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).",
|
|
270
|
+
parameters: { ...subagentTool.parameters, properties: props },
|
|
271
|
+
}
|
|
272
|
+
})()
|
|
273
|
+
: null
|
|
274
|
+
|
|
275
|
+
// consult 工具仅在配置时注册(consultModels 空池时注册会让模型调用后吃一个错误回合)——
|
|
276
|
+
// §19: escalate 已并入常驻 subagent 的 action:"escalate"(无空池注册问题——动作在
|
|
277
|
+
// 池空时返回既有错误语义,工程模式 fail-closed 在 execute 内拒绝)。
|
|
210
278
|
const consultModels = agent.config?.agent?.consultModels ?? []
|
|
211
|
-
const engineering = agent.config?.agent?.engineering
|
|
212
279
|
const consultTools = consultModels.length
|
|
213
|
-
? [withPool(consultStartTool), consultCheckTool, consultStopTool
|
|
280
|
+
? [withPool(consultStartTool), consultCheckTool, consultStopTool]
|
|
214
281
|
: []
|
|
215
|
-
const depthOnly = depth === 0 ? [filteredSubagent, skillTool, goalTool, engTool, verifyTool, recentChangesTool, advisorTool, ...consultTools]
|
|
216
|
-
//
|
|
217
|
-
//
|
|
282
|
+
const depthOnly = depth === 0 ? [filteredSubagent, skillTool, goalTool, engTool, verifyTool, recentChangesTool, readHistoryTool, advisorTool, ...consultTools]
|
|
283
|
+
// SESSION.md §9 D-S2: read_history is depth-0 ONLY — a subagent querying "the session"
|
|
284
|
+
// would mix its throwaway context with the parent's record (semantic confusion).
|
|
285
|
+
// It is readonly:true, so planMode pass and no permission ask come automatically (T-S9).
|
|
286
|
+
// Write-permission coder sub-agents (subagent role="coder" + escalate action):
|
|
287
|
+
// the system prompt names verify (system.md) and advisor (discipline.md) — without them an
|
|
218
288
|
// escalate hit "unknown tool" and fell back to bash node --check / npm test to
|
|
219
289
|
// self-verify (2026-08-16 deepseek escalate diagnosis; plugin parity).
|
|
220
|
-
:
|
|
290
|
+
// eng-coder: advisor + verify + the §18 audit-only subagent channel (D-E3).
|
|
291
|
+
: agent._role === "eng-coder" ? [advisorTool, verifyTool, ...(engAuditSubagent ? [engAuditSubagent] : [])]
|
|
221
292
|
: agent._role === "coder" ? [verifyTool, advisorTool]
|
|
222
293
|
: agent._role === "consult" ? [recentChangesTool]
|
|
223
294
|
: []
|
|
295
|
+
// NOTE: every depth-0 tool schema is estimated into the compaction overhead per turn
|
|
296
|
+
// (context.mjs extras.tools) — a tool-schema change shifts the compaction fixture
|
|
297
|
+
// knife-edges (agent.test T3b: read_history's schema +~470 tokens once crossed its
|
|
298
|
+
// 11000 threshold; fixture adjusted to 12500 — rationale in the test comment).
|
|
224
299
|
const tools = [...agent.tools, taskTool, planTool, timerTool, ...depthOnly]
|
|
225
300
|
const toolSchemas = tools.map(toOpenAISchema)
|
|
226
301
|
const toolByName = new Map(tools.map((t) => [t.name, t]))
|
|
@@ -252,7 +327,15 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
252
327
|
warnings.push(`Engineering template (${agent._role === "eng-coder" ? "engineering-sub.md" : "engineering.md"}) not found — using degraded constraints.`)
|
|
253
328
|
}
|
|
254
329
|
if (engResult.methodologyMissing) {
|
|
255
|
-
|
|
330
|
+
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."
|
|
331
|
+
// 2026-09-02 D-M1/D-M2 (template accessibility): absolute path + full body — the model
|
|
332
|
+
// can read the template directly instead of hand-writing one from an unreachable source
|
|
333
|
+
// path. Body read failure → degraded warning above (path/body not injected). VS Code
|
|
334
|
+
// setup-reminders.mjs parity (两端警告文本一致,本端以 CLI 为准).
|
|
335
|
+
if (engResult.methodologyTemplateBody) {
|
|
336
|
+
warning += `\n\nbuilt-in template(可 read ${engResult.methodologyTemplatePath} 或直接参考以下内容):\n\n${engResult.methodologyTemplateBody}`
|
|
337
|
+
}
|
|
338
|
+
warnings.push(warning)
|
|
256
339
|
}
|
|
257
340
|
if (warnings.length > 0) {
|
|
258
341
|
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 上,多轮/并行子代理互不冲突。
|
|
@@ -51,7 +76,9 @@ export function makeRelay(parent, label, emit, model) {
|
|
|
51
76
|
// Single source for the event grammar branch lists (consult P3, 2026-08-30):
|
|
52
77
|
// stripEventToken (display) and stripEventTokensForCapture (capture) shared them
|
|
53
78
|
// literally — extending the event set meant touching both regexes.
|
|
54
|
-
|
|
79
|
+
// "done" = §15 D-A3 async-child completion event (emitted by the parent's
|
|
80
|
+
// turn-end collection, not by children — listed so the grammar stays honest).
|
|
81
|
+
const EVENT_PHASE = "turn|approval|done"
|
|
55
82
|
const EVENT_TYPE = "llm|tool|approval|done"
|
|
56
83
|
const WELL_FORMED_EVENT = new RegExp(`^${EVENT_SENTINEL}(${EVENT_PHASE})${RS}[^${RS}]*${RS}[^${RS}]*${RS}(${EVENT_TYPE})${RS}`)
|
|
57
84
|
export function stripEventToken(text) {
|
|
@@ -77,11 +77,26 @@ export const advisorTool = {
|
|
|
77
77
|
"For design review: single-pass review against methodology and requirements. " +
|
|
78
78
|
"Review criteria come from .thincoder/advisor.md (if present) or sensible defaults. " +
|
|
79
79
|
"After the review, you MUST produce a response table (see discipline rules for format). " +
|
|
80
|
-
"If advisor says all clear, call verify."
|
|
80
|
+
"If advisor says all clear, call verify. " +
|
|
81
|
+
"Optionally pass object={type,target,status,reason,exclude} to anchor the review target " +
|
|
82
|
+
"(AGENT-LOOP.md §18.8 — the review-object declaration is mechanically injected into the review message); " +
|
|
83
|
+
"absent → legacy behavior (no injection). " +
|
|
84
|
+
"Returns the review report — the advisor's findings verdict: all-clear (call verify) or a findings list to fix.",
|
|
81
85
|
parameters: {
|
|
82
86
|
type: "object",
|
|
83
87
|
properties: {
|
|
84
88
|
type: { type: "string", enum: ["code", "design"], description: "Review type: 'design' for design doc review, 'code' for code review (default)" },
|
|
89
|
+
object: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
type: { type: "string", description: "Review type as declared by the caller (design/code)" },
|
|
93
|
+
target: { type: "string", description: "Review target — document + section, or file(s)" },
|
|
94
|
+
status: { type: "string", description: "Object state: 待评审 / 已批准 / 已实现 (pending-review / approved / implemented)" },
|
|
95
|
+
reason: { type: "string", description: "Why this review runs: user-initiated / delivery verification" },
|
|
96
|
+
exclude: { type: "string", description: "Explicit exclusion list — approved/implemented items NOT in this review" },
|
|
97
|
+
},
|
|
98
|
+
description: "Review-object declaration (§18.8): mechanically injected at the start of the review user message so the advisor does not re-derive the review target. Absent → no injection (legacy behavior).",
|
|
99
|
+
},
|
|
85
100
|
paths: {
|
|
86
101
|
type: "array",
|
|
87
102
|
items: { type: "string" },
|
|
@@ -101,6 +116,13 @@ export const advisorTool = {
|
|
|
101
116
|
const agent = ctx.agent
|
|
102
117
|
const reviewType = args.type || "code"
|
|
103
118
|
const documents = args.documents || null
|
|
119
|
+
// Review-object declaration (§18.8 D-OA3): the PARENT constructs it and the
|
|
120
|
+
// advisor tool passes it through — mechanical anchoring, not model inference.
|
|
121
|
+
// Any non-object value (string/array/primitive, possibly from a malformed
|
|
122
|
+
// tool call) degrades to null = no injection (legacy calls unchanged).
|
|
123
|
+
const reviewObject = args.object && typeof args.object === "object" && !Array.isArray(args.object)
|
|
124
|
+
? args.object
|
|
125
|
+
: null
|
|
104
126
|
// Scope fallback: the runtime mutation record (zero git) covers guard-triggered
|
|
105
127
|
// reviews where the model did not pass explicit paths.
|
|
106
128
|
const paths = args.paths || (agent._touchedFiles?.length ? [...agent._touchedFiles] : null)
|
|
@@ -130,11 +152,16 @@ export const advisorTool = {
|
|
|
130
152
|
// Generate the design token BEFORE the review and inject it into the advisor's prompt.
|
|
131
153
|
// The advisor (LLM) decides pass/fail itself and echoes the token only on approval —
|
|
132
154
|
// the gate is a mechanical string match, not fragile semantics parsing.
|
|
155
|
+
// A random designId is minted for EVERY design-review call (2026-09-01 multi-design
|
|
156
|
+
// slots): on pass the token is stored in parent._engDesignTokens keyed by this id and
|
|
157
|
+
// the id is echoed to the parent; on failure the id is dropped — never stored, so it
|
|
158
|
+
// cannot clobber any other design's slot. Not a document anchor (rejected 2026-08-31).
|
|
133
159
|
const designToken = reviewType === "design" ? generateDesignToken(agent) : null
|
|
160
|
+
const designId = reviewType === "design" ? randomUUID() : null
|
|
134
161
|
const result = await runAdvisorReview(agent, reviewType, {
|
|
135
162
|
onOutput: ctx.onOutput,
|
|
136
163
|
signal: ctx.signal,
|
|
137
|
-
}, designToken, documents, paths)
|
|
164
|
+
}, designToken, documents, paths, reviewObject)
|
|
138
165
|
|
|
139
166
|
if (reviewType === "design") {
|
|
140
167
|
// Whitespace-tolerant match (LLM may add spaces or wrap in fences).
|
|
@@ -144,6 +171,11 @@ export const advisorTool = {
|
|
|
144
171
|
if (designToken && result && tokenPattern.test(result)) {
|
|
145
172
|
// Advisor echoed the token → review passed. Issue it to the parent for eng-coder.
|
|
146
173
|
// (session cleanup for design reviews is owned by runAdvisorReview)
|
|
174
|
+
// Multi-design slots (2026-09-01): store under this review's designId; the single
|
|
175
|
+
// `_engDesignToken` mirror stays for the legacy boolean gates (dispatch "has token",
|
|
176
|
+
// session persistence) — key decision ② of ENGINEERING-MODE.md §7 2026-09-01.
|
|
177
|
+
agent._engDesignTokens ??= new Map()
|
|
178
|
+
agent._engDesignTokens.set(designId, designToken)
|
|
147
179
|
agent._engDesignToken = designToken
|
|
148
180
|
// Unlock the dispatch design gate (dispatch.mjs) for eng-coder SELF-review:
|
|
149
181
|
// an eng-coder whose own design review passed may write files without the
|
|
@@ -156,16 +188,16 @@ export const advisorTool = {
|
|
|
156
188
|
if (agent._role === "eng-coder") agent._engDesignReviewed = true
|
|
157
189
|
// Strip the bracketed token so only ONE unambiguous format (plain UUID) reaches the main agent
|
|
158
190
|
const cleanResult = result.replace(makeDesignTokenRegex(designToken, "g"), "").trim()
|
|
159
|
-
|
|
191
|
+
// designId rides the Approved block (review #1): the parent needs it to aim the FIRST
|
|
192
|
+
// eng-coder spawn when several designs live in the same session.
|
|
193
|
+
return `${cleanResult}\n\nApproved. 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)`
|
|
160
194
|
}
|
|
161
|
-
// Review failed (or advisor chose not to pass) →
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
const isCompletedReview = result !== null && !result.startsWith("Advisor:")
|
|
168
|
-
if (isCompletedReview) agent._engDesignToken = null
|
|
195
|
+
// Review failed (or advisor chose not to pass) → do NOT touch ANY slot (方案 ②, review #2:
|
|
196
|
+
// a failed RE-review leaves the previously approved token alive until TTL; the failed call's
|
|
197
|
+
// own designId was never stored, so there is nothing to clear). Isolation (2026-08-30,
|
|
198
|
+
// extended to the multi-slot Map 2026-09-01): a network glitch must not clear / other
|
|
199
|
+
// designs' slots must not be affected — only a COMPLETED non-passing review lands here,
|
|
200
|
+
// and it revokes nothing.
|
|
169
201
|
// Strip every dead token occurrence from the raw output so the main agent can't grab an invalid one
|
|
170
202
|
if (result) {
|
|
171
203
|
const stripped = result.replace(makeDesignTokenRegex(designToken, "g"), "").trim()
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { createAgent, runAgent, readonlyToolNames } from "../agent.mjs"
|
|
15
15
|
import { resolveChildProvider } from "./subagent.mjs"
|
|
16
|
+
import { logEvent, errText } from "../log.mjs"
|
|
16
17
|
import { makeRelay, wrapChildCallbacks, runWithContinue, ensureChildApiKey, clampEffort } from "../agent/spawn-child.mjs"
|
|
17
18
|
|
|
18
19
|
// Named consult defaults (consult P2, 2026-08-30).
|
|
@@ -27,7 +28,7 @@ function consultLabel(m) {
|
|
|
27
28
|
/** Narrow the configured consultModels pool to a requested subset.
|
|
28
29
|
* Each selector is "provider:model", a bare provider name, or a bare model name
|
|
29
30
|
* (case-insensitive). A trailing " (effort)" suffix is tolerated (round2 复核
|
|
30
|
-
* 对齐 escalate
|
|
31
|
+
* 对齐 escalate 动作(subagent action:"escalate"):withPool 列表会带 " (high)" 后缀,模型照抄应可匹配).
|
|
31
32
|
* Returns { models, error } — error set when a selector matches
|
|
32
33
|
* nothing (surface the typo rather than silently dropping it). Absent/empty selectors
|
|
33
34
|
* → the full pool. */
|
|
@@ -145,6 +146,21 @@ async function runConsultChild(ctx, session, id, m, problem, ctrl) {
|
|
|
145
146
|
}
|
|
146
147
|
let watchdog = armWatchdog()
|
|
147
148
|
const label = consultLabel(m)
|
|
149
|
+
// LOGGING(LOGGING.md):child:*(consult)——logSettle 在函数作用域声明(外层 catch
|
|
150
|
+
// 覆盖 spawn 前失败路径);spawn 事件在 relay 建立后发射(logArmed 翻转——provider/
|
|
151
|
+
// 创建失败 = 从未启动,不落子事件、错误仅经 settleChild 进会话)。
|
|
152
|
+
let childLogId = null
|
|
153
|
+
let logT0 = 0
|
|
154
|
+
let logArmed = false
|
|
155
|
+
let logDone = false
|
|
156
|
+
const logSettle = (kind, payload) => {
|
|
157
|
+
if (!logArmed || logDone || !childLogId) return
|
|
158
|
+
logDone = true
|
|
159
|
+
const ms = Date.now() - logT0
|
|
160
|
+
const base = { role: "consult", id: childLogId, ms }
|
|
161
|
+
if (kind === "ok" || kind === "partial") logEvent("child:done", { ...base, kind })
|
|
162
|
+
else logEvent("child:error", { ...base, err: errText(payload, 200) })
|
|
163
|
+
}
|
|
148
164
|
try {
|
|
149
165
|
// Provider resolution: consultModels entries are { provider, model, effort? } — resolve
|
|
150
166
|
// via the subagent's provider resolver ("provider:model" handles cross-provider picks).
|
|
@@ -156,9 +172,10 @@ async function runConsultChild(ctx, session, id, m, problem, ctrl) {
|
|
|
156
172
|
}
|
|
157
173
|
// Clamp the pool's effort to the model's reasoningEffortEnum — an out-of-enum
|
|
158
174
|
// value makes provider/core throw on EVERY chat call (candidate dies on takeoff).
|
|
159
|
-
// Symmetric with escalate
|
|
160
|
-
// effort "high" (enum is xhigh/medium/low).
|
|
161
|
-
// (the provider preset default may ALSO be
|
|
175
|
+
// Symmetric with the escalate action (subagent action:"escalate"); 2026-08-16
|
|
176
|
+
// a real consult died on qwen3.8-max effort "high" (enum is xhigh/medium/low).
|
|
177
|
+
// Out-of-enum: DROP the effort entirely (the provider preset default may ALSO be
|
|
178
|
+
// out-of-enum for this override model).
|
|
162
179
|
clampEffort(provider, m.model, m.effort)
|
|
163
180
|
|
|
164
181
|
// Read-only consultant: filter the parent tool set down to readonly tools + main_history.
|
|
@@ -183,6 +200,12 @@ async function runConsultChild(ctx, session, id, m, problem, ctrl) {
|
|
|
183
200
|
// prefix (same channel subagent uses — parallel consultants stay independent) +
|
|
184
201
|
// onToolOutput passthrough so the consultant's tool output lands in its TUI block.
|
|
185
202
|
const relayPrefix = makeRelay(agent, "consult", ctx.callbacks?.onToken, provider.model ?? "")
|
|
203
|
+
// LOGGING:arm(spawn 事件——relay 建立后;子内事件归属 _logId)
|
|
204
|
+
childLogId = relayPrefix.slice(0, -1)
|
|
205
|
+
child._logId = childLogId
|
|
206
|
+
logT0 = Date.now()
|
|
207
|
+
logArmed = true
|
|
208
|
+
logEvent("child:spawn", { role: "consult", id: childLogId, kind: "consult" })
|
|
186
209
|
const childCallbacks = wrapChildCallbacks(relayPrefix, ctx.callbacks ?? {})
|
|
187
210
|
let declined = false // review #1: guard against double-settle when onDeclined fired
|
|
188
211
|
|
|
@@ -216,6 +239,7 @@ async function runConsultChild(ctx, session, id, m, problem, ctrl) {
|
|
|
216
239
|
onDeclined: (e) => {
|
|
217
240
|
declined = true
|
|
218
241
|
settleChild(session, id, label, false, `turn cap reached (${e.turn} turns) — stopped, diagnosis may be partial`)
|
|
242
|
+
logSettle("partial", null)
|
|
219
243
|
return undefined
|
|
220
244
|
},
|
|
221
245
|
},
|
|
@@ -224,18 +248,23 @@ async function runConsultChild(ctx, session, id, m, problem, ctrl) {
|
|
|
224
248
|
// settling again here would push a phantom empty success reply and decrement
|
|
225
249
|
// `pending` twice (negative pending → consult_check's two exits both
|
|
226
250
|
// unreachable → permanent block until user abort).
|
|
227
|
-
if (!declined)
|
|
251
|
+
if (!declined) {
|
|
252
|
+
settleChild(session, id, label, true, String(result ?? ""))
|
|
253
|
+
logSettle("ok", null)
|
|
254
|
+
}
|
|
228
255
|
} catch (e) {
|
|
229
256
|
// Runner errors (incl. the watchdog's abort) settle as a failed reply — the
|
|
230
257
|
// continue/declined paths are already handled inside runWithContinue.
|
|
231
258
|
const note = timedOut ? `consultation timed out after ${Math.round(timeoutMs / 60000)}min (agent.consultTimeoutMs)` : e?.message ?? String(e)
|
|
232
259
|
settleChild(session, id, label, false, note)
|
|
260
|
+
logSettle("error", note)
|
|
233
261
|
}
|
|
234
262
|
} catch (e) {
|
|
235
263
|
// Errors BEFORE the runner (provider resolution, createAgent) or a throwing
|
|
236
264
|
// continue-prompt settle as failed replies — the runner's own errors are already
|
|
237
265
|
// handled inside the loop above.
|
|
238
266
|
settleChild(session, id, label, false, e?.message ?? String(e))
|
|
267
|
+
logSettle("error", e?.message ?? String(e))
|
|
239
268
|
} finally {
|
|
240
269
|
clearTimeout(watchdog)
|
|
241
270
|
}
|
|
@@ -326,6 +355,7 @@ export const consultCheckTool = {
|
|
|
326
355
|
"replies are coming.\n" +
|
|
327
356
|
"Call it ALONE in a turn — do NOT batch it with calls that depend on its reply (readonly tools run in parallel).\n" +
|
|
328
357
|
"Replies arrive in arrival order: call it repeatedly (n = 1, 2, 3, …) until done is true.\n" +
|
|
358
|
+
"Returns JSON: {reply, model, failedReply, received, failed, terminated, total, done} for a reply — or {done: true, received, failed, total} when none are left.\n" +
|
|
329
359
|
"Parameters:\n" +
|
|
330
360
|
"- id (required): the consult id from consult_start\n" +
|
|
331
361
|
"- n (required): the 1-based read number for this consult — pass 1 on the first check, 2 on the next, and so on. It exists so consecutive checks are distinct tool calls (loop detectors) and the transcript reads as a sequence.",
|
|
@@ -382,7 +412,8 @@ export const consultStopTool = {
|
|
|
382
412
|
sideEffectExempt: true,
|
|
383
413
|
description:
|
|
384
414
|
"Terminate the still-running consultations of a session once a reply is good enough — saves tokens and time. " +
|
|
385
|
-
"Already-answered replies stay available for consult_check
|
|
415
|
+
"Already-answered replies stay available for consult_check. " +
|
|
416
|
+
"Returns JSON {stopped: <n>, abandoned: <pending count>} — or {error: \"unknown consult id\"}.\n" +
|
|
386
417
|
"Parameters:\n" +
|
|
387
418
|
"- id (required): the consult id from consult_start\n" +
|
|
388
419
|
"- n (required): incrementing call number for this consult (next value after the last consult_check/consult_stop) — keeps repeated calls distinct.",
|
package/src/agent-tools/eng.mjs
CHANGED
|
@@ -8,7 +8,8 @@ import { ENG_ON_REMINDER, ENG_OFF_REMINDER } from "../agent.mjs"
|
|
|
8
8
|
export const engTool = {
|
|
9
9
|
name: "eng",
|
|
10
10
|
description:
|
|
11
|
-
"Enter or exit engineering mode. In engineering mode, follow design-before-code: write a design document, run advisor design review, get user approval, then implement via eng-coder subagents."
|
|
11
|
+
"Enter or exit engineering mode. In engineering mode, follow design-before-code: write a design document, run advisor design review, get user approval, then implement via eng-coder subagents. " +
|
|
12
|
+
"Returns the mode state — 'Engineering mode activated/exited' (an already-active state is acknowledged).",
|
|
12
13
|
parameters: {
|
|
13
14
|
type: "object",
|
|
14
15
|
properties: {
|
|
@@ -22,6 +23,7 @@ export const engTool = {
|
|
|
22
23
|
if (args.action === "exit") {
|
|
23
24
|
ctx.agent.config.agent.engineering = false
|
|
24
25
|
ctx.agent._engDesignToken = null // stale token from prior design review invalidated
|
|
26
|
+
ctx.agent._engDesignTokens = new Map() // multi-design slots die with the mode (2026-09-01 fix #2)
|
|
25
27
|
ctx.agent._engDesignReviewed = false // reset gate state
|
|
26
28
|
ctx.agent._advisorRound = 0 // reset convergence budget
|
|
27
29
|
ctx.agent._touchedFiles = [] // clear mutation tracking
|
|
@@ -49,6 +51,7 @@ export const engTool = {
|
|
|
49
51
|
}
|
|
50
52
|
ctx.agent.config.agent.engineering = true
|
|
51
53
|
ctx.agent._engDesignToken = null // off→on transition requires a fresh design review
|
|
54
|
+
ctx.agent._engDesignTokens = new Map() // multi-design slots die with the mode (2026-09-01 fix #2)
|
|
52
55
|
ctx.agent._lastEngState = true
|
|
53
56
|
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
54
57
|
ctx.agent._pendingReminders.push(ENG_ON_REMINDER)
|
package/src/agent-tools/goal.mjs
CHANGED
|
@@ -11,7 +11,8 @@ export const goalTool = {
|
|
|
11
11
|
"action='set': create or replace the goal — must have a verifiable completion criterion (a machine-checkable proof, not vague effort). " +
|
|
12
12
|
"action='complete': mark achieved — only after the criterion's check has actually passed. " +
|
|
13
13
|
"action='blocked': report an impasse (requires 'reason') — only after 3 genuine attempts. " +
|
|
14
|
-
"action='cancel': abandon the goal."
|
|
14
|
+
"action='cancel': abandon the goal. " +
|
|
15
|
+
"Returns a status line — the goal set/updated/completed/blocked/cancelled confirmation, or Error: ... with the reason.",
|
|
15
16
|
parameters: {
|
|
16
17
|
type: "object",
|
|
17
18
|
properties: {
|
|
@@ -76,6 +77,15 @@ Has this goal been achieved? Answer ONLY "YES" or "NO" followed by a one-sentenc
|
|
|
76
77
|
}],
|
|
77
78
|
tools: [],
|
|
78
79
|
signal: AbortSignal.timeout(10_000),
|
|
80
|
+
// §18.6 D-TR4/D-TR6(2026-09-04 fix round1):goal 独立评审调用经 chat()
|
|
81
|
+
// 唯一采集点——补轨迹元数据 + traces 开关透传(agent.config.traces.enabled
|
|
82
|
+
// ——关=不落盘必须全覆盖——与 agent.mjs/context.mjs 同模式)
|
|
83
|
+
logCtx: {
|
|
84
|
+
stage: "goal", kind: "goal",
|
|
85
|
+
role: agent._role ?? null, depth: ctx.depth,
|
|
86
|
+
session: agent._sessionStart ?? null, cwd: agent.cwd,
|
|
87
|
+
traces: agent.config?.traces?.enabled !== false,
|
|
88
|
+
},
|
|
79
89
|
})
|
|
80
90
|
const verdict = (judgeRes.content ?? "").trim()
|
|
81
91
|
if (verdict.toUpperCase().startsWith("NO")) {
|