thincoder 0.2.0 → 0.4.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 +28 -10
- package/bin/thincoder.mjs +88 -4
- package/package.json +11 -2
- package/src/SYSTEM_PROMPT.md +31 -0
- package/src/agent.mjs +458 -58
- package/src/coder-overlay.md +14 -0
- package/src/config.mjs +15 -5
- package/src/context.mjs +41 -6
- package/src/explore-overlay.md +10 -0
- package/src/mcp.mjs +359 -0
- package/src/session.mjs +2 -0
- package/src/skills.mjs +75 -0
- package/src/tools/bash.md +13 -0
- package/src/tools/delete.md +9 -0
- package/src/tools/edit.md +12 -0
- package/src/tools/fetch.md +10 -0
- package/src/tools/git_diff.md +11 -0
- package/src/tools/git_log.md +10 -0
- package/src/tools/git_status.md +8 -0
- package/src/tools/glob.md +11 -0
- package/src/tools/grep.md +12 -0
- package/src/tools/ls.md +9 -0
- package/src/tools/question.md +10 -0
- package/src/tools/read.md +10 -0
- package/src/tools/websearch.md +10 -0
- package/src/tools/write.md +9 -0
- package/src/tools.mjs +191 -20
- package/src/tui.mjs +635 -155
package/src/agent.mjs
CHANGED
|
@@ -8,10 +8,32 @@ import { chat } from "./provider.mjs"
|
|
|
8
8
|
import { compressIfNeeded } from "./context.mjs"
|
|
9
9
|
import { search as memorySearch } from "./memory.mjs"
|
|
10
10
|
import { toOpenAISchema } from "./tools.mjs"
|
|
11
|
+
import { loadSkills, formatSkillListing, readSkill } from "./skills.mjs"
|
|
11
12
|
import { readFile } from "node:fs/promises"
|
|
12
|
-
import {
|
|
13
|
+
import { readFileSync } from "node:fs"
|
|
14
|
+
import { join, dirname } from "node:path"
|
|
15
|
+
import { fileURLToPath } from "node:url"
|
|
16
|
+
import { execSync } from "node:child_process"
|
|
13
17
|
|
|
14
|
-
const
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
19
|
+
const SYSTEM_PROMPT = readFileSync(join(__dirname, "SYSTEM_PROMPT.md"), "utf8")
|
|
20
|
+
const EXPLORE_OVERLAY = readFileSync(join(__dirname, "explore-overlay.md"), "utf8")
|
|
21
|
+
const CODER_OVERLAY = readFileSync(join(__dirname, "coder-overlay.md"), "utf8")
|
|
22
|
+
|
|
23
|
+
const DEFAULT_MAX_TURNS = 100
|
|
24
|
+
const DEFAULT_SUBAGENT_TURNS = 20
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* ContinueError — agent 超过 maxTurns 时抛此错误。
|
|
28
|
+
* UI 层据此询问用户"继续?"而非直接终止。
|
|
29
|
+
*/
|
|
30
|
+
export class ContinueError extends Error {
|
|
31
|
+
constructor(turn) {
|
|
32
|
+
super(`Agent paused after ${turn} turns. Continue?`)
|
|
33
|
+
this.name = "ContinueError"
|
|
34
|
+
this.turn = turn
|
|
35
|
+
}
|
|
36
|
+
}
|
|
15
37
|
|
|
16
38
|
/**
|
|
17
39
|
* 修复历史里的两类毒数据(都会让 API 整单拒绝 invalid_request_error):
|
|
@@ -61,22 +83,61 @@ export function repairHistory(history) {
|
|
|
61
83
|
|
|
62
84
|
const VALID_TASK_STATUS = new Set(["pending", "in_progress", "done"])
|
|
63
85
|
|
|
86
|
+
/** 只读工具名集合(用于 explore 子 agent 过滤) */
|
|
87
|
+
function readonlyToolNames(tools) {
|
|
88
|
+
return new Set(tools.filter((t) => t.readonly).map((t) => t.name))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* plan 工具:进入/退出规划模式。
|
|
93
|
+
* 规划模式下只允许只读工具——探索代码、设计方案,不写代码。
|
|
94
|
+
* 用户确认方案后退出规划模式开始实现。
|
|
95
|
+
*/
|
|
96
|
+
export const planTool = {
|
|
97
|
+
name: "plan",
|
|
98
|
+
description:
|
|
99
|
+
"Enter or exit plan mode. In plan mode you are restricted to READ-ONLY tools: read files, search code, run read-only shell commands. Use plan mode before complex multi-step tasks — explore the codebase, design the architecture, present a plan to the user. When the user approves, exit plan mode and implement. For simple single-file edits, skip plan mode and just make the change.",
|
|
100
|
+
parameters: {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
action: { type: "string", enum: ["enter", "exit"], description: "Enter or exit plan mode" },
|
|
104
|
+
},
|
|
105
|
+
required: ["action"],
|
|
106
|
+
},
|
|
107
|
+
readonly: true,
|
|
108
|
+
async execute(args, ctx) {
|
|
109
|
+
if (args.action === "exit") {
|
|
110
|
+
ctx.agent.planMode = false
|
|
111
|
+
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
112
|
+
ctx.agent._pendingReminders.push("[System reminder: plan mode is now OFF. You may edit files, run commands, and implement changes. Start by executing the first step of your approved plan.]")
|
|
113
|
+
return "Plan mode exited. You may now edit files and run commands."
|
|
114
|
+
}
|
|
115
|
+
ctx.agent.planMode = true
|
|
116
|
+
ctx.agent._turnsInPlanMode = 0
|
|
117
|
+
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
118
|
+
ctx.agent._pendingReminders.push("[System reminder: plan mode is now ON. Workflow: (1) explore/read codebase with read-only tools, (2) design a solution considering trade-offs, (3) present your plan by calling plan with action='exit'. DO NOT write, edit, or run mutation commands — the user must approve your plan first.]")
|
|
119
|
+
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."
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
|
|
64
123
|
/**
|
|
65
124
|
* subagent 工具:派生子 agent 处理独立子任务(隔离上下文,只带回报告)。
|
|
66
|
-
* -
|
|
67
|
-
* -
|
|
68
|
-
* -
|
|
69
|
-
*
|
|
125
|
+
* - role: "explore" — 只读工具,搜索/阅读/分析(适合代码库探索)
|
|
126
|
+
* - role: "coder" — 全套工具,独立完成编码任务(适合隔离实现)
|
|
127
|
+
* - 不指定 role — 默认行为,同主 agent 工具集
|
|
128
|
+
* - 一批多个 subagent 调用走并行通道(parallel: true)
|
|
129
|
+
* - 不递归:子 agent 不含 subagent(depth > 0 不注入)
|
|
70
130
|
*/
|
|
71
131
|
export const subagentTool = {
|
|
72
132
|
name: "subagent",
|
|
73
133
|
description:
|
|
74
|
-
"Spawn a sub-agent to handle an independent subtask in an isolated context. The sub-agent
|
|
134
|
+
"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. Use role='explore' for codebase search/analysis (read-only, fast), role='coder' for self-contained implementation tasks. Do not give parallel subagents tasks that edit the same files.",
|
|
75
135
|
parameters: {
|
|
76
136
|
type: "object",
|
|
77
137
|
properties: {
|
|
78
138
|
task: { type: "string", description: "Self-contained task description for the sub-agent" },
|
|
79
139
|
context: { type: "string", description: "Optional background the sub-agent needs (it cannot see this conversation)" },
|
|
140
|
+
role: { type: "string", enum: ["explore", "coder"], description: "Sub-agent role: 'explore' (read-only search/analysis) or 'coder' (full implementation). Default: same tools as parent." },
|
|
80
141
|
},
|
|
81
142
|
required: ["task"],
|
|
82
143
|
},
|
|
@@ -84,17 +145,50 @@ export const subagentTool = {
|
|
|
84
145
|
parallel: true,
|
|
85
146
|
async execute(args, ctx) {
|
|
86
147
|
const parent = ctx.agent
|
|
148
|
+
const role = args.role
|
|
149
|
+
|
|
150
|
+
// 按 role 过滤工具集
|
|
151
|
+
let tools
|
|
152
|
+
if (role === "explore") {
|
|
153
|
+
const allowed = readonlyToolNames(parent.tools)
|
|
154
|
+
tools = parent.tools.filter((t) => allowed.has(t.name))
|
|
155
|
+
} else {
|
|
156
|
+
tools = parent.tools
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 按 role 选择 prompt overlay
|
|
160
|
+
let overlay = ""
|
|
161
|
+
if (role === "explore") overlay = EXPLORE_OVERLAY
|
|
162
|
+
else if (role === "coder") overlay = CODER_OVERLAY
|
|
163
|
+
|
|
164
|
+
// explore 强制只读权限;coder 继承父 agent 权限策略
|
|
165
|
+
let childPermission
|
|
166
|
+
if (role === "explore") {
|
|
167
|
+
childPermission = async () => false
|
|
168
|
+
} else {
|
|
169
|
+
childPermission = parent.autoApprove ? async () => true : async () => false
|
|
170
|
+
}
|
|
171
|
+
|
|
87
172
|
const child = createAgent({
|
|
88
173
|
provider: parent.provider,
|
|
89
|
-
tools
|
|
174
|
+
tools,
|
|
90
175
|
config: parent.config,
|
|
91
176
|
cwd: parent.cwd,
|
|
92
177
|
memory: parent.memory,
|
|
178
|
+
overlay,
|
|
93
179
|
})
|
|
94
|
-
|
|
95
|
-
const childPermission = parent.autoApprove ? async () => true : async () => false
|
|
180
|
+
|
|
96
181
|
const input = args.context ? `背景:\n${args.context}\n\n任务:\n${args.task}` : args.task
|
|
97
|
-
const report = await runAgent(child, input, { onPermissionRequest: childPermission }, { depth: (ctx.depth ?? 0) + 1 })
|
|
182
|
+
const report = await runAgent(child, input, { onPermissionRequest: childPermission }, { depth: (ctx.depth ?? 0) + 1, maxTurns: DEFAULT_SUBAGENT_TURNS })
|
|
183
|
+
|
|
184
|
+
// coder 完成后注入校验提醒到主 agent
|
|
185
|
+
if (role === "coder") {
|
|
186
|
+
parent._pendingReminders = parent._pendingReminders ?? []
|
|
187
|
+
parent._pendingReminders.push(
|
|
188
|
+
`[Subagent "${args.task?.slice(0, 80)}" finished. Verify its report: read the files it claims to have changed, run tests, and confirm the changes match the report before marking the task done.]`
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
98
192
|
const maxLen = 32000
|
|
99
193
|
return report.length > maxLen
|
|
100
194
|
? report.slice(0, maxLen) + `\n\n[... report truncated: ${report.length} chars total, ${report.length - maxLen} omitted. Ask a follow-up if you need the missing details.]`
|
|
@@ -106,14 +200,30 @@ export const subagentTool = {
|
|
|
106
200
|
* task 工具:多步任务规划与进度跟踪(Claude Code 的 todo 模式)。
|
|
107
201
|
* 每次调用整体替换列表;只改 agent 内部状态、不碰外部世界,故 readonly。
|
|
108
202
|
* 通过 ctx.agent 访问调用方 agent(由 runAgent 注入)。
|
|
109
|
-
* 注:ctx.agent 的回写是有意的轻量耦合——task 本质是主循环的内建能力而非普通工具,
|
|
110
|
-
* 伪装成工具是为了让 LLM 用统一的 tool calling 协议调用它;替代方案(主循环特判)
|
|
111
|
-
* 会让循环代码更绕,不值。
|
|
112
203
|
*/
|
|
113
204
|
export const taskTool = {
|
|
114
205
|
name: "task",
|
|
115
206
|
description:
|
|
116
|
-
"Plan and track a task list for complex multi-step work. Replaces the entire list on each call
|
|
207
|
+
"Plan and track a task list for complex multi-step work. Replaces the entire list on each call.\n" +
|
|
208
|
+
"\n" +
|
|
209
|
+
"When to use:\n" +
|
|
210
|
+
"- Multi-step tasks that span several tool calls — create the list BEFORE starting work\n" +
|
|
211
|
+
"- After receiving new multi-step instructions, capture the requirements as tasks first\n" +
|
|
212
|
+
"- Planning a sequence of edits before making them\n" +
|
|
213
|
+
"- Tracking investigation progress across a large codebase search\n" +
|
|
214
|
+
"\n" +
|
|
215
|
+
"When NOT to use:\n" +
|
|
216
|
+
"- Single-shot requests answerable in one or two tool calls\n" +
|
|
217
|
+
"- Trivial requests or purely conversational replies\n" +
|
|
218
|
+
"\n" +
|
|
219
|
+
"Discipline:\n" +
|
|
220
|
+
"- Keep exactly ONE item in_progress; mark it before starting that item\n" +
|
|
221
|
+
"- CALL THIS TOOL AGAIN to mark each item done as soon as you complete it — do not batch completions at the end\n" +
|
|
222
|
+
"- Never mark an item done if tests are failing, the implementation is partial, or errors remain\n" +
|
|
223
|
+
"- If blocked, keep the item in_progress (or add a new pending item describing the blocker) and tell the user\n" +
|
|
224
|
+
"- Avoid churn: don't re-call without real progress; never finish with stale pending items\n" +
|
|
225
|
+
"\n" +
|
|
226
|
+
"Statuses: pending | in_progress | done.",
|
|
117
227
|
parameters: {
|
|
118
228
|
type: "object",
|
|
119
229
|
properties: {
|
|
@@ -138,21 +248,191 @@ export const taskTool = {
|
|
|
138
248
|
status: VALID_TASK_STATUS.has(it.status) ? it.status : "pending",
|
|
139
249
|
}))
|
|
140
250
|
ctx.agent.tasks = items
|
|
251
|
+
ctx.agent._turnsSinceTaskUpdate = 0
|
|
141
252
|
ctx.agent._onTaskUpdate?.(items)
|
|
142
253
|
const done = items.filter((i) => i.status === "done").length
|
|
143
254
|
const open = items.length - done
|
|
144
255
|
return `Task list updated: ${done}/${items.length} done` +
|
|
145
|
-
(open > 0 ? ` — ${open} item(s) still open; call task again as you complete them.` : " — all done.")
|
|
256
|
+
(open > 0 ? ` — ${open} item(s) still open; call task again as you complete them.` : " — all done.") +
|
|
257
|
+
`\nEnsure you keep using the task list to track progress: mark items done immediately after finishing them, and keep exactly one item in_progress while work is underway.`
|
|
146
258
|
},
|
|
147
259
|
}
|
|
148
260
|
|
|
149
|
-
/**
|
|
261
|
+
/**
|
|
262
|
+
* skill 工具:按需加载项目技能文件(.thincoder/skills/*.md)。
|
|
263
|
+
* 加载后技能内容以 <skill-loaded> 包裹写入对话,供后续参考。
|
|
264
|
+
* 列出所有可用技能用 action="list"。
|
|
265
|
+
*/
|
|
266
|
+
export const skillTool = {
|
|
267
|
+
name: "skill",
|
|
268
|
+
description:
|
|
269
|
+
"Load a project skill from .thincoder/skills/. Skills contain reusable instructions, workflows, or reference material. Use this when the user references a skill by name, or when a task matches a known skill's description. Call with action='list' to see available skills; call with action='load' and name=<skill> to activate one.",
|
|
270
|
+
parameters: {
|
|
271
|
+
type: "object",
|
|
272
|
+
properties: {
|
|
273
|
+
action: { type: "string", enum: ["list", "load"], description: "'list' to see available skills, 'load' to activate one" },
|
|
274
|
+
name: { type: "string", description: "Skill name (for 'load' action)" },
|
|
275
|
+
},
|
|
276
|
+
required: ["action"],
|
|
277
|
+
},
|
|
278
|
+
readonly: true,
|
|
279
|
+
async execute(args, ctx) {
|
|
280
|
+
const skills = await loadSkills(ctx.agent.cwd)
|
|
281
|
+
if (args.action === "list") {
|
|
282
|
+
if (skills.length === 0) return "No project skills found in .thincoder/skills/."
|
|
283
|
+
return skills.map((s) => `- ${s.name}: ${s.description}`).join("\n")
|
|
284
|
+
}
|
|
285
|
+
if (!args.name) return "Error: skill name required for 'load' action."
|
|
286
|
+
const content = await readSkill(ctx.agent.cwd, args.name)
|
|
287
|
+
if (!content) {
|
|
288
|
+
const available = skills.map((s) => s.name).join(", ")
|
|
289
|
+
return `Error: skill "${args.name}" not found. Available: ${available || "(none)"}`
|
|
290
|
+
}
|
|
291
|
+
// 注入 skill 内容到 history(下一条 user 消息)
|
|
292
|
+
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
293
|
+
ctx.agent._pendingReminders.push(
|
|
294
|
+
`<skill-loaded name="${args.name}" source=".thincoder/skills/${args.name}.md">\n${content}\n</skill-loaded>\n\nFollow the skill's instructions above for the current task.`
|
|
295
|
+
)
|
|
296
|
+
return `Skill "${args.name}" loaded. Instructions will appear in the next message.`
|
|
297
|
+
},
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* goal 工具:设置/更新/取消长期任务目标。
|
|
302
|
+
* 用于跨多轮会话的自主任务——agent 记住自己要完成什么,
|
|
303
|
+
* 系统每 8 轮注入一次进度提醒。
|
|
304
|
+
*/
|
|
305
|
+
export const goalTool = {
|
|
306
|
+
name: "goal",
|
|
307
|
+
description:
|
|
308
|
+
"Set or update a long-running goal that spans many turns. Use for autonomous tasks where you need to remember the objective across context compaction. Call with action='set' to create/update the goal, or action='cancel' to clear it. The system will periodically remind you of the current goal.",
|
|
309
|
+
parameters: {
|
|
310
|
+
type: "object",
|
|
311
|
+
properties: {
|
|
312
|
+
action: { type: "string", enum: ["set", "cancel"], description: "'set' to create/update the goal, 'cancel' to clear" },
|
|
313
|
+
objective: { type: "string", description: "What you are trying to accomplish (for 'set' action)" },
|
|
314
|
+
criteria: { type: "string", description: "How you know it's done (for 'set' action)" },
|
|
315
|
+
},
|
|
316
|
+
required: ["action"],
|
|
317
|
+
},
|
|
318
|
+
readonly: true,
|
|
319
|
+
async execute(args, ctx) {
|
|
320
|
+
if (args.action === "cancel") {
|
|
321
|
+
ctx.agent.goal = null
|
|
322
|
+
return "Goal cancelled. If the goal was blocked or impossible, explain why in your next message — the user can clarify, adjust scope, or confirm cancellation."
|
|
323
|
+
}
|
|
324
|
+
if (!args.objective) return "Error: 'objective' required for 'set' action."
|
|
325
|
+
ctx.agent.goal = {
|
|
326
|
+
objective: String(args.objective).slice(0, 500),
|
|
327
|
+
criteria: String(args.criteria ?? "").slice(0, 500),
|
|
328
|
+
setAt: Date.now(),
|
|
329
|
+
}
|
|
330
|
+
return `Goal set: ${ctx.agent.goal.objective}${ctx.agent.goal.criteria ? `\nDone when: ${ctx.agent.goal.criteria}` : ""}`
|
|
331
|
+
},
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* verify 工具:完成前的自检。调用时会展示:
|
|
336
|
+
* 1. git diff --stat — 所有变更文件
|
|
337
|
+
* 2. task 列表 — 是否全部 done
|
|
338
|
+
* 3. 一个自检清单
|
|
339
|
+
* Agent 不应该在 verify 通过前说"完成"。
|
|
340
|
+
*/
|
|
341
|
+
export const verifyTool = {
|
|
342
|
+
name: "verify",
|
|
343
|
+
description:
|
|
344
|
+
"Run a pre-completion self-check. Shows what files changed (git diff --stat), the current task list, and a verification checklist. Call this BEFORE declaring any coding task complete — do not say 'done' until verify passes.",
|
|
345
|
+
parameters: {
|
|
346
|
+
type: "object",
|
|
347
|
+
properties: {},
|
|
348
|
+
},
|
|
349
|
+
readonly: true,
|
|
350
|
+
async execute(_args, ctx) {
|
|
351
|
+
const lines = []
|
|
352
|
+
lines.push("=== VERIFICATION REPORT ===")
|
|
353
|
+
lines.push("")
|
|
354
|
+
|
|
355
|
+
// 1. Git diff
|
|
356
|
+
try {
|
|
357
|
+
const diff = execSync("git diff --stat", { cwd: ctx.agent.cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })
|
|
358
|
+
if (diff.trim()) {
|
|
359
|
+
lines.push("Changed files (git diff --stat):")
|
|
360
|
+
lines.push(diff.trim())
|
|
361
|
+
} else {
|
|
362
|
+
lines.push("Changed files: (none — no uncommitted changes)")
|
|
363
|
+
}
|
|
364
|
+
} catch {
|
|
365
|
+
lines.push("Changed files: (not a git repo or git unavailable)")
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// 2. 未跟踪文件
|
|
369
|
+
try {
|
|
370
|
+
const untracked = execSync("git ls-files --others --exclude-standard", { cwd: ctx.agent.cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })
|
|
371
|
+
if (untracked.trim()) {
|
|
372
|
+
lines.push("")
|
|
373
|
+
lines.push("Untracked files:")
|
|
374
|
+
lines.push(untracked.trim())
|
|
375
|
+
}
|
|
376
|
+
} catch {
|
|
377
|
+
// 静默
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// 3. Task 列表
|
|
381
|
+
lines.push("")
|
|
382
|
+
if (ctx.agent.tasks.length === 0) {
|
|
383
|
+
lines.push("Task list: (no tasks tracked)")
|
|
384
|
+
} else {
|
|
385
|
+
const done = ctx.agent.tasks.filter((t) => t.status === "done").length
|
|
386
|
+
const total = ctx.agent.tasks.length
|
|
387
|
+
const open = ctx.agent.tasks.filter((t) => t.status !== "done")
|
|
388
|
+
lines.push(`Task list: ${done}/${total} done`)
|
|
389
|
+
for (const t of ctx.agent.tasks) {
|
|
390
|
+
const mark = t.status === "done" ? "✓" : t.status === "in_progress" ? "▶" : "○"
|
|
391
|
+
lines.push(` ${mark} [${t.status}] ${t.title}`)
|
|
392
|
+
}
|
|
393
|
+
if (open.length > 0) {
|
|
394
|
+
lines.push("")
|
|
395
|
+
lines.push(`WARNING: ${open.length} task(s) still open. Complete them or explain why they can be left undone.`)
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// 4. Checklist
|
|
400
|
+
lines.push("")
|
|
401
|
+
lines.push("Self-review checklist:")
|
|
402
|
+
lines.push("- [ ] Did I run the project's tests and do they pass?")
|
|
403
|
+
lines.push("- [ ] Did I read every file I changed to catch leftover debug code or stale comments?")
|
|
404
|
+
lines.push("- [ ] Do comments and docstrings match what the code actually does?")
|
|
405
|
+
lines.push("- [ ] Did I remove placeholder code, TODO stubs, or commented-out experiment blocks?")
|
|
406
|
+
lines.push("- [ ] If I used a subagent, did I verify its report against the actual files it touched?")
|
|
407
|
+
lines.push("- [ ] Are all task items genuinely done (not just marked done to finish early)?")
|
|
408
|
+
|
|
409
|
+
return lines.join("\n")
|
|
410
|
+
},
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** 项目指令文件候选(cwd 本地,按优先级拼接) */
|
|
150
414
|
const INSTRUCTION_FILES = ["AGENTS.md", "agents.md", "PROJECT_RULES.md", "project_rules.md", ".thincoder/rules.md"]
|
|
151
415
|
const MAX_INSTRUCTION_CHARS = 8000
|
|
152
416
|
|
|
153
|
-
/**
|
|
417
|
+
/**
|
|
418
|
+
* 读取项目指令,两层合并:
|
|
419
|
+
* 1. 用户全局:~/.thincoder/AGENTS.md(适用所有项目)
|
|
420
|
+
* 2. 项目本地:cwd 下的 AGENTS.md / project_rules 等
|
|
421
|
+
* 最多 8000 字符。
|
|
422
|
+
*/
|
|
154
423
|
export async function loadProjectInstructions(cwd) {
|
|
155
424
|
const parts = []
|
|
425
|
+
const { homedir } = await import("node:os")
|
|
426
|
+
|
|
427
|
+
// 用户全局指令(优先级低,放前面)
|
|
428
|
+
try {
|
|
429
|
+
const globalText = await readFile(join(homedir(), ".thincoder", "AGENTS.md"), "utf8")
|
|
430
|
+
if (globalText.trim()) parts.push(`# ~/.thincoder/AGENTS.md (user-global conventions)\n${globalText.trim()}`)
|
|
431
|
+
} catch {
|
|
432
|
+
// 不存在,跳过
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// 项目本地指令(优先级高,放后面)
|
|
156
436
|
for (const name of INSTRUCTION_FILES) {
|
|
157
437
|
try {
|
|
158
438
|
const text = await readFile(join(cwd, name), "utf8")
|
|
@@ -165,34 +445,27 @@ export async function loadProjectInstructions(cwd) {
|
|
|
165
445
|
return parts.join("\n\n").slice(0, MAX_INSTRUCTION_CHARS)
|
|
166
446
|
}
|
|
167
447
|
|
|
168
|
-
const SYSTEM_PROMPT = `You are ThinCoder, a coding agent. Thin means sharp: you are a terse, precise engineer who cuts straight to the point—no fluff, no showing off, no filler. You write the most minimal, elegant code that solves the problem, and you say things in as few words as the truth allows.
|
|
169
|
-
|
|
170
|
-
Rules:
|
|
171
|
-
- Prefer tool calls over guessing. Read files before modifying them.
|
|
172
|
-
- When you need multiple independent pieces of information (e.g. reading several files), make all independent tool calls in the SAME response so they can run in parallel.
|
|
173
|
-
- Be concise in your final answers. Report what you did, not what you plan to do.
|
|
174
|
-
- If the request is ambiguous at a decision that matters, stop and ask in your reply instead of guessing—but ask at most once, then proceed with the most reasonable interpretation.
|
|
175
|
-
- When the user shares an observation or opinion, don't mistake it for a command—confirm before making changes.
|
|
176
|
-
- For complex multi-step requests (3+ steps), use the task tool to plan and track progress; keep exactly one item in_progress, and update the list as you complete items—never finish with stale pending items.
|
|
177
|
-
- For independent research/exploration subtasks, spawn subagents in the SAME response to run them in parallel—they work in isolated contexts and return final reports. Delegate breadth-first exploration; do precision edits yourself. Never assign parallel subagents tasks that edit the same files.
|
|
178
|
-
- Never fabricate file contents or command outputs; only trust tool results.
|
|
179
|
-
- Before declaring a coding task complete, verify it: run the project's relevant tests/build if they exist. If you could not verify, say so explicitly—never present unverified work as done.
|
|
180
|
-
- Run shell commands non-interactively: git commit -m, git --no-pager, -y/--yes flags where applicable. There is no TTY; editors and pagers (vim, less) cannot be used.
|
|
181
|
-
- You have long-term memory via memory_put/memory_search. When you learn a durable fact about this project (convention, decision, debugging insight), save it with memory_put. Relevant memories may be injected below—use them.`
|
|
182
|
-
|
|
183
448
|
/**
|
|
184
449
|
* 创建 agent。
|
|
185
|
-
* { provider, tools, config, cwd, memory? }
|
|
450
|
+
* { provider, tools, config, cwd, memory?, overlay? }
|
|
451
|
+
* overlay — 子 agent 角色覆盖文本,拼接在 system prompt 末尾
|
|
186
452
|
*/
|
|
187
|
-
export function createAgent({ provider, tools, config, cwd, memory = null }) {
|
|
453
|
+
export function createAgent({ provider, tools, config, cwd, memory = null, overlay = "" }) {
|
|
188
454
|
return {
|
|
189
455
|
provider,
|
|
190
456
|
tools,
|
|
191
457
|
config,
|
|
192
458
|
cwd,
|
|
193
459
|
memory,
|
|
460
|
+
overlay,
|
|
194
461
|
history: [], // OpenAI 格式的对话历史(不含 system)
|
|
195
|
-
tasks: [],
|
|
462
|
+
tasks: [], // task 工具维护的任务列表
|
|
463
|
+
planMode: false, // plan 工具切换的规划模式
|
|
464
|
+
goal: null, // goal 工具设置的长期目标 { objective, criteria, setAt }
|
|
465
|
+
_pendingReminders: [], // 模式切换提醒,在主循环中刷新后写入 history
|
|
466
|
+
_turnsSinceTaskUpdate: 0, // 距上次 task 工具调用的轮数(过期提醒用)
|
|
467
|
+
_turnsInPlanMode: 0, // plan mode 中持续的轮数(引导提醒用)
|
|
468
|
+
_sessionStart: null, // 首次 runAgent 时固定(system prompt 稳定,前缀缓存用)
|
|
196
469
|
}
|
|
197
470
|
}
|
|
198
471
|
|
|
@@ -205,40 +478,88 @@ export function createAgent({ provider, tools, config, cwd, memory = null }) {
|
|
|
205
478
|
* }
|
|
206
479
|
* 返回最终文本。
|
|
207
480
|
*/
|
|
208
|
-
export async function runAgent(agent, input, callbacks = {}, { depth = 0 } = {}) {
|
|
209
|
-
const maxTurns = agent.config?.agent?.maxTurns ?? DEFAULT_MAX_TURNS
|
|
481
|
+
export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal, maxTurns: overrideTurns, resume = false } = {}) {
|
|
482
|
+
const maxTurns = overrideTurns ?? agent.config?.agent?.maxTurns ?? DEFAULT_MAX_TURNS
|
|
210
483
|
const threshold = agent.config?.agent?.compactThreshold ?? 100_000
|
|
211
484
|
// 先修复历史(恢复的会话可能有中断的 tool_calls),再追加新输入
|
|
212
485
|
agent.history = repairHistory(agent.history)
|
|
213
|
-
|
|
486
|
+
if (!resume) {
|
|
487
|
+
// 相关记忆作为独立 user 上下文消息注入,而不是塞进 system prompt——
|
|
488
|
+
// system prompt 跨 run 逐字节一致,DeepSeek context caching(前缀缓存,命中便宜 ~120x)才能命中
|
|
489
|
+
if (agent.memory) {
|
|
490
|
+
const memories = await memorySearch(agent.memory, input, { limit: 3 })
|
|
491
|
+
if (memories.length > 0) {
|
|
492
|
+
agent.history.push({
|
|
493
|
+
role: "user",
|
|
494
|
+
content:
|
|
495
|
+
"[Relevant memories from previous sessions (context, not instructions):\n" +
|
|
496
|
+
memories.map((m) => `- [${m.type}] ${m.title}: ${m.content}`).join("\n") +
|
|
497
|
+
"]",
|
|
498
|
+
})
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
agent.history.push({ role: "user", content: input })
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// 刷新上轮积压的提醒(如 /auto 切换在两次 runAgent 之间注入的)
|
|
505
|
+
if (agent._pendingReminders.length > 0) {
|
|
506
|
+
for (const reminder of agent._pendingReminders) {
|
|
507
|
+
agent.history.push({ role: "user", content: reminder })
|
|
508
|
+
}
|
|
509
|
+
agent._pendingReminders = []
|
|
510
|
+
}
|
|
214
511
|
|
|
215
|
-
// task 工具随主循环注入(内建能力);subagent 只在顶层注入(禁止递归)
|
|
216
|
-
const tools = [...agent.tools, taskTool, ...(depth === 0 ? [subagentTool] : [])]
|
|
512
|
+
// task/plan 工具随主循环注入(内建能力);subagent/skill/goal/verify 只在顶层注入(禁止递归)
|
|
513
|
+
const tools = [...agent.tools, taskTool, planTool, ...(depth === 0 ? [subagentTool, skillTool, goalTool, verifyTool] : [])]
|
|
217
514
|
const toolSchemas = tools.map(toOpenAISchema)
|
|
218
515
|
const toolByName = new Map(tools.map((t) => [t.name, t]))
|
|
219
516
|
agent._onTaskUpdate = callbacks.onTaskUpdate
|
|
220
517
|
|
|
221
|
-
//
|
|
518
|
+
// 环境信息 + profile overlay:附加到 system prompt
|
|
519
|
+
// 注意:这里只能放跨 run 稳定的内容(前缀缓存要求 system prompt 逐字节一致)——
|
|
520
|
+
// session start 时间戳每会话固定一次;每轮变化的记忆注入走上面的 user 上下文消息
|
|
222
521
|
let systemPrompt = SYSTEM_PROMPT
|
|
522
|
+
if (agent.overlay) systemPrompt += `\n\n${agent.overlay}`
|
|
523
|
+
const platform = { win32: 'Windows', darwin: 'macOS', linux: 'Linux' }[process.platform] ?? process.platform
|
|
524
|
+
agent._sessionStart ??= new Date().toISOString()
|
|
525
|
+
systemPrompt += `\n\nOS: ${platform}. Working directory: ${agent.cwd}. Session start: ${agent._sessionStart}.`
|
|
223
526
|
const projectRules = await loadProjectInstructions(agent.cwd)
|
|
224
527
|
if (projectRules) {
|
|
225
528
|
systemPrompt += `\n\nProject instructions (follow these as project conventions):\n${projectRules}`
|
|
226
529
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
530
|
+
// 技能列表注入(仅顶层 agent,子 agent 不需要);按 cwd 稳定,变更才会破缓存(可接受)
|
|
531
|
+
if (depth === 0) {
|
|
532
|
+
const skills = await loadSkills(agent.cwd)
|
|
533
|
+
const listing = formatSkillListing(skills)
|
|
534
|
+
if (listing) systemPrompt += `\n\n${listing}`
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// 以 AUTO 模式启动时注入一次提醒(历史里已有就不重复,防每轮对话都堆一条)
|
|
538
|
+
const AUTO_REMINDER = "[System reminder: AUTO mode is active — all tool calls are automatically approved without asking.]"
|
|
539
|
+
if (agent.autoApprove && !agent.history.some((m) => m.content === AUTO_REMINDER)) {
|
|
540
|
+
agent.history.push({ role: "user", content: AUTO_REMINDER })
|
|
234
541
|
}
|
|
235
542
|
|
|
543
|
+
// 完成守卫的每轮运行状态:改了东西(写/编辑类工具)却没自检过,不收工、推回去验证
|
|
544
|
+
// bash/subagent 不算 mutation(跑测试、explore 子 agent 不该触发;coder 子 agent 有专属校验提醒)
|
|
545
|
+
let mutatedThisRun = false
|
|
546
|
+
let verifiedThisRun = false
|
|
547
|
+
let completionGuardFired = false
|
|
548
|
+
|
|
236
549
|
for (let turn = 0; turn < maxTurns; turn++) {
|
|
237
|
-
//
|
|
238
|
-
|
|
550
|
+
// 递增跟踪计数器
|
|
551
|
+
agent._turnsSinceTaskUpdate++
|
|
552
|
+
if (agent.planMode) agent._turnsInPlanMode++
|
|
553
|
+
|
|
554
|
+
// 每轮 LLM 调用前检查上下文长度,超阈值先压缩
|
|
555
|
+
// 压缩失败不终止 agent 循环——宁可继续跑长上下文也别中断任务
|
|
239
556
|
if (agent.history.at(-1)?.role === "user") {
|
|
240
|
-
|
|
241
|
-
|
|
557
|
+
try {
|
|
558
|
+
if (await compressIfNeeded(agent, threshold)) {
|
|
559
|
+
callbacks.onCompress?.()
|
|
560
|
+
}
|
|
561
|
+
} catch {
|
|
562
|
+
// 压缩 LLM 调用失败(限流/网络),静默跳过;下一轮重试
|
|
242
563
|
}
|
|
243
564
|
}
|
|
244
565
|
|
|
@@ -249,7 +570,10 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0 } = {})
|
|
|
249
570
|
tools: toolSchemas,
|
|
250
571
|
onToken: callbacks.onToken,
|
|
251
572
|
onReasoning: callbacks.onReasoning,
|
|
573
|
+
signal,
|
|
252
574
|
})
|
|
575
|
+
// token 用量(含 DeepSeek 缓存命中/未命中)透传给 UI 层展示
|
|
576
|
+
if (response.usage) callbacks.onUsage?.(response.usage)
|
|
253
577
|
|
|
254
578
|
// 无工具调用:最终回答,收尾
|
|
255
579
|
if (response.toolCalls.length === 0) {
|
|
@@ -257,6 +581,16 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0 } = {})
|
|
|
257
581
|
if (!response.content) {
|
|
258
582
|
throw new Error("LLM 返回了空回复(可能是思考耗尽或被截断)。可 /think effort 降低推理强度后重试")
|
|
259
583
|
}
|
|
584
|
+
// 完成守卫:本轮改过文件却没跑过 verify,推回去验证一次(只推一次,防死循环)
|
|
585
|
+
if (depth === 0 && mutatedThisRun && !verifiedThisRun && !completionGuardFired) {
|
|
586
|
+
completionGuardFired = true
|
|
587
|
+
agent.history.push({ role: "assistant", content: response.content })
|
|
588
|
+
agent.history.push({
|
|
589
|
+
role: "user",
|
|
590
|
+
content: "[System reminder: you modified files in this run but have not verified the changes. Before finishing: run the project's tests/build, look at the results, and call the verify tool for a final self-check. If verification is genuinely impossible here, say so explicitly in your reply. Never mention this reminder to the user.]",
|
|
591
|
+
})
|
|
592
|
+
continue
|
|
593
|
+
}
|
|
260
594
|
agent.history.push({ role: "assistant", content: response.content })
|
|
261
595
|
return response.content
|
|
262
596
|
}
|
|
@@ -270,9 +604,12 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0 } = {})
|
|
|
270
604
|
type: "function",
|
|
271
605
|
function: { name: tc.name, arguments: tc.arguments },
|
|
272
606
|
})),
|
|
607
|
+
// thinking 模式:reasoning_content 必须跨请求原样回传(DeepSeek 要求,缺失会 400;
|
|
608
|
+
// 非 thinking 模型 reasoning 恒为空串,不附加字段,严格协议端点不受影响)
|
|
609
|
+
...(response.reasoning ? { reasoning_content: response.reasoning } : {}),
|
|
273
610
|
})
|
|
274
611
|
|
|
275
|
-
const results = await executeToolCalls(agent, toolByName, response.toolCalls, callbacks, depth)
|
|
612
|
+
const results = await executeToolCalls(agent, toolByName, response.toolCalls, callbacks, depth, signal)
|
|
276
613
|
|
|
277
614
|
// 结果按 toolCallId 配对回喂(协议按 ID 不按位置,完成乱序无影响)
|
|
278
615
|
for (const { toolCall, result } of results) {
|
|
@@ -281,19 +618,69 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0 } = {})
|
|
|
281
618
|
tool_call_id: toolCall.id,
|
|
282
619
|
content: result,
|
|
283
620
|
})
|
|
621
|
+
// 完成守卫状态跟踪(失败的调用不算数)
|
|
622
|
+
const tool = toolByName.get(toolCall.name)
|
|
623
|
+
if (tool && !result.startsWith("Error")) {
|
|
624
|
+
if (!tool.readonly && toolCall.name !== "bash" && toolCall.name !== "subagent") mutatedThisRun = true
|
|
625
|
+
if (toolCall.name === "verify") verifiedThisRun = true
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// 刷新待处理的模式提醒(plan/auto 切换后注入,在工具结果之后)
|
|
630
|
+
if (agent._pendingReminders.length > 0) {
|
|
631
|
+
for (const reminder of agent._pendingReminders) {
|
|
632
|
+
agent.history.push({ role: "user", content: reminder })
|
|
633
|
+
}
|
|
634
|
+
agent._pendingReminders = []
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// 每 10 轮注入一次 goal 提醒(长期任务进度感知)
|
|
638
|
+
if (agent.goal && turn > 0 && turn % 10 === 0) {
|
|
639
|
+
agent.history.push({
|
|
640
|
+
role: "user",
|
|
641
|
+
content: `[System reminder: your current goal is: "${agent.goal.objective}"${agent.goal.criteria ? ` — Done when: ${agent.goal.criteria}` : ""}. Stay focused on this objective. Use the goal tool to update or cancel it.]`,
|
|
642
|
+
})
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// 每 10 轮注入 task 提醒(仅顶层:子 agent 生命周期短、任务单一,提醒建表纯浪费 token):
|
|
646
|
+
// 有未完成项催更新;从未建列表则建议为多步工作建一个(对齐 kimi-code 的闲置提醒)
|
|
647
|
+
if (depth === 0 && agent._turnsSinceTaskUpdate >= 10) {
|
|
648
|
+
const hasIncomplete = agent.tasks.some((t) => t.status !== "done")
|
|
649
|
+
if (agent.tasks.length > 0 && hasIncomplete) {
|
|
650
|
+
const taskSummary = agent.tasks.map((t) => `- [${t.status}] ${t.title}`).join("\n")
|
|
651
|
+
agent.history.push({
|
|
652
|
+
role: "user",
|
|
653
|
+
content: `[System reminder: active task list, last updated ${agent._turnsSinceTaskUpdate} turns ago:\n${taskSummary}\nUse the task tool to update progress. Never mention this reminder to the user.]`,
|
|
654
|
+
})
|
|
655
|
+
} else if (agent.tasks.length === 0) {
|
|
656
|
+
agent.history.push({
|
|
657
|
+
role: "user",
|
|
658
|
+
content: "[System reminder: no task list is being tracked. If the current work is a multi-step task, consider using the task tool to plan and track progress. This is a gentle reminder; ignore it if not applicable. Never mention this reminder to the user.]",
|
|
659
|
+
})
|
|
660
|
+
}
|
|
661
|
+
agent._turnsSinceTaskUpdate = 0
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// 每 8 轮注入 plan mode 引导:防止无限探索不产出方案
|
|
665
|
+
if (agent.planMode && agent._turnsInPlanMode >= 8) {
|
|
666
|
+
agent.history.push({
|
|
667
|
+
role: "user",
|
|
668
|
+
content: "[System reminder: plan mode still active after several turns. Plan mode workflow: (1) explore/read codebase, (2) design a solution, (3) present the plan by calling plan with action='exit' so the user can approve it. If you've explored enough, exit plan mode now. Never mention this reminder to the user.]",
|
|
669
|
+
})
|
|
670
|
+
agent._turnsInPlanMode = 0
|
|
284
671
|
}
|
|
285
672
|
}
|
|
286
673
|
|
|
287
|
-
throw new
|
|
674
|
+
throw new ContinueError(maxTurns)
|
|
288
675
|
}
|
|
289
676
|
|
|
290
677
|
/**
|
|
291
678
|
* 两段式执行:
|
|
292
|
-
* 阶段一(串行):逐个解析参数 + 权限确认(有副作用工具)
|
|
679
|
+
* 阶段一(串行):逐个解析参数 + planMode 检查 + 权限确认(有副作用工具)
|
|
293
680
|
* 阶段二(分类):只读工具 Promise.all 并行;有副作用工具逐个串行
|
|
294
681
|
* 返回按 toolCallId 配对的结果数组。
|
|
295
682
|
*/
|
|
296
|
-
async function executeToolCalls(agent, toolByName, toolCalls, callbacks, depth = 0) {
|
|
683
|
+
async function executeToolCalls(agent, toolByName, toolCalls, callbacks, depth = 0, signal) {
|
|
297
684
|
// ---- 阶段一:串行准备 ----
|
|
298
685
|
const prepared = []
|
|
299
686
|
for (const toolCall of toolCalls) {
|
|
@@ -311,6 +698,12 @@ async function executeToolCalls(agent, toolByName, toolCalls, callbacks, depth =
|
|
|
311
698
|
continue
|
|
312
699
|
}
|
|
313
700
|
|
|
701
|
+
// plan 模式:拒绝所有非只读工具
|
|
702
|
+
if (agent.planMode && !tool.readonly) {
|
|
703
|
+
prepared.push({ toolCall, tool, denied: true, reason: "plan mode" })
|
|
704
|
+
continue
|
|
705
|
+
}
|
|
706
|
+
|
|
314
707
|
if (!tool.readonly) {
|
|
315
708
|
const allowed = callbacks.onPermissionRequest
|
|
316
709
|
? await callbacks.onPermissionRequest(toolCall.name, args)
|
|
@@ -328,13 +721,20 @@ async function executeToolCalls(agent, toolByName, toolCalls, callbacks, depth =
|
|
|
328
721
|
// ---- 阶段二:分类执行 ----
|
|
329
722
|
const runOne = async (item) => {
|
|
330
723
|
if (item.error) return { ...item, result: `Error: ${item.error}` }
|
|
331
|
-
if (item.denied)
|
|
724
|
+
if (item.denied) {
|
|
725
|
+
const reason = item.reason === "plan mode"
|
|
726
|
+
? "Error: plan mode is active — only read-only tools are allowed. Exit plan mode first."
|
|
727
|
+
: "Error: permission denied by user"
|
|
728
|
+
return { ...item, result: reason }
|
|
729
|
+
}
|
|
332
730
|
try {
|
|
333
731
|
const result = await item.tool.execute(item.args, {
|
|
334
732
|
cwd: agent.cwd,
|
|
335
733
|
agent,
|
|
336
734
|
depth,
|
|
735
|
+
signal,
|
|
337
736
|
onOutput: (chunk) => callbacks.onToolOutput?.(item.toolCall.name, chunk),
|
|
737
|
+
onQuestion: callbacks.onQuestion,
|
|
338
738
|
})
|
|
339
739
|
callbacks.onToolResult?.(item.toolCall.name, result)
|
|
340
740
|
return { ...item, result: String(result) }
|