thincoder 0.11.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/advisor.mjs +360 -72
- package/src/agent/helpers.mjs +7 -3
- package/src/agent/setup.mjs +2 -2
- package/src/agent-tools/advisor.mjs +36 -0
- package/src/agent-tools/plan.mjs +53 -2
- package/src/agent-tools/subagent.mjs +7 -1
- package/src/agent-tools/timer.mjs +1 -1
- package/src/agent-tools/verify.mjs +1 -0
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +73 -21
- package/src/auto-think.mjs +23 -5
- package/src/config.mjs +1 -1
- package/src/prompts/advisor-round1.md +23 -0
- package/src/prompts/advisor-round2.md +26 -0
- package/src/prompts/advisor-round3.md +24 -0
- package/src/prompts/coder.md +1 -0
- package/src/prompts/discipline.md +15 -1
- package/src/prompts/explore.md +2 -0
- package/src/prompts/plan.md +2 -0
- package/src/prompts/system.md +5 -1
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +6 -126
- package/src/provider/google.mjs +4 -2
- package/src/provider/sse.mjs +112 -0
- package/src/tools/bash.md +8 -0
- package/src/tools/codemode.mjs +5 -16
- package/src/tools/edit.md +8 -0
- package/src/tools/git.mjs +9 -6
- package/src/tools/read.md +7 -0
- package/src/tools/shared.mjs +16 -0
- package/src/tools/system.mjs +14 -10
- package/src/tools/web.mjs +21 -16
- package/src/tui/agent-turn.mjs +76 -64
- package/src/tui/cmd-advisor.mjs +119 -18
- package/src/tui/index.mjs +7 -187
- package/src/tui/key-handler.mjs +8 -2
- package/src/tui/layout.mjs +1 -1
- package/src/tui/render-conversation.mjs +92 -0
- package/src/tui/render-frame.mjs +27 -103
- package/src/tui/render-loop.mjs +181 -0
package/package.json
CHANGED
package/src/advisor.mjs
CHANGED
|
@@ -1,105 +1,393 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* advisor.mjs —
|
|
2
|
+
* advisor.mjs — Code review engine.
|
|
3
|
+
* Called by the advisor tool (agent-tools/advisor.mjs) when the agent
|
|
4
|
+
* explicitly requests a review at the end of a coding task.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
+
* The advisor runs as a read-only exploration sub-agent with tools
|
|
7
|
+
* (read, glob, grep, ls, git, lsp, code_search). It discovers changes
|
|
8
|
+
* via git diff, reads files for context, and traces callers via grep/lsp.
|
|
6
9
|
*
|
|
7
10
|
* Config:
|
|
8
11
|
* { advisor: { enabled: true, provider: "deepseek", model: "deepseek-chat" } }
|
|
9
12
|
* provider + model are optional — defaults to the main agent's provider/model.
|
|
13
|
+
*
|
|
14
|
+
* Convergence protocol:
|
|
15
|
+
* Round 1: full review → produces a numbered issue table.
|
|
16
|
+
* Agent responds with a response table per issue.
|
|
17
|
+
* Round 2: semi-convergence — verifies table + can flag obvious new issues.
|
|
18
|
+
* Round 3+: strict convergence — only checks the prior issue table.
|
|
19
|
+
* No hard round cap — the convergence protocol naturally limits divergence.
|
|
20
|
+
*
|
|
21
|
+
*
|
|
22
|
+
* Project customisation: .thincoder/advisor.md in the project root.
|
|
10
23
|
*/
|
|
11
24
|
import { chat } from "./provider/core.mjs"
|
|
12
25
|
import { findProvider } from "./config.mjs"
|
|
26
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
27
|
+
import { join, dirname } from "node:path"
|
|
28
|
+
import { fileURLToPath } from "node:url"
|
|
29
|
+
import { execFileSync } from "node:child_process"
|
|
30
|
+
import { toOpenAISchema } from "./tools/index.mjs"
|
|
13
31
|
|
|
14
|
-
const
|
|
32
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
15
33
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
34
|
+
const ADVISOR_MD_PATH = ".thincoder/advisor.md"
|
|
35
|
+
const MAX_TASK_SUMMARY = 500
|
|
36
|
+
const GIT_TIMEOUT = 5_000
|
|
37
|
+
|
|
38
|
+
const DEFAULT_CRITERIA = `Review the code changes, focusing on:
|
|
39
|
+
1. Correctness: logic errors, edge cases, off-by-one, incomplete modifications
|
|
40
|
+
2. Security: unhandled exceptions, null references, resource leaks, race conditions
|
|
41
|
+
3. Consistency: alignment with existing project patterns and conventions
|
|
42
|
+
4. Completeness: missing callers, imports, or follow-up changes
|
|
43
|
+
5. Maintainability: vague naming, missing comments, overly complex logic`
|
|
44
|
+
|
|
45
|
+
// ────────────────────────────────────────
|
|
46
|
+
// Advisor's read-only tool set
|
|
47
|
+
// ────────────────────────────────────────
|
|
22
48
|
|
|
23
49
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* Returns the advisor's message, or null if advisor is disabled or there's nothing to review.
|
|
50
|
+
* Restricted git tool: diff / status / log only.
|
|
51
|
+
* Checkpoint create/rewind are blocked — the advisor must not mutate state.
|
|
27
52
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
53
|
+
const { gitTool, readTool, globTool, grepTool, lsTool } = await import("./tools/index.mjs")
|
|
54
|
+
const { lspTool } = await import("./tools/lsp.mjs")
|
|
55
|
+
const { codeModeTool: codeSearchTool } = await import("./tools/codemode.mjs")
|
|
56
|
+
|
|
57
|
+
const advisorGitTool = {
|
|
58
|
+
...gitTool,
|
|
59
|
+
readonly: true,
|
|
60
|
+
async execute(args, ctx) {
|
|
61
|
+
// Block checkpoint create/rewind — advisor is read-only
|
|
62
|
+
if (args.action === "checkpoint") {
|
|
63
|
+
if (args.checkpointAction === "create" || args.checkpointAction === "rewind") {
|
|
64
|
+
return "Error: checkpoint create/rewind is disabled in advisor mode. Use diff/status/log only."
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return gitTool.execute(args, ctx)
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const ADVISOR_TOOLS = [readTool, globTool, grepTool, lsTool, advisorGitTool, lspTool, codeSearchTool]
|
|
72
|
+
const ADVISOR_TOOL_SCHEMAS = ADVISOR_TOOLS.map(toOpenAISchema)
|
|
73
|
+
const ADVISOR_TOOL_BY_NAME = new Map(ADVISOR_TOOLS.map((t) => [t.name, t]))
|
|
31
74
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
75
|
+
// ────────────────────────────────────────
|
|
76
|
+
// Prompt files — loaded at module init
|
|
77
|
+
// ────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
const ADVISOR_ROUND1 = readFileSync(join(__dirname, "prompts", "advisor-round1.md"), "utf8")
|
|
80
|
+
const ADVISOR_ROUND2 = readFileSync(join(__dirname, "prompts", "advisor-round2.md"), "utf8")
|
|
81
|
+
const ADVISOR_ROUND3 = readFileSync(join(__dirname, "prompts", "advisor-round3.md"), "utf8")
|
|
82
|
+
|
|
83
|
+
// ────────────────────────────────────────
|
|
84
|
+
// History extraction — issue/response tables
|
|
85
|
+
// ────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
const ADVISOR_TABLE_HEADER = "| # | File | Severity | Issue | Suggestion |"
|
|
88
|
+
const CONVERGENCE_TABLE_HEADER = "| # | Orig# | File | Severity | Status | Notes |"
|
|
89
|
+
const AGENT_RESPONSE_HEADER = "| # | Action | Detail |"
|
|
90
|
+
const LEGACY_ADVISOR_HEADER = "| # | 文件 | 严重程度 | 问题描述 | 建议修复 |"
|
|
91
|
+
const LEGACY_CONVERGENCE_HEADER = "| # | 原# | 文件 | 严重程度 | 当前状态 | 说明 |"
|
|
92
|
+
const LEGACY_RESPONSE_HEADER = "| # | 处理 | 详情 |"
|
|
93
|
+
const ALL_CLEAR_PHRASES = [
|
|
94
|
+
"No issues found",
|
|
95
|
+
"All issues resolved",
|
|
96
|
+
"review passed",
|
|
97
|
+
"未发现问题",
|
|
98
|
+
"所有问题已解决",
|
|
99
|
+
"审查通过",
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
export function extractPriorIssueTable(history) {
|
|
103
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
104
|
+
const m = history[i]
|
|
105
|
+
if (m.role !== "tool") continue
|
|
106
|
+
const content = typeof m.content === "string" ? m.content : ""
|
|
107
|
+
if (ALL_CLEAR_PHRASES.some((p) => content.includes(p))) return null
|
|
108
|
+
if (content.includes(ADVISOR_TABLE_HEADER) || content.includes(CONVERGENCE_TABLE_HEADER) ||
|
|
109
|
+
content.includes(LEGACY_ADVISOR_HEADER) || content.includes(LEGACY_CONVERGENCE_HEADER)) {
|
|
110
|
+
const table = extractTableBlock(content)
|
|
111
|
+
if (table) return { text: table, sinceIdx: i }
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
50
114
|
}
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
51
117
|
|
|
118
|
+
export function extractAgentResponseTable(history, sinceIdx) {
|
|
119
|
+
for (let i = sinceIdx + 1; i < history.length; i++) {
|
|
120
|
+
const m = history[i]
|
|
121
|
+
if (m.role !== "assistant") continue
|
|
122
|
+
const content = typeof m.content === "string" ? m.content : ""
|
|
123
|
+
if (content.includes(AGENT_RESPONSE_HEADER) || content.includes(LEGACY_RESPONSE_HEADER)) {
|
|
124
|
+
return extractTableBlock(content)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function extractTableBlock(text) {
|
|
131
|
+
const lines = text.split("\n")
|
|
132
|
+
let start = -1
|
|
133
|
+
for (let i = 0; i < lines.length; i++) {
|
|
134
|
+
if (lines[i].startsWith("|")) { start = i; break }
|
|
135
|
+
}
|
|
136
|
+
if (start < 0) return null
|
|
137
|
+
let end = start
|
|
138
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
139
|
+
if (lines[i].startsWith("|")) end = i
|
|
140
|
+
else break
|
|
141
|
+
}
|
|
142
|
+
return lines.slice(start, end + 1).join("\n")
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ────────────────────────────────────────
|
|
146
|
+
// Review scope — repos to review
|
|
147
|
+
// ────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Find the git repository roots that contain the agent's touched files.
|
|
151
|
+
* Falls back to cwd if no repos found.
|
|
152
|
+
*/
|
|
153
|
+
function findReviewRepos(agent) {
|
|
154
|
+
const touched = agent._touchedFiles ?? []
|
|
155
|
+
const repos = []
|
|
156
|
+
|
|
157
|
+
for (const abs of touched) {
|
|
158
|
+
try {
|
|
159
|
+
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
160
|
+
cwd: dirname(abs), encoding: "utf8", timeout: GIT_TIMEOUT,
|
|
161
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
162
|
+
}).trim()
|
|
163
|
+
if (root && !repos.includes(root)) repos.push(root)
|
|
164
|
+
} catch { /* not a git repo */ }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (repos.length > 0) return repos
|
|
168
|
+
|
|
169
|
+
// Fallback: cwd itself
|
|
52
170
|
try {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
signal: new AbortController().signal,
|
|
60
|
-
})
|
|
61
|
-
if (!response.content?.trim()) return null
|
|
171
|
+
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
172
|
+
cwd: agent.cwd, encoding: "utf8", timeout: GIT_TIMEOUT,
|
|
173
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
174
|
+
}).trim()
|
|
175
|
+
if (root) return [root]
|
|
176
|
+
} catch { /* not a git repo */ }
|
|
62
177
|
|
|
63
|
-
|
|
178
|
+
return []
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
export function loadAdvisorMd(cwd) {
|
|
184
|
+
const path = join(cwd, ADVISOR_MD_PATH)
|
|
185
|
+
if (!existsSync(path)) return DEFAULT_CRITERIA
|
|
186
|
+
try {
|
|
187
|
+
const content = readFileSync(path, "utf8").trim()
|
|
188
|
+
return content || DEFAULT_CRITERIA
|
|
64
189
|
} catch {
|
|
65
|
-
|
|
66
|
-
return null
|
|
190
|
+
return DEFAULT_CRITERIA
|
|
67
191
|
}
|
|
68
192
|
}
|
|
69
193
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (
|
|
194
|
+
function extractTaskSummary(history) {
|
|
195
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
196
|
+
const m = history[i]
|
|
197
|
+
if (m.role !== "user") continue
|
|
198
|
+
const content = typeof m.content === "string" ? m.content : ""
|
|
199
|
+
if (content.startsWith("[System reminder:") || content.startsWith("[User interrupt:")) continue
|
|
200
|
+
const firstPara = content.split("\n\n")[0]
|
|
201
|
+
return firstPara.length > MAX_TASK_SUMMARY ? firstPara.slice(0, MAX_TASK_SUMMARY) + "…" : firstPara
|
|
74
202
|
}
|
|
75
|
-
return
|
|
203
|
+
return null
|
|
76
204
|
}
|
|
77
205
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
lines.push("## Last user message")
|
|
82
|
-
lines.push(truncate(String(history[lastUserIdx]?.content ?? "")))
|
|
83
|
-
lines.push("")
|
|
84
|
-
lines.push("## Assistant response and tool calls")
|
|
206
|
+
// ────────────────────────────────────────
|
|
207
|
+
// System prompt routing
|
|
208
|
+
// ────────────────────────────────────────
|
|
85
209
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
210
|
+
export function buildAdvisorSystemPrompt(agent, _prior) {
|
|
211
|
+
const prior = _prior ?? extractPriorIssueTable(agent.history)
|
|
212
|
+
if (!prior || (agent._advisorRound || 0) === 0) return ADVISOR_ROUND1
|
|
213
|
+
const round = (agent._advisorRound || 0) + 1
|
|
214
|
+
if (round === 2) return ADVISOR_ROUND2
|
|
215
|
+
return ADVISOR_ROUND3
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ────────────────────────────────────────
|
|
219
|
+
// User message building
|
|
220
|
+
// ────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
export function buildAdvisorUserMessage(agent, _prior) {
|
|
223
|
+
const prior = _prior ?? extractPriorIssueTable(agent.history)
|
|
224
|
+
|
|
225
|
+
// Repos to review
|
|
226
|
+
const repos = findReviewRepos(agent)
|
|
227
|
+
const repoList = repos.length > 0
|
|
228
|
+
? repos.map((r, i) => `${i + 1}. ${r}`).join("\n")
|
|
229
|
+
: `(no git repository — working directory: ${agent.cwd})`
|
|
230
|
+
|
|
231
|
+
const parts = []
|
|
232
|
+
|
|
233
|
+
// Convergence data (round 2+)
|
|
234
|
+
if (prior && (agent._advisorRound || 0) > 0) {
|
|
235
|
+
const response = extractAgentResponseTable(agent.history, prior.sinceIdx)
|
|
236
|
+
|| "(Agent did not provide a response table — re-evaluate each issue)"
|
|
237
|
+
const round = (agent._advisorRound || 0) + 1
|
|
238
|
+
const label = round === 2 ? "Verify Prior Table + Flag New Issues" : "Strict Verification"
|
|
239
|
+
parts.push(`## Round ${round} — ${label}`)
|
|
240
|
+
parts.push("")
|
|
241
|
+
parts.push("## Prior Issue Table")
|
|
242
|
+
parts.push(prior.text)
|
|
243
|
+
parts.push("")
|
|
244
|
+
parts.push("## Agent Response")
|
|
245
|
+
parts.push(response)
|
|
246
|
+
parts.push("")
|
|
247
|
+
parts.push("---")
|
|
248
|
+
parts.push("")
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Review scope
|
|
252
|
+
parts.push("## Review Scope")
|
|
253
|
+
parts.push(`Review the following git repositor${repos.length === 1 ? "y" : "ies"}:`)
|
|
254
|
+
parts.push(repoList)
|
|
255
|
+
parts.push("")
|
|
256
|
+
|
|
257
|
+
// Task summary
|
|
258
|
+
const taskSummary = extractTaskSummary(agent.history)
|
|
259
|
+
if (taskSummary) {
|
|
260
|
+
parts.push("## Task")
|
|
261
|
+
parts.push(taskSummary)
|
|
262
|
+
parts.push("")
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Review criteria
|
|
266
|
+
const criteria = loadAdvisorMd(agent.cwd)
|
|
267
|
+
parts.push("## Review Criteria")
|
|
268
|
+
parts.push(criteria)
|
|
269
|
+
parts.push("")
|
|
270
|
+
|
|
271
|
+
// Instructions
|
|
272
|
+
parts.push("## Instructions")
|
|
273
|
+
parts.push("1. Read `AGENTS.md` and any design documents first — understand project conventions, version requirements, and architecture decisions before flagging issues.")
|
|
274
|
+
parts.push("2. Run `git diff HEAD` in each repo to discover uncommitted changes.")
|
|
275
|
+
parts.push("3. `read` changed files for full context beyond the diff.")
|
|
276
|
+
parts.push("4. Use `grep` or `lsp` to trace callers, imports, and dependencies.")
|
|
277
|
+
parts.push("5. Produce your review table based on the review criteria above.")
|
|
278
|
+
parts.push("Do NOT flag features that are valid under the project's stated platform requirements.")
|
|
279
|
+
|
|
280
|
+
return parts.join("\n")
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ────────────────────────────────────────
|
|
284
|
+
// Advisor tool loop
|
|
285
|
+
// ────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Run the advisor's tool loop: chat → execute tools → repeat.
|
|
289
|
+
* Stops when the model produces text without tool calls.
|
|
290
|
+
*/
|
|
291
|
+
async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, cwd) {
|
|
292
|
+
while (true) {
|
|
293
|
+
const response = await chat(provider, {
|
|
294
|
+
messages,
|
|
295
|
+
tools: ADVISOR_TOOL_SCHEMAS,
|
|
296
|
+
signal: (signal && !signal.aborted) ? signal : new AbortController().signal,
|
|
297
|
+
onToken: onOutput,
|
|
298
|
+
onReasoning: onOutput,
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
// No tool calls — this is the final review text
|
|
302
|
+
if (!response.toolCalls?.length) {
|
|
303
|
+
if (!response.content?.trim()) return "Advisor: (empty response — review was inconclusive)"
|
|
304
|
+
return response.content.trim()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Push assistant message with tool calls
|
|
308
|
+
messages.push({
|
|
309
|
+
role: "assistant",
|
|
310
|
+
content: response.content || null,
|
|
311
|
+
tool_calls: response.toolCalls.map((tc) => ({
|
|
312
|
+
id: tc.id, type: "function",
|
|
313
|
+
function: { name: tc.name, arguments: tc.arguments },
|
|
314
|
+
})),
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
// Execute each tool call
|
|
318
|
+
for (const tc of response.toolCalls) {
|
|
319
|
+
const tool = ADVISOR_TOOL_BY_NAME.get(tc.name)
|
|
320
|
+
let result
|
|
321
|
+
if (!tool) {
|
|
322
|
+
result = `Error: unknown tool "${tc.name}". Available: ${[...ADVISOR_TOOL_BY_NAME.keys()].join(", ")}`
|
|
323
|
+
} else {
|
|
324
|
+
try {
|
|
325
|
+
const args = JSON.parse(tc.arguments || "{}")
|
|
326
|
+
result = await tool.execute(args, {
|
|
327
|
+
cwd,
|
|
328
|
+
agent,
|
|
329
|
+
onOutput,
|
|
330
|
+
signal,
|
|
331
|
+
})
|
|
332
|
+
} catch (e) {
|
|
333
|
+
result = `Error: ${e.message}`
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: String(result) })
|
|
95
337
|
}
|
|
96
338
|
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ────────────────────────────────────────
|
|
342
|
+
// Main entry point
|
|
343
|
+
// ────────────────────────────────────────
|
|
97
344
|
|
|
98
|
-
|
|
345
|
+
export function resolveAdvisorProvider(agent) {
|
|
346
|
+
const cfg = agent.config?.advisor
|
|
347
|
+
if (cfg?.provider) {
|
|
348
|
+
try {
|
|
349
|
+
const provider = findProvider(agent.providers ?? [agent.provider], cfg.provider)
|
|
350
|
+
const result = cfg.model ? { ...provider, model: cfg.model } : { ...provider }
|
|
351
|
+
if (cfg.thinking === null) result.thinking = undefined // explicitly off
|
|
352
|
+
else if (cfg.thinking !== undefined) result.thinking = cfg.thinking
|
|
353
|
+
if (cfg.reasoningEffort !== undefined) result.reasoningEffort = cfg.reasoningEffort
|
|
354
|
+
return result
|
|
355
|
+
} catch {
|
|
356
|
+
// Provider not found — fall back to main provider
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const provider = { ...agent.provider }
|
|
360
|
+
if (cfg?.model) provider.model = cfg.model
|
|
361
|
+
if (cfg?.thinking === null) provider.thinking = undefined // explicitly off
|
|
362
|
+
else if (cfg?.thinking !== undefined) provider.thinking = cfg.thinking
|
|
363
|
+
if (cfg?.reasoningEffort !== undefined) provider.reasoningEffort = cfg.reasoningEffort
|
|
364
|
+
return provider
|
|
99
365
|
}
|
|
100
366
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
|
|
367
|
+
export async function runAdvisorReview(agent, onOutput, signal) {
|
|
368
|
+
const cfg = agent.config?.advisor
|
|
369
|
+
if (!cfg?.enabled) return null
|
|
370
|
+
|
|
371
|
+
const repos = findReviewRepos(agent)
|
|
372
|
+
if ((agent._touchedFiles ?? []).length === 0) return null
|
|
373
|
+
|
|
374
|
+
const provider = resolveAdvisorProvider(agent)
|
|
375
|
+
const priorIssue = extractPriorIssueTable(agent.history)
|
|
376
|
+
const systemPrompt = buildAdvisorSystemPrompt(agent, priorIssue)
|
|
377
|
+
const userMessage = buildAdvisorUserMessage(agent, priorIssue)
|
|
378
|
+
|
|
379
|
+
// Set the advisor's cwd to the first repo (for tool context)
|
|
380
|
+
const advisorCwd = repos.length > 0 ? repos[0] : agent.cwd
|
|
381
|
+
|
|
382
|
+
const messages = [
|
|
383
|
+
{ role: "system", content: systemPrompt },
|
|
384
|
+
{ role: "user", content: userMessage },
|
|
385
|
+
]
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
return await runAdvisorToolLoop(provider, messages, onOutput, signal, agent, advisorCwd)
|
|
389
|
+
} catch (e) {
|
|
390
|
+
if (e.name === "AbortError" && signal?.reason?.interrupt) throw e
|
|
391
|
+
return `Advisor: review failed — ${e.message || "unknown error"}. You may retry or proceed to verify.`
|
|
392
|
+
}
|
|
105
393
|
}
|
package/src/agent/helpers.mjs
CHANGED
|
@@ -12,8 +12,12 @@ export const DEFAULT_SUBAGENT_TURNS = 100
|
|
|
12
12
|
export const DEFAULT_GOAL_TURNS = 200
|
|
13
13
|
export const MIN_REPORT_CHARS = 200
|
|
14
14
|
export const REPORT_CONTINUATION =
|
|
15
|
-
"Your report
|
|
16
|
-
"
|
|
15
|
+
"Your report was sent back: too brief to be a complete handoff — the parent agent sees nothing else from your run. " +
|
|
16
|
+
"Rewrite your final message as a checklist:\n" +
|
|
17
|
+
"1. What you changed and why\n" +
|
|
18
|
+
"2. The path of every file you touched\n" +
|
|
19
|
+
"3. How you verified (tests run, commands executed, with results)\n" +
|
|
20
|
+
"4. Anything left undone or worth follow-up"
|
|
17
21
|
|
|
18
22
|
const TOOL_RESULT_OFFLOAD_LIMIT = 16_000
|
|
19
23
|
const TOOL_RESULT_PREVIEW = 2_000
|
|
@@ -22,7 +26,7 @@ const GIT_TIMEOUT_MS = 5000
|
|
|
22
26
|
const MAX_GIT_CHANGES_DISPLAY = 20
|
|
23
27
|
|
|
24
28
|
export const OUTLINE_INJECT_PREFIX = "[System reminder: project dependency outline:"
|
|
25
|
-
export const FILE_MUTATORS = new Set(["write", "edit", "insert_after", "apply_patch", "delete"])
|
|
29
|
+
export const FILE_MUTATORS = new Set(["write", "edit", "insert_after", "apply_patch", "delete", "hashline_edit"])
|
|
26
30
|
|
|
27
31
|
/** Escape XML special characters in a string for safe embedding in XML/HTML */
|
|
28
32
|
export function escapeXml(s) {
|
package/src/agent/setup.mjs
CHANGED
|
@@ -117,8 +117,8 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
// task/plan tools are injected with the main loop; subagent/skill/goal/verify only at top level
|
|
120
|
-
const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool } = await import("../agent-tools.mjs")
|
|
121
|
-
const tools = [...agent.tools, taskTool, planTool, timerTool, ...(depth === 0 ? [subagentTool, skillTool, goalTool, verifyTool, recentChangesTool] : [])]
|
|
120
|
+
const { planTool, subagentTool, taskTool, skillTool, goalTool, verifyTool, recentChangesTool, timerTool, advisorTool } = await import("../agent-tools.mjs")
|
|
121
|
+
const tools = [...agent.tools, taskTool, planTool, timerTool, ...(depth === 0 ? [subagentTool, skillTool, goalTool, verifyTool, recentChangesTool, advisorTool] : [])]
|
|
122
122
|
const toolSchemas = tools.map(toOpenAISchema)
|
|
123
123
|
const toolByName = new Map(tools.map((t) => [t.name, t]))
|
|
124
124
|
agent._onTaskUpdate = callbacks.onTaskUpdate
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-tools/advisor.mjs — advisor tool wrapper.
|
|
3
|
+
* The agent calls this explicitly at the end of a coding task to get a code review.
|
|
4
|
+
*/
|
|
5
|
+
import { runAdvisorReview } from "../advisor.mjs"
|
|
6
|
+
|
|
7
|
+
export const advisorTool = {
|
|
8
|
+
name: "advisor",
|
|
9
|
+
description:
|
|
10
|
+
"Run a code review on your changes (convergence protocol). " +
|
|
11
|
+
"Call this when you have finished coding and want an independent review before finalising. " +
|
|
12
|
+
"The advisor is an independent read-only sub-agent that explores the codebase, runs git diff, " +
|
|
13
|
+
"reads files, and traces callers via grep/lsp. " +
|
|
14
|
+
"Review criteria come from .thincoder/advisor.md (if present) or sensible defaults. " +
|
|
15
|
+
"Round 1 does a full review and produces a numbered issue table. " +
|
|
16
|
+
"After the review, you MUST produce a response table (see discipline rules for format). " +
|
|
17
|
+
"Round 2 verifies the table + can flag obvious new issues. " +
|
|
18
|
+
"Round 3+ strictly checks only the prior table — convergence, not divergence. " +
|
|
19
|
+
"If issues are found, fix them, update your response table, then re-run advisor. " +
|
|
20
|
+
"If advisor says all clear, call verify.",
|
|
21
|
+
parameters: {
|
|
22
|
+
type: "object",
|
|
23
|
+
properties: {},
|
|
24
|
+
},
|
|
25
|
+
readonly: true,
|
|
26
|
+
sideEffectExempt: true,
|
|
27
|
+
outputPanel: true,
|
|
28
|
+
async execute(_args, ctx) {
|
|
29
|
+
const agent = ctx.agent
|
|
30
|
+
|
|
31
|
+
const result = await runAdvisorReview(agent, ctx.onOutput, ctx.signal)
|
|
32
|
+
if (!result) return "Advisor: review is disabled or no changes to review."
|
|
33
|
+
|
|
34
|
+
return result
|
|
35
|
+
},
|
|
36
|
+
}
|
package/src/agent-tools/plan.mjs
CHANGED
|
@@ -2,7 +2,56 @@
|
|
|
2
2
|
* plan tool: enter/exit plan mode.
|
|
3
3
|
* In plan mode only read-only tools are allowed — explore code, design solutions, no code writing.
|
|
4
4
|
* After the user approves the plan, exit plan mode and start implementing.
|
|
5
|
+
*
|
|
6
|
+
* Reminder cadence (kimi-code style): while plan mode is active the agent loop
|
|
7
|
+
* re-injects reminders — sparse every 2 turns, full every 5 turns or when the
|
|
8
|
+
* user sends a new message — so the constraint never fades from context.
|
|
5
9
|
*/
|
|
10
|
+
|
|
11
|
+
export const PLAN_FULL_REMINDER =
|
|
12
|
+
"[System reminder: plan mode is ON. Workflow: (1) explore/read codebase with read-only tools, " +
|
|
13
|
+
"(2) design a solution considering trade-offs, (3) present your plan by calling plan with action='exit' " +
|
|
14
|
+
"so the user can approve it. Only read-only tools are allowed — do not write, edit, or run mutation commands. " +
|
|
15
|
+
"Your turn must end with either a clarifying question to the user or a call to plan with action='exit'.]"
|
|
16
|
+
|
|
17
|
+
export const PLAN_SPARSE_REMINDER =
|
|
18
|
+
"[System reminder: plan mode still active — read-only tools only (the current plan file exempt). " +
|
|
19
|
+
"Design the solution, then call plan with action='exit' for user approval.]"
|
|
20
|
+
|
|
21
|
+
export const PLAN_EXIT_REMINDER =
|
|
22
|
+
"[System reminder: plan mode is now OFF. Start implementing your plan — edit files, run commands. " +
|
|
23
|
+
"No need for a task list (plan already covered that) or further confirmation.]"
|
|
24
|
+
|
|
25
|
+
/** Turns between reminder re-injections while plan mode is active */
|
|
26
|
+
const SPARSE_INTERVAL = 2
|
|
27
|
+
const FULL_INTERVAL = 5
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Decide which plan-mode reminder (if any) to inject this turn.
|
|
31
|
+
* @param {object} agent — the agent object (mutated: tracks reminder state)
|
|
32
|
+
* @param {boolean} userMessageSince — whether a user message arrived since the last reminder
|
|
33
|
+
* @returns {string|null} reminder text or null
|
|
34
|
+
*/
|
|
35
|
+
export function planReminderForTurn(agent, userMessageSince) {
|
|
36
|
+
if (!agent.planMode) {
|
|
37
|
+
agent._planTurnsSinceReminder = 0
|
|
38
|
+
agent._planTurnsSinceSparse = 0
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
agent._planTurnsSinceReminder = (agent._planTurnsSinceReminder ?? 0) + 1
|
|
42
|
+
agent._planTurnsSinceSparse = (agent._planTurnsSinceSparse ?? 0) + 1
|
|
43
|
+
if (userMessageSince || agent._planTurnsSinceReminder >= FULL_INTERVAL) {
|
|
44
|
+
agent._planTurnsSinceReminder = 0
|
|
45
|
+
agent._planTurnsSinceSparse = 0
|
|
46
|
+
return PLAN_FULL_REMINDER
|
|
47
|
+
}
|
|
48
|
+
if (agent._planTurnsSinceSparse >= SPARSE_INTERVAL) {
|
|
49
|
+
agent._planTurnsSinceSparse = 0
|
|
50
|
+
return PLAN_SPARSE_REMINDER
|
|
51
|
+
}
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
6
55
|
export const planTool = {
|
|
7
56
|
name: "plan",
|
|
8
57
|
description:
|
|
@@ -18,13 +67,15 @@ export const planTool = {
|
|
|
18
67
|
async execute(args, ctx) {
|
|
19
68
|
if (args.action === "exit") {
|
|
20
69
|
ctx.agent.planMode = false
|
|
70
|
+
ctx.agent._planTurnsSinceReminder = 0
|
|
21
71
|
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
22
|
-
ctx.agent._pendingReminders.push(
|
|
72
|
+
ctx.agent._pendingReminders.push(PLAN_EXIT_REMINDER)
|
|
23
73
|
return "Plan mode exited. You may now edit files and run commands."
|
|
24
74
|
}
|
|
25
75
|
ctx.agent.planMode = true
|
|
76
|
+
ctx.agent._planTurnsSinceReminder = 0
|
|
26
77
|
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
27
|
-
ctx.agent._pendingReminders.push(
|
|
78
|
+
ctx.agent._pendingReminders.push(PLAN_FULL_REMINDER)
|
|
28
79
|
return "Plan mode activated. You are now restricted to READ-ONLY tools. Explore the codebase, understand the architecture, design a solution. Present your plan to the user for approval before writing any code."
|
|
29
80
|
},
|
|
30
81
|
}
|
|
@@ -16,7 +16,13 @@ import {
|
|
|
16
16
|
export const subagentTool = {
|
|
17
17
|
name: "subagent",
|
|
18
18
|
description:
|
|
19
|
-
"Spawn a sub-agent to handle an independent subtask in an isolated context. The sub-agent returns only its final report. Spawn MULTIPLE subagents in the SAME response for parallel work—they run concurrently
|
|
19
|
+
"Spawn a sub-agent to handle an independent subtask in an isolated context. The sub-agent returns only its final report. Spawn MULTIPLE subagents in the SAME response for parallel work—they run concurrently.\n" +
|
|
20
|
+
"Use role='explore' for codebase search/analysis (read-only, fast), role='plan' for read-only implementation planning (returns a step-by-step plan, never edits), role='coder' for self-contained implementation tasks. Do not give parallel subagents tasks that edit the same files.\n\n" +
|
|
21
|
+
"Writing the prompt:\n" +
|
|
22
|
+
"- The sub-agent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics.\n" +
|
|
23
|
+
"- Put exact paths and commands in the prompt when you know them. The sub-agent should not search for things you already know.\n" +
|
|
24
|
+
"- Do not delegate understanding: if the task hinges on a file path or line number, find it yourself first and write it into the prompt.\n" +
|
|
25
|
+
"- Once a sub-agent is running, leave that scope to it: don't redo its searches in parallel, and don't abandon it midway to finish manually.",
|
|
20
26
|
parameters: {
|
|
21
27
|
type: "object",
|
|
22
28
|
properties: {
|
|
@@ -29,7 +29,7 @@ export const timerTool = {
|
|
|
29
29
|
readonly: true,
|
|
30
30
|
sideEffectExempt: true,
|
|
31
31
|
execute(args, ctx) {
|
|
32
|
-
const seconds = args.seconds ??
|
|
32
|
+
const seconds = args.seconds ?? 180
|
|
33
33
|
const expiresAt = Date.now() + seconds * 1000
|
|
34
34
|
const message = args.message || `⏰ Time's up (${seconds}s). Have you tried running the code, adding a console.log, or checking the output? Thinking more without data is guessing.`
|
|
35
35
|
|