thincoder 0.12.58 → 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 +42 -1
- package/README.md +1 -1
- package/bin/thincoder.mjs +8 -0
- package/package.json +1 -1
- package/src/acp/bridge.mjs +132 -26
- package/src/advisor/messages.mjs +34 -1
- package/src/advisor/run.mjs +89 -51
- package/src/advisor.mjs +15 -7
- package/src/agent/dispatch.mjs +91 -14
- package/src/agent/helpers.mjs +35 -4
- package/src/agent/setup.mjs +90 -19
- package/src/agent/spawn-child.mjs +25 -0
- package/src/agent-tools/advisor.mjs +24 -2
- package/src/agent-tools/consult.mjs +37 -6
- package/src/agent-tools/eng.mjs +2 -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 +467 -193
- 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 +161 -125
- package/src/auto-think.mjs +14 -0
- package/src/cli/make-agent.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/config.mjs +5 -0
- package/src/context.mjs +87 -27
- package/src/distill.mjs +19 -1
- package/src/escape.mjs +6 -4
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +1 -1
- package/src/memory/core.mjs +126 -0
- package/src/memory/docs.mjs +196 -87
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +15 -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 +10 -5
- package/src/prompts/eng-coder.md +2 -2
- package/src/prompts/engineering-sub.md +23 -1
- package/src/prompts/engineering.md +106 -56
- package/src/prompts/explore.md +1 -2
- package/src/prompts/main.md +11 -6
- package/src/prompts/methodology-template.md +14 -0
- package/src/prompts/system.md +4 -2
- package/src/provider/core.mjs +56 -2
- package/src/tools/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +31 -43
- package/src/tools/edit-diff.mjs +265 -0
- package/src/tools/edit.md +10 -8
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +24 -20
- package/src/tools/file.mjs +18 -68
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- 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 +2 -0
- 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 +4 -12
- 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 +385 -22
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +29 -9
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +9 -0
- package/src/tui/cmd-think.mjs +1 -1
- package/src/tui/index.mjs +29 -95
- package/src/tui/interaction.mjs +13 -2
- package/src/tui/key-handler.mjs +105 -155
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +40 -0
- package/src/tui/pickers.mjs +11 -3
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +27 -10
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +165 -0
- package/src/tui/startup.mjs +36 -0
- package/src/tui/subagent-blocks.mjs +322 -144
- package/src/tui/subagent-panel.mjs +88 -13
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-events.mjs +132 -100
- 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/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
package/src/advisor.mjs
CHANGED
|
@@ -45,12 +45,13 @@ import { readFileSync } from "node:fs"
|
|
|
45
45
|
import { join, dirname } from "node:path"
|
|
46
46
|
import { fileURLToPath } from "node:url"
|
|
47
47
|
import { extractAgentResponseTable } from "./advisor/history.mjs"
|
|
48
|
-
import { buildAdvisorUserMessage, resolveScopeFiles } from "./advisor/messages.mjs"
|
|
48
|
+
import { buildAdvisorUserMessage, resolveScopeFiles, buildObjectDeclarationBlock } from "./advisor/messages.mjs"
|
|
49
49
|
import { buildConvergenceBody } from "./advisor/convergence.mjs"
|
|
50
50
|
import { escapeLiteralEscapes } from "./escape.mjs"
|
|
51
51
|
// Re-export for run.mjs and tests (keeps their imports from "../advisor.mjs" stable)
|
|
52
52
|
export { ADVISOR_MD_PATH, extractAgentResponseTable, extractConversationBackground } from "./advisor/history.mjs"
|
|
53
53
|
export { buildAdvisorUserMessage } from "./advisor/messages.mjs"
|
|
54
|
+
export { buildObjectDeclarationBlock } from "./advisor/messages.mjs"
|
|
54
55
|
|
|
55
56
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
56
57
|
|
|
@@ -135,13 +136,15 @@ export function buildAdvisorSystemPrompt(agent, prior, reviewType) {
|
|
|
135
136
|
* @param {Object} agent — the parent agent (history used for the response table)
|
|
136
137
|
* @param {Object|null} prior — prior issue table (extracted from history when null)
|
|
137
138
|
* @param {string[]|null} [scopeFiles] — review surface for the no-response fallback (cwd-relative)
|
|
139
|
+
* @param {Object|null} [object] — review-object declaration (§18.8): mechanically
|
|
140
|
+
* prepended to the round-2+ follow-up so every round stays anchored (T-OA2).
|
|
138
141
|
* @returns {string} the follow-up user message — or a plain "System reminder: …"
|
|
139
142
|
* fresh-review fallback (NO brackets — some OpenAI-compatible servers parse
|
|
140
143
|
* '['-prefixed content as structured data / expand escapes) when no prior
|
|
141
144
|
* review exists at all (caller misuse; the response-table extraction would
|
|
142
145
|
* otherwise scan history from index 0 and could match an unrelated stale table)
|
|
143
146
|
*/
|
|
144
|
-
export function buildAdvisorFollowUp(agent, prior, scopeFiles = null) {
|
|
147
|
+
export function buildAdvisorFollowUp(agent, prior, scopeFiles = null, object = null) {
|
|
145
148
|
// Convergence follow-up REQUIRES a prior review record — the full output of
|
|
146
149
|
// the last review, injected VERBATIM (decision 2026-08-08: the model
|
|
147
150
|
// understands the review output; no table/header/phrase parsing). The caller
|
|
@@ -164,7 +167,9 @@ export function buildAdvisorFollowUp(agent, prior, scopeFiles = null) {
|
|
|
164
167
|
: "(Agent did not provide a response table — perform a fresh full review; the review surface is unknown, ask the user for the file list)"
|
|
165
168
|
const response = extractAgentResponseTable(agent.history) || noResponseFallback
|
|
166
169
|
const round = (agent._advisorRound || 0) + 1
|
|
167
|
-
|
|
170
|
+
// Review-object declaration FIRST (T-OA2 — round 2+ stays anchored, no re-archaeology).
|
|
171
|
+
const declaration = buildObjectDeclarationBlock(object)
|
|
172
|
+
return (declaration ? declaration + "\n" : "") + buildConvergenceBody(p, response, round, scopeFiles)
|
|
168
173
|
}
|
|
169
174
|
|
|
170
175
|
/**
|
|
@@ -187,8 +192,11 @@ export { escapeLiteralEscapes }
|
|
|
187
192
|
* @param {string|null} [designToken] — design-review approval token (design only)
|
|
188
193
|
* @param {string[]|null} [documents] — design review only: explicit list of doc paths to review (passed through to buildAdvisorUserMessage)
|
|
189
194
|
* @param {string[]|null} [paths] — code review only: explicit list of file/dir paths to review
|
|
195
|
+
* @param {Object|null} [object] — review-object declaration (§18.8 D-OA1): passed through
|
|
196
|
+
* to the user-message builders; mechanically injected at the start of every review
|
|
197
|
+
* round (round 1 design/code + round 2+ follow-up). Absent → legacy behavior.
|
|
190
198
|
*/
|
|
191
|
-
export function prepareAdvisorMessages(agent, reviewType, designToken = null, documents = null, paths = null, priorParam = null) {
|
|
199
|
+
export function prepareAdvisorMessages(agent, reviewType, designToken = null, documents = null, paths = null, priorParam = null, object = null) {
|
|
192
200
|
// Deterministic convergence state (decision 2026-08-08): round 2+ requires
|
|
193
201
|
// _advisorRound > 0 AND a stored prior review output. No history parsing.
|
|
194
202
|
// priorParam (direct callers) wins over the stored output — same derivation
|
|
@@ -201,7 +209,7 @@ export function prepareAdvisorMessages(agent, reviewType, designToken = null, do
|
|
|
201
209
|
if (reviewType === "design" && (agent._advisorRound || 0) === 0) {
|
|
202
210
|
return [
|
|
203
211
|
{ role: "system", content: withTime(buildAdvisorSystemPrompt(agent, prior, reviewType)) },
|
|
204
|
-
{ role: "user", content: escapeLiteralEscapes(buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths)) },
|
|
212
|
+
{ role: "user", content: escapeLiteralEscapes(buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths, object)) },
|
|
205
213
|
]
|
|
206
214
|
}
|
|
207
215
|
|
|
@@ -226,7 +234,7 @@ export function prepareAdvisorMessages(agent, reviewType, designToken = null, do
|
|
|
226
234
|
agent._advisorRound = 0
|
|
227
235
|
}
|
|
228
236
|
// Mutations exist → KEEP the round (cap keeps advancing through retries).
|
|
229
|
-
const user = buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths)
|
|
237
|
+
const user = buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths, object)
|
|
230
238
|
return [
|
|
231
239
|
{ role: "system", content: withTime(buildAdvisorSystemPrompt(agent, prior, reviewType)) },
|
|
232
240
|
{
|
|
@@ -257,6 +265,6 @@ export function prepareAdvisorMessages(agent, reviewType, designToken = null, do
|
|
|
257
265
|
const scopeFiles = resolveScopeFiles(agent, paths)
|
|
258
266
|
return [
|
|
259
267
|
{ role: "system", content: withTime(buildAdvisorSystemPrompt(agent, prior, reviewType)) },
|
|
260
|
-
{ role: "user", content: escapeLiteralEscapes(buildAdvisorFollowUp(agent, prior, scopeFiles)) },
|
|
268
|
+
{ role: "user", content: escapeLiteralEscapes(buildAdvisorFollowUp(agent, prior, scopeFiles, object)) },
|
|
261
269
|
]
|
|
262
270
|
}
|
package/src/agent/dispatch.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* agent/dispatch.mjs — two-phase tool call execution
|
|
3
3
|
*/
|
|
4
|
+
import { logEvent, errText, headText } from "../log.mjs"
|
|
4
5
|
import { offloadToolResult, FILE_MUTATORS } from "./helpers.mjs"
|
|
5
6
|
import { runHooks } from "../hooks.mjs"
|
|
6
7
|
import { snapshotForUndo } from "../tui/cmd-undo.mjs"
|
|
@@ -37,6 +38,55 @@ function logToolError(toolName, args, error) {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* §19 action-level classification (AGENT-LOOP.md §19 D-M1): the merged subagent
|
|
43
|
+
* tool expresses spawn (side effect) and check/status (read-only queries) through
|
|
44
|
+
* its `action` parameter — the tool-level readonly flag can no longer express both.
|
|
45
|
+
* dispatch Phase-1/Phase-2 classifies per action: check/status behave as readonly
|
|
46
|
+
* (planMode pass / no permission ask / batchable), spawn keeps its non-readonly
|
|
47
|
+
* gates, escalate runs non-readonly AND serially (the retired escalate tool had no
|
|
48
|
+
* parallel flag — zero behavior change under the merged surface).
|
|
49
|
+
* §19.5 cancel (19.5.2b round2 #4): CONTROL-class exemption — cancel only
|
|
50
|
+
* stops, never starts. isSubagentControlAction feeds the SAME two gate sites as
|
|
51
|
+
* readonly (planMode pass / no permission ask — never joins a batch approval
|
|
52
|
+
* group / no handler → not denied — digest 内 cancel 放行).
|
|
53
|
+
* §19.6 panel (round1 #5): view 面归只读类(同 check/status——planMode 放行、免
|
|
54
|
+
* 审批、可批并行);freeze 面归控制类(同 cancel——planMode 放行、免权限审批、
|
|
55
|
+
* 批审批不入组、digest 内放行)。freeze 存在(非空 key)即控制类——否则只读类。
|
|
56
|
+
*/
|
|
57
|
+
function isSubagentReadonlyAction(toolName, args) {
|
|
58
|
+
// §6 memory 工具面重构(MEMORY.md §6 D-M5):memory search/list 是只读动作——与
|
|
59
|
+
// subagent check/status 同分类(planMode 放行/免审批——Phase-2 批并行只认工具级
|
|
60
|
+
// readonly/parallel,memory 无 parallel → 按非只读串行,见 MEMORY.md §6.4 实现注)。
|
|
61
|
+
// 动作级判定——不能按工具名(同一 memory 工具的 put/delete/clear 保持侧效门)。
|
|
62
|
+
if (toolName === "memory") {
|
|
63
|
+
const action = args?.action
|
|
64
|
+
return action === "search" || action === "list"
|
|
65
|
+
}
|
|
66
|
+
// SETTINGS-TOOL.md(2026-09-05):settings list/get 是只读动作(memory search/list 同分类——
|
|
67
|
+
// planMode 放行/免审批);set 保持侧效门。
|
|
68
|
+
if (toolName === "settings") {
|
|
69
|
+
const action = args?.action
|
|
70
|
+
return action === "list" || action === "get"
|
|
71
|
+
}
|
|
72
|
+
if (toolName !== "subagent" || !args || typeof args !== "object") return false
|
|
73
|
+
const action = args.action
|
|
74
|
+
if (action === "check" || action === "status") return true
|
|
75
|
+
// §19.6 panel view 面(freeze 缺省/空 = 视图请求——readonly;非空 freeze 归控制类)
|
|
76
|
+
if (action === "panel" && (args.freeze === undefined || args.freeze === null || String(args.freeze) === "")) return true
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
function isSubagentControlAction(toolName, args) {
|
|
80
|
+
if (toolName !== "subagent") return false
|
|
81
|
+
if (args?.action === "cancel") return true
|
|
82
|
+
// §19.6 panel freeze 面(D-P3 门控在 executor——只读/控制分类在此)
|
|
83
|
+
if (args?.action === "panel" && args.freeze !== undefined && args.freeze !== null && String(args.freeze) !== "") return true
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
function isSubagentEscalateAction(toolName, args) {
|
|
87
|
+
return toolName === "subagent" && args?.action === "escalate"
|
|
88
|
+
}
|
|
89
|
+
|
|
40
90
|
/**
|
|
41
91
|
* Two-phase execution:
|
|
42
92
|
* Phase 1 (serial): parse args one by one + planMode check + permission confirmation (side-effecting tools)
|
|
@@ -69,7 +119,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
69
119
|
continue
|
|
70
120
|
}
|
|
71
121
|
|
|
72
|
-
if (agent.planMode && !tool.readonly) {
|
|
122
|
+
if (agent.planMode && !tool.readonly && !isSubagentReadonlyAction(toolCall.name, args) && !isSubagentControlAction(toolCall.name, args)) {
|
|
73
123
|
prepared.push({ toolCall, tool, denied: true, reason: "plan mode" })
|
|
74
124
|
continue
|
|
75
125
|
}
|
|
@@ -113,7 +163,15 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
113
163
|
|
|
114
164
|
// Readonly tools (and autoApprove — the short-circuit, unchanged for the
|
|
115
165
|
// whole batch too) skip the permission stage entirely.
|
|
116
|
-
|
|
166
|
+
// §18 D-E3 task-domain authorization (spawn-time): an eng-coder child's
|
|
167
|
+
// tools skip the permission ASK stage exactly like autoApprove — granted by
|
|
168
|
+
// the parent spawn (approved design + task = authorization; subagent.mjs
|
|
169
|
+
// sets _engTaskAuthorized on the child). Everything EARLIER in Phase 1
|
|
170
|
+
// (JSON parse / unknown tool / planMode / design-token gates) ran unchanged
|
|
171
|
+
// — the exemption never widens what reaches this stage (round4 #3, T-E14).
|
|
172
|
+
// PreToolUse hooks still run below. Non-eng-coder children keep the manual
|
|
173
|
+
// parent ask (human in the loop).
|
|
174
|
+
if (tool.readonly || isSubagentReadonlyAction(toolCall.name, args) || isSubagentControlAction(toolCall.name, args) || agent.autoApprove || agent._engTaskAuthorized) {
|
|
117
175
|
if (!(await runHooks("PreToolUse", { agent, toolName: toolCall.name, toolArgs: args }))) {
|
|
118
176
|
prepared.push({ toolCall, tool, denied: true, reason: "blocked by PreToolUse hook" })
|
|
119
177
|
continue
|
|
@@ -203,6 +261,11 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
203
261
|
// 捕获分离(父恢复原始后子的拦截期间父捕获停止、子恢复后父继续)——正确。
|
|
204
262
|
// 声明在 try 之外:catch 块(异常路径)也要访问(报错前的探查输出回显)。
|
|
205
263
|
const capturedConsole = []
|
|
264
|
+
// LOGGING(LOGGING.md):tool:* 事件——仅真实执行(pre-gate 拦截项在下方早退分支不入事件)。
|
|
265
|
+
// 参数值永不落盘(NF-L3——工具事件不记 args);child=子代理 id(agent._logId,spawn 时 stamp)。
|
|
266
|
+
const toolT0 = Date.now()
|
|
267
|
+
const toolName = item.toolCall.name
|
|
268
|
+
logEvent("tool:call", { tool: toolName, child: agent?._logId })
|
|
206
269
|
try {
|
|
207
270
|
// Snapshot for undo before side-effect tools (setupOutputPanel already fired in Phase 1)
|
|
208
271
|
if (!item.tool?.readonly && item.args) {
|
|
@@ -214,6 +277,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
214
277
|
const routed = await callbacks.toolRouter(item.toolCall.name, item.args)
|
|
215
278
|
if (routed?.handled) {
|
|
216
279
|
callbacks.onToolResult?.(item.toolCall.name, routed.result, item.toolCall.id)
|
|
280
|
+
logEvent("tool:done", { tool: toolName, ms: Date.now() - toolT0, head: headText(routed.result, 200), child: agent?._logId })
|
|
217
281
|
return { ...item, result: routed.result, ok: true }
|
|
218
282
|
}
|
|
219
283
|
}
|
|
@@ -222,17 +286,22 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
222
286
|
console.log = (...a) => capturedConsole.push(a.map(String).join(" "))
|
|
223
287
|
console.error = (...a) => capturedConsole.push("[err] " + a.map(String).join(" "))
|
|
224
288
|
let rawResult
|
|
289
|
+
// ctx 对象提升为变量(§7.2.3):subagent 阻塞 execute 返回前在 ctx 上留
|
|
290
|
+
// _subagentKey(relayPrefix 去尾)——runOne 在 execute 返回后读它作 onToolResult
|
|
291
|
+
// 第 4 参(普通工具/错误路径无此字段——undefined 兼容既有签名)。每次工具调用
|
|
292
|
+
// 独立 ctx——并行同名工具(批并行 runOne)各自带自己的 key,互不串扰。
|
|
293
|
+
const toolCtx = {
|
|
294
|
+
cwd: agent.cwd,
|
|
295
|
+
agent,
|
|
296
|
+
depth,
|
|
297
|
+
signal,
|
|
298
|
+
callbacks,
|
|
299
|
+
onOutput: (chunk) => callbacks.onToolOutput?.(item.toolCall.name, chunk, item.toolCall.id),
|
|
300
|
+
onQuestion: callbacks.onQuestion,
|
|
301
|
+
onPermissionRequest: callbacks.onPermissionRequest,
|
|
302
|
+
}
|
|
225
303
|
try {
|
|
226
|
-
rawResult = await item.tool.execute(item.args,
|
|
227
|
-
cwd: agent.cwd,
|
|
228
|
-
agent,
|
|
229
|
-
depth,
|
|
230
|
-
signal,
|
|
231
|
-
callbacks,
|
|
232
|
-
onOutput: (chunk) => callbacks.onToolOutput?.(item.toolCall.name, chunk, item.toolCall.id),
|
|
233
|
-
onQuestion: callbacks.onQuestion,
|
|
234
|
-
onPermissionRequest: callbacks.onPermissionRequest,
|
|
235
|
-
})
|
|
304
|
+
rawResult = await item.tool.execute(item.args, toolCtx)
|
|
236
305
|
} finally {
|
|
237
306
|
console.log = origConsoleLog
|
|
238
307
|
console.error = origConsoleErr
|
|
@@ -247,9 +316,10 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
247
316
|
const resultWithConsole = capturedConsole.length > 0
|
|
248
317
|
? `${result}\n[console during ${item.toolCall.name}]\n${capturedConsole.join("\n")}`
|
|
249
318
|
: result
|
|
250
|
-
callbacks.onToolResult?.(item.toolCall.name, resultWithConsole, item.toolCall.id)
|
|
319
|
+
callbacks.onToolResult?.(item.toolCall.name, resultWithConsole, item.toolCall.id, toolCtx._subagentKey)
|
|
251
320
|
// PostToolUse hooks: fire-and-forget (result not awaited on hook failure)
|
|
252
321
|
runHooks("PostToolUse", { agent, toolName: item.toolCall.name, toolArgs: item.args, result: raw }).catch(() => {})
|
|
322
|
+
logEvent("tool:done", { tool: toolName, ms: Date.now() - toolT0, head: headText(resultWithConsole, 200), child: agent?._logId })
|
|
253
323
|
return { ...item, result: resultWithConsole, ok: true }
|
|
254
324
|
} catch (error) {
|
|
255
325
|
// Persist to ~/.thincoder/tool-errors/ for post-mortem; only pass message to the model (stack traces confuse LLMs and may leak paths)
|
|
@@ -259,6 +329,9 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
259
329
|
// asked to stop — worst case with subagents, where the child runs its
|
|
260
330
|
// whole turn budget and the interrupt appears to do nothing.
|
|
261
331
|
if (signal?.aborted) throw error
|
|
332
|
+
// LOGGING(2026-09-03 code review #4):中止先于事件——用户停不落 tool:error
|
|
333
|
+
//(vscode execute-tools parity;阻塞子代理 child:error 同款抑制)
|
|
334
|
+
logEvent("tool:error", { tool: toolName, ms: Date.now() - toolT0, err: errText(error, 200), child: agent?._logId })
|
|
262
335
|
runHooks("PostToolUseFailure", { agent, toolName: item.toolCall.name, toolArgs: item.args, error }).catch(() => {})
|
|
263
336
|
// Build contextual error: tool name + key args so the model can reason about what went wrong
|
|
264
337
|
const ctxParts = []
|
|
@@ -282,7 +355,11 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
282
355
|
batch = []
|
|
283
356
|
}
|
|
284
357
|
for (const item of prepared) {
|
|
285
|
-
|
|
358
|
+
// escalate action keeps the retired escalate tool's serial placement (no
|
|
359
|
+
// parallel flag): it flushes the batch and runs alone in call order (§19 —
|
|
360
|
+
// spawn stays parallel; check/status classify as readonly and batch freely).
|
|
361
|
+
if (item.tool && !item.tool.readonly
|
|
362
|
+
&& (!item.tool.parallel || isSubagentEscalateAction(item.tool.name, item.args))) {
|
|
286
363
|
await flush()
|
|
287
364
|
results.push(await runOne(item))
|
|
288
365
|
} else {
|
package/src/agent/helpers.mjs
CHANGED
|
@@ -33,7 +33,9 @@ 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)
|
|
37
39
|
|
|
38
40
|
/** UTF-16 安全截断(2026-09-02 deepseek 400 根因):slice(0, N) 按码元切会把 emoji 代理对切成孤立
|
|
39
41
|
* 高代理(如 🔴=U+D83D+DD34 只剩 D83D)——deepseek 解析器严格 UTF-16 报 400
|
|
@@ -46,6 +48,33 @@ function safeSliceUTF16(text, max) {
|
|
|
46
48
|
return text.slice(0, max)
|
|
47
49
|
}
|
|
48
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
|
+
}
|
|
77
|
+
|
|
49
78
|
/** Offload-dir write-time self-cleanup retention window (2026-08-21): files older than 3 days are deleted on the next offload. */
|
|
50
79
|
export const TMP_RETENTION_MS = 3 * 24 * 3600 * 1000
|
|
51
80
|
|
|
@@ -88,7 +117,8 @@ export async function cleanupOldToolResults(dir) {
|
|
|
88
117
|
}
|
|
89
118
|
}
|
|
90
119
|
|
|
91
|
-
/** 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).
|
|
92
122
|
* Writes trigger write-time self-cleanup of the offload dir first (dir param overridable for tests). */
|
|
93
123
|
export async function offloadToolResult(text, callId, dir = join(configDir, "tool-results")) {
|
|
94
124
|
if (text.length <= TOOL_RESULT_OFFLOAD_LIMIT) return text
|
|
@@ -98,12 +128,13 @@ export async function offloadToolResult(text, callId, dir = join(configDir, "too
|
|
|
98
128
|
const file = join(dir, `${Date.now()}-${String(callId).replace(/[^a-zA-Z0-9_-]/g, "_")}.log`)
|
|
99
129
|
await writeFile(file, text, "utf8")
|
|
100
130
|
return (
|
|
101
|
-
|
|
131
|
+
buildDualEndPreview(text) +
|
|
102
132
|
`\n\n[... output too large (${text.length} chars total), full content saved to: ${file}\n` +
|
|
103
133
|
`Page through it with the read tool (offset/limit) or sed -n 'START,ENDp' — do NOT re-run the tool blindly.]`
|
|
104
134
|
)
|
|
105
135
|
} catch {
|
|
106
|
-
|
|
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]`
|
|
107
138
|
}
|
|
108
139
|
}
|
|
109
140
|
|
package/src/agent/setup.mjs
CHANGED
|
@@ -36,7 +36,7 @@ function safeSliceUTF16(text, max) {
|
|
|
36
36
|
const MEMORY_SEARCH_LIMIT = 3
|
|
37
37
|
|
|
38
38
|
/** Build engineering-mode system prompt by reading METHODOLOGY.md and wrapping it in the engineering template */
|
|
39
|
-
async function buildEngineeringPrompt(cwd, role) {
|
|
39
|
+
export async function buildEngineeringPrompt(cwd, role) {
|
|
40
40
|
const engFile = role === "eng-coder" ? "engineering-sub.md" : "engineering.md"
|
|
41
41
|
const engTemplatePath = resolve(dirname(fileURLToPath(import.meta.url)), "..", "prompts", engFile)
|
|
42
42
|
let engTemplate = ""
|
|
@@ -49,8 +49,17 @@ async function buildEngineeringPrompt(cwd, role) {
|
|
|
49
49
|
const methodologyPath = resolve(cwd, "METHODOLOGY.md")
|
|
50
50
|
if (!existsSync(methodologyPath)) {
|
|
51
51
|
// Template-only — engineering constraints stay active, minus project rules.
|
|
52
|
-
// The caller injects a warning into the history.
|
|
53
|
-
|
|
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 }
|
|
54
63
|
}
|
|
55
64
|
const methodology = readFileSync(methodologyPath, "utf8")
|
|
56
65
|
const prompt = engTemplate
|
|
@@ -178,13 +187,13 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
178
187
|
|
|
179
188
|
// task/plan tools are injected with the main loop; subagent/skill/goal/verify only at top level
|
|
180
189
|
// eng-coder subagents get advisor for mandatory design review before coding
|
|
181
|
-
const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool, engTool } = await import("../agent-tools.mjs")
|
|
190
|
+
const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool, engTool, readHistoryTool } = await import("../agent-tools.mjs")
|
|
182
191
|
const { consultStartTool, consultCheckTool, consultStopTool } = await import("../agent-tools/consult.mjs")
|
|
183
|
-
const { subagentCheckTool } = await import("../agent-tools/subagent-check.mjs")
|
|
184
|
-
const { escalateTool } = await import("../agent-tools/escalate.mjs")
|
|
185
192
|
const { CONSULT_BASE } = await import("../agent.mjs")
|
|
186
|
-
// withPool: decorate consult_start
|
|
187
|
-
// so the model knows which models it can pick (CLI parity with the plugin).
|
|
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).
|
|
188
197
|
const withPool = (tool) => {
|
|
189
198
|
const models = agent.config?.agent?.consultModels ?? []
|
|
190
199
|
const list = models.map((m) => `${m.provider}:${m.model}${m.effort ? ` (${m.effort})` : ""}`).join(", ")
|
|
@@ -211,28 +220,82 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
211
220
|
properties: {
|
|
212
221
|
...subagentTool.parameters.properties,
|
|
213
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,
|
|
214
233
|
},
|
|
215
234
|
},
|
|
216
235
|
} : subagentTool
|
|
217
236
|
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
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 内拒绝)。
|
|
222
278
|
const consultModels = agent.config?.agent?.consultModels ?? []
|
|
223
|
-
const engineering = agent.config?.agent?.engineering
|
|
224
279
|
const consultTools = consultModels.length
|
|
225
|
-
? [withPool(consultStartTool), consultCheckTool, consultStopTool
|
|
280
|
+
? [withPool(consultStartTool), consultCheckTool, consultStopTool]
|
|
226
281
|
: []
|
|
227
|
-
const depthOnly = depth === 0 ? [filteredSubagent,
|
|
228
|
-
//
|
|
229
|
-
//
|
|
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
|
|
230
288
|
// escalate hit "unknown tool" and fell back to bash node --check / npm test to
|
|
231
289
|
// self-verify (2026-08-16 deepseek escalate diagnosis; plugin parity).
|
|
232
|
-
:
|
|
290
|
+
// eng-coder: advisor + verify + the §18 audit-only subagent channel (D-E3).
|
|
291
|
+
: agent._role === "eng-coder" ? [advisorTool, verifyTool, ...(engAuditSubagent ? [engAuditSubagent] : [])]
|
|
233
292
|
: agent._role === "coder" ? [verifyTool, advisorTool]
|
|
234
293
|
: agent._role === "consult" ? [recentChangesTool]
|
|
235
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).
|
|
236
299
|
const tools = [...agent.tools, taskTool, planTool, timerTool, ...depthOnly]
|
|
237
300
|
const toolSchemas = tools.map(toOpenAISchema)
|
|
238
301
|
const toolByName = new Map(tools.map((t) => [t.name, t]))
|
|
@@ -264,7 +327,15 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
264
327
|
warnings.push(`Engineering template (${agent._role === "eng-coder" ? "engineering-sub.md" : "engineering.md"}) not found — using degraded constraints.`)
|
|
265
328
|
}
|
|
266
329
|
if (engResult.methodologyMissing) {
|
|
267
|
-
|
|
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)
|
|
268
339
|
}
|
|
269
340
|
if (warnings.length > 0) {
|
|
270
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 上,多轮/并行子代理互不冲突。
|
|
@@ -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)
|
|
@@ -139,7 +161,7 @@ export const advisorTool = {
|
|
|
139
161
|
const result = await runAdvisorReview(agent, reviewType, {
|
|
140
162
|
onOutput: ctx.onOutput,
|
|
141
163
|
signal: ctx.signal,
|
|
142
|
-
}, designToken, documents, paths)
|
|
164
|
+
}, designToken, documents, paths, reviewObject)
|
|
143
165
|
|
|
144
166
|
if (reviewType === "design") {
|
|
145
167
|
// Whitespace-tolerant match (LLM may add spaces or wrap in fences).
|