mini-Kode coding agent 学习记录
Date: Nov 16, 2025 Sun
|
Estimated Reading Time: 2 min
| Author: Madinah
系统架构概览
Mini-Kode 是一个基于 LLM 的命令行编程助手,采用 工具调用(Tool Calling) 模式与大语言模型交互。
核心组件
┌─────────────────────────────────────────────────────────────┐
│ 用户界面层 │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Interactive UI │ │ Non-Interactive │ │
│ │ (Ink + React) │ │ Mode │ │
│ └──────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Agent 执行引擎 │
│ (src/agent/executor.ts) │
│ • 管理 LLM 对话循环 │
│ • 协调工具执行 │
│ • 处理权限请求 │
└─────────────────────────────────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ LLM Client │ │ Tool System │ │ Permission │
│ │ │ │ │ System │
│ • 流式响应 │ │ • 工具注册 │ │ • 权限检查 │
│ • Token 统计 │ │ • 并发执行 │ │ • 策略管理 │
└──────────────┘ └──────────────┘ └──────────────┘
技术栈
- TypeScript - 类型安全
- Bun - 运行时和构建工具
- Ink - 基于 React 的 CLI UI
- OpenAI SDK - LLM 集成
- Zod - 运行时类型验证
启动流程
1. 入口点 ( src/index.ts )
#!/usr/bin/env -S node --no-warnings=ExperimentalWarning
import { EventEmitter } from "events";
EventEmitter.defaultMaxListeners = 200; // 支持多个 LLM 流式调用
import { runCli } from "./cli";
void runCli();
2. CLI 解析 ( src/cli.ts )
系统支持两种运行模式:
交互模式(Interactive Mode)
# 启动交互式 UI
mini-kode
非交互模式(Non-Interactive Mode)
# 直接执行任务
mini-kode "修复 auth.ts 中的 bug"
CLI 参数:
-
-a, --approval-mode <mode>: 权限模式-
default: 每次都询问 -
autoEdit: 自动批准文件编辑 -
yolo: 自动批准所有操作
-
-
-w, --work-dir <path>: 工作目录
代码示例:
// src/cli.ts
if (prompt) {
// 非交互模式:直接执行任务
const exitCode = await runNonInteractive(prompt, workDir, approvalMode);
process.exit(exitCode);
} else {
// 交互模式:启动 UI
const element = React.createElement(App, { cwd: workDir, approvalMode });
const instance = render(element, { exitOnCtrlC: false });
await instance.waitUntilExit();
}
核心执行循环
Agent 执行引擎 ( src/agent/executor.ts )
这是整个系统的核心,负责管理 LLM 与工具之间的交互循环。
执行流程图
┌─────────────────────────────────────────────────────────────┐
│ 1. 用户输入 Prompt │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. 构建对话上下文 │
│ • System Message (环境信息 + AGENTS.md) │
│ • 历史消息 │
│ • 当前 Prompt │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. 发送请求到 LLM │
│ • 流式响应 │
│ • 实时更新 UI │
└─────────────────────────────────────────────────────────────┘
│
┌───────────┴───────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ finish_reason: │ │ finish_reason: │
│ "stop" │ │ "tool_calls" │
└──────────────────┘ └──────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ 返回最终响应 │ │ 执行工具调用 │
└──────────────────┘ └──────────────────┘
│
▼
┌──────────────────┐
│ 工具执行完成 │
│ 添加结果到对话 │
└──────────────────┘
│
▼
┌──────────────────┐
│ 回到步骤 3 │
│ (继续循环) │
└──────────────────┘
核心代码解析
// src/agent/executor.ts - executeAgent 函数
export async function executeAgent(
prompt: string,
context: ExecutionContext,
callbacks: ExecutionCallbacks = {},
): Promise<ExecutionResult> {
// 1. 初始化 LLM 客户端
const client = createClient({ cwd });
// 2. 构建对话历史
const systemMessage = await buildSystemMessage(cwd);
let conversationHistory: ChatCompletionMessageParam[] = [
systemMessage,
...toOpenAIMessages(session.messages),
{ role: "user", content: prompt }
];
// 3. 主循环
while (true) {
// 3.1 流式调用 LLM
const stream = streamChatCompletion(client, conversationHistory, {
signal,
tools: openaiTools,
});
// 3.2 处理流式响应
for await (const response of stream) {
callbacks.onLLMMessageUpdate?.({
kind: "api",
status: response.isComplete ? "complete" : "streaming",
message: response.completeMessage,
});
}
// 3.3 判断 finish_reason
if (finishReason === "tool_calls" && parsedCalls.length > 0) {
// 执行工具
const toolCalls = await executeToolsWithPermission(
parsedCalls,
context,
callbacks,
);
// 添加工具结果到对话
for (const toolCall of toolCalls) {
const toolMessage = formatToolResultMessage(toolCall);
conversationHistory.push(toolMessage);
}
// 继续循环
continue;
}
// 3.4 返回最终响应
return { success: true, response: assembled };
}
}
系统消息构建
系统消息包含环境信息和项目上下文:
// src/agent/context.ts
async function buildSystemMessage(effectiveCwd: string) {
const envInfo: EnvironmentInfo = {
cwd: effectiveCwd,
isGitRepo: isGitRepository(effectiveCwd),
platform: process.platform,
date: new Date().toISOString().split("T")[0],
model: client.model,
};
// 读取 AGENTS.md 作为项目上下文
let projectContext = "";
const agentsPath = path.join(envInfo.cwd, "AGENTS.md");
if (fs.existsSync(agentsPath)) {
projectContext = fs.readFileSync(agentsPath, "utf8");
}
return { role: "system", content: buildSystemPrompt(envDetails, projectContext) };
}
AGENTS.md 的作用:
- 提供项目特定的上下文信息
- 记录技术栈、架构、开发规范
- 帮助 LLM 更好地理解项目
工具执行机制
工具系统架构
┌─────────────────────────────────────────────────────────────┐
│ Tool Definition │
│ • name: 工具名称 │
│ • description: 工具描述 │
│ • inputSchema: Zod 验证模式 │
│ • readonly: 是否只读 │
│ • execute: 执行函数 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Tool Executor │
│ (src/agent/toolExecutor.ts) │
└─────────────────────────────────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Concurrent │ │ Sequential │ │ Permission │
│ Execution │ │ Execution │ │ Handling │
│ (只读工具) │ │ (写入工具) │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
并发执行策略
// src/agent/toolExecutor.ts
async function executeToolsWithPermission(
calls: ParsedToolCall[],
context: ExecutionContext,
callbacks: ExecutionCallbacks,
): Promise<ToolCall[]> {
// 检查是否所有工具都是只读的
const allReadonly = toolCallsToExecute.every((tc) => {
const tool = toolsByName[tc.toolName];
return tool?.readonly === true;
});
if (allReadonly) {
// 并发执行只读工具
return await executeToolsConcurrently(toolCallsToExecute, context, callbacks);
} else {
// 顺序执行包含写入操作的工具
return await executeToolsSequentially(toolCallsToExecute, context, callbacks);
}
}
为什么需要区分并发和顺序执行?
-
只读工具(Concurrent):
- 例如:
fileRead,grepSearch - 无副作用,可以并发执行提高性能
- 结果按照调用顺序返回
- 例如:
-
写入工具(Sequential):
- 例如:
fsWrite,bash - 有副作用,必须顺序执行避免冲突
- 确保操作的原子性
- 例如:
工具执行流程
// src/tools/runner.ts
async function executeSingleTool(
toolCall: ToolCall,
execContext: ToolExecutionContext,
startedAt: string,
): Promise<ToolCall> {
try {
const tool = toolsByName[toolCall.toolName];
const result = await tool.execute(toolCall.input, execContext);
// 检查是否是业务逻辑错误
if ("isError" in result && result.isError === true) {
return {
...toolCall,
status: "error",
result,
};
}
return {
...toolCall,
status: "success",
result,
};
} catch (err) {
// 捕获权限错误
if (err instanceof PermissionRequiredError) {
return {
...toolCall,
status: "permission_required",
uiHint: err.uiHint,
};
}
return {
...toolCall,
status: "error",
result: { isError: true, message: String(err?.message) },
};
}
}
权限系统
权限架构
Mini-Kode 实现了两层权限系统:
┌─────────────────────────────────────────────────────────────┐
│ Approval Mode │
│ • yolo: 自动批准所有操作 │
│ • autoEdit: 自动批准文件编辑 │
│ • default: 每次都询问 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Permission Policies │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Project Policy │ │ Session Policy │ │
│ │ (持久化到磁盘) │ │ (内存中) │ │
│ │ .mini-kode/ │ │ 运行时授权 │ │
│ │ config.json │ │ │ │
│ └──────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
权限检查流程
// src/permissions/policyResolver.ts
export function checkFsPermission(
cwd: string,
targetPath: string,
approvalMode: ApprovalMode,
): { ok: true } | { ok: false; message: string } {
// 1. YOLO 模式:自动批准
if (approvalMode === "yolo") {
return { ok: true };
}
// 2. AutoEdit 模式:自动批准写操作
if (approvalMode === "autoEdit") {
return { ok: true };
}
// 3. 检查 Session 权限(内存,快速)
if (checkSessionFsPermission(targetPath)) {
return { ok: true };
}
// 4. 检查 Project 权限(磁盘,持久化)
if (checkProjectFsPermission(cwd, targetPath)) {
return { ok: true };
}
// 5. 需要用户授权
return {
ok: false,
message: `Permission required to modify: ${relativePath}`,
};
}
权限类型
-
文件系统权限(FS)
type FsGrant = { type: "fs"; path: string; // 绝对路径或 "*" 表示全局 }; -
Bash 命令权限
type BashGrant = { type: "bash"; command: string; // 命令或 "npm:*" 表示前缀匹配 }; -
MCP 工具权限
type MCPGrant = { type: "mcp"; serverName: string; toolName?: string; // 可选,特定工具 };
异步权限请求流程
┌─────────────────────────────────────────────────────────────┐
│ 1. 工具执行时检查权限 │
│ checkFsPermission() / checkBashApproval() │
└─────────────────────────────────────────────────────────────┘
│
▼
┌───────────────┐
│ 权限已授予? │
└───────────────┘
│ │
Yes │ │ No
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ 直接执行 │ │ 抛出 │
│ │ │ PermissionRequired│
│ │ │ Error │
└──────────────┘ └──────────────────┘
│
▼
┌──────────────────────┐
│ Executor 捕获错误 │
│ 调用 onPermission │
│ Required 回调 │
└──────────────────────┘
│
▼
┌──────────────────────┐
│ UI 显示权限请求 │
│ 等待用户决策 │
└──────────────────────┘
│
┌───────────┴───────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ 用户批准 │ │ 用户拒绝 │
└──────────────────┘ └──────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ 应用授权并重新 │ │ 返回 │
│ 执行工具 │ │ permission_denied │
└──────────────────┘ └──────────────────┘
代码示例:
// src/agent/toolExecutor.ts
async function executeSingleToolWithPermission(
toolCallToExecute: ToolCallPending,
context: ExecutionContext,
callbacks: ExecutionCallbacks,
): Promise<ToolCall> {
// 执行工具
const result = await executeSingleToolCall(toolCallToExecute, toolContext);
if (result.status === "permission_required") {
// 处理权限请求
const finalResult = await handlePermissionRequest(
result,
toolContext,
callbacks,
);
return finalResult;
}
return result;
}
async function handlePermissionRequest(
toolCallToExecute: ToolCallPermissionRequired,
toolContext: ToolExecutionContext,
callbacks: ExecutionCallbacks,
): Promise<ToolCall> {
// 调用回调,等待用户决策
const decision = await callbacks.onPermissionRequired?.(
toolCallToExecute.uiHint,
toolCallToExecute.requestId,
);
if (decision?.approved) {
// 应用授权
applyPermissionGrant(decision.grant, toolContext);
// 重新执行工具
return await executeSingleToolCall(toolCallToExecute, toolContext);
} else {
// 用户拒绝
return {
...toolCallToExecute,
status: "permission_denied",
result: { isError: true, message: "Permission denied by user" },
};
}
}
消息流转
OpenAI 消息顺序规则
OpenAI API 对消息顺序有严格要求,违反规则会导致 400 错误。
关键规则:
-
Tool 消息必须跟在包含 tool_calls 的 Assistant 消息后面
// ✅ 正确 [ { role: "user", content: "..." }, { role: "assistant", tool_calls: [{ id: "call_1", ... }] }, { role: "tool", tool_call_id: "call_1", content: "..." }, ] // ❌ 错误 [ { role: "user", content: "..." }, { role: "tool", tool_call_id: "call_1", content: "..." }, // 没有前置 tool_calls ] -
每个 tool_call 必须有且仅有一个对应的 tool 消息
-
Tool 消息必须按照 tool_calls 数组的顺序
// ✅ 正确 [ { role: "assistant", tool_calls: [{ id: "call_1" }, { id: "call_2" }] }, { role: "tool", tool_call_id: "call_1", content: "..." }, // 第一个 { role: "tool", tool_call_id: "call_2", content: "..." }, // 第二个 ] // ❌ 错误(顺序颠倒) [ { role: "assistant", tool_calls: [{ id: "call_1" }, { id: "call_2" }] }, { role: "tool", tool_call_id: "call_2", content: "..." }, // 顺序错误 { role: "tool", tool_call_id: "call_1", content: "..." }, ]
消息格式化
// src/agent/formatters.ts
export function formatToolResultMessage(
result: ToolCall,
): ChatCompletionToolMessageParam {
let content: string;
if (result.status === "success") {
content = JSON.stringify(result.result, null, 2);
} else if (result.status === "error") {
content = `Error: ${result.result.message}`;
} else if (result.status === "abort") {
content = result.result.message;
} else if (result.status === "permission_denied") {
content = `${result.toolName} was rejected by user`;
}
return {
role: "tool",
tool_call_id: result.requestId,
content,
};
}
典型消息流
Round 1: 简单工具调用
1. user: "读取 file.txt"
2. assistant: { tool_calls: [{ id: "call_1", function: "fileRead" }] }
3. tool: { tool_call_id: "call_1", content: "{ 文件内容 }" }
4. assistant: "文件包含..."
Round 2: 多工具调用
5. user: "比较 file1.txt 和 file2.txt"
6. assistant: { tool_calls: [
{ id: "call_2", function: "fileRead", arguments: "file1.txt" },
{ id: "call_3", function: "fileRead", arguments: "file2.txt" }
]}
7. tool: { tool_call_id: "call_2", content: "{ file1 内容 }" }
8. tool: { tool_call_id: "call_3", content: "{ file2 内容 }" }
9. assistant: "比较结果..."
错误处理
错误分类
// src/agent/types.ts
type ExecutionError = {
type:
| "permission_denied" // 用户拒绝权限 (exit 1)
| "aborted" // 用户取消 (exit 3)
| "llm_error" // LLM API 错误 (exit 2)
| "internal_error"; // 内部错误 (exit 4)
message: string;
cause?: unknown;
};
错误处理流程
// src/agent/executor.ts
try {
// 执行循环
while (true) {
// LLM 调用和工具执行
}
} catch (err) {
// 1. 错误分类
let errorType: "aborted" | "llm_error" | "internal_error" = "internal_error";
if (err instanceof APIUserAbortError) {
errorType = "aborted";
} else if (err instanceof OpenAIError) {
errorType = "llm_error";
}
// 2. 更新 UI 状态
callbacks.onGeneratingChange?.(false);
// 3. 调用错误回调(用户取消除外)
if (errorType !== "aborted") {
callbacks.onError?.(err);
}
// 4. 返回错误结果
return {
success: false,
error: { type: errorType, message: errorMessage, cause: err },
};
}
常见错误场景
-
LLM API 错误
- 速率限制(Rate Limit)
- 认证失败(Authentication)
- 网络错误(Network)
-
工具执行错误
- 文件不存在
- 命令执行失败
- 权限不足
-
用户中断
- Ctrl+C 取消
- 拒绝权限请求
完整执行示例
让我们通过一个完整的例子来理解整个流程:
场景:修改文件
用户输入:
mini-kode "在 src/main.ts 中添加一个 hello 函数"
执行步骤
Step 1: 启动和初始化
// CLI 解析参数
const prompt = "在 src/main.ts 中添加一个 hello 函数";
const cwd = "/project";
const approvalMode = "default";
// 创建 Session
const session = createSession();
// 调用 executeAgent
await executeAgent(prompt, { cwd, signal, getApprovalMode, session }, callbacks);
Step 2: 构建对话上下文
// 系统消息
const systemMessage = {
role: "system",
content: `
Environment: macOS, /project
Date: 2025-11-16
Model: gpt-4
Project Context (AGENTS.md):
# Mini-Kode - Development Guide
...
`
};
// 对话历史
const conversationHistory = [
systemMessage,
{ role: "user", content: "在 src/main.ts 中添加一个 hello 函数" }
];
Step 3: LLM 第一次调用
// LLM 决定先读取文件
{
role: "assistant",
tool_calls: [{
id: "call_1",
function: {
name: "fileRead",
arguments: { path: "src/main.ts" }
}
}]
}
Step 4: 执行 fileRead 工具
// fileRead 是只读工具,无需权限
const result = {
status: "success",
result: {
content: "export function main() { console.log('Hello'); }"
}
};
// 添加 tool 消息到对话
conversationHistory.push({
role: "tool",
tool_call_id: "call_1",
content: JSON.stringify(result.result)
});
Step 5: LLM 第二次调用
// LLM 决定写入文件
{
role: "assistant",
tool_calls: [{
id: "call_2",
function: {
name: "fsWrite",
arguments: {
path: "src/main.ts",
text: "export function hello() { return 'Hello'; }\n\nexport function main() { console.log('Hello'); }"
}
}
}]
}
Step 6: 执行 fsWrite 工具(需要权限)
// 检查权限
const permission = checkFsPermission(cwd, "/project/src/main.ts", "default");
// => { ok: false, message: "Permission required to modify: src/main.ts" }
// 抛出 PermissionRequiredError
throw new PermissionRequiredError({
kind: "fs",
path: "/project/src/main.ts",
message: "Permission required to modify: src/main.ts"
});
Step 7: 处理权限请求
// Executor 捕获错误,调用回调
const decision = await callbacks.onPermissionRequired({
kind: "fs",
path: "/project/src/main.ts",
message: "Permission required to modify: src/main.ts"
}, "call_2");
// UI 显示权限请求,用户批准
// decision = {
// approved: true,
// grant: { type: "fs", path: "/project/src/main.ts" },
// scope: "once"
// }
// 应用授权到 Session Policy
applySessionGrant(decision.grant);
// 重新执行工具
const result = await tool.execute(input, context);
// => { status: "success" }
Step 8: LLM 第三次调用
// 添加 tool 消息
conversationHistory.push({
role: "tool",
tool_call_id: "call_2",
content: JSON.stringify({ success: true })
});
// LLM 生成最终响应
{
role: "assistant",
content: "我已经在 src/main.ts 中添加了 hello 函数。"
}
// 返回结果
return {
success: true,
response: "我已经在 src/main.ts 中添加了 hello 函数。"
};
总结
Mini-Kode 的 Agent 执行流程可以总结为:
- 用户输入 → CLI 解析 → 选择运行模式
- Agent 初始化 → 构建对话上下文 → 加载 AGENTS.md
- LLM 循环:
- 发送请求到 LLM
- 流式接收响应
- 判断 finish_reason
- 如果是 tool_calls:执行工具 → 添加结果 → 继续循环
- 如果是 stop:返回最终响应
- 工具执行:
- 只读工具并发执行
- 写入工具顺序执行
- 权限检查和异步请求
- 权限系统:
- Approval Mode 快速路径
- Session Policy(内存)
- Project Policy(持久化)
- 错误处理:
- 分类错误类型
- 更新 UI 状态
- 返回适当的退出码
这个架构确保了:
- ✅ 类型安全:TypeScript + Zod
- ✅ 高性能:并发执行只读工具
- ✅ 安全性:细粒度权限控制
- ✅ 可扩展:工具系统易于扩展
- ✅ 用户友好:流式响应和实时反馈
This work is licensed under CC BY-NC-SA 4.0