Skip to content

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 接口的 unknownFlagsdiagnostics 给后一步使用。

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":

process.exitCode = result.status === "ready" ? 0
                  : result.status === "not_ready" ? 1 : 2;

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)

如果都没指定,依次尝试:

  1. settingsManager.getDefaultProvider() / .getDefaultModel()
  2. 若有 saved model 也在 scopedModels 中 → 用 saved
  3. 否则用 scopedModels[0]
  4. 否则退出并提示 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 主调用流程关键观察

  1. pi-agent-core 不直接 import pi-ai/compat——事实上它只 import AgentMessage / ThinkingLevel / setDefaultStreamFn 这些接口;streamSimple 通过 setDefaultStreamFn 全局注入(sdk.ts L36)。这是一个非常聪明的解耦设计:扩展或第三方库可以在不解开完整 sdk.ts 的情况下替换 stream 行为。
  2. AgentSession 包了一层 Agent——后者只是机器,前者负责人机协作(priority queue / steering message / TUI 事件总线)。
  3. 三个 hook 嵌入 provider 调用链before_provider_request 修改 payload;before_provider_headers 修改 headers;after_provider_response 上报 response(不修改内容)。三者联合实现"transparent middleware"。
  4. resourceLoader.reload()createAgentSession 中异步等待——意味着扩展注册会延迟到 reload 完成;这保证了扩展能在 Agent 构造后立即看到 hooks 已经挂好。
  5. withFileMutationQueuewrite/edit 等工具串行化——这是并发安全的 micro-correctness 措施。

这一节的所有断言都可以在 /tmp/pi-snap/packages/coding-agent/src/main.tscore/sdk.ts行对行复核。