Post

Pi - Loop

Pi - Loop

Loop

  • loop = outer_loop + inner_loop
  • outer_loop允许在inner_loop执行中, 接收getSteeringMessages(),

Main loop logic

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
44
45
46
47
48
/**
 * `agentLoop()` 与 `agentLoopContinue()` 共用的主循环骨架。
 *
 * 可以把它理解成“两层循环”:
 * - 外层循环:agent 原本要停下时,看看 follow-up 队列里有没有“收尾接力”消息;
 * - 内层循环:持续处理 assistant 响应、tool call 批次,以及 turn 之间注入的 steering 消息。
 *
 * 核心不变量:
 * 1. 每次真正请求模型前,`currentContext.messages` 都是一份完整、可见、合法的 transcript;
 * 2. assistant turn 完成后,toolResult 会被补回 transcript,再决定是否下一轮继续;
 * 3. `prepareNextTurn()` 只影响“下一次 provider 请求”,绝不回改已经在飞的请求。
 */
// Outer loop: continues when queued follow-up messages arrive after agent would stop
while (true) {
    let hasMoreToolCalls = true;

    // Inner loop: process tool calls and steering messages
    while (hasMoreToolCalls || pendingMessages.length > 0) {

        // Process pending messages (inject before next assistant response)
        if (pendingMessages.length > 0) {
            for (const message of pendingMessages) {
                await emit({ type: "message_start", message });
                await emit({ type: "message_end", message });
                currentContext.messages.push(message);
                newMessages.push(message);
            }
            pendingMessages = [];
        }

        // ...

        // inner_loop每轮turn结束后检查是否还有pendingMessages
        // executeToolCalls()
        pendingMessages = (await config.getSteeringMessages?.()) || [];
    }

    // Agent would stop here. Check for follow-up messages.
    const followUpMessages = (await config.getFollowUpMessages?.()) || [];
    if (followUpMessages.length > 0) {
        // Set as pending so inner loop processes them
        pendingMessages = followUpMessages;
        continue;
    }

    // No more messages, exit
    break;
}
  • 支持 runAgentLoop & runAgentLoopContinue

Agent

  • 进入loop前先normalizePromptInput
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * `Agent` 是 low-level loop 之上的有状态包装层。
 *
 * 它的职责不是替代 `runAgentLoop()`,而是把“运行时拥有者”需要承担的状态集中起来:
 * - 持有当前 transcript 与工具列表;
 * - 维护 activeRun / abortController / streamingMessage / pendingToolCalls;
 * - 对外暴露 `prompt()` / `continue()` / `steer()` / `followUp()` 等 API;
 * - 把 low-level loop 的事件归约回本地状态,并转发给订阅者。
 *
 * 可以把它理解成:
 * - `agent-loop.ts` 负责“如何跑一轮循环”;
 * - `agent.ts` 负责“谁拥有这次运行,以及如何把运行折叠回可观察状态”。
 */
export class Agent {}
// `normalizePromptInput()` 负责把多种输入形态统一折叠成标准 `AgentMessage[]`。
const messages = this.normalizePromptInput(input, images);
await this.runPromptMessages(messages);

最外层交互式loop

  • TUI层循环接收用户输入
1
2
3
4
5
6
7
8
9
10
while (true) {
    const userInput = await this.getUserInput();
    try {
        // 里面最终会调用runLoop
        await this.session.prompt(userInput);
    } catch (error: unknown) {
        const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
        this.showError(errorMessage);
    }
}

Execute Tool Calls

  • Sequential or Parallel
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
/**
 * 执行 assistant message 中携带的 tool calls。
 *
 * 这里先做一次“批次级调度决策”:
 * - 如果全局配置要求顺序执行,走 sequential;
 * - 如果任意单个 tool 声明自己必须顺序执行,也整体降级为 sequential;
 * - 否则才走 parallel。
 *
 * 之所以按“整批降级”处理,而不是部分并行、部分串行混搭,是为了保持批次语义简单,
 * 避免在一个 assistant turn 里出现难以解释的依赖竞态。
 */
async function executeToolCalls(
	currentContext: AgentContext,
	assistantMessage: AssistantMessage,
	config: AgentLoopConfig,
	signal: AbortSignal | undefined,
	emit: AgentEventSink,
): Promise<ExecutedToolCallBatch> {
	// 这里再次从 assistantMessage 中取 toolCall,保证调度逻辑只依赖最终定稿消息,
	// 不依赖 streaming 期间的 partial toolCall 片段。
	const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall");
	const hasSequentialToolCall = toolCalls.some(
		(tc) => currentContext.tools?.find((t) => t.name === tc.name)?.executionMode === "sequential",
	);
	if (config.toolExecution === "sequential" || hasSequentialToolCall) {
		return executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit);
	}
	return executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit);
}

executeToolCallsParallel

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
async function executeToolCallsParallel(
	currentContext: AgentContext,
	assistantMessage: AssistantMessage,
	toolCalls: AgentToolCall[],
	config: AgentLoopConfig,
	signal: AbortSignal | undefined,
	emit: AgentEventSink,
): Promise<ExecutedToolCallBatch> {
	// 并行模式下,prepare 阶段仍按 assistant 原顺序串行进行;
	// 真正并发的是 execute + finalize。
	// 这样做的好处是:
	// 1. 参数准备/校验/beforeHook 仍具确定性;
	// 2. 真正耗时的工具执行可以并发;
	// 3. 结果消息最终仍能按原 toolCall 顺序落回 transcript。
	const finalizedCalls: FinalizedToolCallEntry[] = [];

	for (const toolCall of toolCalls) {

		const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
        // immediate 直接执行了
		if (preparation.kind === "immediate") {
			const finalized = {
				toolCall,
				result: preparation.result,
				isError: preparation.isError,
			} satisfies FinalizedToolCallOutcome;
			finalizedCalls.push(finalized);
			if (signal?.aborted) {
				break;
			}
			continue;
		}
        // ts 语法糖并发
		finalizedCalls.push(async () => {
			const executed = await executePreparedToolCall(preparation, signal, emit);
			const finalized = await finalizeExecutedToolCall(
				currentContext,
				assistantMessage,
				preparation,
				executed,
				config,
				signal,
			);
			await emitToolExecutionEnd(finalized, emit);
			return finalized;
		});
		if (signal?.aborted) {
			break;
		}
	}

	const orderedFinalizedCalls = await Promise.all(
		finalizedCalls.map((entry) => (typeof entry === "function" ? entry() : Promise.resolve(entry))),
	);
	// 注意:`Promise.all` 返回结果顺序与输入数组顺序一致,
	// 因此即使并发执行,toolResult message 仍会按 assistant 原始 toolCall 顺序写回 transcript。
	const messages: ToolResultMessage[] = [];
	for (const finalized of orderedFinalizedCalls) {
		const toolResultMessage = createToolResultMessage(finalized);
		await emitToolResultMessage(toolResultMessage, emit);
		messages.push(toolResultMessage);
	}

	return {
		messages,
		terminate: shouldTerminateToolBatch(orderedFinalizedCalls),
	};
}
This post is licensed under CC BY 4.0 by the author.