|
| 1 | +/** |
| 2 | + * query.ts - 查询循环(Agentic Loop) |
| 3 | + * |
| 4 | + * 对应真实 Claude Code: src/query.ts + src/QueryEngine.ts |
| 5 | + * |
| 6 | + * 这是 mini-claude 最核心的模块。它实现了 AI Agent 的核心循环: |
| 7 | + * 用户输入 → 调用 API → 收到工具调用 → 执行工具 → 结果发回 API → 重复 |
| 8 | + * |
| 9 | + * 循环持续直到 AI 返回 end_turn(认为任务完成), |
| 10 | + * 或达到最大轮次限制。 |
| 11 | + */ |
| 12 | + |
| 13 | +import type { Message, ContentBlock, ToolUseBlock } from "./types/index.js"; |
| 14 | +import { createClient, streamMessage } from "./services/api/claude.js"; |
| 15 | +import { buildSystemPrompt } from "./context.js"; |
| 16 | +import { allTools, findToolByName, getToolsForAPI } from "./tools.js"; |
| 17 | +import { |
| 18 | + messagesToAPIParams, |
| 19 | + createUserMessage, |
| 20 | + createAssistantMessage, |
| 21 | + createToolResultBlock, |
| 22 | + extractToolUseBlocks, |
| 23 | +} from "./utils/messages.js"; |
| 24 | + |
| 25 | +/** 查询循环配置 */ |
| 26 | +export interface QueryOptions { |
| 27 | + model: string; |
| 28 | + maxTokens: number; |
| 29 | + maxTurns?: number; // 最大循环轮次,防止无限循环 |
| 30 | + apiKey?: string; |
| 31 | + cwd?: string; |
| 32 | + /** 文本输出回调——用于实时渲染 AI 的文字输出 */ |
| 33 | + onText?: (text: string) => void; |
| 34 | + /** 工具调用回调——用于显示工具执行状态 */ |
| 35 | + onToolUse?: (name: string, input: Record<string, unknown>) => void; |
| 36 | + /** 工具结果回调 */ |
| 37 | + onToolResult?: (name: string, result: string, isError: boolean) => void; |
| 38 | +} |
| 39 | + |
| 40 | +/** 查询结果 */ |
| 41 | +export interface QueryResult { |
| 42 | + messages: Message[]; // 完整对话历史 |
| 43 | + turns: number; // 实际循环轮次 |
| 44 | + inputTokens: number; // 总输入 token |
| 45 | + outputTokens: number; // 总输出 token |
| 46 | +} |
| 47 | + |
| 48 | +/** |
| 49 | + * 执行查询循环(Agentic Loop) |
| 50 | + * |
| 51 | + * 这是 mini-claude 的核心函数。完整流程: |
| 52 | + * |
| 53 | + * 1. 构建系统提示词 |
| 54 | + * 2. 将消息历史转换为 API 格式 |
| 55 | + * 3. 调用 API(流式) |
| 56 | + * 4. 收集 AI 回复(文本 + 工具调用) |
| 57 | + * 5. 如果有工具调用: |
| 58 | + * a. 执行所有工具(只读工具并发,写工具串行) |
| 59 | + * b. 将工具结果作为 user 消息追加 |
| 60 | + * c. 回到步骤 2(继续循环) |
| 61 | + * 6. 如果没有工具调用(end_turn):返回结果 |
| 62 | + */ |
| 63 | +export async function query( |
| 64 | + userInput: string, |
| 65 | + messages: Message[], |
| 66 | + options: QueryOptions |
| 67 | +): Promise<QueryResult> { |
| 68 | + const { |
| 69 | + model, |
| 70 | + maxTokens, |
| 71 | + maxTurns = 10, |
| 72 | + apiKey, |
| 73 | + cwd = process.cwd(), |
| 74 | + onText, |
| 75 | + onToolUse, |
| 76 | + onToolResult, |
| 77 | + } = options; |
| 78 | + |
| 79 | + const client = createClient(apiKey); |
| 80 | + const systemPrompt = buildSystemPrompt(allTools, cwd); |
| 81 | + const apiTools = getToolsForAPI(); |
| 82 | + |
| 83 | + // 添加用户消息到历史 |
| 84 | + const userMsg = createUserMessage(userInput); |
| 85 | + messages.push(userMsg); |
| 86 | + |
| 87 | + let totalInputTokens = 0; |
| 88 | + let totalOutputTokens = 0; |
| 89 | + let turn = 0; |
| 90 | + |
| 91 | + // ─── Agentic Loop ─────────────────────────────────────────────────── |
| 92 | + while (turn < maxTurns) { |
| 93 | + turn++; |
| 94 | + |
| 95 | + // 1. 将消息历史转换为 API 格式 |
| 96 | + const apiMessages = messagesToAPIParams(messages); |
| 97 | + |
| 98 | + // 2. 调用 API(流式) |
| 99 | + const contentBlocks: ContentBlock[] = []; |
| 100 | + let currentText = ""; |
| 101 | + const toolUseBuffers = new Map<string, { id: string; name: string; input: string }>(); |
| 102 | + let stopReason: string | undefined; |
| 103 | + |
| 104 | + for await (const event of streamMessage(client, { |
| 105 | + model, |
| 106 | + maxTokens, |
| 107 | + system: systemPrompt, |
| 108 | + messages: apiMessages, |
| 109 | + tools: apiTools, |
| 110 | + })) { |
| 111 | + switch (event.type) { |
| 112 | + case "text": |
| 113 | + currentText += event.text ?? ""; |
| 114 | + onText?.(event.text ?? ""); |
| 115 | + break; |
| 116 | + |
| 117 | + case "tool_use_start": |
| 118 | + toolUseBuffers.set(event.toolUseId!, { |
| 119 | + id: event.toolUseId!, |
| 120 | + name: event.toolName!, |
| 121 | + input: "", |
| 122 | + }); |
| 123 | + break; |
| 124 | + |
| 125 | + case "tool_use_delta": |
| 126 | + // 累积工具输入 JSON |
| 127 | + for (const buf of toolUseBuffers.values()) { |
| 128 | + buf.input += event.inputDelta ?? ""; |
| 129 | + } |
| 130 | + break; |
| 131 | + |
| 132 | + case "tool_use_end": { |
| 133 | + const buf = toolUseBuffers.get(event.toolUseId!); |
| 134 | + if (buf) { |
| 135 | + // 解析完整的工具输入 JSON |
| 136 | + let input: Record<string, unknown> = {}; |
| 137 | + try { |
| 138 | + input = JSON.parse(buf.input || "{}"); |
| 139 | + } catch { |
| 140 | + // JSON 解析失败时使用空对象 |
| 141 | + } |
| 142 | + contentBlocks.push({ |
| 143 | + type: "tool_use", |
| 144 | + id: buf.id, |
| 145 | + name: buf.name, |
| 146 | + input, |
| 147 | + }); |
| 148 | + toolUseBuffers.delete(event.toolUseId!); |
| 149 | + onToolUse?.(buf.name, input); |
| 150 | + } |
| 151 | + break; |
| 152 | + } |
| 153 | + |
| 154 | + case "message_end": |
| 155 | + stopReason = event.stopReason; |
| 156 | + totalInputTokens += event.usage?.inputTokens ?? 0; |
| 157 | + totalOutputTokens += event.usage?.outputTokens ?? 0; |
| 158 | + break; |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + // 3. 将文本块添加到内容 |
| 163 | + if (currentText) { |
| 164 | + contentBlocks.unshift({ type: "text", text: currentText }); |
| 165 | + } |
| 166 | + |
| 167 | + // 4. 创建助手消息并追加到历史 |
| 168 | + const assistantMsg = createAssistantMessage( |
| 169 | + contentBlocks, |
| 170 | + model, |
| 171 | + (stopReason as "end_turn" | "tool_use" | "max_tokens") ?? null |
| 172 | + ); |
| 173 | + messages.push(assistantMsg); |
| 174 | + |
| 175 | + // 5. 提取工具调用 |
| 176 | + const toolUses = extractToolUseBlocks(assistantMsg); |
| 177 | + |
| 178 | + // 6. 如果没有工具调用,循环结束 |
| 179 | + if (toolUses.length === 0) { |
| 180 | + break; |
| 181 | + } |
| 182 | + |
| 183 | + // 7. 执行工具并收集结果 |
| 184 | + const toolResultBlocks: ContentBlock[] = []; |
| 185 | + |
| 186 | + // 分离只读和读写工具 |
| 187 | + const readOnlyTools: ToolUseBlock[] = []; |
| 188 | + const writeTools: ToolUseBlock[] = []; |
| 189 | + |
| 190 | + for (const tu of toolUses) { |
| 191 | + const tool = findToolByName(tu.name); |
| 192 | + if (tool?.isReadOnly) { |
| 193 | + readOnlyTools.push(tu); |
| 194 | + } else { |
| 195 | + writeTools.push(tu); |
| 196 | + } |
| 197 | + } |
| 198 | + |
| 199 | + // 并发执行只读工具 |
| 200 | + if (readOnlyTools.length > 0) { |
| 201 | + const results = await Promise.all( |
| 202 | + readOnlyTools.map(async (tu) => { |
| 203 | + const tool = findToolByName(tu.name); |
| 204 | + if (!tool) { |
| 205 | + return createToolResultBlock(tu.id, `Error: Unknown tool '${tu.name}'`, true); |
| 206 | + } |
| 207 | + const result = await tool.call(tu.input); |
| 208 | + onToolResult?.(tu.name, result.content.substring(0, 100), !!result.isError); |
| 209 | + return createToolResultBlock(tu.id, result.content, result.isError); |
| 210 | + }) |
| 211 | + ); |
| 212 | + toolResultBlocks.push(...results); |
| 213 | + } |
| 214 | + |
| 215 | + // 串行执行读写工具 |
| 216 | + for (const tu of writeTools) { |
| 217 | + const tool = findToolByName(tu.name); |
| 218 | + if (!tool) { |
| 219 | + toolResultBlocks.push( |
| 220 | + createToolResultBlock(tu.id, `Error: Unknown tool '${tu.name}'`, true) |
| 221 | + ); |
| 222 | + continue; |
| 223 | + } |
| 224 | + const result = await tool.call(tu.input); |
| 225 | + onToolResult?.(tu.name, result.content.substring(0, 100), !!result.isError); |
| 226 | + toolResultBlocks.push( |
| 227 | + createToolResultBlock(tu.id, result.content, result.isError) |
| 228 | + ); |
| 229 | + } |
| 230 | + |
| 231 | + // 8. 将工具结果作为 user 消息追加到历史 |
| 232 | + const toolResultMsg = createUserMessage(toolResultBlocks); |
| 233 | + messages.push(toolResultMsg); |
| 234 | + |
| 235 | + // 继续循环(回到步骤 1) |
| 236 | + } |
| 237 | + |
| 238 | + return { |
| 239 | + messages, |
| 240 | + turns: turn, |
| 241 | + inputTokens: totalInputTokens, |
| 242 | + outputTokens: totalOutputTokens, |
| 243 | + }; |
| 244 | +} |
0 commit comments