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/src/agent-tools.mjs
CHANGED
|
@@ -11,3 +11,4 @@ export { goalTool } from "./agent-tools/goal.mjs"
|
|
|
11
11
|
export { verifyTool } from "./agent-tools/verify.mjs"
|
|
12
12
|
export { recentChangesTool } from "./agent-tools/recent-changes.mjs"
|
|
13
13
|
export { timerTool } from "./agent-tools/timer.mjs"
|
|
14
|
+
export { advisorTool } from "./agent-tools/advisor.mjs"
|
package/src/agent.mjs
CHANGED
|
@@ -31,7 +31,7 @@ export const EXPLORE_OVERLAY = _EXPLORE
|
|
|
31
31
|
export const CODER_OVERLAY = _CODER
|
|
32
32
|
export const PLAN_OVERLAY = _PLAN
|
|
33
33
|
|
|
34
|
-
//
|
|
34
|
+
// exported for consumption by agent-tools.mjs
|
|
35
35
|
export {
|
|
36
36
|
ContinueError,
|
|
37
37
|
repairHistory, listWorkDir, loadProjectInstructions,
|
|
@@ -61,8 +61,8 @@ export function createAgent({
|
|
|
61
61
|
provider, tools, config, cwd, memory, _role: role,
|
|
62
62
|
overlay, tasks, history,
|
|
63
63
|
planMode, autoApprove, goal,
|
|
64
|
-
_mutatedThisRun: false, _verifiedThisRun: false, _verifyPassed: undefined,
|
|
65
|
-
_touchedFiles: [], _verifyRetries: 0,
|
|
64
|
+
_mutatedThisRun: false, _verifiedThisRun: false, _verifyPassed: undefined, _calledAdvisorThisRun: false,
|
|
65
|
+
_touchedFiles: [], _verifyRetries: 0, _advisorRound: 0,
|
|
66
66
|
_pendingReminders: [],
|
|
67
67
|
_pendingTimers: [],
|
|
68
68
|
_sessionStart: sessionStart,
|
|
@@ -81,11 +81,16 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
81
81
|
agent._mutatedThisRun = false
|
|
82
82
|
agent._verifiedThisRun = false
|
|
83
83
|
agent._verifyPassed = undefined
|
|
84
|
+
agent._calledAdvisorThisRun = false
|
|
84
85
|
agent._touchedFiles = []
|
|
85
86
|
agent._verifyRetries = 0
|
|
87
|
+
agent._advisorRound = 0
|
|
86
88
|
let guardPushbacks = 0
|
|
87
89
|
let honestReminderInjected = false
|
|
88
90
|
const recentCallSigs = []
|
|
91
|
+
// repeat: "once" stream rules fire at most once per runAgent call (user turn):
|
|
92
|
+
// this set survives across chat() calls (rule abort-retry, tool loop) within the turn.
|
|
93
|
+
const streamRuleFired = new Set()
|
|
89
94
|
|
|
90
95
|
for (let turn = 0; turn < maxTurns; turn++) {
|
|
91
96
|
|
|
@@ -94,6 +99,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
94
99
|
try {
|
|
95
100
|
if (await compressIfNeeded(agent, threshold, callbacks)) {
|
|
96
101
|
agent._compressFailures = 0
|
|
102
|
+
agent._planReminderAtLen = 0 // After compression history shrinks, reset cadence so reminders resume
|
|
97
103
|
recentCallSigs.length = 0 // After compression history is rebuilt, reset stall detection counter
|
|
98
104
|
callbacks.onCompress?.()
|
|
99
105
|
if (agent.autoApprove && !agent.history.some((m) => m.content === AUTO_REMINDER)) {
|
|
@@ -111,6 +117,24 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
111
117
|
}
|
|
112
118
|
}
|
|
113
119
|
|
|
120
|
+
// Plan-mode reminder cadence: re-inject constraint reminders while plan mode is active
|
|
121
|
+
// (sparse every 2 turns, full every 5 turns or when the user sends a new message),
|
|
122
|
+
// so the read-only restriction never fades from context.
|
|
123
|
+
if (agent.planMode) {
|
|
124
|
+
const lastMsg = agent.history.at(-1)
|
|
125
|
+
const realUserMsg = lastMsg?.role === "user"
|
|
126
|
+
&& typeof lastMsg.content === "string"
|
|
127
|
+
&& !lastMsg.content.startsWith("[System reminder:")
|
|
128
|
+
&& !lastMsg.content.startsWith("[User interrupt:")
|
|
129
|
+
const newUserSince = realUserMsg && agent.history.length > (agent._planReminderAtLen ?? 0)
|
|
130
|
+
const { planReminderForTurn } = await import("./agent-tools/plan.mjs")
|
|
131
|
+
const reminder = planReminderForTurn(agent, newUserSince)
|
|
132
|
+
if (reminder) {
|
|
133
|
+
agent._planReminderAtLen = agent.history.length + 1
|
|
134
|
+
agent.history.push({ role: "user", content: reminder, transient: true })
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
114
138
|
const messages = [{ role: "system", content: systemPrompt }, ...agent.history]
|
|
115
139
|
let response
|
|
116
140
|
|
|
@@ -129,15 +153,18 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
129
153
|
onWait: callbacks.onWait,
|
|
130
154
|
signal,
|
|
131
155
|
streamRules: agent.config.agent?.streamRules ?? [],
|
|
156
|
+
firedPatterns: streamRuleFired,
|
|
132
157
|
})
|
|
133
158
|
} catch (e) {
|
|
134
159
|
// User interrupt (Ctrl+I): controller.abort({ interrupt: true, message: "…" }).
|
|
135
160
|
// Inject the message into history and let the outer loop recreate the controller.
|
|
136
161
|
if (e.name === "AbortError" && signal?.reason?.interrupt) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
162
|
+
const msg = `[User interrupt: ${signal.reason.message}]`
|
|
163
|
+
// Dedup: if the interrupt was already handled during tool execution (L302-310),
|
|
164
|
+
// don't push a duplicate — the outer loop will still recreate the controller.
|
|
165
|
+
if (agent.history.at(-1)?.content !== msg) {
|
|
166
|
+
agent.history.push({ role: "user", content: msg })
|
|
167
|
+
}
|
|
141
168
|
}
|
|
142
169
|
throw e
|
|
143
170
|
}
|
|
@@ -219,6 +246,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
219
246
|
role: "user",
|
|
220
247
|
content: `[System reminder: you still have pending tasks: ${pending}. Update their status with the task tool before finishing — if they're done, mark them done; if they're not applicable, remove them.]`,
|
|
221
248
|
})
|
|
249
|
+
callbacks.onTurnEnd?.(agent, turn)
|
|
222
250
|
continue
|
|
223
251
|
}
|
|
224
252
|
// --- verify guard: push model to verify mutated files before completion ---
|
|
@@ -230,15 +258,15 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
230
258
|
role: "user",
|
|
231
259
|
content: "[System reminder: you modified files in this run but have not verified the changes. Before finishing: call the verify tool to run syntax checks and tests. If verify reports failures, fix them and run verify again. If verification is genuinely impossible here, say so explicitly in your reply.]",
|
|
232
260
|
})
|
|
261
|
+
callbacks.onTurnEnd?.(agent, turn)
|
|
233
262
|
continue
|
|
234
|
-
}
|
|
235
|
-
if (agent._verifiedThisRun && agent._verifyPassed === false && agent._verifyRetries < MAX_VERIFY_RETRIES) {
|
|
236
263
|
agent._verifyRetries++
|
|
237
264
|
agent.history.push({ role: "assistant", content: response.content })
|
|
238
265
|
agent.history.push({
|
|
239
266
|
role: "user",
|
|
240
267
|
content: `[System reminder: verify reported test failures (retry ${agent._verifyRetries}/${MAX_VERIFY_RETRIES}). Review the failures, fix the issues, then run verify again. If you cannot fix after ${MAX_VERIFY_RETRIES} attempts, explain honestly what's blocking you.]`,
|
|
241
268
|
})
|
|
269
|
+
callbacks.onTurnEnd?.(agent, turn)
|
|
242
270
|
continue
|
|
243
271
|
}
|
|
244
272
|
if (agent._verifyPassed === false && agent._verifyRetries >= MAX_VERIFY_RETRIES) {
|
|
@@ -252,6 +280,22 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
252
280
|
role: "user",
|
|
253
281
|
content: `[System reminder: ${MAX_VERIFY_RETRIES} verify attempts exhausted and tests are still failing. In your response to the user, you MUST state explicitly: (1) what tests are still failing, (2) what you tried, (3) what you believe the root cause is. Do not present this as complete — the user needs to know the work is unfinished.]`,
|
|
254
282
|
})
|
|
283
|
+
callbacks.onTurnEnd?.(agent, turn)
|
|
284
|
+
continue
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// --- advisor guard: push model to review changes before completion ---
|
|
288
|
+
// When advisor is enabled and guard is not explicitly disabled, mutated files
|
|
289
|
+
// must be reviewed before the turn ends. No hard round cap — convergence protocol
|
|
290
|
+
// (round 3+ strict verification) naturally limits divergence.
|
|
291
|
+
if (depth === 0 && agent.config?.advisor?.enabled && agent.config?.advisor?.guard !== false) {
|
|
292
|
+
if (agent._mutatedThisRun && !agent._calledAdvisorThisRun && (agent._touchedFiles ?? []).length > 0) {
|
|
293
|
+
agent.history.push({ role: "assistant", content: response.content })
|
|
294
|
+
agent.history.push({
|
|
295
|
+
role: "user",
|
|
296
|
+
content: `[System reminder: you changed code in this run but haven't reviewed with advisor (round ${agent._advisorRound + 1}). Call the \`advisor\` tool to get an independent code review. After the review, produce a response table for every issue found (see discipline rules for format). If the changes are trivial (typo, one-liner, formatting only), you may skip and explain why in your reply.]`,
|
|
297
|
+
})
|
|
298
|
+
callbacks.onTurnEnd?.(agent, turn)
|
|
255
299
|
continue
|
|
256
300
|
}
|
|
257
301
|
}
|
|
@@ -276,6 +320,17 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
276
320
|
|
|
277
321
|
const results = await executeToolCalls(agent, toolByName, response.toolCalls, callbacks, depth, signal)
|
|
278
322
|
|
|
323
|
+
// Ctrl+I interrupt during tool execution: skip committing partial results —
|
|
324
|
+
// the tool failure messages would mislead the model. Inject the interrupt and retry.
|
|
325
|
+
if (signal?.reason?.interrupt) {
|
|
326
|
+
agent.history.push({
|
|
327
|
+
role: "user",
|
|
328
|
+
content: `[User interrupt: ${signal.reason.message}]`,
|
|
329
|
+
})
|
|
330
|
+
callbacks.onTurnEnd?.(agent, turn)
|
|
331
|
+
continue
|
|
332
|
+
}
|
|
333
|
+
|
|
279
334
|
// Model is executing tools → doing real work, reset guard pushback counter
|
|
280
335
|
guardPushbacks = 0
|
|
281
336
|
|
|
@@ -308,8 +363,16 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
308
363
|
}
|
|
309
364
|
agent.history.push({ role: "tool", tool_call_id: toolCall.id, content: result })
|
|
310
365
|
if (tool && ok) {
|
|
311
|
-
if (!tool.readonly && !tool.sideEffectExempt)
|
|
366
|
+
if (!tool.readonly && !tool.sideEffectExempt) {
|
|
367
|
+
agent._mutatedThisRun = true
|
|
368
|
+
// Any mutation after advisor invalidates the review — need re-review
|
|
369
|
+
if (agent._calledAdvisorThisRun) agent._calledAdvisorThisRun = false
|
|
370
|
+
}
|
|
312
371
|
if (toolCall.name === "verify") agent._verifiedThisRun = true
|
|
372
|
+
if (toolCall.name === "advisor") {
|
|
373
|
+
agent._calledAdvisorThisRun = true
|
|
374
|
+
agent._advisorRound++ // advance convergence round
|
|
375
|
+
}
|
|
313
376
|
if (FILE_MUTATORS.has(toolCall.name)) {
|
|
314
377
|
const args = JSON.parse(toolCall.arguments)
|
|
315
378
|
const paths = tool.touchedPaths ? tool.touchedPaths(args) : [args.path]
|
|
@@ -386,17 +449,6 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
386
449
|
}
|
|
387
450
|
|
|
388
451
|
callbacks.onTurnEnd?.(agent, turn)
|
|
389
|
-
|
|
390
|
-
// Advisor: automated code review after each tool-execution turn.
|
|
391
|
-
// Runs asynchronously — failure is silent, main loop continues regardless.
|
|
392
|
-
if (agent.config?.advisor?.enabled) {
|
|
393
|
-
const { runAdvisor } = await import("./advisor.mjs")
|
|
394
|
-
const note = await runAdvisor(agent)
|
|
395
|
-
if (note) {
|
|
396
|
-
agent.history.push({ role: "user", content: note })
|
|
397
|
-
callbacks.onAdvisor?.(note)
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
452
|
}
|
|
401
453
|
|
|
402
454
|
throw new ContinueError(maxTurns)
|
package/src/auto-think.mjs
CHANGED
|
@@ -31,6 +31,26 @@ const EFFORT_MAP = {
|
|
|
31
31
|
high: ["max", "max", "xhigh", "max"],
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Build classifier input from history: the latest real user message (reminders and
|
|
36
|
+
* interrupt injections excluded), plus the previous user message as context when the
|
|
37
|
+
* latest is too short to classify on its own (e.g. "继续" / "还有几个问题").
|
|
38
|
+
* Exported for tests.
|
|
39
|
+
*/
|
|
40
|
+
export function buildClassifierInput(history) {
|
|
41
|
+
const isRealUser = (m) =>
|
|
42
|
+
m.role === "user" && typeof m.content === "string"
|
|
43
|
+
&& !m.content.startsWith("[System reminder:") && !m.content.startsWith("[User interrupt:")
|
|
44
|
+
const users = history.filter(isRealUser)
|
|
45
|
+
const last = users.at(-1)
|
|
46
|
+
if (!last) return null
|
|
47
|
+
let prompt = last.content
|
|
48
|
+
if (prompt.length < 200 && users.length > 1) {
|
|
49
|
+
prompt = `Previous request (context):\n${users.at(-2).content.slice(0, 1200)}\n\nLatest message:\n${prompt}`
|
|
50
|
+
}
|
|
51
|
+
return prompt.slice(0, 2000)
|
|
52
|
+
}
|
|
53
|
+
|
|
34
54
|
/**
|
|
35
55
|
* Classify the difficulty of the user's prompt and adjust reasoning effort.
|
|
36
56
|
* Only runs on the first turn (turn === 0) of a user message.
|
|
@@ -47,10 +67,8 @@ export async function classifyAndApply(agent, turn) {
|
|
|
47
67
|
const validEfforts = spec.reasoningEffortEnum
|
|
48
68
|
if (!validEfforts) return null // Model doesn't support reasoning effort
|
|
49
69
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (!lastUser) return null
|
|
53
|
-
const prompt = typeof lastUser.content === "string" ? lastUser.content : ""
|
|
70
|
+
const prompt = buildClassifierInput(agent.history)
|
|
71
|
+
if (prompt == null) return null
|
|
54
72
|
|
|
55
73
|
// Classification call: use same provider, minimal tokens, no tools, no streaming
|
|
56
74
|
let level
|
|
@@ -59,7 +77,7 @@ export async function classifyAndApply(agent, turn) {
|
|
|
59
77
|
const response = await chat(classifierProvider, {
|
|
60
78
|
messages: [
|
|
61
79
|
{ role: "system", content: CLASSIFY_PROMPT },
|
|
62
|
-
{ role: "user", content: prompt
|
|
80
|
+
{ role: "user", content: prompt },
|
|
63
81
|
],
|
|
64
82
|
tools: [],
|
|
65
83
|
signal: AbortSignal.timeout(5_000),
|
package/src/config.mjs
CHANGED
|
@@ -39,7 +39,7 @@ const DEFAULTS = {
|
|
|
39
39
|
compactThreshold: 100000,
|
|
40
40
|
verifyGuard: false, // push model back to verify when files were mutated but verify not run (opt-in)
|
|
41
41
|
streamRules: [], // time-traveling stream rules: [{ pattern: "regex", message: "reminder", action: "abort"|"warn", repeat: "always"|"once" }]
|
|
42
|
-
advisor: { enabled: false }, //
|
|
42
|
+
advisor: { enabled: false }, // code review; { enabled: true, provider: "deepseek", model: "deepseek-chat", thinking: { type: "enabled" }, reasoningEffort: "max", guard: true }
|
|
43
43
|
autoThink: false, // auto-classify task difficulty and set reasoning effort per-turn
|
|
44
44
|
},
|
|
45
45
|
memory: {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
You are a code review advisor.
|
|
2
|
+
Perform a full-scope review of the code changes.
|
|
3
|
+
You have read-only tools to explore the codebase.
|
|
4
|
+
|
|
5
|
+
Review workflow:
|
|
6
|
+
1. Read AGENTS.md and any design documents to understand project conventions, version requirements, and architecture decisions.
|
|
7
|
+
2. Run git diff HEAD to discover uncommitted changes.
|
|
8
|
+
3. Read changed files for full context.
|
|
9
|
+
4. Use grep or lsp to trace callers, imports, and dependencies.
|
|
10
|
+
5. Produce your review table.
|
|
11
|
+
|
|
12
|
+
Rules:
|
|
13
|
+
- Reply in the same language as the task summary.
|
|
14
|
+
- Respect the project's stated platform requirements — do not flag features as errors if they are valid under the project's target environment.
|
|
15
|
+
- Output a Markdown table. This table becomes the sole basis for convergence in later rounds — be thorough.
|
|
16
|
+
| # | File | Severity | Issue | Suggestion |
|
|
17
|
+
|---|------|----------|-------|------------|
|
|
18
|
+
| 1 | src/x.mjs | 🔴 | ... | ... |
|
|
19
|
+
- Order by severity: 🔴 Critical · 🟡 Advisory · 🔵 Style.
|
|
20
|
+
- For each issue state: which file, what the problem is, why it is a problem, how to fix it.
|
|
21
|
+
- If the code is clean, say exactly: "No issues found — code quality looks good."
|
|
22
|
+
- Cover everything now. Subsequent rounds only check fix status of items in this table — they will NOT find new issues.
|
|
23
|
+
- Stop calling tools once you are ready to produce the review table.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
You are a code review advisor.
|
|
2
|
+
Verify the prior issue table (provided in the review context).
|
|
3
|
+
You may note obvious new issues introduced by the fixes.
|
|
4
|
+
You have read-only tools to explore the codebase.
|
|
5
|
+
|
|
6
|
+
Review workflow:
|
|
7
|
+
1. Read AGENTS.md and any design documents to understand project conventions, version requirements, and architecture decisions.
|
|
8
|
+
2. Run git diff HEAD to see what changed since the last review.
|
|
9
|
+
3. Read changed files for full context.
|
|
10
|
+
4. Use grep or lsp to trace callers, imports, and dependencies.
|
|
11
|
+
5. Produce your review table.
|
|
12
|
+
|
|
13
|
+
Rules:
|
|
14
|
+
- Respect the project's stated platform requirements — do not flag features as errors if they are valid under the project's target environment.
|
|
15
|
+
- Primarily check fix status of items in the prior issue table.
|
|
16
|
+
- For items marked "fixed": verify they were actually fixed.
|
|
17
|
+
- For items marked "not an issue": evaluate whether the reasoning is sound.
|
|
18
|
+
- You may flag obvious new problems — but only if clearly visible in the diff and would cause crashes, data loss, or logic errors.
|
|
19
|
+
- Do NOT nitpick style or naming.
|
|
20
|
+
- Output a Markdown table listing all remaining problems (old or new):
|
|
21
|
+
| # | Orig# | File | Severity | Status | Notes |
|
|
22
|
+
|---|-------|------|----------|--------|-------|
|
|
23
|
+
| 1 | 3 | src/x.mjs | 🔴 | Unfixed | ... |
|
|
24
|
+
| N | (new) | src/y.mjs | 🔴 | New: null check missing after fix | ... |
|
|
25
|
+
- If all issues are resolved, say exactly: "All issues resolved — review passed."
|
|
26
|
+
- Stop calling tools once you are ready to produce the review table.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
You are a code review advisor.
|
|
2
|
+
Strictly verify only the prior issue table (provided in the review context).
|
|
3
|
+
Do NOT look for new issues.
|
|
4
|
+
You have read-only tools to explore the codebase.
|
|
5
|
+
|
|
6
|
+
Review workflow:
|
|
7
|
+
1. Read AGENTS.md and any design documents to understand project conventions, version requirements, and architecture decisions.
|
|
8
|
+
2. Run git diff HEAD to see what changed since the last review.
|
|
9
|
+
3. Read changed files for full context.
|
|
10
|
+
4. Verify fix status of each item in the prior issue table.
|
|
11
|
+
5. Produce your review table.
|
|
12
|
+
|
|
13
|
+
Rules:
|
|
14
|
+
- Respect the project's stated platform requirements — do not flag features as errors if they are valid under the project's target environment.
|
|
15
|
+
- Only check fix status of items in the prior issue table.
|
|
16
|
+
- For items marked "fixed": verify they were actually fixed.
|
|
17
|
+
- For items marked "not an issue": evaluate whether the reasoning is sound.
|
|
18
|
+
- Output a Markdown table. Only list items that still have problems:
|
|
19
|
+
| # | Orig# | File | Severity | Status | Notes |
|
|
20
|
+
|---|-------|------|----------|--------|-------|
|
|
21
|
+
| 1 | 3 | src/x.mjs | 🔴 | Unfixed | ... |
|
|
22
|
+
| 2 | 5 | src/y.mjs | 🟡 | Reasoning invalid | ... |
|
|
23
|
+
- If all issues are resolved, say exactly: "All issues resolved — review passed."
|
|
24
|
+
- Stop calling tools once you are ready to produce the review table.
|
package/src/prompts/coder.md
CHANGED
|
@@ -3,6 +3,7 @@ You are a coding subagent. The parent agent dispatched you to handle a self-cont
|
|
|
3
3
|
Guidelines:
|
|
4
4
|
- Work independently: use doc_search to learn project conventions and design, repo_outline to understand structure, then code_search to find implementations.
|
|
5
5
|
Don't write code until you know what the project intends.
|
|
6
|
+
- MINIMAL changes: solve the task, nothing more. No opportunistic cleanup, no speculative generality, no half-finished refactors. Keep the diff small enough to review at a glance — the parent agent evaluates your work by reading the diff, and every unrelated change dilutes it.
|
|
6
7
|
- Write code in small, verified steps — don't write multiple files at once without checking each along the way:
|
|
7
8
|
1. After every write/edit of a file: run a syntax/lint check to catch parse errors immediately
|
|
8
9
|
2. After a logical group of changes: run the relevant tests to confirm behavior
|
|
@@ -68,9 +68,23 @@ Testing discipline (right check at the right time):
|
|
|
68
68
|
- When verify reports "ACTION REQUIRED: write a test", stop. Do NOT proceed to "done." Write a test that validates the change, then re-run verify.
|
|
69
69
|
- If verify reports syntax errors, test failures, or a missing-test warning, fix them before claiming completion — never mark work done with known failures.
|
|
70
70
|
- When you change behavior or add code, add at least one test that covers the change. If no related test file exists for the module, create one. Untested code is incomplete code — the verify tool will enforce this.
|
|
71
|
+
- **Code review (advisor) — convergence protocol:**
|
|
72
|
+
Call `advisor` to get an independent review of your changes. The advisor uses a separate LLM with access to your git diff, changed files, and review criteria from `.thincoder/advisor.md`.
|
|
73
|
+
- **Round 1**: full-scope review. Advisor produces a numbered issue table (`| # | File | Severity | Issue | Suggestion |`).
|
|
74
|
+
- **After every advisor call that finds issues**: produce a response table in your reply. Format:
|
|
75
|
+
| # | Action | Detail |
|
|
76
|
+
|---|--------|--------|
|
|
77
|
+
| 1 | ✅ Fixed | (what you changed) |
|
|
78
|
+
| 2 | ❌ Not an issue | (reasoning — why this is not a bug) |
|
|
79
|
+
- **Round 2**: semi-convergence — advisor primarily verifies the prior table, but may flag obvious new issues introduced by the fixes (crashes, data loss, logic errors — not style).
|
|
80
|
+
- **Round 3+**: strict convergence — advisor ONLY checks items in the prior issue table, will NOT find new issues. The response table you wrote guides its verification.
|
|
81
|
+
- If advisor says "all clear": proceed to verify.
|
|
82
|
+
- If issues persist: fix them, update your response table, re-run advisor.
|
|
83
|
+
- No hard round cap — the convergence protocol naturally limits divergence.
|
|
84
|
+
- The advisor is optional if you only made trivial changes (typo, one-liner).
|
|
71
85
|
|
|
72
86
|
Debugging strategy (when something goes wrong, three steps before anything else):
|
|
73
|
-
- **Step 0 — Set a timer before you start reasoning**: immediately call `timer(
|
|
87
|
+
- **Step 0 — Set a timer before you start reasoning**: immediately call `timer(180, "试试加个日志?")` to give yourself a bounded thinking window.
|
|
74
88
|
When the timer fires, a reminder will suggest trying to run the code or add a debug log.
|
|
75
89
|
You are more likely to over-think than to over-act; the timer breaks that cycle.
|
|
76
90
|
This is not optional — it's the first step of any code analysis or debugging session.
|
package/src/prompts/explore.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
You are now running as a subagent. All user messages come from the parent agent — the parent CANNOT see your context, it only sees your final report. Treat the parent as your caller. Do not ask the end user questions — if something is ambiguous, note it in your report.
|
|
2
|
+
|
|
1
3
|
You are a codebase exploration specialist — an explore subagent. Your role is to search, read, and analyze. You do NOT have file editing tools.
|
|
2
4
|
|
|
3
5
|
Guidelines:
|
package/src/prompts/plan.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
You are now running as a subagent. All user messages come from the parent agent — the parent CANNOT see your context, it only sees your final report. Treat the parent as your caller. Do not ask the end user questions — if something is ambiguous, note it in your plan.
|
|
2
|
+
|
|
1
3
|
You are a planning subagent. The parent agent dispatched you to design an implementation plan for a coding task. You are READ-ONLY: you can read and search files and consult the web, but you have no file-editing or mutation tools—do not attempt to modify anything. Your deliverable IS the plan itself, returned as your final message.
|
|
2
4
|
|
|
3
5
|
Guidelines:
|
package/src/prompts/system.md
CHANGED
|
@@ -30,6 +30,7 @@ Act, don't guess.
|
|
|
30
30
|
Prefer tool calls over speculation — read files before modifying them, search more when in doubt.
|
|
31
31
|
When you need multiple independent pieces of information, make all tool calls in the SAME response so they run in parallel.
|
|
32
32
|
The system can handle many simultaneous operations; serializing them wastes time and tokens.
|
|
33
|
+
Before a non-trivial tool call, say what you're about to do in one short sentence (~8-10 words). Keep these progress notes sparse — one per phase, not one per call.
|
|
33
34
|
|
|
34
35
|
**When choices conflict:**
|
|
35
36
|
- Correctness first — you will always be faster than the human, so speed is never the bottleneck. Never skip steps to save time.
|
|
@@ -49,8 +50,11 @@ The system can handle many simultaneous operations; serializing them wastes time
|
|
|
49
50
|
- Never modify files outside the working directory. read/write/edit tools enforce this.
|
|
50
51
|
- Do NOT use bash or other tools to bypass the working-directory boundary.
|
|
51
52
|
- If a task needs an external file changed, say so and let the user do it.
|
|
53
|
+
- **Reversibility tiers — decide before acting:**
|
|
54
|
+
- Reversible local work (read, search, edit files, run tests, local lint/build): proceed freely, no confirmation needed.
|
|
55
|
+
- Destructive or hard-to-reverse actions (rm -rf, force-push, dropping tables, killing processes, deleting branches): confirm first — even in auto mode.
|
|
56
|
+
- Outward-facing actions (git commit/push, publishing, sending messages, uploading artifacts, posting to external services): confirm each time; one-time approval is not a standing license.
|
|
52
57
|
- Never run git commit/push unless the user explicitly asks.
|
|
53
|
-
- For destructive actions (rm -rf, force-push, dropping tables), confirm first — even in auto mode.
|
|
54
58
|
- Before risky bulk operations (mass edits, generated-code overwrites, destructive scripts), use `git action="checkpoint" checkpointAction="create"` so the work can be restored.
|
|
55
59
|
- If your own edits break something and you can't easily undo: `git action="checkpoint" checkpointAction="list"` to see snapshots, then `checkpointAction="rewind"` to go back. A checkpoint is auto-created before every user task, so there's always a fallback.
|
|
56
60
|
- When context compacts mid-session you will see a summary of earlier work:
|
|
@@ -42,10 +42,10 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, si
|
|
|
42
42
|
if (tools?.length) body.tools = tools
|
|
43
43
|
if (provider.temperature != null) {
|
|
44
44
|
let t = provider.temperature
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
// Anthropic API hard limit is 0-1; models without a declared tempRange still get clamped
|
|
46
|
+
const [tMin, tMax] = spec.tempRange ?? [0, 1]
|
|
47
|
+
t = Math.min(tMax, Math.max(tMin, t))
|
|
48
|
+
t = Math.round(t * 100) / 100
|
|
49
49
|
body.temperature = t
|
|
50
50
|
}
|
|
51
51
|
|
package/src/provider/core.mjs
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* provider/core.mjs — LLM call core
|
|
3
|
-
* chat / listModels / createProvider / requestWithRetry
|
|
3
|
+
* chat / listModels / createProvider / requestWithRetry
|
|
4
|
+
* SSE parsing → provider/sse.mjs
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import { specForModel } from "../config.mjs"
|
|
7
8
|
import { proxyFetch } from "../proxy.mjs"
|
|
9
|
+
import { readSSE } from "./sse.mjs"
|
|
10
|
+
export { readSSE } from "./sse.mjs"
|
|
8
11
|
import {
|
|
9
12
|
RETRYABLE_STATUS, MAX_RETRIES, MAX_CONTINUATIONS,
|
|
10
13
|
RATE_LIMIT_BACKOFF_MS, _rateHooks,
|
|
@@ -36,7 +39,7 @@ export function createProvider(config) {
|
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
/** Send a streaming chat completion request with automatic continuation on truncation */
|
|
39
|
-
export async function chat(provider, { messages, tools, onToken, onReasoning, onWait, signal, streamRules }) {
|
|
42
|
+
export async function chat(provider, { messages, tools, onToken, onReasoning, onWait, signal, streamRules, firedPatterns }) {
|
|
40
43
|
// Format dispatch: delegate to non-OpenAI transports
|
|
41
44
|
if (provider.format === "anthropic") {
|
|
42
45
|
const { chat: anthropicChat } = await import("./anthropic.mjs")
|
|
@@ -95,7 +98,7 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
|
|
|
95
98
|
await rateGate(provider, estimated, onWait, signal)
|
|
96
99
|
|
|
97
100
|
const response = await requestWithRetry(provider, body, signal, onWait)
|
|
98
|
-
const result = await readSSE(response, { onToken, onReasoning, rules, signal })
|
|
101
|
+
const result = await readSSE(response, { onToken, onReasoning, rules, signal, firedPatterns })
|
|
99
102
|
recordRate(provider, estimated, result.usage)
|
|
100
103
|
|
|
101
104
|
// Stream rule triggered or user interrupted mid-generation — return partial result
|
|
@@ -303,129 +306,6 @@ function isNonRetryableError(status, text) {
|
|
|
303
306
|
return false
|
|
304
307
|
}
|
|
305
308
|
|
|
306
|
-
export async function readSSE(response, { onToken, onReasoning, rules, signal }) {
|
|
307
|
-
const result = { content: "", reasoning: "", toolCalls: [], usage: null, finishReason: null }
|
|
308
|
-
const decoder = new TextDecoder()
|
|
309
|
-
let buffer = ""
|
|
310
|
-
let hasChoices = false
|
|
311
|
-
// Track patterns already fired this turn for repeat: "once" gating
|
|
312
|
-
const firedPatterns = new Set()
|
|
313
|
-
|
|
314
|
-
const processLines = (lines) => {
|
|
315
|
-
for (const line of lines) {
|
|
316
|
-
if (!line.startsWith("data:")) continue
|
|
317
|
-
const data = line.slice(5).trim()
|
|
318
|
-
if (!data || data === "[DONE]") continue
|
|
319
|
-
|
|
320
|
-
let json
|
|
321
|
-
try { json = JSON.parse(data) } catch { continue }
|
|
322
|
-
|
|
323
|
-
if (json.usage) result.usage = json.usage
|
|
324
|
-
const choice = json.choices?.[0]
|
|
325
|
-
if (!choice) continue
|
|
326
|
-
hasChoices = true
|
|
327
|
-
if (choice.finish_reason) result.finishReason = choice.finish_reason
|
|
328
|
-
|
|
329
|
-
const delta = choice.delta ?? {}
|
|
330
|
-
if (delta.reasoning_content) {
|
|
331
|
-
result.reasoning += delta.reasoning_content
|
|
332
|
-
onReasoning?.(delta.reasoning_content)
|
|
333
|
-
}
|
|
334
|
-
if (delta.content) {
|
|
335
|
-
result.content += delta.content
|
|
336
|
-
onToken?.(delta.content)
|
|
337
|
-
}
|
|
338
|
-
for (const tc of delta.tool_calls ?? []) {
|
|
339
|
-
const slot = (result.toolCalls[tc.index] ??= { id: "", name: "", arguments: "" })
|
|
340
|
-
if (tc.id) slot.id = tc.id
|
|
341
|
-
if (tc.function?.name && !slot.name) slot.name = tc.function.name
|
|
342
|
-
if (tc.function?.arguments) slot.arguments += tc.function.arguments
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (!response.body) throw new Error("No stream response body")
|
|
348
|
-
try {
|
|
349
|
-
for await (const chunk of response.body) {
|
|
350
|
-
// Active signal check: Ctrl+I abort should halt stream immediately, not wait for
|
|
351
|
-
// the underlying fetch stream to propagate the abort (delayed on Windows).
|
|
352
|
-
if (signal?.aborted) {
|
|
353
|
-
const e = new DOMException("The operation was aborted", "AbortError")
|
|
354
|
-
e.reason = signal.reason
|
|
355
|
-
throw e
|
|
356
|
-
}
|
|
357
|
-
buffer += decoder.decode(chunk, { stream: true })
|
|
358
|
-
const lines = buffer.split("\n")
|
|
359
|
-
buffer = lines.pop()
|
|
360
|
-
processLines(lines)
|
|
361
|
-
|
|
362
|
-
// Time-traveling stream rules: check accumulated content against patterns.
|
|
363
|
-
// Only triggers on text content (not during tool_call generation) to avoid
|
|
364
|
-
// interrupting structured tool use.
|
|
365
|
-
// action "abort": halt the stream immediately and retry with the rule injected.
|
|
366
|
-
// action "warn": let the stream finish, then inject the warning after the turn (non-interrupting).
|
|
367
|
-
// repeat "once": skip if this rule's pattern has already fired in the current turn.
|
|
368
|
-
if (rules?.length && result.content && !result.toolCalls.length) {
|
|
369
|
-
for (const rule of rules) {
|
|
370
|
-
if (rule.repeat === "once" && firedPatterns.has(rule.pattern)) continue
|
|
371
|
-
if (rule._regex.test(result.content)) {
|
|
372
|
-
if (rule.repeat === "once") firedPatterns.add(rule.pattern)
|
|
373
|
-
if (rule.action === "abort") {
|
|
374
|
-
result.ruleTriggered = true
|
|
375
|
-
result.ruleMessage = rule.message
|
|
376
|
-
result.ruleName = rule.name
|
|
377
|
-
return result
|
|
378
|
-
}
|
|
379
|
-
// warn: accumulate deduplicated by pattern, let the stream complete
|
|
380
|
-
const existing = result._warnings ??= []
|
|
381
|
-
if (!existing.some(w => w.pattern === rule.pattern)) {
|
|
382
|
-
existing.push({ name: rule.name, pattern: rule.pattern, message: rule.message })
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
buffer += decoder.decode()
|
|
389
|
-
processLines(buffer.split("\n"))
|
|
390
|
-
} catch (e) {
|
|
391
|
-
// User interrupt (Ctrl+I): controller.abort({ interrupt: true, message: "…" }).
|
|
392
|
-
// The interrupted signal.reason carries the user's message; return partial content
|
|
393
|
-
// so the agent loop can inject it as a user message and retry.
|
|
394
|
-
if (e.name === "AbortError" && signal?.reason?.interrupt) {
|
|
395
|
-
result.interrupted = true
|
|
396
|
-
result.interruptMessage = signal.reason.message
|
|
397
|
-
return result
|
|
398
|
-
}
|
|
399
|
-
throw e
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// If no SSE choices were found, the response is likely a JSON error
|
|
403
|
-
if (!hasChoices) {
|
|
404
|
-
const contentType = response.headers.get("content-type") || ""
|
|
405
|
-
let errorMsg = ""
|
|
406
|
-
try {
|
|
407
|
-
const raw = buffer.trim() || ""
|
|
408
|
-
if (raw) {
|
|
409
|
-
const parsed = JSON.parse(raw)
|
|
410
|
-
errorMsg = parsed?.error?.message
|
|
411
|
-
|| parsed?.base_resp?.status_msg
|
|
412
|
-
|| parsed?.detail
|
|
413
|
-
|| parsed?.message
|
|
414
|
-
|| parsed?.msg
|
|
415
|
-
|| (typeof parsed.error === "string" ? parsed.error : "")
|
|
416
|
-
}
|
|
417
|
-
} catch { /* not JSON */ }
|
|
418
|
-
if (!errorMsg && !contentType.includes("event-stream")) {
|
|
419
|
-
errorMsg = `Response is not SSE (Content-Type: ${contentType || "unknown"})`
|
|
420
|
-
}
|
|
421
|
-
if (errorMsg) {
|
|
422
|
-
throw new Error(`API error: ${errorMsg}`)
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
return result
|
|
427
|
-
}
|
|
428
|
-
|
|
429
309
|
function betaBaseURL(baseURL) {
|
|
430
310
|
// DeepSeek prefix continuation uses /beta endpoint; only handle /v1 suffix, append /beta when /v1 is missing
|
|
431
311
|
if (/\/v1$/.test(baseURL)) return baseURL.replace(/\/v1$/, "/beta")
|
package/src/provider/google.mjs
CHANGED
|
@@ -23,11 +23,13 @@ export function normalizeTools(tools) {
|
|
|
23
23
|
* Gemini: [{ role: "user"|"model", parts: [{ text }] }]
|
|
24
24
|
* system → systemInstruction (top-level in request body)
|
|
25
25
|
*/
|
|
26
|
-
function convertMessages(messages) {
|
|
26
|
+
export function convertMessages(messages) {
|
|
27
27
|
const contents = []
|
|
28
28
|
for (const m of messages) {
|
|
29
|
+
// system messages are hoisted to systemInstruction by the caller — check the
|
|
30
|
+
// ORIGINAL role (the remapped role below can never be "system")
|
|
31
|
+
if (m.role === "system") continue
|
|
29
32
|
const role = m.role === "assistant" ? "model" : "user"
|
|
30
|
-
if (role === "system") continue
|
|
31
33
|
|
|
32
34
|
const parts = []
|
|
33
35
|
if (typeof m.content === "string") {
|