thincoder 0.12.58 → 0.12.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -2
- package/README.md +3 -3
- package/bin/thincoder.mjs +88 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +135 -26
- package/src/advisor/messages.mjs +57 -4
- package/src/advisor/run.mjs +119 -79
- package/src/advisor.mjs +34 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +182 -22
- package/src/agent/helpers.mjs +71 -4
- package/src/agent/record-results.mjs +46 -10
- package/src/agent/run-stages.mjs +227 -0
- package/src/agent/setup-reminders.mjs +62 -0
- package/src/agent/setup.mjs +107 -20
- package/src/agent/spawn-child.mjs +54 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +133 -109
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +154 -104
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +26 -30
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +284 -0
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +152 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +470 -0
- package/src/agent-tools/subagent-async.mjs +382 -0
- package/src/agent-tools/subagent-panel.mjs +153 -0
- package/src/agent-tools/subagent-run.mjs +202 -0
- package/src/agent-tools/subagent-scheduler.mjs +343 -0
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +203 -377
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +198 -238
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +145 -242
- package/src/auto-think.mjs +14 -0
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +4 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +61 -8
- package/src/context.mjs +81 -163
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +30 -12
- package/src/escape.mjs +6 -4
- package/src/explore-distill.mjs +155 -0
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +11 -72
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +206 -87
- package/src/memory.mjs +3 -1
- package/src/model-specs.mjs +15 -1
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +25 -0
- package/src/prompts/advisor-design.md +18 -39
- package/src/prompts/advisor-round1.md +20 -32
- package/src/prompts/advisor-round2.md +16 -16
- package/src/prompts/advisor-round3.md +16 -16
- package/src/prompts/coder.md +7 -28
- package/src/prompts/consult-base.md +4 -11
- package/src/prompts/discipline.md +31 -44
- package/src/prompts/eng-coder.md +9 -34
- package/src/prompts/engineering-sub.md +10 -8
- package/src/prompts/engineering.md +61 -264
- package/src/prompts/explore.md +4 -14
- package/src/prompts/main.md +18 -35
- package/src/prompts/methodology-template.md +32 -38
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +18 -35
- package/src/provider/core.mjs +62 -69
- package/src/provider/errors.mjs +76 -0
- package/src/provider/retry.mjs +8 -45
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +181 -58
- package/src/session.mjs +48 -89
- package/src/token-ttl.mjs +273 -0
- package/src/tools/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +131 -44
- package/src/tools/edit-diff.mjs +348 -0
- package/src/tools/edit.md +20 -13
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +55 -24
- package/src/tools/file.mjs +25 -70
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.mjs +14 -6
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -3
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +2 -0
- package/src/tools/lsp.md +4 -1
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +84 -13
- package/src/tools/question.md +5 -1
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +18 -25
- package/src/tools/system.mjs +50 -30
- package/src/tools/tree.md +2 -1
- package/src/tools/wait_for.md +22 -0
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +179 -27
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +77 -16
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +17 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +11 -11
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +31 -96
- package/src/tui/interaction.mjs +13 -2
- package/src/tui/key-handler.mjs +105 -155
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +46 -0
- package/src/tui/pickers.mjs +51 -25
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +27 -10
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +182 -0
- package/src/tui/startup.mjs +40 -0
- package/src/tui/subagent-blocks.mjs +272 -262
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +125 -12
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +127 -231
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +76 -0
- package/src/tui/wizard.mjs +48 -12
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
package/src/advisor/messages.mjs
CHANGED
|
@@ -105,6 +105,49 @@ function injectProjectGuide(agent, parts, scopeFiles = []) {
|
|
|
105
105
|
return root // guide injected — requirement-fit criteria apply (truthy root)
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Build the mechanical review-object declaration block (AGENT-LOOP.md §18.8
|
|
110
|
+
* D-OA2 — English anchored form). Injected at the START of the review user
|
|
111
|
+
* message every round: round 1 (design + code), the legacy convergence path,
|
|
112
|
+
* and the round-2+ follow-up (see buildAdvisorFollowUp) — the reviewer must
|
|
113
|
+
* not re-derive "who is being reviewed / why" from the documents (T-OA2:
|
|
114
|
+
* every round stays anchored). Absent object → "" (legacy calls degrade to
|
|
115
|
+
* the current behavior — T-OA3).
|
|
116
|
+
* @param {Object|null} [object] — { type, target, status, reason, exclude }
|
|
117
|
+
* (strings; `exclude` may also be a list — joined with ", ")
|
|
118
|
+
* @returns {string} the declaration block (empty when no object)
|
|
119
|
+
*/
|
|
120
|
+
export function buildObjectDeclarationBlock(object = null) {
|
|
121
|
+
if (!object || typeof object !== "object" || Array.isArray(object)) return ""
|
|
122
|
+
const field = (v) => (Array.isArray(v) ? v.join(", ") : v == null ? "" : String(v))
|
|
123
|
+
return [
|
|
124
|
+
"## Review-object declaration (mechanical — do not infer)",
|
|
125
|
+
`Review type: ${field(object.type)} | Target: ${field(object.target)} | Object state: ${field(object.status)} | Trigger: ${field(object.reason)}`,
|
|
126
|
+
`Excluded (not in this review): ${field(object.exclude)}`,
|
|
127
|
+
"Follow this declaration — do not infer the review target from the documents.",
|
|
128
|
+
"",
|
|
129
|
+
].join("\n")
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Approval-signal block for design reviews (round 1 and round 2+ — §24 D-24b:
|
|
134
|
+
* an async fix-round continuation must be able to re-approve, so the token is
|
|
135
|
+
* injected into EVERY design round; the reviewer echoes it only on a clean pass).
|
|
136
|
+
* §29.1 F2a (2026-09-07): BOTH values are injected — the token AND the designId
|
|
137
|
+
* (anchor sentence verbatim — Copy BOTH values). designId null (legacy direct
|
|
138
|
+
* callers without a resolved instance) degrades to the token-only form.
|
|
139
|
+
*/
|
|
140
|
+
export function buildDesignApprovalBlock(designToken, designId) {
|
|
141
|
+
const echo = designId
|
|
142
|
+
? `If — and ONLY if — your review finds NO 🔴 (Critical) issues, end your reply with this exact token: [DESIGN-TOKEN:${designToken}] and this exact designId: ${designId}. Copy BOTH values verbatim.`
|
|
143
|
+
: `If — and ONLY if — your review finds NO 🔴 (Critical) issues, end your reply with this exact token: [DESIGN-TOKEN:${designToken}]`
|
|
144
|
+
return [
|
|
145
|
+
"## Approval Signal",
|
|
146
|
+
echo,
|
|
147
|
+
"🟡 (Advisory) and 🔵 (Note) findings do NOT block approval — list them if present, but still include the token. If there are any 🔴 issues, do NOT include the token.",
|
|
148
|
+
].join("\n")
|
|
149
|
+
}
|
|
150
|
+
|
|
108
151
|
/**
|
|
109
152
|
* Build the user message for an advisor review session.
|
|
110
153
|
* @param {Object} agent — the parent agent
|
|
@@ -115,15 +158,27 @@ function injectProjectGuide(agent, parts, scopeFiles = []) {
|
|
|
115
158
|
* When set, the review input is built from this list ONLY — no git-diff change-set collection.
|
|
116
159
|
* When absent, the legacy git-diff-based scope is kept (backward compatible).
|
|
117
160
|
* @param {string[]|null} [paths] — code review only: explicit list of file/dir paths to review (deduped; shown under Review Scope)
|
|
161
|
+
* @param {Object|null} [object] — review-object declaration (§18.8 D-OA1/D-OA3):
|
|
162
|
+
* { type, target, status, reason, exclude } — mechanically injected at the
|
|
163
|
+
* start of the user message; absent → no injection (legacy calls unchanged).
|
|
164
|
+
* @param {string|null} [designId] — §29.1 F2a: injected next to the token in the
|
|
165
|
+
* Approval Signal (both values — the reviewer copies both verbatim); null →
|
|
166
|
+
* token-only degradation (legacy direct callers).
|
|
118
167
|
* @returns {string} the user message
|
|
119
168
|
*/
|
|
120
|
-
export function buildAdvisorUserMessage(agent, prior, reviewType, designToken = null, documents = null, paths = null) {
|
|
169
|
+
export function buildAdvisorUserMessage(agent, prior, reviewType, designToken = null, documents = null, paths = null, object = null, designId = null) {
|
|
121
170
|
// prior = the full prior review output (string) when a convergence round is
|
|
122
171
|
// being built (decision 2026-08-08 — verbatim injection, model understands it).
|
|
123
172
|
// Deterministic: only _advisorRound > 0 with stored output counts.
|
|
124
173
|
const p = prior ?? ((agent._advisorRound || 0) > 0 ? agent._lastAdvisorOutput : null)
|
|
125
174
|
|
|
126
175
|
const parts = []
|
|
176
|
+
// Review-object declaration FIRST — D-OA1: at the start of the user message
|
|
177
|
+
// (after the system prompt, before the review content). Covers round 1
|
|
178
|
+
// design/code and the legacy convergence path; the round-2+ normal path
|
|
179
|
+
// prepends it in buildAdvisorFollowUp (T-OA2 — every round stays anchored).
|
|
180
|
+
const declaration = buildObjectDeclarationBlock(object)
|
|
181
|
+
if (declaration) parts.push(declaration)
|
|
127
182
|
const docList = Array.isArray(documents) ? documents.filter((d) => typeof d === "string" && d.trim()) : []
|
|
128
183
|
const pathList = Array.isArray(paths) ? [...new Set(paths.filter((p) => typeof p === "string" && p.trim()))] : []
|
|
129
184
|
|
|
@@ -213,9 +268,7 @@ export function buildAdvisorUserMessage(agent, prior, reviewType, designToken =
|
|
|
213
268
|
parts.push("5. If you find issues, produce your review table with the format: | # | Category | Severity | Issue | Suggestion |. If the design passes, no table is needed.")
|
|
214
269
|
if (designToken) {
|
|
215
270
|
parts.push("")
|
|
216
|
-
parts.push(
|
|
217
|
-
parts.push(`If — and ONLY if — your review finds NO 🔴 (Critical) issues, end your reply with this exact token: [DESIGN-TOKEN:${designToken}]`)
|
|
218
|
-
parts.push("🟡 (Advisory) and 🔵 (Note) findings do NOT block approval — list them if present, but still include the token. If there are any 🔴 issues, do NOT include the token.")
|
|
271
|
+
parts.push(buildDesignApprovalBlock(designToken, designId))
|
|
219
272
|
}
|
|
220
273
|
return parts.join("\n")
|
|
221
274
|
}
|
package/src/advisor/run.mjs
CHANGED
|
@@ -10,11 +10,10 @@ import { appendCitationReport } from "./citations.mjs"
|
|
|
10
10
|
import { describeToolArgs } from "../tui/tool-args.mjs"
|
|
11
11
|
|
|
12
12
|
const MAX_ADVISOR_TURNS = 100
|
|
13
|
-
// Mechanical convergence cap:
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// _advisorRound in agent.mjs; the cap no longer exempts design).
|
|
13
|
+
// Mechanical convergence cap: up to 5 rounds suffice; a 6th call means the model
|
|
14
|
+
// is looping — refuse it instead of burning tokens. §24 D-24b (2026-09-06): PER
|
|
15
|
+
// REVIEW INSTANCE (agent._advisorRuns); CODE REVIEWS ONLY (2026-09-07 §8 ruling)
|
|
16
|
+
// — design reviews are EXEMPT: their rounds keep advancing, the cap never refuses.
|
|
18
17
|
export const MAX_ADVISOR_ROUNDS = 5
|
|
19
18
|
|
|
20
19
|
// NOTE: prompts/advisor-round{1,2,3}.md encourage the model to finish within
|
|
@@ -120,6 +119,9 @@ function renderTimeline(timeline, tail = "") {
|
|
|
120
119
|
}
|
|
121
120
|
// Test seam (mirrors _advisorToolsFor).
|
|
122
121
|
export { renderTimeline as _renderTimeline }
|
|
122
|
+
// Test seam (T-TS8/9): the tool loop itself — toolsOverride injects a mock tool
|
|
123
|
+
// set with controllable timing/errors (the real set comes from advisorToolsFor).
|
|
124
|
+
export { runAdvisorToolLoop as _runAdvisorToolLoop }
|
|
123
125
|
|
|
124
126
|
/**
|
|
125
127
|
* Run the advisor's tool loop: chat → execute tools → repeat.
|
|
@@ -129,7 +131,7 @@ export { renderTimeline as _renderTimeline }
|
|
|
129
131
|
* the panel keeps moving while the advisor explores — otherwise the panel sits
|
|
130
132
|
* frozen through every tool-call phase and the review appears to have stalled.
|
|
131
133
|
*/
|
|
132
|
-
async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, cwd) {
|
|
134
|
+
async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, cwd, toolsOverride = null) {
|
|
133
135
|
// Kind-tagged wrappers: the TUI panel colors reasoning / answer / tool progress differently.
|
|
134
136
|
// Every chunk is ALSO recorded into an ordered timeline — the persisted record
|
|
135
137
|
// must show the review process (thinking ↔ tool progress ↔ final text) at its
|
|
@@ -145,7 +147,9 @@ async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, c
|
|
|
145
147
|
const onThink = emit("think")
|
|
146
148
|
const onText = emit("text")
|
|
147
149
|
const onTool = emit("tool")
|
|
148
|
-
|
|
150
|
+
// toolsOverride = test seam (T-TS8/9): the real advisor tool set, or a mock
|
|
151
|
+
// set with controllable timing/errors.
|
|
152
|
+
const { schemas: toolSchemas, byName: toolByName } = toolsOverride ?? advisorToolsFor(agent)
|
|
149
153
|
let turns = 0
|
|
150
154
|
const startTime = Date.now()
|
|
151
155
|
|
|
@@ -198,13 +202,25 @@ async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, c
|
|
|
198
202
|
signal: signal ?? null,
|
|
199
203
|
onToken: onText,
|
|
200
204
|
onReasoning: onThink,
|
|
205
|
+
// LOGGING(vscode advisor/run.mjs parity——按 stage 可 grep)
|
|
206
|
+
// §18.6 D-TR4:轨迹元数据增补——kind=advisor(评审独立于子代理——T-TR2);role
|
|
207
|
+
// 透出调用方角色(eng-coder 内嵌评审时为 "eng-coder");session/cwd 供轨迹对回;
|
|
208
|
+
// traces 开关沿 agent.config(D-TR6)。
|
|
209
|
+
logCtx: {
|
|
210
|
+
stage: "advisor",
|
|
211
|
+
role: agent?._role ?? null,
|
|
212
|
+
kind: "advisor",
|
|
213
|
+
session: agent?._sessionStart ?? null,
|
|
214
|
+
cwd,
|
|
215
|
+
traces: agent?.config?.traces?.enabled !== false,
|
|
216
|
+
},
|
|
201
217
|
})
|
|
202
218
|
|
|
203
219
|
// No tool calls — this is the final review text. The final answer was
|
|
204
220
|
// already streamed into the timeline via onText; fall back to
|
|
205
221
|
// response.content only if nothing was recorded.
|
|
206
222
|
if (!response.toolCalls?.length) {
|
|
207
|
-
if (!response.content?.trim()) return renderTimeline(timeline) || "Advisor:
|
|
223
|
+
if (!response.content?.trim()) return renderTimeline(timeline) || "Advisor: empty response — review was inconclusive"
|
|
208
224
|
return renderTimeline(timeline) || response.content.trim()
|
|
209
225
|
}
|
|
210
226
|
|
|
@@ -226,8 +242,19 @@ async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, c
|
|
|
226
242
|
: {}),
|
|
227
243
|
})
|
|
228
244
|
|
|
229
|
-
//
|
|
230
|
-
|
|
245
|
+
// B1 (AGENT-LOOP.md §18.7 D-TS7): the SAME LLM reply's multiple read-only
|
|
246
|
+
// tool calls run in PARALLEL (Promise.all) — results are backfilled in
|
|
247
|
+
// toolCalls order (Promise.all preserves the input order → tool_call_id
|
|
248
|
+
// never mismatches); each tool's timeout/error is captured independently
|
|
249
|
+
// (the existing TOOL_TIMEOUT stays — one failing tool does not block the
|
|
250
|
+
// others); progress lines are emitted in toolCalls order. The read-only
|
|
251
|
+
// tool set has no side effects — no sequencing/serialization needed.
|
|
252
|
+
// Scope note (round1 review #10): B1 is ONLY in-loop tool parallelism — it
|
|
253
|
+
// does NOT solve the TODO "platform execution: advisor parallel calls are
|
|
254
|
+
// actually serial" mystery (docs/TODO.md — LOGGING evidence item), which
|
|
255
|
+
// concerns multiple advisor CALLS observed as serial, not one reply's
|
|
256
|
+
// tool calls.
|
|
257
|
+
const parsed = response.toolCalls.map((tc) => {
|
|
231
258
|
const tool = toolByName.get(tc.name)
|
|
232
259
|
let args = {}
|
|
233
260
|
let parseError = null
|
|
@@ -236,63 +263,69 @@ async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, c
|
|
|
236
263
|
} catch (e) {
|
|
237
264
|
parseError = `Error: invalid JSON in tool arguments: ${e.message}\nRaw arguments: ${(tc.arguments || "").slice(0, 200)}`
|
|
238
265
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
266
|
+
return { tc, tool, args, parseError }
|
|
267
|
+
})
|
|
268
|
+
// Progress lines first, in toolCalls order (emitted before the parallel
|
|
269
|
+
// run — display order is independent of completion order).
|
|
270
|
+
for (const p of parsed) {
|
|
271
|
+
if (p.parseError) continue // parse-error tools get no progress line (legacy behavior)
|
|
272
|
+
const argsLine = describeToolArgs(p.tc.name, p.args)
|
|
273
|
+
onTool(`\n→ ${p.tc.name}${argsLine ? " " + argsLine : ""}\n`)
|
|
274
|
+
}
|
|
275
|
+
// Every tool runs CONCURRENTLY; each result/error lands in its own slot —
|
|
276
|
+
// Promise.all preserves input order, so index i always matches parsed[i].
|
|
277
|
+
const executed = await Promise.all(parsed.map(async (p) => {
|
|
278
|
+
// Parse failure → error to model immediately (no execution)
|
|
279
|
+
if (p.parseError) return p.parseError
|
|
280
|
+
if (!p.tool) return `Error: unknown tool "${p.tc.name}". Available: ${[...toolByName.keys()].join(", ")}`
|
|
281
|
+
// Execute with timeout (clear the timer when the tool wins the race —
|
|
282
|
+
// otherwise up to MAX_ADVISOR_TURNS dangling timers accumulate)
|
|
283
|
+
try {
|
|
284
|
+
let timeoutId
|
|
285
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
286
|
+
timeoutId = setTimeout(() => reject(new Error(`tool timeout after ${TOOL_TIMEOUT_MS}ms`)), TOOL_TIMEOUT_MS)
|
|
287
|
+
})
|
|
288
|
+
let toolPromise
|
|
254
289
|
try {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
clearTimeout(timeoutId)
|
|
265
|
-
// Timeout won → toolPromise is still pending; a later rejection
|
|
266
|
-
// would surface as an unhandled rejection. The race already
|
|
267
|
-
// consumed the result/error in the normal path, so this no-op
|
|
268
|
-
// catch only fires for the abandoned-tool case.
|
|
269
|
-
toolPromise?.catch(() => {})
|
|
270
|
-
}
|
|
271
|
-
} catch (e) {
|
|
272
|
-
const errorType = e.message.includes("timeout") ? "timeout"
|
|
273
|
-
: e.message.includes("ENOENT") ? "file_not_found"
|
|
274
|
-
: e.message.includes("permission") ? "permission_denied"
|
|
275
|
-
: "execution_error"
|
|
276
|
-
result = `Error (${errorType}): ${e.message}`
|
|
290
|
+
toolPromise = p.tool.execute(p.args, { cwd, agent, onOutput, signal })
|
|
291
|
+
return await Promise.race([toolPromise, timeoutPromise])
|
|
292
|
+
} finally {
|
|
293
|
+
clearTimeout(timeoutId)
|
|
294
|
+
// Timeout won → toolPromise is still pending; a later rejection
|
|
295
|
+
// would surface as an unhandled rejection. The race already
|
|
296
|
+
// consumed the result/error in the normal path, so this no-op
|
|
297
|
+
// catch only fires for the abandoned-tool case.
|
|
298
|
+
toolPromise?.catch(() => {})
|
|
277
299
|
}
|
|
300
|
+
} catch (e) {
|
|
301
|
+
const errorType = e.message.includes("timeout") ? "timeout"
|
|
302
|
+
: e.message.includes("ENOENT") ? "file_not_found"
|
|
303
|
+
: e.message.includes("permission") ? "permission_denied"
|
|
304
|
+
: "execution_error"
|
|
305
|
+
return `Error (${errorType}): ${e.message}`
|
|
278
306
|
}
|
|
307
|
+
}))
|
|
308
|
+
|
|
309
|
+
// Backfill in toolCalls order (executed[i] ↔ parsed[i]); per-result
|
|
310
|
+
// non-string serialization + line-aware truncation stay per-tool.
|
|
311
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
312
|
+
let result = executed[i]
|
|
279
313
|
if (typeof result !== "string") result = JSON.stringify(result)
|
|
280
|
-
|
|
281
|
-
// Line-aware truncation: preserve line integrity
|
|
314
|
+
|
|
282
315
|
if (result.length > MAX_RESULT_CHARS) {
|
|
283
316
|
const lines = result.split("\n")
|
|
284
317
|
let truncated = ""
|
|
285
318
|
let charCount = 0
|
|
286
319
|
let keptLines = 0
|
|
287
|
-
|
|
288
|
-
for (let
|
|
289
|
-
const line = lines[
|
|
320
|
+
|
|
321
|
+
for (let j = 0; j < lines.length; j++) {
|
|
322
|
+
const line = lines[j]
|
|
290
323
|
if (charCount + line.length + 1 > MAX_RESULT_CHARS) break
|
|
291
324
|
truncated += line + "\n"
|
|
292
325
|
charCount += line.length + 1
|
|
293
326
|
keptLines++
|
|
294
327
|
}
|
|
295
|
-
|
|
328
|
+
|
|
296
329
|
const remainingLines = lines.length - keptLines
|
|
297
330
|
result = (
|
|
298
331
|
truncated +
|
|
@@ -300,8 +333,8 @@ async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, c
|
|
|
300
333
|
`To see more content, use: read(path, offset=${keptLines + 1}, limit=200)`
|
|
301
334
|
)
|
|
302
335
|
}
|
|
303
|
-
|
|
304
|
-
messages.push({ role: "tool", tool_call_id: tc.id, content: result })
|
|
336
|
+
|
|
337
|
+
messages.push({ role: "tool", tool_call_id: parsed[i].tc.id, content: result })
|
|
305
338
|
}
|
|
306
339
|
}
|
|
307
340
|
}
|
|
@@ -354,13 +387,35 @@ function extractUnfixedIssues(priorText) {
|
|
|
354
387
|
.filter(Boolean)
|
|
355
388
|
.slice(0, MAX_UNFIXED_DISPLAY)
|
|
356
389
|
}
|
|
390
|
+
/** Review-looking guard (async settle parity): a markdown table row or ≥200 chars of prose counts as a prior. */
|
|
391
|
+
export function looksLikeReviewOutput(text) {
|
|
392
|
+
const trimmed = String(text ?? "").trim()
|
|
393
|
+
return /\|.*\|.*\|/.test(trimmed) || trimmed.length >= 200
|
|
394
|
+
}
|
|
395
|
+
/** Cap message (shared by runAdvisorReview and the async pre-check — per-review refusal). */
|
|
396
|
+
export function buildCapMessage(agent) {
|
|
397
|
+
const prior = agent._lastAdvisorOutput
|
|
398
|
+
const unfixed = prior ? extractUnfixedIssues(prior) : []
|
|
399
|
+
let message = `Advisor: convergence cap reached after ${MAX_ADVISOR_ROUNDS} rounds.\n`
|
|
400
|
+
if (unfixed.length > 0) {
|
|
401
|
+
message += `\nUnresolved issues from prior rounds:\n${unfixed.map((i) => `- ${i}`).join("\n")}\n`
|
|
402
|
+
} else {
|
|
403
|
+
message += "\nAll prior issues appear resolved.\n"
|
|
404
|
+
}
|
|
405
|
+
message += "\nOptions:\n1. Accept current state and proceed\n2. Manually review specific concerns with read/grep\n3. Start a new session (/new) to reset the advisor"
|
|
406
|
+
return message
|
|
407
|
+
}
|
|
357
408
|
|
|
358
409
|
/**
|
|
359
410
|
* Run an advisor review. reviewType: "code" (default) or "design". Returns review text or null when skipped.
|
|
360
411
|
* @param {string|null} [designToken] — injected into the design-review prompt; the advisor echoes it only on approval.
|
|
361
412
|
* @param {string[]|null} [documents] — design review only: explicit list of doc paths to review; passed through to the message builder.
|
|
413
|
+
* @param {string[]|null} [paths] — code review only: explicit list of file/dir paths to review.
|
|
414
|
+
* @param {Object|null} [object] — review-object declaration (§18.8 D-OA1/D-OA3): { type, target, status, reason, exclude }; absent → legacy behavior (no injection).
|
|
415
|
+
* @param {string|null} [designId] — §29.1 F2a: injected next to the design token in
|
|
416
|
+
* the Approval Signal (passed through to the message builders).
|
|
362
417
|
*/
|
|
363
|
-
export async function runAdvisorReview(agent, reviewType, callbacks, designToken = null, documents = null, paths = null) {
|
|
418
|
+
export async function runAdvisorReview(agent, reviewType, callbacks, designToken = null, documents = null, paths = null, object = null, designId = null) {
|
|
364
419
|
const onOutput = callbacks?.onOutput
|
|
365
420
|
const signal = callbacks?.signal
|
|
366
421
|
const startTime = Date.now()
|
|
@@ -369,33 +424,19 @@ export async function runAdvisorReview(agent, reviewType, callbacks, designToken
|
|
|
369
424
|
// former advisor.enabled gate is removed — review capability has no off
|
|
370
425
|
// switch; only the guard (completion pushback) is opt-in via advisor.guard.
|
|
371
426
|
|
|
372
|
-
// Mechanical convergence cap —
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
// Summarize unresolved items from the last review output for guidance
|
|
379
|
-
// (line-level status-word scan — no table-header parsing, decision 2026-08-08).
|
|
380
|
-
const prior = agent._lastAdvisorOutput
|
|
381
|
-
const unfixed = prior ? extractUnfixedIssues(prior) : []
|
|
382
|
-
|
|
383
|
-
let message = `Advisor: convergence cap reached after ${MAX_ADVISOR_ROUNDS} rounds.\n`
|
|
384
|
-
if (unfixed.length > 0) {
|
|
385
|
-
message += `\nUnresolved issues from prior rounds:\n${unfixed.map((i) => `- ${i}`).join("\n")}\n`
|
|
386
|
-
} else {
|
|
387
|
-
message += "\nAll prior issues appear resolved.\n"
|
|
388
|
-
}
|
|
389
|
-
message += "\nOptions:\n1. Accept current state and proceed\n2. Manually review specific concerns with read/grep\n3. Start a new session (/new) to reset the advisor"
|
|
390
|
-
|
|
391
|
-
return message
|
|
427
|
+
// Mechanical convergence cap — CODE REVIEWS ONLY (2026-09-07 §8 ruling: design
|
|
428
|
+
// reviews are exempt). _advisorRound is scoped to the current review instance
|
|
429
|
+
// (§24 D-24b ③), so >= MAX_ADVISOR_ROUNDS blocks the next call of THIS instance.
|
|
430
|
+
// 5 rounds max; after that the review is never pushed back.
|
|
431
|
+
if (reviewType !== "design" && (agent._advisorRound || 0) >= MAX_ADVISOR_ROUNDS) {
|
|
432
|
+
return buildCapMessage(agent)
|
|
392
433
|
}
|
|
393
434
|
|
|
394
435
|
const provider = resolveAdvisorProvider(agent)
|
|
395
436
|
// Advisor always works in the agent's cwd — scope is defined by paths/documents.
|
|
396
437
|
const advisorCwd = agent.cwd
|
|
397
438
|
|
|
398
|
-
const messages = prepareAdvisorMessages(agent, reviewType, designToken, documents, paths)
|
|
439
|
+
const messages = prepareAdvisorMessages(agent, reviewType, designToken, documents, paths, null, object, designId)
|
|
399
440
|
|
|
400
441
|
try {
|
|
401
442
|
const result = await runAdvisorToolLoop(provider, messages, onOutput, signal, agent, advisorCwd)
|
|
@@ -416,8 +457,7 @@ export async function runAdvisorReview(agent, reviewType, callbacks, designToken
|
|
|
416
457
|
// table row (`| a | b | c |`) or substantial prose (>200 chars). An
|
|
417
458
|
// empty or tool-progress-only reply must not become the "prior review"
|
|
418
459
|
// of round 2+.
|
|
419
|
-
const
|
|
420
|
-
const looksLikeReview = /\|.*\|.*\|/.test(trimmed) || trimmed.length >= 200
|
|
460
|
+
const looksLikeReview = looksLikeReviewOutput(final)
|
|
421
461
|
if (looksLikeReview) {
|
|
422
462
|
agent._lastAdvisorOutput = final
|
|
423
463
|
}
|
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, buildDesignApprovalBlock } 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, buildDesignApprovalBlock } 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,13 @@ 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.
|
|
198
|
+
* @param {string|null} [designId] — §29.1 F2a: injected next to the design token in
|
|
199
|
+
* the Approval Signal (round 1 + round 2+ — both values, verbatim anchor).
|
|
190
200
|
*/
|
|
191
|
-
export function prepareAdvisorMessages(agent, reviewType, designToken = null, documents = null, paths = null, priorParam = null) {
|
|
201
|
+
export function prepareAdvisorMessages(agent, reviewType, designToken = null, documents = null, paths = null, priorParam = null, object = null, designId = null) {
|
|
192
202
|
// Deterministic convergence state (decision 2026-08-08): round 2+ requires
|
|
193
203
|
// _advisorRound > 0 AND a stored prior review output. No history parsing.
|
|
194
204
|
// priorParam (direct callers) wins over the stored output — same derivation
|
|
@@ -201,7 +211,7 @@ export function prepareAdvisorMessages(agent, reviewType, designToken = null, do
|
|
|
201
211
|
if (reviewType === "design" && (agent._advisorRound || 0) === 0) {
|
|
202
212
|
return [
|
|
203
213
|
{ role: "system", content: withTime(buildAdvisorSystemPrompt(agent, prior, reviewType)) },
|
|
204
|
-
{ role: "user", content: escapeLiteralEscapes(buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths)) },
|
|
214
|
+
{ role: "user", content: escapeLiteralEscapes(buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths, object, designId)) },
|
|
205
215
|
]
|
|
206
216
|
}
|
|
207
217
|
|
|
@@ -226,7 +236,7 @@ export function prepareAdvisorMessages(agent, reviewType, designToken = null, do
|
|
|
226
236
|
agent._advisorRound = 0
|
|
227
237
|
}
|
|
228
238
|
// Mutations exist → KEEP the round (cap keeps advancing through retries).
|
|
229
|
-
const user = buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths)
|
|
239
|
+
const user = buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths, object, designId)
|
|
230
240
|
return [
|
|
231
241
|
{ role: "system", content: withTime(buildAdvisorSystemPrompt(agent, prior, reviewType)) },
|
|
232
242
|
{
|
|
@@ -255,8 +265,25 @@ export function prepareAdvisorMessages(agent, reviewType, designToken = null, do
|
|
|
255
265
|
// scopeFiles gives the fallback (agent gave no response table) a concrete
|
|
256
266
|
// review surface.
|
|
257
267
|
const scopeFiles = resolveScopeFiles(agent, paths)
|
|
268
|
+
const followUp = buildAdvisorFollowUp(agent, prior, scopeFiles, object)
|
|
269
|
+
// §24 D-24b (design round 2+ — async fix-round continuations must be able to
|
|
270
|
+
// re-approve): re-anchor the review scope (the convergence follow-up carries no
|
|
271
|
+
// document list) and inject the round's design token with the approval signal.
|
|
272
|
+
if (reviewType === "design") {
|
|
273
|
+
const docList = Array.isArray(documents)
|
|
274
|
+
? documents.filter((d) => typeof d === "string" && d.trim())
|
|
275
|
+
: []
|
|
276
|
+
const scopeBlock = docList.length > 0
|
|
277
|
+
? `\n\n## Documents to Review\nThe documents below are the review scope. Review ONLY these files — do not scan git diff or read any other files.\n${docList.map((d) => `- ${d} — Read this file in full`).join("\n")}`
|
|
278
|
+
: ""
|
|
279
|
+
const tokenBlock = designToken ? `\n\n${buildDesignApprovalBlock(designToken, designId)}` : ""
|
|
280
|
+
return [
|
|
281
|
+
{ role: "system", content: withTime(buildAdvisorSystemPrompt(agent, prior, reviewType)) },
|
|
282
|
+
{ role: "user", content: escapeLiteralEscapes(followUp + scopeBlock + tokenBlock) },
|
|
283
|
+
]
|
|
284
|
+
}
|
|
258
285
|
return [
|
|
259
286
|
{ role: "system", content: withTime(buildAdvisorSystemPrompt(agent, prior, reviewType)) },
|
|
260
|
-
{ role: "user", content: escapeLiteralEscapes(
|
|
287
|
+
{ role: "user", content: escapeLiteralEscapes(followUp) },
|
|
261
288
|
]
|
|
262
289
|
}
|
package/src/agent/completion.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { hasCodeMutations } from "../advisor/repos.mjs"
|
|
8
8
|
import { pushReal } from "../context.mjs"
|
|
9
9
|
import { MAX_ADVISOR_ROUNDS } from "../advisor/run.mjs"
|
|
10
|
+
import { advisorReviewPending, effectiveAdvisorRound } from "../agent-tools/advisor-async.mjs"
|
|
10
11
|
|
|
11
12
|
const MAX_VERIFY_PUSHBACKS = 2
|
|
12
13
|
const MAX_VERIFY_RETRIES = 3
|
|
@@ -77,18 +78,20 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
|
|
|
77
78
|
pushReal(agent, { role: "assistant", content: response.content })
|
|
78
79
|
agent.history.push({
|
|
79
80
|
role: "user",
|
|
80
|
-
content: "[System reminder: you modified files in this run but have not verified the changes. Before finishing:
|
|
81
|
+
content: "[System reminder: you modified files in this run but have not verified the changes. Before finishing: run the project's verification yourself (per its AGENTS.md test method), then call verify declaring the outcome via verification.status. verify mechanically gates on your declaration. If verification is genuinely impossible here, say so explicitly in your reply.]",
|
|
81
82
|
})
|
|
82
83
|
callbacks.onTurnEnd?.(agent, turn)
|
|
83
84
|
return { action: "continue", guardPushbacks, honestReminderInjected, advisorPushbacks }
|
|
84
85
|
}
|
|
85
|
-
// Verified but
|
|
86
|
+
// Verified but not passed → pushback to fix/complete (up to MAX_VERIFY_RETRIES).
|
|
87
|
+
// _verifyPassed === false means the declaration was failed, skipped without a
|
|
88
|
+
// reason, or not declared — not necessarily that a test failed.
|
|
86
89
|
if (agent._verifiedThisRun && agent._verifyPassed === false && agent._verifyRetries < MAX_VERIFY_RETRIES) {
|
|
87
90
|
agent._verifyRetries++
|
|
88
91
|
pushReal(agent, { role: "assistant", content: response.content })
|
|
89
92
|
agent.history.push({
|
|
90
93
|
role: "user",
|
|
91
|
-
content: `[System reminder:
|
|
94
|
+
content: `[System reminder: (retry ${agent._verifyRetries}/${MAX_VERIFY_RETRIES}) verify was not passed — either your verification declared failed, was skipped without a reason, or was not declared. Fix or complete your verification, then call verify again declaring the outcome. If you cannot fix after ${MAX_VERIFY_RETRIES} attempts, explain honestly what's blocking you.]`,
|
|
92
95
|
})
|
|
93
96
|
callbacks.onTurnEnd?.(agent, turn)
|
|
94
97
|
return { action: "continue", guardPushbacks, honestReminderInjected, advisorPushbacks }
|
|
@@ -103,7 +106,7 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
|
|
|
103
106
|
pushReal(agent, { role: "assistant", content: response.content })
|
|
104
107
|
agent.history.push({
|
|
105
108
|
role: "user",
|
|
106
|
-
content: `[System reminder: ${MAX_VERIFY_RETRIES} verify attempts exhausted
|
|
109
|
+
content: `[System reminder: ${MAX_VERIFY_RETRIES} verify attempts exhausted. You have not passed verification. Either state explicitly that your verification could not be completed, or run verify again once it is. If your verification could not be completed, say so explicitly in your reply to the user — state what you tried and what you believe is blocking you, and do not present the work as complete; the user needs to know it is unfinished.]`,
|
|
107
110
|
})
|
|
108
111
|
callbacks.onTurnEnd?.(agent, turn)
|
|
109
112
|
return { action: "continue", guardPushbacks, honestReminderInjected, advisorPushbacks }
|
|
@@ -117,18 +120,21 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
|
|
|
117
120
|
const cfg = agent.config?.advisor
|
|
118
121
|
const advisorReview = cfg?.guard === true
|
|
119
122
|
if (depth === 0 && advisorReview && !agent.config?.agent?.engineering) {
|
|
120
|
-
//
|
|
121
|
-
// (
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
123
|
+
// §24 D-24b (T-24b4 — guard timing): an async review that is still in flight
|
|
124
|
+
// (or queued in the advisor pool) means the review was launched — the guard
|
|
125
|
+
// does NOT push back while it is pending (未决不算未评审); once it settles
|
|
126
|
+
// non-stale it marks _calledAdvisorThisRun, and a STALE settle leaves the
|
|
127
|
+
// mark unset so the guard pushes back here again (fix #2 — no silent skip).
|
|
128
|
+
const pending = advisorReviewPending(agent)
|
|
129
|
+
const rounds = effectiveAdvisorRound(agent)
|
|
130
|
+
if (!pending && agent._mutatedThisRun && !agent._calledAdvisorThisRun && hasCodeMutations(agent)
|
|
125
131
|
&& advisorPushbacks < MAX_ADVISOR_PUSHBACKS
|
|
126
|
-
&&
|
|
132
|
+
&& rounds < MAX_ADVISOR_ROUNDS) {
|
|
127
133
|
advisorPushbacks++
|
|
128
134
|
pushReal(agent, { role: "assistant", content: response.content })
|
|
129
135
|
agent.history.push({
|
|
130
136
|
role: "user",
|
|
131
|
-
content: `[System reminder: you changed code in this run and MUST get an advisor review before finishing (round ${
|
|
137
|
+
content: `[System reminder: you changed code in this run and MUST get an advisor review before finishing (round ${rounds + 1}). Call the \`advisor\` tool now. This is required, not optional — do not skip it even if you believe the changes are trivial — the review will be quick either way. After the review, produce a response table for every issue found (see discipline rules for format).]`,
|
|
132
138
|
})
|
|
133
139
|
callbacks.onTurnEnd?.(agent, turn)
|
|
134
140
|
return { action: "continue", guardPushbacks, honestReminderInjected, advisorPushbacks }
|