实现一个最小 AgentLoop
从 Claude Code 抽象出来,一个最小 AgentLoop 可以这样写:
ts
export async function* runAgentLoop(input: {
messages: Message[]
model: ModelClient
tools: ToolRegistry
permissions: PermissionGate
maxTurns: number
}) {
let messages = input.messages
for (let turn = 0; turn < input.maxTurns; turn++) {
const response = await input.model.createMessage({
messages,
tools: input.tools.toModelSchemas(),
})
yield { type: "assistant", message: response.message }
messages = [...messages, response.message]
const toolUses = response.message.content.filter(isToolUse)
if (toolUses.length === 0) {
return { reason: "complete", messages }
}
for (const toolUse of toolUses) {
const tool = input.tools.get(toolUse.name)
const decision = await input.permissions.check(tool, toolUse.input)
if (decision.type !== "allow") {
const denied = createToolResult(toolUse.id, "Permission denied", true)
messages = [...messages, denied]
yield { type: "tool_result", message: denied }
continue
}
const output = await tool.execute(toolUse.input)
const result = createToolResult(toolUse.id, output, false)
messages = [...messages, result]
yield { type: "tool_result", message: result }
}
}
return { reason: "max_turns", messages }
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
最小实现的模块
第一版只需要 5 个模块:
text
ModelClient
MessageStore
ToolRegistry
ToolExecutor
PermissionGate1
2
3
4
5
2
3
4
5
不要一开始就做插件、MCP、复杂 UI 和自动压缩。先保证工具调用闭环是正确的。
最重要的不变量
实现 AgentLoop 时要守住 3 个不变量:
- 每个
tool_use.id必须有对应tool_result.tool_use_id。 - 工具执行失败也要返回错误型
tool_result。 - 每一轮进入模型的 messages 必须满足 API 的消息格式要求。
如果这 3 个不变量守不住,Agent 会很快进入不可恢复状态。