thincoder 0.1.0 → 0.3.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 +59 -35
- package/bin/thincoder.mjs +194 -23
- package/package.json +1 -1
- package/src/SYSTEM_PROMPT.md +30 -0
- package/src/agent.mjs +471 -61
- package/src/coder-overlay.md +14 -0
- package/src/config.mjs +91 -20
- package/src/context.mjs +41 -6
- package/src/explore-overlay.md +10 -0
- package/src/mcp.mjs +359 -0
- package/src/provider.mjs +7 -1
- package/src/session.mjs +7 -4
- 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 +1181 -122
package/src/agent.mjs
CHANGED
|
@@ -8,15 +8,40 @@ 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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
39
|
+
* 修复历史里的两类毒数据(都会让 API 整单拒绝 invalid_request_error):
|
|
40
|
+
* 1. 空 assistant 消息:无正文、无 tool_calls(思考流跑完正文为空、被截断时可能产生),
|
|
41
|
+
* 直接丢弃——"assistant must not be empty"。
|
|
42
|
+
* 2. 断头 tool_calls:assistant 消息带了 tool_calls 但后面缺对应的 tool 结果
|
|
43
|
+
* (进程在工具执行中途被杀、会话中断等)。为每个缺失的 tool_call_id 补一条
|
|
44
|
+
* 中断占位消息。
|
|
20
45
|
* 返回修复后的新数组;无问题时返回原数组。
|
|
21
46
|
*/
|
|
22
47
|
export function repairHistory(history) {
|
|
@@ -24,6 +49,11 @@ export function repairHistory(history) {
|
|
|
24
49
|
let dirty = false
|
|
25
50
|
for (let i = 0; i < history.length; i++) {
|
|
26
51
|
const m = history[i]
|
|
52
|
+
// 空 assistant 消息:无正文且无 tool_calls,丢弃
|
|
53
|
+
if (m.role === "assistant" && !m.tool_calls?.length && !m.content) {
|
|
54
|
+
dirty = true
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
27
57
|
out.push(m)
|
|
28
58
|
if (m.role !== "assistant" || !m.tool_calls?.length) continue
|
|
29
59
|
|
|
@@ -53,22 +83,61 @@ export function repairHistory(history) {
|
|
|
53
83
|
|
|
54
84
|
const VALID_TASK_STATUS = new Set(["pending", "in_progress", "done"])
|
|
55
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
|
+
|
|
56
123
|
/**
|
|
57
124
|
* subagent 工具:派生子 agent 处理独立子任务(隔离上下文,只带回报告)。
|
|
58
|
-
* -
|
|
59
|
-
* -
|
|
60
|
-
* -
|
|
61
|
-
*
|
|
125
|
+
* - role: "explore" — 只读工具,搜索/阅读/分析(适合代码库探索)
|
|
126
|
+
* - role: "coder" — 全套工具,独立完成编码任务(适合隔离实现)
|
|
127
|
+
* - 不指定 role — 默认行为,同主 agent 工具集
|
|
128
|
+
* - 一批多个 subagent 调用走并行通道(parallel: true)
|
|
129
|
+
* - 不递归:子 agent 不含 subagent(depth > 0 不注入)
|
|
62
130
|
*/
|
|
63
131
|
export const subagentTool = {
|
|
64
132
|
name: "subagent",
|
|
65
133
|
description:
|
|
66
|
-
"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.",
|
|
67
135
|
parameters: {
|
|
68
136
|
type: "object",
|
|
69
137
|
properties: {
|
|
70
138
|
task: { type: "string", description: "Self-contained task description for the sub-agent" },
|
|
71
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." },
|
|
72
141
|
},
|
|
73
142
|
required: ["task"],
|
|
74
143
|
},
|
|
@@ -76,17 +145,50 @@ export const subagentTool = {
|
|
|
76
145
|
parallel: true,
|
|
77
146
|
async execute(args, ctx) {
|
|
78
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
|
+
|
|
79
172
|
const child = createAgent({
|
|
80
173
|
provider: parent.provider,
|
|
81
|
-
tools
|
|
174
|
+
tools,
|
|
82
175
|
config: parent.config,
|
|
83
176
|
cwd: parent.cwd,
|
|
84
177
|
memory: parent.memory,
|
|
178
|
+
overlay,
|
|
85
179
|
})
|
|
86
|
-
|
|
87
|
-
const childPermission = parent.autoApprove ? async () => true : async () => false
|
|
180
|
+
|
|
88
181
|
const input = args.context ? `背景:\n${args.context}\n\n任务:\n${args.task}` : args.task
|
|
89
|
-
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
|
+
|
|
90
192
|
const maxLen = 32000
|
|
91
193
|
return report.length > maxLen
|
|
92
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.]`
|
|
@@ -98,14 +200,30 @@ export const subagentTool = {
|
|
|
98
200
|
* task 工具:多步任务规划与进度跟踪(Claude Code 的 todo 模式)。
|
|
99
201
|
* 每次调用整体替换列表;只改 agent 内部状态、不碰外部世界,故 readonly。
|
|
100
202
|
* 通过 ctx.agent 访问调用方 agent(由 runAgent 注入)。
|
|
101
|
-
* 注:ctx.agent 的回写是有意的轻量耦合——task 本质是主循环的内建能力而非普通工具,
|
|
102
|
-
* 伪装成工具是为了让 LLM 用统一的 tool calling 协议调用它;替代方案(主循环特判)
|
|
103
|
-
* 会让循环代码更绕,不值。
|
|
104
203
|
*/
|
|
105
204
|
export const taskTool = {
|
|
106
205
|
name: "task",
|
|
107
206
|
description:
|
|
108
|
-
"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.",
|
|
109
227
|
parameters: {
|
|
110
228
|
type: "object",
|
|
111
229
|
properties: {
|
|
@@ -130,21 +248,191 @@ export const taskTool = {
|
|
|
130
248
|
status: VALID_TASK_STATUS.has(it.status) ? it.status : "pending",
|
|
131
249
|
}))
|
|
132
250
|
ctx.agent.tasks = items
|
|
251
|
+
ctx.agent._turnsSinceTaskUpdate = 0
|
|
133
252
|
ctx.agent._onTaskUpdate?.(items)
|
|
134
253
|
const done = items.filter((i) => i.status === "done").length
|
|
135
254
|
const open = items.length - done
|
|
136
255
|
return `Task list updated: ${done}/${items.length} done` +
|
|
137
|
-
(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.`
|
|
258
|
+
},
|
|
259
|
+
}
|
|
260
|
+
|
|
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."
|
|
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")
|
|
138
410
|
},
|
|
139
411
|
}
|
|
140
412
|
|
|
141
|
-
/**
|
|
413
|
+
/** 项目指令文件候选(cwd 本地,按优先级拼接) */
|
|
142
414
|
const INSTRUCTION_FILES = ["AGENTS.md", "agents.md", "PROJECT_RULES.md", "project_rules.md", ".thincoder/rules.md"]
|
|
143
415
|
const MAX_INSTRUCTION_CHARS = 8000
|
|
144
416
|
|
|
145
|
-
/**
|
|
417
|
+
/**
|
|
418
|
+
* 读取项目指令,两层合并:
|
|
419
|
+
* 1. 用户全局:~/.thincoder/AGENTS.md(适用所有项目)
|
|
420
|
+
* 2. 项目本地:cwd 下的 AGENTS.md / project_rules 等
|
|
421
|
+
* 最多 8000 字符。
|
|
422
|
+
*/
|
|
146
423
|
export async function loadProjectInstructions(cwd) {
|
|
147
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
|
+
// 项目本地指令(优先级高,放后面)
|
|
148
436
|
for (const name of INSTRUCTION_FILES) {
|
|
149
437
|
try {
|
|
150
438
|
const text = await readFile(join(cwd, name), "utf8")
|
|
@@ -157,34 +445,27 @@ export async function loadProjectInstructions(cwd) {
|
|
|
157
445
|
return parts.join("\n\n").slice(0, MAX_INSTRUCTION_CHARS)
|
|
158
446
|
}
|
|
159
447
|
|
|
160
|
-
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.
|
|
161
|
-
|
|
162
|
-
Rules:
|
|
163
|
-
- Prefer tool calls over guessing. Read files before modifying them.
|
|
164
|
-
- 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.
|
|
165
|
-
- Be concise in your final answers. Report what you did, not what you plan to do.
|
|
166
|
-
- 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.
|
|
167
|
-
- When the user shares an observation or opinion, don't mistake it for a command—confirm before making changes.
|
|
168
|
-
- 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.
|
|
169
|
-
- 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.
|
|
170
|
-
- Never fabricate file contents or command outputs; only trust tool results.
|
|
171
|
-
- 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.
|
|
172
|
-
- 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.
|
|
173
|
-
- 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.`
|
|
174
|
-
|
|
175
448
|
/**
|
|
176
449
|
* 创建 agent。
|
|
177
|
-
* { provider, tools, config, cwd, memory? }
|
|
450
|
+
* { provider, tools, config, cwd, memory?, overlay? }
|
|
451
|
+
* overlay — 子 agent 角色覆盖文本,拼接在 system prompt 末尾
|
|
178
452
|
*/
|
|
179
|
-
export function createAgent({ provider, tools, config, cwd, memory = null }) {
|
|
453
|
+
export function createAgent({ provider, tools, config, cwd, memory = null, overlay = "" }) {
|
|
180
454
|
return {
|
|
181
455
|
provider,
|
|
182
456
|
tools,
|
|
183
457
|
config,
|
|
184
458
|
cwd,
|
|
185
459
|
memory,
|
|
460
|
+
overlay,
|
|
186
461
|
history: [], // OpenAI 格式的对话历史(不含 system)
|
|
187
|
-
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 稳定,前缀缓存用)
|
|
188
469
|
}
|
|
189
470
|
}
|
|
190
471
|
|
|
@@ -197,40 +478,88 @@ export function createAgent({ provider, tools, config, cwd, memory = null }) {
|
|
|
197
478
|
* }
|
|
198
479
|
* 返回最终文本。
|
|
199
480
|
*/
|
|
200
|
-
export async function runAgent(agent, input, callbacks = {}, { depth = 0 } = {}) {
|
|
201
|
-
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
|
|
202
483
|
const threshold = agent.config?.agent?.compactThreshold ?? 100_000
|
|
203
484
|
// 先修复历史(恢复的会话可能有中断的 tool_calls),再追加新输入
|
|
204
485
|
agent.history = repairHistory(agent.history)
|
|
205
|
-
|
|
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
|
+
}
|
|
206
511
|
|
|
207
|
-
// task 工具随主循环注入(内建能力);subagent 只在顶层注入(禁止递归)
|
|
208
|
-
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] : [])]
|
|
209
514
|
const toolSchemas = tools.map(toOpenAISchema)
|
|
210
515
|
const toolByName = new Map(tools.map((t) => [t.name, t]))
|
|
211
516
|
agent._onTaskUpdate = callbacks.onTaskUpdate
|
|
212
517
|
|
|
213
|
-
//
|
|
518
|
+
// 环境信息 + profile overlay:附加到 system prompt
|
|
519
|
+
// 注意:这里只能放跨 run 稳定的内容(前缀缓存要求 system prompt 逐字节一致)——
|
|
520
|
+
// session start 时间戳每会话固定一次;每轮变化的记忆注入走上面的 user 上下文消息
|
|
214
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}.`
|
|
215
526
|
const projectRules = await loadProjectInstructions(agent.cwd)
|
|
216
527
|
if (projectRules) {
|
|
217
528
|
systemPrompt += `\n\nProject instructions (follow these as project conventions):\n${projectRules}`
|
|
218
529
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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 })
|
|
226
541
|
}
|
|
227
542
|
|
|
543
|
+
// 完成守卫的每轮运行状态:改了东西(写/编辑类工具)却没自检过,不收工、推回去验证
|
|
544
|
+
// bash/subagent 不算 mutation(跑测试、explore 子 agent 不该触发;coder 子 agent 有专属校验提醒)
|
|
545
|
+
let mutatedThisRun = false
|
|
546
|
+
let verifiedThisRun = false
|
|
547
|
+
let completionGuardFired = false
|
|
548
|
+
|
|
228
549
|
for (let turn = 0; turn < maxTurns; turn++) {
|
|
229
|
-
//
|
|
230
|
-
|
|
550
|
+
// 递增跟踪计数器
|
|
551
|
+
agent._turnsSinceTaskUpdate++
|
|
552
|
+
if (agent.planMode) agent._turnsInPlanMode++
|
|
553
|
+
|
|
554
|
+
// 每轮 LLM 调用前检查上下文长度,超阈值先压缩
|
|
555
|
+
// 压缩失败不终止 agent 循环——宁可继续跑长上下文也别中断任务
|
|
231
556
|
if (agent.history.at(-1)?.role === "user") {
|
|
232
|
-
|
|
233
|
-
|
|
557
|
+
try {
|
|
558
|
+
if (await compressIfNeeded(agent, threshold)) {
|
|
559
|
+
callbacks.onCompress?.()
|
|
560
|
+
}
|
|
561
|
+
} catch {
|
|
562
|
+
// 压缩 LLM 调用失败(限流/网络),静默跳过;下一轮重试
|
|
234
563
|
}
|
|
235
564
|
}
|
|
236
565
|
|
|
@@ -241,10 +570,25 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0 } = {})
|
|
|
241
570
|
tools: toolSchemas,
|
|
242
571
|
onToken: callbacks.onToken,
|
|
243
572
|
onReasoning: callbacks.onReasoning,
|
|
573
|
+
signal,
|
|
244
574
|
})
|
|
245
575
|
|
|
246
576
|
// 无工具调用:最终回答,收尾
|
|
247
577
|
if (response.toolCalls.length === 0) {
|
|
578
|
+
// 空回复(思考流跑完正文为空、被截断等)不入历史——空 assistant 消息会毒害后续所有请求
|
|
579
|
+
if (!response.content) {
|
|
580
|
+
throw new Error("LLM 返回了空回复(可能是思考耗尽或被截断)。可 /think effort 降低推理强度后重试")
|
|
581
|
+
}
|
|
582
|
+
// 完成守卫:本轮改过文件却没跑过 verify,推回去验证一次(只推一次,防死循环)
|
|
583
|
+
if (depth === 0 && mutatedThisRun && !verifiedThisRun && !completionGuardFired) {
|
|
584
|
+
completionGuardFired = true
|
|
585
|
+
agent.history.push({ role: "assistant", content: response.content })
|
|
586
|
+
agent.history.push({
|
|
587
|
+
role: "user",
|
|
588
|
+
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.]",
|
|
589
|
+
})
|
|
590
|
+
continue
|
|
591
|
+
}
|
|
248
592
|
agent.history.push({ role: "assistant", content: response.content })
|
|
249
593
|
return response.content
|
|
250
594
|
}
|
|
@@ -258,9 +602,12 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0 } = {})
|
|
|
258
602
|
type: "function",
|
|
259
603
|
function: { name: tc.name, arguments: tc.arguments },
|
|
260
604
|
})),
|
|
605
|
+
// thinking 模式:reasoning_content 必须跨请求原样回传(DeepSeek 要求,缺失会 400;
|
|
606
|
+
// 非 thinking 模型 reasoning 恒为空串,不附加字段,严格协议端点不受影响)
|
|
607
|
+
...(response.reasoning ? { reasoning_content: response.reasoning } : {}),
|
|
261
608
|
})
|
|
262
609
|
|
|
263
|
-
const results = await executeToolCalls(agent, toolByName, response.toolCalls, callbacks, depth)
|
|
610
|
+
const results = await executeToolCalls(agent, toolByName, response.toolCalls, callbacks, depth, signal)
|
|
264
611
|
|
|
265
612
|
// 结果按 toolCallId 配对回喂(协议按 ID 不按位置,完成乱序无影响)
|
|
266
613
|
for (const { toolCall, result } of results) {
|
|
@@ -269,19 +616,69 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0 } = {})
|
|
|
269
616
|
tool_call_id: toolCall.id,
|
|
270
617
|
content: result,
|
|
271
618
|
})
|
|
619
|
+
// 完成守卫状态跟踪(失败的调用不算数)
|
|
620
|
+
const tool = toolByName.get(toolCall.name)
|
|
621
|
+
if (tool && !result.startsWith("Error")) {
|
|
622
|
+
if (!tool.readonly && toolCall.name !== "bash" && toolCall.name !== "subagent") mutatedThisRun = true
|
|
623
|
+
if (toolCall.name === "verify") verifiedThisRun = true
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// 刷新待处理的模式提醒(plan/auto 切换后注入,在工具结果之后)
|
|
628
|
+
if (agent._pendingReminders.length > 0) {
|
|
629
|
+
for (const reminder of agent._pendingReminders) {
|
|
630
|
+
agent.history.push({ role: "user", content: reminder })
|
|
631
|
+
}
|
|
632
|
+
agent._pendingReminders = []
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// 每 10 轮注入一次 goal 提醒(长期任务进度感知)
|
|
636
|
+
if (agent.goal && turn > 0 && turn % 10 === 0) {
|
|
637
|
+
agent.history.push({
|
|
638
|
+
role: "user",
|
|
639
|
+
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.]`,
|
|
640
|
+
})
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// 每 10 轮注入 task 提醒:不管有没有建列表——
|
|
644
|
+
// 有未完成项催更新;从未建列表则建议为多步工作建一个(对齐 kimi-code 的闲置提醒)
|
|
645
|
+
if (agent._turnsSinceTaskUpdate >= 10) {
|
|
646
|
+
const hasIncomplete = agent.tasks.some((t) => t.status !== "done")
|
|
647
|
+
if (agent.tasks.length > 0 && hasIncomplete) {
|
|
648
|
+
const taskSummary = agent.tasks.map((t) => `- [${t.status}] ${t.title}`).join("\n")
|
|
649
|
+
agent.history.push({
|
|
650
|
+
role: "user",
|
|
651
|
+
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.]`,
|
|
652
|
+
})
|
|
653
|
+
} else if (agent.tasks.length === 0) {
|
|
654
|
+
agent.history.push({
|
|
655
|
+
role: "user",
|
|
656
|
+
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.]",
|
|
657
|
+
})
|
|
658
|
+
}
|
|
659
|
+
agent._turnsSinceTaskUpdate = 0
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// 每 8 轮注入 plan mode 引导:防止无限探索不产出方案
|
|
663
|
+
if (agent.planMode && agent._turnsInPlanMode >= 8) {
|
|
664
|
+
agent.history.push({
|
|
665
|
+
role: "user",
|
|
666
|
+
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.]",
|
|
667
|
+
})
|
|
668
|
+
agent._turnsInPlanMode = 0
|
|
272
669
|
}
|
|
273
670
|
}
|
|
274
671
|
|
|
275
|
-
throw new
|
|
672
|
+
throw new ContinueError(maxTurns)
|
|
276
673
|
}
|
|
277
674
|
|
|
278
675
|
/**
|
|
279
676
|
* 两段式执行:
|
|
280
|
-
* 阶段一(串行):逐个解析参数 + 权限确认(有副作用工具)
|
|
677
|
+
* 阶段一(串行):逐个解析参数 + planMode 检查 + 权限确认(有副作用工具)
|
|
281
678
|
* 阶段二(分类):只读工具 Promise.all 并行;有副作用工具逐个串行
|
|
282
679
|
* 返回按 toolCallId 配对的结果数组。
|
|
283
680
|
*/
|
|
284
|
-
async function executeToolCalls(agent, toolByName, toolCalls, callbacks, depth = 0) {
|
|
681
|
+
async function executeToolCalls(agent, toolByName, toolCalls, callbacks, depth = 0, signal) {
|
|
285
682
|
// ---- 阶段一:串行准备 ----
|
|
286
683
|
const prepared = []
|
|
287
684
|
for (const toolCall of toolCalls) {
|
|
@@ -299,6 +696,12 @@ async function executeToolCalls(agent, toolByName, toolCalls, callbacks, depth =
|
|
|
299
696
|
continue
|
|
300
697
|
}
|
|
301
698
|
|
|
699
|
+
// plan 模式:拒绝所有非只读工具
|
|
700
|
+
if (agent.planMode && !tool.readonly) {
|
|
701
|
+
prepared.push({ toolCall, tool, denied: true, reason: "plan mode" })
|
|
702
|
+
continue
|
|
703
|
+
}
|
|
704
|
+
|
|
302
705
|
if (!tool.readonly) {
|
|
303
706
|
const allowed = callbacks.onPermissionRequest
|
|
304
707
|
? await callbacks.onPermissionRequest(toolCall.name, args)
|
|
@@ -316,13 +719,20 @@ async function executeToolCalls(agent, toolByName, toolCalls, callbacks, depth =
|
|
|
316
719
|
// ---- 阶段二:分类执行 ----
|
|
317
720
|
const runOne = async (item) => {
|
|
318
721
|
if (item.error) return { ...item, result: `Error: ${item.error}` }
|
|
319
|
-
if (item.denied)
|
|
722
|
+
if (item.denied) {
|
|
723
|
+
const reason = item.reason === "plan mode"
|
|
724
|
+
? "Error: plan mode is active — only read-only tools are allowed. Exit plan mode first."
|
|
725
|
+
: "Error: permission denied by user"
|
|
726
|
+
return { ...item, result: reason }
|
|
727
|
+
}
|
|
320
728
|
try {
|
|
321
729
|
const result = await item.tool.execute(item.args, {
|
|
322
730
|
cwd: agent.cwd,
|
|
323
731
|
agent,
|
|
324
732
|
depth,
|
|
733
|
+
signal,
|
|
325
734
|
onOutput: (chunk) => callbacks.onToolOutput?.(item.toolCall.name, chunk),
|
|
735
|
+
onQuestion: callbacks.onQuestion,
|
|
326
736
|
})
|
|
327
737
|
callbacks.onToolResult?.(item.toolCall.name, result)
|
|
328
738
|
return { ...item, result: String(result) }
|