thincoder 0.11.0 → 0.12.0
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 +8 -0
- package/package.json +1 -1
- package/src/advisor.mjs +535 -72
- package/src/agent/helpers.mjs +18 -5
- 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 +80 -21
- package/src/auto-think.mjs +23 -5
- package/src/cli/make-agent.mjs +20 -0
- package/src/config.mjs +1 -1
- package/src/mcp/transport-stdio.mjs +4 -3
- package/src/mcp.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/skills.mjs +67 -31
- 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 +43 -2
- package/src/tools/system.mjs +14 -10
- package/src/tools/web.mjs +21 -16
- package/src/tui/agent-turn.mjs +130 -73
- package/src/tui/cmd-advisor.mjs +237 -41
- package/src/tui/cmd-auto.mjs +6 -8
- package/src/tui/cmd-mcp.mjs +4 -2
- package/src/tui/cmd-plan.mjs +6 -8
- package/src/tui/cmd-think.mjs +93 -70
- package/src/tui/index.mjs +9 -188
- package/src/tui/interaction.mjs +2 -1
- package/src/tui/key-handler.mjs +8 -4
- package/src/tui/layout.mjs +3 -2
- package/src/tui/render-conversation.mjs +92 -0
- package/src/tui/render-frame.mjs +94 -168
- package/src/tui/render-loop.mjs +110 -0
package/src/agent/helpers.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { configDir } from "../config.mjs"
|
|
5
5
|
import { readFileSync, readdirSync, existsSync } from "node:fs"
|
|
6
|
+
import { homedir } from "node:os"
|
|
6
7
|
import { writeFile, mkdir } from "node:fs/promises"
|
|
7
8
|
import { join } from "node:path"
|
|
8
9
|
import { execSync } from "node:child_process"
|
|
@@ -12,8 +13,12 @@ export const DEFAULT_SUBAGENT_TURNS = 100
|
|
|
12
13
|
export const DEFAULT_GOAL_TURNS = 200
|
|
13
14
|
export const MIN_REPORT_CHARS = 200
|
|
14
15
|
export const REPORT_CONTINUATION =
|
|
15
|
-
"Your report
|
|
16
|
-
"
|
|
16
|
+
"Your report was sent back: too brief to be a complete handoff — the parent agent sees nothing else from your run. " +
|
|
17
|
+
"Rewrite your final message as a checklist:\n" +
|
|
18
|
+
"1. What you changed and why\n" +
|
|
19
|
+
"2. The path of every file you touched\n" +
|
|
20
|
+
"3. How you verified (tests run, commands executed, with results)\n" +
|
|
21
|
+
"4. Anything left undone or worth follow-up"
|
|
17
22
|
|
|
18
23
|
const TOOL_RESULT_OFFLOAD_LIMIT = 16_000
|
|
19
24
|
const TOOL_RESULT_PREVIEW = 2_000
|
|
@@ -22,7 +27,7 @@ const GIT_TIMEOUT_MS = 5000
|
|
|
22
27
|
const MAX_GIT_CHANGES_DISPLAY = 20
|
|
23
28
|
|
|
24
29
|
export const OUTLINE_INJECT_PREFIX = "[System reminder: project dependency outline:"
|
|
25
|
-
export const FILE_MUTATORS = new Set(["write", "edit", "insert_after", "apply_patch", "delete"])
|
|
30
|
+
export const FILE_MUTATORS = new Set(["write", "edit", "insert_after", "apply_patch", "delete", "hashline_edit"])
|
|
26
31
|
|
|
27
32
|
/** Escape XML special characters in a string for safe embedding in XML/HTML */
|
|
28
33
|
export function escapeXml(s) {
|
|
@@ -174,14 +179,22 @@ export function readonlyToolNames(tools) {
|
|
|
174
179
|
|
|
175
180
|
const MAX_INSTRUCTION_CHARS = 32_000
|
|
176
181
|
|
|
177
|
-
/** Load AGENTS.md / project_rules.md from
|
|
182
|
+
/** Load AGENTS.md / project_rules.md from user home and project root.
|
|
183
|
+
* User-level (~/.thincoder/AGENTS.md) loaded first (lower priority).
|
|
184
|
+
* Project-level overrides take precedence. */
|
|
178
185
|
export async function loadProjectInstructions(cwd) {
|
|
179
186
|
const parts = []
|
|
187
|
+
// 1. User-level: global preferences across all projects
|
|
188
|
+
try {
|
|
189
|
+
const userPath = join(homedir(), ".thincoder", "AGENTS.md")
|
|
190
|
+
const content = readFileSync(userPath, "utf8").trim()
|
|
191
|
+
if (content) parts.push(`<!-- From: ${userPath} -->\n${content}`)
|
|
192
|
+
} catch { /* file does not exist */ }
|
|
193
|
+
// 2. Project-level: project-specific conventions
|
|
180
194
|
for (const name of ["AGENTS.md", "project_rules.md"]) {
|
|
181
195
|
try {
|
|
182
196
|
const content = readFileSync(join(cwd, name), "utf8").trim()
|
|
183
197
|
if (!content) continue
|
|
184
|
-
const key = name.toLowerCase()
|
|
185
198
|
parts.push(`<!-- From: ${join(cwd, name)} -->\n${content}`)
|
|
186
199
|
} catch { /* file does not exist */ }
|
|
187
200
|
}
|
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
|
|
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,
|
|
@@ -49,6 +49,7 @@ const STALL_THRESHOLD = 3
|
|
|
49
49
|
const GOAL_BUDGET_WARN_RATIO = 0.75
|
|
50
50
|
const MAX_VERIFY_PUSHBACKS = 2
|
|
51
51
|
const MAX_VERIFY_RETRIES = 3
|
|
52
|
+
const MAX_ADVISOR_PUSHBACKS = 3
|
|
52
53
|
|
|
53
54
|
/** Create a new agent state object with all fields initialized to defaults */
|
|
54
55
|
export function createAgent({
|
|
@@ -61,8 +62,8 @@ export function createAgent({
|
|
|
61
62
|
provider, tools, config, cwd, memory, _role: role,
|
|
62
63
|
overlay, tasks, history,
|
|
63
64
|
planMode, autoApprove, goal,
|
|
64
|
-
_mutatedThisRun: false, _verifiedThisRun: false, _verifyPassed: undefined,
|
|
65
|
-
_touchedFiles: [], _verifyRetries: 0,
|
|
65
|
+
_mutatedThisRun: false, _verifiedThisRun: false, _verifyPassed: undefined, _calledAdvisorThisRun: false,
|
|
66
|
+
_touchedFiles: [], _verifyRetries: 0, _advisorRound: 0, _advisorSession: null,
|
|
66
67
|
_pendingReminders: [],
|
|
67
68
|
_pendingTimers: [],
|
|
68
69
|
_sessionStart: sessionStart,
|
|
@@ -81,11 +82,18 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
81
82
|
agent._mutatedThisRun = false
|
|
82
83
|
agent._verifiedThisRun = false
|
|
83
84
|
agent._verifyPassed = undefined
|
|
85
|
+
agent._calledAdvisorThisRun = false
|
|
84
86
|
agent._touchedFiles = []
|
|
85
87
|
agent._verifyRetries = 0
|
|
88
|
+
agent._advisorRound = 0
|
|
89
|
+
agent._advisorSession = null // advisor session is per-run: discard when the task ends, next task starts fresh
|
|
86
90
|
let guardPushbacks = 0
|
|
91
|
+
let advisorPushbacks = 0
|
|
87
92
|
let honestReminderInjected = false
|
|
88
93
|
const recentCallSigs = []
|
|
94
|
+
// repeat: "once" stream rules fire at most once per runAgent call (user turn):
|
|
95
|
+
// this set survives across chat() calls (rule abort-retry, tool loop) within the turn.
|
|
96
|
+
const streamRuleFired = new Set()
|
|
89
97
|
|
|
90
98
|
for (let turn = 0; turn < maxTurns; turn++) {
|
|
91
99
|
|
|
@@ -94,6 +102,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
94
102
|
try {
|
|
95
103
|
if (await compressIfNeeded(agent, threshold, callbacks)) {
|
|
96
104
|
agent._compressFailures = 0
|
|
105
|
+
agent._planReminderAtLen = 0 // After compression history shrinks, reset cadence so reminders resume
|
|
97
106
|
recentCallSigs.length = 0 // After compression history is rebuilt, reset stall detection counter
|
|
98
107
|
callbacks.onCompress?.()
|
|
99
108
|
if (agent.autoApprove && !agent.history.some((m) => m.content === AUTO_REMINDER)) {
|
|
@@ -111,6 +120,24 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
111
120
|
}
|
|
112
121
|
}
|
|
113
122
|
|
|
123
|
+
// Plan-mode reminder cadence: re-inject constraint reminders while plan mode is active
|
|
124
|
+
// (sparse every 2 turns, full every 5 turns or when the user sends a new message),
|
|
125
|
+
// so the read-only restriction never fades from context.
|
|
126
|
+
if (agent.planMode) {
|
|
127
|
+
const lastMsg = agent.history.at(-1)
|
|
128
|
+
const realUserMsg = lastMsg?.role === "user"
|
|
129
|
+
&& typeof lastMsg.content === "string"
|
|
130
|
+
&& !lastMsg.content.startsWith("[System reminder:")
|
|
131
|
+
&& !lastMsg.content.startsWith("[User interrupt:")
|
|
132
|
+
const newUserSince = realUserMsg && agent.history.length > (agent._planReminderAtLen ?? 0)
|
|
133
|
+
const { planReminderForTurn } = await import("./agent-tools/plan.mjs")
|
|
134
|
+
const reminder = planReminderForTurn(agent, newUserSince)
|
|
135
|
+
if (reminder) {
|
|
136
|
+
agent._planReminderAtLen = agent.history.length + 1
|
|
137
|
+
agent.history.push({ role: "user", content: reminder, transient: true })
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
114
141
|
const messages = [{ role: "system", content: systemPrompt }, ...agent.history]
|
|
115
142
|
let response
|
|
116
143
|
|
|
@@ -129,15 +156,18 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
129
156
|
onWait: callbacks.onWait,
|
|
130
157
|
signal,
|
|
131
158
|
streamRules: agent.config.agent?.streamRules ?? [],
|
|
159
|
+
firedPatterns: streamRuleFired,
|
|
132
160
|
})
|
|
133
161
|
} catch (e) {
|
|
134
162
|
// User interrupt (Ctrl+I): controller.abort({ interrupt: true, message: "…" }).
|
|
135
163
|
// Inject the message into history and let the outer loop recreate the controller.
|
|
136
164
|
if (e.name === "AbortError" && signal?.reason?.interrupt) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
165
|
+
const msg = `[User interrupt: ${signal.reason.message}]`
|
|
166
|
+
// Dedup: if the interrupt was already handled during tool execution (L302-310),
|
|
167
|
+
// don't push a duplicate — the outer loop will still recreate the controller.
|
|
168
|
+
if (agent.history.at(-1)?.content !== msg) {
|
|
169
|
+
agent.history.push({ role: "user", content: msg })
|
|
170
|
+
}
|
|
141
171
|
}
|
|
142
172
|
throw e
|
|
143
173
|
}
|
|
@@ -219,6 +249,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
219
249
|
role: "user",
|
|
220
250
|
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
251
|
})
|
|
252
|
+
callbacks.onTurnEnd?.(agent, turn)
|
|
222
253
|
continue
|
|
223
254
|
}
|
|
224
255
|
// --- verify guard: push model to verify mutated files before completion ---
|
|
@@ -230,15 +261,15 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
230
261
|
role: "user",
|
|
231
262
|
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
263
|
})
|
|
264
|
+
callbacks.onTurnEnd?.(agent, turn)
|
|
233
265
|
continue
|
|
234
|
-
}
|
|
235
|
-
if (agent._verifiedThisRun && agent._verifyPassed === false && agent._verifyRetries < MAX_VERIFY_RETRIES) {
|
|
236
266
|
agent._verifyRetries++
|
|
237
267
|
agent.history.push({ role: "assistant", content: response.content })
|
|
238
268
|
agent.history.push({
|
|
239
269
|
role: "user",
|
|
240
270
|
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
271
|
})
|
|
272
|
+
callbacks.onTurnEnd?.(agent, turn)
|
|
242
273
|
continue
|
|
243
274
|
}
|
|
244
275
|
if (agent._verifyPassed === false && agent._verifyRetries >= MAX_VERIFY_RETRIES) {
|
|
@@ -252,6 +283,25 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
252
283
|
role: "user",
|
|
253
284
|
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
285
|
})
|
|
286
|
+
callbacks.onTurnEnd?.(agent, turn)
|
|
287
|
+
continue
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// --- advisor guard: review of mutated files is mandatory before completion ---
|
|
291
|
+
// When advisor is enabled and guard is not explicitly disabled, the model MUST
|
|
292
|
+
// call advisor after mutating files — skipping is not offered as an option
|
|
293
|
+
// (offering it caused a skip → pushback loop). Pushbacks are capped as a
|
|
294
|
+
// backstop against pathological loops; convergence is unbounded by design.
|
|
295
|
+
if (depth === 0 && agent.config?.advisor?.enabled && agent.config?.advisor?.guard !== false) {
|
|
296
|
+
if (agent._mutatedThisRun && !agent._calledAdvisorThisRun && (agent._touchedFiles ?? []).length > 0
|
|
297
|
+
&& advisorPushbacks < MAX_ADVISOR_PUSHBACKS) {
|
|
298
|
+
advisorPushbacks++
|
|
299
|
+
agent.history.push({ role: "assistant", content: response.content })
|
|
300
|
+
agent.history.push({
|
|
301
|
+
role: "user",
|
|
302
|
+
content: `[System reminder: you changed code in this run and MUST get an advisor review before finishing (round ${agent._advisorRound + 1}). Call the \`advisor\` tool now. This is required, not optional — do not skip it even if you believe the changes are trivial; a small diff just makes the review fast. After the review, produce a response table for every issue found (see discipline rules for format).]`,
|
|
303
|
+
})
|
|
304
|
+
callbacks.onTurnEnd?.(agent, turn)
|
|
255
305
|
continue
|
|
256
306
|
}
|
|
257
307
|
}
|
|
@@ -276,8 +326,20 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
276
326
|
|
|
277
327
|
const results = await executeToolCalls(agent, toolByName, response.toolCalls, callbacks, depth, signal)
|
|
278
328
|
|
|
329
|
+
// Ctrl+I interrupt during tool execution: skip committing partial results —
|
|
330
|
+
// the tool failure messages would mislead the model. Inject the interrupt and retry.
|
|
331
|
+
if (signal?.reason?.interrupt) {
|
|
332
|
+
agent.history.push({
|
|
333
|
+
role: "user",
|
|
334
|
+
content: `[User interrupt: ${signal.reason.message}]`,
|
|
335
|
+
})
|
|
336
|
+
callbacks.onTurnEnd?.(agent, turn)
|
|
337
|
+
continue
|
|
338
|
+
}
|
|
339
|
+
|
|
279
340
|
// Model is executing tools → doing real work, reset guard pushback counter
|
|
280
341
|
guardPushbacks = 0
|
|
342
|
+
advisorPushbacks = 0
|
|
281
343
|
|
|
282
344
|
for (const { toolCall, result, ok } of results) {
|
|
283
345
|
const tool = toolByName.get(toolCall.name)
|
|
@@ -308,8 +370,16 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
308
370
|
}
|
|
309
371
|
agent.history.push({ role: "tool", tool_call_id: toolCall.id, content: result })
|
|
310
372
|
if (tool && ok) {
|
|
311
|
-
if (!tool.readonly && !tool.sideEffectExempt)
|
|
373
|
+
if (!tool.readonly && !tool.sideEffectExempt) {
|
|
374
|
+
agent._mutatedThisRun = true
|
|
375
|
+
// Any mutation after advisor invalidates the review — need re-review
|
|
376
|
+
if (agent._calledAdvisorThisRun) agent._calledAdvisorThisRun = false
|
|
377
|
+
}
|
|
312
378
|
if (toolCall.name === "verify") agent._verifiedThisRun = true
|
|
379
|
+
if (toolCall.name === "advisor") {
|
|
380
|
+
agent._calledAdvisorThisRun = true
|
|
381
|
+
agent._advisorRound++ // advance convergence round
|
|
382
|
+
}
|
|
313
383
|
if (FILE_MUTATORS.has(toolCall.name)) {
|
|
314
384
|
const args = JSON.parse(toolCall.arguments)
|
|
315
385
|
const paths = tool.touchedPaths ? tool.touchedPaths(args) : [args.path]
|
|
@@ -386,17 +456,6 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
386
456
|
}
|
|
387
457
|
|
|
388
458
|
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
459
|
}
|
|
401
460
|
|
|
402
461
|
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/cli/make-agent.mjs
CHANGED
|
@@ -52,6 +52,26 @@ export async function assembleAgent() {
|
|
|
52
52
|
|
|
53
53
|
// MCP servers: connect in parallel (a dead server won't block startup), collect failures as warnings (stderr invisible in TUI, passed via agent object)
|
|
54
54
|
const mcpServers = config.mcp?.servers ?? []
|
|
55
|
+
// Read project-level .mcp.json (standard MCP client convention) — merge into mcpServers
|
|
56
|
+
// config.json servers take priority over .mcp.json entries with the same name
|
|
57
|
+
try {
|
|
58
|
+
const { existsSync, readFileSync } = await import("node:fs")
|
|
59
|
+
const mcpJsonPath = join(cwd, ".mcp.json")
|
|
60
|
+
if (existsSync(mcpJsonPath)) {
|
|
61
|
+
const mcpJson = JSON.parse(readFileSync(mcpJsonPath, "utf8"))
|
|
62
|
+
if (mcpJson.mcpServers && typeof mcpJson.mcpServers === "object") {
|
|
63
|
+
const configNames = new Set(mcpServers.map((s) => s.name))
|
|
64
|
+
for (const [name, server] of Object.entries(mcpJson.mcpServers)) {
|
|
65
|
+
if (configNames.has(name)) continue // config.json takes priority
|
|
66
|
+
if (!server || typeof server !== "object") continue
|
|
67
|
+
mcpServers.push({ name, ...server })
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch (e) {
|
|
72
|
+
// .mcp.json parse failure — non-fatal, log and continue
|
|
73
|
+
console.error(`[mcp] Failed to read .mcp.json: ${e.message}`)
|
|
74
|
+
}
|
|
55
75
|
let mcpTools = []
|
|
56
76
|
const mcpWarnings = []
|
|
57
77
|
if (mcpServers.length) {
|
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: {
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
import { spawn } from "node:child_process"
|
|
5
5
|
import { rpcId, CALL_TIMEOUT_MS, withTimeout, quoteArg } from "./helpers.mjs"
|
|
6
6
|
|
|
7
|
-
/** Create an MCP stdio transport over a spawned child process
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
/** Create an MCP stdio transport over a spawned child process.
|
|
8
|
+
* @param {Object} [env] — extra environment variables merged on top of process.env */
|
|
9
|
+
export function stdioTransport(command, args, env) {
|
|
10
|
+
const spawnOptions = { stdio: ["pipe", "pipe", "pipe"], windowsHide: true, env: { ...process.env, ...env } }
|
|
10
11
|
const child =
|
|
11
12
|
process.platform === "win32" && !/\.exe$/i.test(command)
|
|
12
13
|
? spawn("cmd.exe", ["/d", "/s", "/c", [command, ...(args ?? [])].map(quoteArg).join(" ")], {
|
package/src/mcp.mjs
CHANGED
|
@@ -77,7 +77,7 @@ export async function connectMcpServer(config) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
if (config.command) {
|
|
80
|
-
const transport = stdioTransport(config.command, config.args ?? [])
|
|
80
|
+
const transport = stdioTransport(config.command, config.args ?? [], config.env)
|
|
81
81
|
try {
|
|
82
82
|
const mcpTools = await doInitialize(transport, config.name ?? config.command)
|
|
83
83
|
return buildTools(mcpTools, transport, config)
|
|
@@ -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. The uncommitted changes (git status + diff) are already provided in the review context — do not re-run them unless marked truncated.
|
|
7
|
+
2. Read AGENTS.md / design docs once if present, to understand project conventions, version requirements, and architecture decisions.
|
|
8
|
+
3. Read changed files for full context beyond the diff. Batch independent tool calls in one reply.
|
|
9
|
+
4. Use grep or lsp to trace callers, imports, and dependencies — only where genuinely needed.
|
|
10
|
+
5. Produce your review table.
|
|
11
|
+
|
|
12
|
+
Rules:
|
|
13
|
+
- Reply in the same language as the conversation background.
|
|
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. The current changes (git status + diff) are already provided in the review context — do not re-run them unless marked truncated.
|
|
8
|
+
2. Project conventions were established in round 1 — do NOT re-read AGENTS.md / design docs unless a fix appears to contradict the task itself.
|
|
9
|
+
3. Read changed files for full context beyond the diff. Batch independent tool calls in one reply.
|
|
10
|
+
4. Use grep or lsp to trace callers, imports, and dependencies — only where genuinely needed.
|
|
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. The current changes (git status + diff) are already provided in the review context — do not re-run them unless marked truncated.
|
|
8
|
+
2. Project conventions were established in round 1 — do NOT re-read AGENTS.md / design docs.
|
|
9
|
+
3. Read changed files for full context beyond the diff. Batch independent tool calls in one reply.
|
|
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
|