4. 调用链(用户键入 → 最终输出)¶
下面每一步都给出确切的源文件与关键代码片段,以便读者直接验证。
4.1 Step 0:CLI 解析¶
$ pi --model anthropic/claude-opus-4-5 "fix the bug"
↓ coding-agent/src/cli.ts (21 行)
↓ process.title = APP_NAME
↓ process.env.PI_CODING_AGENT = "true"
↓ process.env.AI_AGENT = "pi"
↓ process.emitWarning = noop
↓ configureHttpDispatcher() (undici global)
↓ main(process.argv.slice(2))
4.2 Step 1:参数解析与诊断¶
文件:packages/coding-agent/src/main.ts L1–200
import { type Args, type Mode, parseArgs, printHelp } from "./cli/args.ts";
// ...
const parsed = parseArgs(args); // L397+
const stdinContent = await readPipedStdin(); // 仅非 TTY 时读
// 短路命令
if (await runAuthCommand(args)) return; // 'auth' 子命令
if (parsed.help) { printHelp(); return; } // --help
if (parsed.version) { console.log(VERSION); return; }
if (parsed.listModels !== undefined) { await listModels(...); return; }
CLI 支持 50+ flag(见 cli/args.ts 完整集合)。Args 接口的 unknownFlags 与 diagnostics 给后一步使用。
4.3 Step 2:provider auth 验证¶
文件:packages/coding-agent/src/cli/auth-check.ts
当任何模型选择要走远路(需要真实 API 请求)时:
const credentials = command.noRefresh
? new ReadOnlyAuthStorage()
: AuthStorage.create();
const modelRuntime = await createAuthCheckModelRuntime(credentials);
result = await checkProviderAuth(parsed, modelRuntime, { refresh: !command.noRefresh });
如果 status != "ready":
4.4 Step 3:session 决策¶
文件:packages/coding-agent/src/main.ts L360–451 (createSessionManager)
if (parsed.noSession || parsed.help || parsed.listModels !== undefined) {
return SessionManager.inMemory(cwd, ...);
}
if (parsed.fork) { /* resolveSessionPath → forkFrom */ }
if (parsed.session) { /* resolveSessionPath → open */ }
if (parsed.resume) { /* selectSession(...) */ }
if (parsed.continue) { /* SessionManager.continueRecent */ }
if (parsed.sessionId) { /* findLocalSessionByExactId */ }
return SessionManager.create(cwd, sessionDir, { id: parsed.sessionId });
SessionManager.list / listAll 在 .jsonl 与 session-id prefix 上做模糊匹配。
4.5 Step 4:模型解析¶
文件:packages/coding-agent/src/core/model-resolver.ts L1–775 (resolveCliModel)
支持两种语法:
--provider anthropic --model claude-opus-4-5--model anthropic/claude-opus-4-5(简写:provider/model)--model anthropic/claude-opus-4-5:high(在 model 后用冒号指定 thinking level)
如果都没指定,依次尝试:
settingsManager.getDefaultProvider() / .getDefaultModel()- 若有 saved model 也在
scopedModels中 → 用 saved - 否则用
scopedModels[0] - 否则退出并提示
formatNoModelsAvailableMessage()
4.6 Step 5:装配 AgentSession(核心!)¶
文件:packages/coding-agent/src/core/sdk.ts L169–398 (createAgentSession)
const cwd = resolvePath(options.cwd ?? ...);
const agentDir = options.agentDir ?? getDefaultAgentDir();
const modelRuntime = options.modelRuntime ?? ModelRuntime.create({authPath, modelsPath});
const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);
const sessionManager = options.sessionManager ?? SessionManager.create(cwd, ...);
const resourceLoader = options.resourceLoader
?? new DefaultResourceLoader({cwd, agentDir, settingsManager});
await resourceLoader.reload();
// Restore session state
const existingSession = sessionManager.buildSessionContext();
const hasExistingSession = existingSession.messages.length > 0;
if (!model && hasExistingSession && existingSession.model) {
const restoredModel = modelRuntime.getModel(existingSession.model.provider, existingSession.model.modelId);
if (restoredModel && modelRuntime.hasConfiguredAuth(restoredModel.provider)) {
model = restoredModel;
}
}
if (!model) { model = (await findInitialModel({...})).model; }
let thinkingLevel = options.thinkingLevel ?? /* 恢复自 session 或 settings */
if (!model) thinkingLevel = "off";
else thinkingLevel = clampThinkingLevel(model, thinkingLevel);
// 工具白名单/黑名单
const defaultActiveToolNames: ToolName[] = ["read", "bash", "edit", "write"];
const allowedToolNames = options.tools ?? (options.noTools === "all" ? [] : undefined);
const excludedToolNames = options.excludeTools;
接下来构造 Agent:
const agent = new Agent({
initialState: { systemPrompt: "", model, thinkingLevel, tools: [] },
convertToLlm: convertToLlmWithBlockImages,
streamFn: async (model, context, options) => {
const providerRetrySettings = settingsManager.getProviderRetrySettings();
const httpIdleTimeoutMs = settingsManager.getHttpIdleTimeoutMs();
const effectiveTimeoutMs = httpIdleTimeoutMs === 0 ? 2147483647 : httpIdleTimeoutMs;
const timeoutMs = options?.timeoutMs ?? providerRetrySettings.timeoutMs ?? effectiveTimeoutMs;
return modelRuntime.streamSimple(model, context, {
...options,
timeoutMs,
maxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,
maxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,
transformHeaders: async (requestHeaders) => {
const headers = mergeProviderAttributionHeaders(model, settingsManager, options?.sessionId, requestHeaders);
return extensionRunner?.emitBeforeProviderHeaders(headers) ?? headers;
},
});
},
onPayload: async (payload, _model) => { /* before_provider_request extension hook */ },
onResponse: async (response, _model) => { /* after_provider_response extension hook */ },
sessionId: sessionManager.getSessionId(),
transformContext: async (messages) => extensionRunner?.emitContext(messages) ?? messages,
steeringMode: settingsManager.getSteeringMode(),
followUpMode: settingsManager.getFollowUpMode(),
transport: settingsManager.getTransport(),
thinkingBudgets: settingsManager.getThinkingBudgets(),
maxRetryDelayMs: settingsManager.getProviderRetrySettings().maxRetryDelayMs,
});
最后包装:
const session = new AgentSession({
agent,
sessionManager,
settingsManager,
cwd,
scopedModels: options.scopedModels,
resourceLoader,
customTools: options.customTools,
modelRuntime,
initialActiveToolNames,
allowedToolNames,
excludedToolNames,
extensionRunnerRef,
sessionStartEvent: options.sessionStartEvent,
});
return { session, extensionsResult: resourceLoader.getExtensions(), modelFallbackMessage };
4.7 Step 6:模式分发¶
const mode: AppMode = resolveAppMode(parsed, stdinIsTTY, stdoutIsTTY);
switch (mode) {
case "rpc": return runRpcMode(parsed, session, ...);
case "json": return runPrintMode(parsed, session, "json");
case "print": return runPrintMode(parsed, session, "text");
case "interactive": return InteractiveMode(session, ...);
}
4.8 Step 7:执行(agent loop 主线)¶
用户输入 "fix the bug"
│
▼
runPrintMode / InteractiveMode
│
▼
session.send(userMessage) → 在 AgentSession 包装层处理 steering/queue
│
▼
Agent.agentLoop(state, ctx) → 经典 while-state-has-tool-call-and-not-aborted:
│ while (!done && !aborted) {
│ ctx.streamFn(state.model, ctxForLM, {...});
│ apply LLM delta → state.messages;
│ if final result: break;
│ if tool_call: dispatch to tool, append result;
│ }
▼
streamFn (our injected function)
│
▼
modelRuntime.streamSimple(model, ctx, options)
│ (通过 @earendil-works/pi-ai/compat 重新导出)
│
▼
pi-ai → provider 适配层
├─ anthropic-messages
├─ openai-responses / -codex-responses / -completions
├─ google-generative-ai / -vertex
├─ bedrock-converse-stream
├─ mistral-conversations
├─ azure-openai-responses
└─ pi-messages (lazy)
│
▼
HTTP request with:
- mergeProviderAttributionHeaders()
- timeoutMs (or max-int when disabled)
- maxRetries/maxRetryDelayMs
- websocketConnectTimeoutMs
│
▼
LLM 返回 SSE/streamed JSONL:
- yield text_delta
- yield tool_call
- yield thinking
- yield usage
│
▼
Reverse path: streamSimple back to Agent → state.messages → session persistence → UI
4.9 主调用流程关键观察¶
pi-agent-core不直接 importpi-ai/compat——事实上它只 importAgentMessage / ThinkingLevel / setDefaultStreamFn这些接口;streamSimple通过setDefaultStreamFn全局注入(sdk.tsL36)。这是一个非常聪明的解耦设计:扩展或第三方库可以在不解开完整 sdk.ts 的情况下替换 stream 行为。AgentSession包了一层Agent——后者只是机器,前者负责人机协作(priority queue / steering message / TUI 事件总线)。- 三个 hook 嵌入 provider 调用链:
before_provider_request修改 payload;before_provider_headers修改 headers;after_provider_response上报 response(不修改内容)。三者联合实现"transparent middleware"。 resourceLoader.reload()在createAgentSession中异步等待——意味着扩展注册会延迟到 reload 完成;这保证了扩展能在Agent构造后立即看到 hooks 已经挂好。withFileMutationQueue把write/edit等工具串行化——这是并发安全的 micro-correctness 措施。
这一节的所有断言都可以在
/tmp/pi-snap/packages/coding-agent/src/main.ts与core/sdk.ts中行对行复核。