thincoder 0.12.2 → 0.12.4
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/README.md +29 -6
- package/package.json +3 -3
- package/src/advisor/history.mjs +112 -0
- package/src/advisor/messages.mjs +182 -0
- package/src/advisor/repos.mjs +133 -0
- package/src/advisor/run.mjs +346 -0
- package/src/advisor.mjs +109 -509
- package/src/agent/completion.mjs +134 -0
- package/src/agent/dispatch.mjs +54 -7
- package/src/agent/post-turn.mjs +70 -0
- package/src/agent/setup.mjs +95 -6
- package/src/agent-tools/advisor.mjs +159 -12
- package/src/agent-tools/eng.mjs +64 -0
- package/src/agent-tools/subagent.mjs +73 -3
- package/src/agent-tools/task.mjs +45 -6
- package/src/agent-tools/verify.mjs +18 -0
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +152 -161
- package/src/cli/make-agent.mjs +1 -0
- package/src/cli/setup-wizard.mjs +1 -0
- package/src/config.mjs +34 -4
- package/src/context.mjs +47 -13
- package/src/generate-title.mjs +44 -0
- package/src/prompts/advisor-design.md +43 -0
- package/src/prompts/advisor-round1.md +11 -4
- package/src/prompts/advisor-round2.md +12 -7
- package/src/prompts/advisor-round3.md +11 -6
- package/src/prompts/coder.md +9 -3
- package/src/prompts/discipline.md +12 -96
- package/src/prompts/eng-coder.md +34 -0
- package/src/prompts/engineering-sub.md +12 -0
- package/src/prompts/engineering.md +96 -0
- package/src/prompts/main.md +1 -1
- package/src/prompts/methodology-template.md +39 -0
- package/src/prompts/plan.md +2 -2
- package/src/prompts/system.md +43 -61
- package/src/provider/core.mjs +58 -2
- package/src/session.mjs +291 -94
- package/src/skills.mjs +48 -15
- package/src/tools/apply_patch.md +1 -1
- package/src/tools/checklist.mjs +4 -3
- package/src/tools/codemode.mjs +23 -11
- package/src/tools/delete.md +1 -0
- package/src/tools/edit.md +1 -1
- package/src/tools/execute.md +5 -0
- package/src/tools/file.mjs +4 -0
- package/src/tools/git.md +15 -0
- package/src/tools/git.mjs +1 -6
- package/src/tools/lint.md +8 -0
- package/src/tools/linter.mjs +1 -5
- package/src/tools/lsp.md +7 -0
- package/src/tools/lsp.mjs +8 -9
- package/src/tools/patch.mjs +1 -29
- package/src/tools/read_image.md +5 -1
- package/src/tools/system.mjs +1 -1
- package/src/tools/web.mjs +3 -3
- package/src/tui/agent-turn.mjs +184 -66
- package/src/tui/ansi.mjs +4 -0
- package/src/tui/clipboard.mjs +9 -0
- package/src/tui/cmd-config.mjs +14 -26
- package/src/tui/cmd-eng.mjs +44 -0
- package/src/tui/cmd-exit.mjs +1 -1
- package/src/tui/cmd-fold.mjs +3 -4
- package/src/tui/cmd-model.mjs +11 -6
- package/src/tui/cmd-new.mjs +5 -5
- package/src/tui/cmd-session.mjs +21 -11
- package/src/tui/cmd-think.mjs +1 -0
- package/src/tui/index.mjs +20 -9
- package/src/tui/key-handler.mjs +177 -9
- package/src/tui/layout.mjs +5 -5
- package/src/tui/markdown.mjs +52 -0
- package/src/tui/pickers.mjs +190 -45
- package/src/tui/render-conversation.mjs +54 -13
- package/src/tui/render-frame.mjs +39 -12
- package/src/tui/render-loop.mjs +2 -1
- package/src/tui/render.mjs +13 -7
- package/src/tui/slash-commands.mjs +11 -7
- package/src/tui/startup.mjs +4 -3
- package/src/tui/wizard.mjs +3 -0
- package/src/tools/checkpoint.md +0 -15
- package/src/tools/git_diff.md +0 -11
- package/src/tools/git_log.md +0 -10
- package/src/tools/git_status.md +0 -8
- package/src/tools/linter.md +0 -13
- package/src/tools/syntax_check.md +0 -10
package/src/advisor.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* advisor.mjs —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* advisor.mjs — advisor system-prompt selection, follow-up building, session assembly.
|
|
3
|
+
* User-message building lives in advisor/messages.mjs; execution (tool loop, provider
|
|
4
|
+
* resolution, review entry) in advisor/run.mjs; git discovery/collection in
|
|
5
|
+
* advisor/repos.mjs; history extraction in advisor/history.mjs.
|
|
5
6
|
*
|
|
6
7
|
* The advisor runs as a read-only exploration sub-agent with tools
|
|
7
8
|
* (read, glob, grep, ls, git, lsp, code_search). It discovers changes
|
|
@@ -16,7 +17,11 @@
|
|
|
16
17
|
* Agent responds with a response table per issue.
|
|
17
18
|
* Round 2: semi-convergence — verifies table + can flag obvious new issues.
|
|
18
19
|
* Round 3+: strict convergence — only checks the prior issue table.
|
|
19
|
-
*
|
|
20
|
+
* Each round replaces the system prompt (ROUND1 → ROUND2 → ROUND3) so the
|
|
21
|
+
* round-1 full-scope mandate can't bleed into later rounds, plus a mechanical
|
|
22
|
+
* cap (MAX_ADVISOR_ROUNDS in run.mjs) refuses a 6th review call outright.
|
|
23
|
+
* Rounds 2+ also declare all earlier diffs STALE and require read-verified
|
|
24
|
+
* file:line evidence for any unfixed/new finding — see docs/design/ADVISOR-CONVERGENCE.md.
|
|
20
25
|
*
|
|
21
26
|
* Session memory (agent._advisorSession):
|
|
22
27
|
* All advisor calls within one run share a single conversation — round 2+
|
|
@@ -27,258 +32,49 @@
|
|
|
27
32
|
* in-memory session is gone — falls back to a fresh session seeded from the
|
|
28
33
|
* issue/response tables in the main history.
|
|
29
34
|
*
|
|
30
|
-
*
|
|
31
35
|
* Project customisation: .thincoder/advisor.md in the project root.
|
|
32
36
|
*/
|
|
33
|
-
import {
|
|
34
|
-
import { findProvider } from "./config.mjs"
|
|
35
|
-
import { existsSync, readFileSync } from "node:fs"
|
|
37
|
+
import { readFileSync } from "node:fs"
|
|
36
38
|
import { join, dirname } from "node:path"
|
|
37
39
|
import { fileURLToPath } from "node:url"
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
+
import { createHash } from "node:crypto"
|
|
41
|
+
import { findReviewRepos, collectRepoSnapshots } from "./advisor/repos.mjs"
|
|
42
|
+
import { extractPriorIssueTable, extractAgentResponseTable } from "./advisor/history.mjs"
|
|
43
|
+
import { buildAdvisorUserMessage } from "./advisor/messages.mjs"
|
|
44
|
+
// Re-export for run.mjs and tests (keeps their imports from "../advisor.mjs" stable)
|
|
45
|
+
export { ADVISOR_MD_PATH, extractPriorIssueTable, extractAgentResponseTable, extractConversationBackground } from "./advisor/history.mjs"
|
|
46
|
+
export { buildAdvisorUserMessage } from "./advisor/messages.mjs"
|
|
40
47
|
|
|
41
48
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
42
49
|
|
|
43
|
-
const ADVISOR_MD_PATH = ".thincoder/advisor.md"
|
|
44
|
-
const GIT_TIMEOUT = 5_000
|
|
45
|
-
|
|
46
|
-
const DEFAULT_CRITERIA = `Review the code changes, focusing on:
|
|
47
|
-
1. Correctness: logic errors, edge cases, off-by-one, incomplete modifications
|
|
48
|
-
2. Security: unhandled exceptions, null references, resource leaks, race conditions
|
|
49
|
-
3. Consistency: alignment with existing project patterns and conventions
|
|
50
|
-
4. Completeness: missing callers, imports, or follow-up changes
|
|
51
|
-
5. Maintainability: vague naming, missing comments, overly complex logic`
|
|
52
|
-
|
|
53
|
-
// ────────────────────────────────────────
|
|
54
|
-
// Advisor's read-only tool set
|
|
55
|
-
// ────────────────────────────────────────
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Restricted git tool: diff / status / log only.
|
|
59
|
-
* Checkpoint create/rewind are blocked — the advisor must not mutate state.
|
|
60
|
-
*/
|
|
61
|
-
const { gitTool, readTool, globTool, grepTool, lsTool } = await import("./tools/index.mjs")
|
|
62
|
-
const { lspTool } = await import("./tools/lsp.mjs")
|
|
63
|
-
const { codeModeTool: codeSearchTool } = await import("./tools/codemode.mjs")
|
|
64
|
-
|
|
65
|
-
const advisorGitTool = {
|
|
66
|
-
...gitTool,
|
|
67
|
-
readonly: true,
|
|
68
|
-
async execute(args, ctx) {
|
|
69
|
-
// Block checkpoint create/rewind — advisor is read-only
|
|
70
|
-
if (args.action === "checkpoint") {
|
|
71
|
-
if (args.checkpointAction === "create" || args.checkpointAction === "rewind") {
|
|
72
|
-
return "Error: checkpoint create/rewind is disabled in advisor mode. Use diff/status/log only."
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return gitTool.execute(args, ctx)
|
|
76
|
-
},
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const ADVISOR_TOOLS = [readTool, globTool, grepTool, lsTool, advisorGitTool, lspTool, codeSearchTool]
|
|
80
|
-
const ADVISOR_TOOL_SCHEMAS = ADVISOR_TOOLS.map(toOpenAISchema)
|
|
81
|
-
const ADVISOR_TOOL_BY_NAME = new Map(ADVISOR_TOOLS.map((t) => [t.name, t]))
|
|
82
|
-
|
|
83
50
|
// ────────────────────────────────────────
|
|
84
51
|
// Prompt files — loaded at module init
|
|
85
52
|
// ────────────────────────────────────────
|
|
86
53
|
|
|
87
54
|
const ADVISOR_ROUND1 = readFileSync(join(__dirname, "prompts", "advisor-round1.md"), "utf8")
|
|
55
|
+
// ROUND2/3 are used whenever a convergence round (round 2+) is being built:
|
|
56
|
+
// in-run session continuation replaces the system prompt with them, and a
|
|
57
|
+
// rebuilt fresh session (e.g. after a failed review) also selects them via
|
|
58
|
+
// buildAdvisorSystemPrompt when _advisorRound > 0.
|
|
88
59
|
const ADVISOR_ROUND2 = readFileSync(join(__dirname, "prompts", "advisor-round2.md"), "utf8")
|
|
89
60
|
const ADVISOR_ROUND3 = readFileSync(join(__dirname, "prompts", "advisor-round3.md"), "utf8")
|
|
61
|
+
let ADVISOR_DESIGN = ""
|
|
62
|
+
try { ADVISOR_DESIGN = readFileSync(join(__dirname, "prompts", "advisor-design.md"), "utf8") } catch { /* design review unavailable */ }
|
|
90
63
|
|
|
91
64
|
// ────────────────────────────────────────
|
|
92
|
-
//
|
|
93
|
-
// ────────────────────────────────────────
|
|
94
|
-
|
|
95
|
-
const ADVISOR_TABLE_HEADER = "| # | File | Severity | Issue | Suggestion |"
|
|
96
|
-
const CONVERGENCE_TABLE_HEADER = "| # | Orig# | File | Severity | Status | Notes |"
|
|
97
|
-
const AGENT_RESPONSE_HEADER = "| # | Action | Detail |"
|
|
98
|
-
const LEGACY_ADVISOR_HEADER = "| # | 文件 | 严重程度 | 问题描述 | 建议修复 |"
|
|
99
|
-
const LEGACY_CONVERGENCE_HEADER = "| # | 原# | 文件 | 严重程度 | 当前状态 | 说明 |"
|
|
100
|
-
const LEGACY_RESPONSE_HEADER = "| # | 处理 | 详情 |"
|
|
101
|
-
const ALL_CLEAR_PHRASES = [
|
|
102
|
-
"No issues found",
|
|
103
|
-
"All issues resolved",
|
|
104
|
-
"review passed",
|
|
105
|
-
"未发现问题",
|
|
106
|
-
"所有问题已解决",
|
|
107
|
-
"审查通过",
|
|
108
|
-
]
|
|
109
|
-
|
|
110
|
-
export function extractPriorIssueTable(history) {
|
|
111
|
-
for (let i = history.length - 1; i >= 0; i--) {
|
|
112
|
-
const m = history[i]
|
|
113
|
-
if (m.role !== "tool") continue
|
|
114
|
-
const content = typeof m.content === "string" ? m.content : ""
|
|
115
|
-
if (ALL_CLEAR_PHRASES.some((p) => content.includes(p))) return null
|
|
116
|
-
if (content.includes(ADVISOR_TABLE_HEADER) || content.includes(CONVERGENCE_TABLE_HEADER) ||
|
|
117
|
-
content.includes(LEGACY_ADVISOR_HEADER) || content.includes(LEGACY_CONVERGENCE_HEADER)) {
|
|
118
|
-
const table = extractTableBlock(content)
|
|
119
|
-
if (table) return { text: table, sinceIdx: i }
|
|
120
|
-
return null
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return null
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function extractAgentResponseTable(history, sinceIdx) {
|
|
127
|
-
for (let i = sinceIdx + 1; i < history.length; i++) {
|
|
128
|
-
const m = history[i]
|
|
129
|
-
if (m.role !== "assistant") continue
|
|
130
|
-
const content = typeof m.content === "string" ? m.content : ""
|
|
131
|
-
if (content.includes(AGENT_RESPONSE_HEADER) || content.includes(LEGACY_RESPONSE_HEADER)) {
|
|
132
|
-
return extractTableBlock(content)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return null
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function extractTableBlock(text) {
|
|
139
|
-
const lines = text.split("\n")
|
|
140
|
-
let start = -1
|
|
141
|
-
for (let i = 0; i < lines.length; i++) {
|
|
142
|
-
if (lines[i].startsWith("|")) { start = i; break }
|
|
143
|
-
}
|
|
144
|
-
if (start < 0) return null
|
|
145
|
-
let end = start
|
|
146
|
-
for (let i = start + 1; i < lines.length; i++) {
|
|
147
|
-
if (lines[i].startsWith("|")) end = i
|
|
148
|
-
else break
|
|
149
|
-
}
|
|
150
|
-
return lines.slice(start, end + 1).join("\n")
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ────────────────────────────────────────
|
|
154
|
-
// Review scope — repos to review
|
|
155
|
-
// ────────────────────────────────────────
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Find the git repository roots that contain the agent's touched files.
|
|
159
|
-
* Falls back to cwd if no repos found.
|
|
160
|
-
*/
|
|
161
|
-
function findReviewRepos(agent) {
|
|
162
|
-
const touched = agent._touchedFiles ?? []
|
|
163
|
-
const repos = []
|
|
164
|
-
|
|
165
|
-
for (const abs of touched) {
|
|
166
|
-
try {
|
|
167
|
-
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
168
|
-
cwd: dirname(abs), encoding: "utf8", timeout: GIT_TIMEOUT,
|
|
169
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
170
|
-
}).trim()
|
|
171
|
-
if (root && !repos.includes(root)) repos.push(root)
|
|
172
|
-
} catch { /* not a git repo */ }
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (repos.length > 0) return repos
|
|
176
|
-
|
|
177
|
-
// Fallback: cwd itself
|
|
178
|
-
try {
|
|
179
|
-
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
180
|
-
cwd: agent.cwd, encoding: "utf8", timeout: GIT_TIMEOUT,
|
|
181
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
182
|
-
}).trim()
|
|
183
|
-
if (root) return [root]
|
|
184
|
-
} catch { /* not a git repo */ }
|
|
185
|
-
|
|
186
|
-
return []
|
|
187
|
-
}
|
|
188
|
-
|
|
65
|
+
// System prompt building
|
|
189
66
|
// ────────────────────────────────────────
|
|
190
67
|
|
|
191
|
-
/** Cap per-repo embedded diff — generous (large-context models); advisor can fetch the rest via its git tool */
|
|
192
|
-
const MAX_EMBEDDED_DIFF = 50_000
|
|
193
|
-
|
|
194
68
|
/**
|
|
195
|
-
*
|
|
196
|
-
*
|
|
69
|
+
* Build the system prompt for an advisor review session.
|
|
70
|
+
* @param {Object} agent — the parent agent
|
|
71
|
+
* @param {Object|null} [_prior] — prior issue table (from extractPriorIssueTable)
|
|
72
|
+
* @param {string} [reviewType] — "design" for design review, undefined/"code" for code review
|
|
73
|
+
* @returns {string} the system prompt
|
|
197
74
|
*/
|
|
198
|
-
function
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
for (const repo of targets) {
|
|
202
|
-
let status = "", diff = ""
|
|
203
|
-
try {
|
|
204
|
-
status = execFileSync("git", ["status", "--porcelain"], {
|
|
205
|
-
cwd: repo, encoding: "utf8", timeout: GIT_TIMEOUT, stdio: ["ignore", "pipe", "pipe"],
|
|
206
|
-
}).trim()
|
|
207
|
-
diff = execFileSync("git", ["diff", "HEAD"], {
|
|
208
|
-
cwd: repo, encoding: "utf8", timeout: GIT_TIMEOUT, stdio: ["ignore", "pipe", "pipe"],
|
|
209
|
-
maxBuffer: 8 * 1024 * 1024,
|
|
210
|
-
})
|
|
211
|
-
} catch { continue /* not a git repo or git failed */ }
|
|
212
|
-
if (!status && !diff.trim()) continue
|
|
213
|
-
parts.push(`### ${repo}`)
|
|
214
|
-
if (status) parts.push("```", status, "```")
|
|
215
|
-
if (diff.trim()) {
|
|
216
|
-
const truncated = diff.length > MAX_EMBEDDED_DIFF
|
|
217
|
-
parts.push("```diff", truncated ? diff.slice(0, MAX_EMBEDDED_DIFF) : diff.trimEnd(), "```")
|
|
218
|
-
if (truncated) parts.push(`(diff truncated at ${MAX_EMBEDDED_DIFF} chars — use the git tool to see the rest)`)
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return parts
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export function loadAdvisorMd(cwd) {
|
|
225
|
-
const path = join(cwd, ADVISOR_MD_PATH)
|
|
226
|
-
if (!existsSync(path)) return DEFAULT_CRITERIA
|
|
227
|
-
try {
|
|
228
|
-
const content = readFileSync(path, "utf8").trim()
|
|
229
|
-
return content || DEFAULT_CRITERIA
|
|
230
|
-
} catch {
|
|
231
|
-
return DEFAULT_CRITERIA
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const MAX_BACKGROUND_CHARS = 20_000
|
|
236
|
-
const MAX_BG_USER_CHARS = 2000
|
|
237
|
-
const MAX_BG_ASSISTANT_CHARS = 1500
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Recent conversation context for the advisor: the last few user↔assistant
|
|
241
|
-
* exchanges (default 3 user turns). The last user message alone often lacks
|
|
242
|
-
* context ("把那个问题改一下" means nothing without the preceding turns) —
|
|
243
|
-
* the advisor needs the background to judge whether the changes match intent.
|
|
244
|
-
* Tool messages are skipped (noise); texts are truncated with generous caps
|
|
245
|
-
* (models have large context windows — completeness beats frugality).
|
|
246
|
-
*/
|
|
247
|
-
export function extractConversationBackground(history, maxTurns = 3) {
|
|
248
|
-
const isNoise = (c) => c.startsWith("[System reminder:") || c.startsWith("[User interrupt:")
|
|
249
|
-
const picked = []
|
|
250
|
-
let userCount = 0
|
|
251
|
-
for (let i = history.length - 1; i >= 0 && userCount < maxTurns; i--) {
|
|
252
|
-
const m = history[i]
|
|
253
|
-
if (m.role !== "user" && m.role !== "assistant") continue
|
|
254
|
-
const content = typeof m.content === "string" ? m.content.trim() : ""
|
|
255
|
-
if (!content || isNoise(content)) continue
|
|
256
|
-
picked.unshift({ role: m.role === "user" ? "User" : "Assistant", text: content })
|
|
257
|
-
if (m.role === "user") userCount++
|
|
258
|
-
}
|
|
259
|
-
if (picked.length === 0) return null
|
|
260
|
-
|
|
261
|
-
const lines = picked.map((e) => {
|
|
262
|
-
const cap = e.role === "User" ? MAX_BG_USER_CHARS : MAX_BG_ASSISTANT_CHARS
|
|
263
|
-
const text = e.text.length > cap ? e.text.slice(0, cap) + "…" : e.text
|
|
264
|
-
return `${e.role}: ${text}`
|
|
265
|
-
})
|
|
266
|
-
// Keep the most recent lines within the total budget
|
|
267
|
-
const out = []
|
|
268
|
-
let total = 0
|
|
269
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
270
|
-
if (out.length > 0 && total + lines[i].length > MAX_BACKGROUND_CHARS) break
|
|
271
|
-
total += lines[i].length
|
|
272
|
-
out.unshift(lines[i])
|
|
273
|
-
}
|
|
274
|
-
return out.join("\n")
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// ────────────────────────────────────────
|
|
278
|
-
// System prompt routing
|
|
279
|
-
// ────────────────────────────────────────
|
|
280
|
-
|
|
281
|
-
export function buildAdvisorSystemPrompt(agent, _prior) {
|
|
75
|
+
export function buildAdvisorSystemPrompt(agent, _prior, reviewType) {
|
|
76
|
+
// Design review: dedicated prompt, no convergence rounds
|
|
77
|
+
if (reviewType === "design") return ADVISOR_DESIGN || `You are an independent design reviewer for an engineering-mode project. Review the design document in the changes below. Evaluate: completeness, feasibility, clarity, scope, acceptance criteria. Read METHODOLOGY.md if provided. Produce a review table with | # | Category | Severity | Issue | Suggestion | format.`
|
|
282
78
|
const prior = _prior ?? extractPriorIssueTable(agent.history)
|
|
283
79
|
if (!prior || (agent._advisorRound || 0) === 0) return ADVISOR_ROUND1
|
|
284
80
|
const round = (agent._advisorRound || 0) + 1
|
|
@@ -287,130 +83,59 @@ export function buildAdvisorSystemPrompt(agent, _prior) {
|
|
|
287
83
|
}
|
|
288
84
|
|
|
289
85
|
// ────────────────────────────────────────
|
|
290
|
-
//
|
|
291
|
-
// ────────────────────────────────────────
|
|
292
|
-
|
|
293
|
-
export function buildAdvisorUserMessage(agent, _prior) {
|
|
294
|
-
const prior = _prior ?? extractPriorIssueTable(agent.history)
|
|
295
|
-
|
|
296
|
-
// Repos to review
|
|
297
|
-
const repos = findReviewRepos(agent)
|
|
298
|
-
const repoList = repos.length > 0
|
|
299
|
-
? repos.map((r, i) => `${i + 1}. ${r}`).join("\n")
|
|
300
|
-
: `(no git repository — working directory: ${agent.cwd})`
|
|
301
|
-
|
|
302
|
-
const parts = []
|
|
303
|
-
|
|
304
|
-
// Convergence data (round 2+)
|
|
305
|
-
if (prior && (agent._advisorRound || 0) > 0) {
|
|
306
|
-
const response = extractAgentResponseTable(agent.history, prior.sinceIdx)
|
|
307
|
-
|| "(Agent did not provide a response table — re-evaluate each issue)"
|
|
308
|
-
const round = (agent._advisorRound || 0) + 1
|
|
309
|
-
const label = round === 2 ? "Verify Prior Table + Flag New Issues" : "Strict Verification"
|
|
310
|
-
parts.push(`## Round ${round} — ${label}`)
|
|
311
|
-
parts.push("")
|
|
312
|
-
parts.push("## Prior Issue Table")
|
|
313
|
-
parts.push(prior.text)
|
|
314
|
-
parts.push("")
|
|
315
|
-
parts.push("## Agent Response")
|
|
316
|
-
parts.push(response)
|
|
317
|
-
parts.push("")
|
|
318
|
-
parts.push("---")
|
|
319
|
-
parts.push("")
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Review scope
|
|
323
|
-
parts.push("## Review Scope")
|
|
324
|
-
parts.push(`Review the following git repositor${repos.length === 1 ? "y" : "ies"}:`)
|
|
325
|
-
parts.push(repoList)
|
|
326
|
-
parts.push("")
|
|
327
|
-
|
|
328
|
-
// Pre-collected changes — saves the advisor from spending its first tool
|
|
329
|
-
// calls on discovery (git status / git diff) every single round.
|
|
330
|
-
const snapshots = collectRepoSnapshots(repos, agent.cwd)
|
|
331
|
-
agent._advisorLastSnapshot = snapshots.join("\n") // dedup baseline for follow-up rounds
|
|
332
|
-
if (snapshots.length > 0) {
|
|
333
|
-
parts.push("## Current Changes (git status + git diff HEAD, pre-collected)")
|
|
334
|
-
parts.push(...snapshots)
|
|
335
|
-
parts.push("")
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Conversation background — recent user↔assistant exchanges for intent context
|
|
339
|
-
const background = extractConversationBackground(agent.history)
|
|
340
|
-
if (background) {
|
|
341
|
-
parts.push("## Conversation Background (recent turns)")
|
|
342
|
-
parts.push(background)
|
|
343
|
-
parts.push("")
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Review criteria
|
|
347
|
-
const criteria = loadAdvisorMd(agent.cwd)
|
|
348
|
-
parts.push("## Review Criteria")
|
|
349
|
-
parts.push(criteria)
|
|
350
|
-
parts.push("")
|
|
351
|
-
|
|
352
|
-
// Instructions — round-aware: re-reviews skip convention discovery entirely
|
|
353
|
-
const isReReview = prior && (agent._advisorRound || 0) > 0
|
|
354
|
-
parts.push("## Instructions")
|
|
355
|
-
parts.push("1. The uncommitted changes are already provided above — do NOT re-run `git status` / `git diff` unless the embedded diff is marked truncated.")
|
|
356
|
-
if (isReReview) {
|
|
357
|
-
parts.push("2. Do NOT re-read AGENTS.md / design docs — conventions were established in round 1. Focus on verifying the prior issue table against the current diff.")
|
|
358
|
-
parts.push("3. `read` only the files touched by the fixes. Batch independent reads/greps in a single reply.")
|
|
359
|
-
parts.push("4. Produce your verification table. Do not re-read content you already have.")
|
|
360
|
-
} else {
|
|
361
|
-
parts.push("2. Read `AGENTS.md` / design docs only if they exist (check once; do not re-probe with multiple patterns).")
|
|
362
|
-
parts.push("3. `read` changed files for full context beyond the diff. Batch independent reads/greps in a single reply instead of one call per round-trip.")
|
|
363
|
-
parts.push("4. Use `grep` or `lsp` to trace callers, imports, and dependencies — only where the diff leaves genuine doubt.")
|
|
364
|
-
parts.push("5. Produce your review table based on the review criteria above. Do not re-read content you already have.")
|
|
365
|
-
}
|
|
366
|
-
parts.push("Do NOT flag features that are valid under the project's stated platform requirements.")
|
|
367
|
-
|
|
368
|
-
return parts.join("\n")
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// ────────────────────────────────────────
|
|
372
|
-
// Session continuity — one advisor conversation per run
|
|
86
|
+
// Follow-up building (round 2+)
|
|
373
87
|
// ────────────────────────────────────────
|
|
374
88
|
|
|
375
89
|
/**
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
* the conversation — the follow-up only carries what changed: the agent's
|
|
379
|
-
* response table, the fresh diff snapshot, and this round's rules.
|
|
90
|
+
* Build a follow-up user message for round 2+ — the agent's response table +
|
|
91
|
+
* the refreshed diff, without re-sending the full round-1 context.
|
|
380
92
|
*/
|
|
381
93
|
export function buildAdvisorFollowUp(agent, _prior) {
|
|
382
94
|
const prior = _prior ?? extractPriorIssueTable(agent.history)
|
|
383
|
-
const
|
|
384
|
-
const response = (prior ? extractAgentResponseTable(agent.history, prior.sinceIdx) : null)
|
|
95
|
+
const response = extractAgentResponseTable(agent.history, prior?.sinceIdx ?? 0)
|
|
385
96
|
|| "(Agent did not provide a response table — re-evaluate each issue)"
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
"You may flag obvious NEW issues introduced by the fixes — but only crashes, data loss, or logic errors clearly visible in the diff. Do not nitpick style."
|
|
389
|
-
: "Strictly verify only your prior issue table against the current changes. Do NOT look for new issues."
|
|
97
|
+
const round = (agent._advisorRound || 0) + 1
|
|
98
|
+
const label = round === 2 ? "Verify Prior Table + Flag New Issues" : "Strict Verification"
|
|
390
99
|
|
|
391
100
|
const parts = [
|
|
392
|
-
`## Round ${round} — ${
|
|
101
|
+
`## Round ${round} — ${label}`,
|
|
393
102
|
"",
|
|
394
|
-
|
|
103
|
+
`[System reminder: this is round ${round} of the convergence protocol. The system prompt for this round has already narrowed the review scope — follow it: ${round === 2 ? "verify the prior table and flag only obvious new issues introduced by the fixes" : "strictly verify only the prior table — do NOT look for new issues"}.]`,
|
|
395
104
|
"",
|
|
396
|
-
|
|
105
|
+
"## Prior Issue Table",
|
|
106
|
+
prior?.text ?? "(no prior table — review from scratch)",
|
|
397
107
|
"",
|
|
398
|
-
"
|
|
399
|
-
"",
|
|
400
|
-
"## Agent Response to Your Review",
|
|
108
|
+
"## Agent Response",
|
|
401
109
|
response,
|
|
402
110
|
"",
|
|
111
|
+
"## Instructions",
|
|
112
|
+
round === 2
|
|
113
|
+
? "Verify each item in the prior table. Flag any obvious NEW issues introduced by the fixes (crashes, data loss, logic errors — not style). Produce a verification table."
|
|
114
|
+
: "Strictly verify ONLY the items in the prior table against the current diff. Do NOT look for new issues.",
|
|
115
|
+
"",
|
|
116
|
+
"IMPORTANT: in any embedded diff, `-` lines are REMOVED content (no longer in the file), `+` lines are ADDED. The prior issue table is HISTORY — always verify current file state with `read` before judging an item as fixed or unfixed.",
|
|
117
|
+
// Round-aware evidence rule: "New" entries only exist in round 2 (round 3+ forbids them).
|
|
118
|
+
`STALE-CONTEXT WARNING: all diffs in earlier messages (including round 1) are historical snapshots — files have changed since. Only THIS message's "Current Changes" section and fresh \`read\` results are authoritative. Any "Unfixed" entry${round === 2 ? ' (and any "New" entry)' : ""} MUST cite read-verified evidence (file:line from a \`read\` of the current file); uncited findings are unverified and will be ignored.`,
|
|
119
|
+
"",
|
|
120
|
+
"Do NOT re-read AGENTS.md / design docs or re-run git status/diff (current changes are below) — you already have full context from previous rounds.",
|
|
121
|
+
"",
|
|
403
122
|
]
|
|
404
123
|
const snapshots = collectRepoSnapshots(findReviewRepos(agent), agent.cwd)
|
|
405
124
|
const snapshotText = snapshots.join("\n")
|
|
125
|
+
const snapshotHash = snapshotText ? createHash("sha1").update(snapshotText).digest("hex") : null
|
|
406
126
|
// Skip re-pushing an identical diff (e.g. advisor re-run without any file changes) —
|
|
407
127
|
// the previous snapshot is already in the conversation, duplicating it wastes tokens.
|
|
408
|
-
if (
|
|
409
|
-
parts.push("## Current Changes", "(No
|
|
410
|
-
} else if (
|
|
128
|
+
if (snapshots.length === 0) {
|
|
129
|
+
parts.push("## Current Changes", "(No git repository or no changes detected.)")
|
|
130
|
+
} else if (snapshotHash && snapshotHash === agent._advisorLastSnapshotHash) {
|
|
131
|
+
// Hash match → the snapshot from the previous round is still current.
|
|
132
|
+
// Using hash instead of full-text comparison avoids keeping the entire diff
|
|
133
|
+
// string in memory and handles edge cases (e.g. file changed then reverted).
|
|
134
|
+
parts.push("## Current Changes", "(No changes since your previous review — the diff snapshot is identical, so the one from your last round remains valid for this round.)")
|
|
135
|
+
} else {
|
|
411
136
|
parts.push("## Current Changes (git status + git diff HEAD, refreshed)", ...snapshots)
|
|
412
137
|
}
|
|
413
|
-
agent.
|
|
138
|
+
agent._advisorLastSnapshotHash = snapshotHash
|
|
414
139
|
return parts.join("\n")
|
|
415
140
|
}
|
|
416
141
|
|
|
@@ -418,183 +143,58 @@ export function buildAdvisorFollowUp(agent, _prior) {
|
|
|
418
143
|
* Build or continue the advisor conversation for this run.
|
|
419
144
|
* First call in a run: fresh [system, user] session. Later calls: append a
|
|
420
145
|
* follow-up to the existing session so the advisor keeps its context.
|
|
421
|
-
* After an app restart (session lost),
|
|
422
|
-
*
|
|
146
|
+
* After an app restart (session lost), starts a fresh round-1 full review —
|
|
147
|
+
* exploration context is gone, so prior tables from history are not injected.
|
|
148
|
+
* @param {string[]|null} [paths] — code review only: explicit list of file/dir paths to review
|
|
149
|
+
* @param {string} [reviewType] — "design" or "code" (default)
|
|
150
|
+
* @param {string|null} [designToken] — design-review approval token (design only)
|
|
151
|
+
* @param {string[]|null} [documents] — design review only: explicit list of doc paths to review (passed through to buildAdvisorUserMessage)
|
|
423
152
|
*/
|
|
424
|
-
export function prepareAdvisorMessages(agent) {
|
|
153
|
+
export function prepareAdvisorMessages(agent, reviewType, designToken = null, documents = null, paths = null) {
|
|
425
154
|
const prior = extractPriorIssueTable(agent.history)
|
|
155
|
+
// Design review: always fresh session, no convergence
|
|
156
|
+
if (reviewType === "design") {
|
|
157
|
+
return [
|
|
158
|
+
{ role: "system", content: buildAdvisorSystemPrompt(agent, prior, reviewType) },
|
|
159
|
+
{ role: "user", content: buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths) },
|
|
160
|
+
]
|
|
161
|
+
}
|
|
426
162
|
let session = agent._advisorSession
|
|
427
163
|
if (session) {
|
|
428
|
-
|
|
429
|
-
|
|
164
|
+
// Session exists but no prior table (last review was all-clear or none) —
|
|
165
|
+
// a follow-up "Verify Prior Table" would be meaningless; start a fresh full review
|
|
166
|
+
if (!prior) {
|
|
167
|
+
agent._advisorSession = null
|
|
168
|
+
agent._advisorLastSnapshotHash = null
|
|
169
|
+
session = null
|
|
170
|
+
// Only reset the round counter on a truly fresh start (no prior reviews at all).
|
|
171
|
+
// If _advisorRound > 0, there WAS a prior review — it just passed (all-clear).
|
|
172
|
+
if (!agent._advisorRound) agent._advisorRound = 0
|
|
173
|
+
} else {
|
|
174
|
+
// Convergence rounds (2+): replace the system prompt so the round-1
|
|
175
|
+
// "full-scope review" mandate cannot override the follow-up's narrowed scope.
|
|
176
|
+
// Without this the model re-runs a full review every round, finds new issues
|
|
177
|
+
// each time, and the protocol never converges. buildAdvisorSystemPrompt
|
|
178
|
+
// returns ROUND2 for round 2 and ROUND3 for round 3+.
|
|
179
|
+
session[0] = { role: "system", content: buildAdvisorSystemPrompt(agent, prior, reviewType) }
|
|
180
|
+
session.push({ role: "user", content: buildAdvisorFollowUp(agent, prior) })
|
|
181
|
+
return session
|
|
182
|
+
}
|
|
430
183
|
}
|
|
431
184
|
session = [
|
|
432
|
-
{ role: "system", content: buildAdvisorSystemPrompt(agent, prior) },
|
|
433
|
-
{ role: "user", content: buildAdvisorUserMessage(agent, prior) },
|
|
185
|
+
{ role: "system", content: buildAdvisorSystemPrompt(agent, prior, reviewType) },
|
|
186
|
+
{ role: "user", content: buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths) },
|
|
434
187
|
]
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
* Picks the most identifying field; falls back to truncated JSON.
|
|
445
|
-
*/
|
|
446
|
-
function summarizeToolArgs(args) {
|
|
447
|
-
// e.g. "git diff HEAD", "read src/x.mjs" — action first when present
|
|
448
|
-
const parts = [args.action, args.path ?? args.pattern ?? args.command].filter((v) => v != null)
|
|
449
|
-
let s = parts.length > 0 ? parts.map(String).join(" ") : JSON.stringify(args)
|
|
450
|
-
s = s.replace(/\s+/g, " ").trim()
|
|
451
|
-
return s.length > 80 ? s.slice(0, 79) + "…" : s
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* Run the advisor's tool loop: chat → execute tools → repeat.
|
|
456
|
-
* Stops when the model produces text without tool calls.
|
|
457
|
-
*
|
|
458
|
-
* Progress lines (→ tool args) are emitted via onOutput between model bursts so
|
|
459
|
-
* the panel keeps moving while the advisor explores — otherwise the panel sits
|
|
460
|
-
* frozen through every tool-call phase and the review appears to have stalled.
|
|
461
|
-
*/
|
|
462
|
-
async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, cwd) {
|
|
463
|
-
// Kind-tagged wrappers: the TUI panel colors reasoning / answer / tool progress differently.
|
|
464
|
-
const emit = (kind) => (onOutput ? (text) => onOutput({ kind, text }) : undefined)
|
|
465
|
-
const onThink = emit("think")
|
|
466
|
-
const onText = emit("text")
|
|
467
|
-
while (true) {
|
|
468
|
-
const response = await chat(provider, {
|
|
469
|
-
messages,
|
|
470
|
-
tools: ADVISOR_TOOL_SCHEMAS,
|
|
471
|
-
signal: (signal && !signal.aborted) ? signal : new AbortController().signal,
|
|
472
|
-
onToken: onText,
|
|
473
|
-
onReasoning: onThink,
|
|
474
|
-
})
|
|
475
|
-
|
|
476
|
-
// No tool calls — this is the final review text
|
|
477
|
-
if (!response.toolCalls?.length) {
|
|
478
|
-
if (!response.content?.trim()) return "Advisor: (empty response — review was inconclusive)"
|
|
479
|
-
return response.content.trim()
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// Push assistant message with tool calls
|
|
483
|
-
messages.push({
|
|
484
|
-
role: "assistant",
|
|
485
|
-
content: response.content || null,
|
|
486
|
-
tool_calls: response.toolCalls.map((tc) => ({
|
|
487
|
-
id: tc.id, type: "function",
|
|
488
|
-
function: { name: tc.name, arguments: tc.arguments },
|
|
489
|
-
})),
|
|
490
|
-
})
|
|
491
|
-
|
|
492
|
-
// Execute each tool call
|
|
493
|
-
for (const tc of response.toolCalls) {
|
|
494
|
-
const tool = ADVISOR_TOOL_BY_NAME.get(tc.name)
|
|
495
|
-
let args = {}
|
|
496
|
-
try { args = JSON.parse(tc.arguments || "{}") } catch { /* summarized as raw JSON below */ }
|
|
497
|
-
onOutput?.({ kind: "tool", text: `\n→ ${tc.name} ${summarizeToolArgs(args)}\n` })
|
|
498
|
-
let result
|
|
499
|
-
if (!tool) {
|
|
500
|
-
result = `Error: unknown tool "${tc.name}". Available: ${[...ADVISOR_TOOL_BY_NAME.keys()].join(", ")}`
|
|
501
|
-
} else {
|
|
502
|
-
try {
|
|
503
|
-
result = await tool.execute(args, {
|
|
504
|
-
cwd,
|
|
505
|
-
agent,
|
|
506
|
-
onOutput,
|
|
507
|
-
signal,
|
|
508
|
-
})
|
|
509
|
-
} catch (e) {
|
|
510
|
-
result = `Error: ${e.message}`
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
messages.push({ role: "tool", tool_call_id: tc.id, content: String(result) })
|
|
188
|
+
// Fresh session. Only reset round if this is truly the first review.
|
|
189
|
+
// If _advisorRound > 0, there was a prior review that passed (all-clear).
|
|
190
|
+
if (!agent._advisorRound) agent._advisorRound = 0
|
|
191
|
+
agent._advisorLastSnapshot = null
|
|
192
|
+
if (!prior) {
|
|
193
|
+
// Tell the advisor why no prior issue table is present
|
|
194
|
+
session[1] = {
|
|
195
|
+
role: "user",
|
|
196
|
+
content: `[System reminder: no prior issue table is being carried into this review (first review, app restart, or session clear) — start with a fresh full review.]\n\n${session[1].content}`,
|
|
514
197
|
}
|
|
515
198
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
// ────────────────────────────────────────
|
|
519
|
-
// Main entry point
|
|
520
|
-
// ────────────────────────────────────────
|
|
521
|
-
|
|
522
|
-
export function resolveAdvisorProvider(agent) {
|
|
523
|
-
const cfg = agent.config?.advisor
|
|
524
|
-
if (cfg?.provider) {
|
|
525
|
-
try {
|
|
526
|
-
const provider = findProvider(agent.providers ?? [agent.provider], cfg.provider)
|
|
527
|
-
const result = cfg.model ? { ...provider, model: cfg.model } : { ...provider }
|
|
528
|
-
if (cfg.thinking === null) result.thinking = undefined // explicitly off
|
|
529
|
-
else if (cfg.thinking !== undefined) result.thinking = cfg.thinking
|
|
530
|
-
if (cfg.reasoningEffort !== undefined) result.reasoningEffort = cfg.reasoningEffort
|
|
531
|
-
return result
|
|
532
|
-
} catch {
|
|
533
|
-
// Provider not found — fall back to main provider
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
const provider = { ...agent.provider }
|
|
537
|
-
if (cfg?.model) provider.model = cfg.model
|
|
538
|
-
if (cfg?.thinking === null) provider.thinking = undefined // explicitly off
|
|
539
|
-
else if (cfg?.thinking !== undefined) provider.thinking = cfg.thinking
|
|
540
|
-
if (cfg?.reasoningEffort !== undefined) provider.reasoningEffort = cfg.reasoningEffort
|
|
541
|
-
return provider
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* Whether every changed file across the review repos is documentation-only.
|
|
546
|
-
* Used to skip pointless code reviews for doc updates (README, docs/, LICENSE…).
|
|
547
|
-
*/
|
|
548
|
-
function isDocOnlyChange(repos, cwd) {
|
|
549
|
-
const DOC_FILE = /(?:^|[/\\])(?:LICENSE|NOTICE|CHANGELOG|AUTHORS)(?:\.\w+)?$|\.(?:md|markdown|mdx|txt|rst|adoc)$/i
|
|
550
|
-
const targets = repos.length > 0 ? repos : [cwd]
|
|
551
|
-
let sawChanges = false
|
|
552
|
-
for (const repo of targets) {
|
|
553
|
-
let status = ""
|
|
554
|
-
try {
|
|
555
|
-
status = execFileSync("git", ["status", "--porcelain"], {
|
|
556
|
-
cwd: repo, encoding: "utf8", timeout: GIT_TIMEOUT, stdio: ["ignore", "pipe", "pipe"],
|
|
557
|
-
}).trim()
|
|
558
|
-
} catch { return false /* can't tell — let the advisor run */ }
|
|
559
|
-
if (!status) continue
|
|
560
|
-
sawChanges = true
|
|
561
|
-
for (const line of status.split("\n")) {
|
|
562
|
-
// porcelain: "XY path" or "XY old -> new" (rename)
|
|
563
|
-
const filePath = line.slice(3).split(" -> ").pop().replace(/^"|"$/g, "")
|
|
564
|
-
if (!DOC_FILE.test(filePath)) return false
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
return sawChanges
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
export async function runAdvisorReview(agent, onOutput, signal) {
|
|
571
|
-
const cfg = agent.config?.advisor
|
|
572
|
-
if (!cfg?.enabled) return null
|
|
573
|
-
|
|
574
|
-
const repos = findReviewRepos(agent)
|
|
575
|
-
if ((agent._touchedFiles ?? []).length === 0) return null
|
|
576
|
-
|
|
577
|
-
// Fast path: documentation-only changes need no code review — unless the project
|
|
578
|
-
// customized review criteria (.thincoder/advisor.md may genuinely care about docs).
|
|
579
|
-
if (!existsSync(join(agent.cwd, ADVISOR_MD_PATH)) && isDocOnlyChange(repos, agent.cwd)) {
|
|
580
|
-
return "No issues found — documentation-only changes, code review skipped."
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
const provider = resolveAdvisorProvider(agent)
|
|
584
|
-
|
|
585
|
-
// Set the advisor's cwd to the first repo (for tool context)
|
|
586
|
-
const advisorCwd = repos.length > 0 ? repos[0] : agent.cwd
|
|
587
|
-
|
|
588
|
-
const messages = prepareAdvisorMessages(agent)
|
|
589
|
-
|
|
590
|
-
try {
|
|
591
|
-
const result = await runAdvisorToolLoop(provider, messages, onOutput, signal, agent, advisorCwd)
|
|
592
|
-
// Persist the conversation: the next advisor call in this run continues here
|
|
593
|
-
// (reset by runAgent when the run ends — each task gets a fresh advisor session)
|
|
594
|
-
agent._advisorSession = messages
|
|
595
|
-
return result
|
|
596
|
-
} catch (e) {
|
|
597
|
-
if (e.name === "AbortError" && signal?.reason?.interrupt) throw e
|
|
598
|
-
return `Advisor: review failed — ${e.message || "unknown error"}. You may retry or proceed to verify.`
|
|
599
|
-
}
|
|
199
|
+
return session
|
|
600
200
|
}
|