文章
合集Pi Agent 源码阅读第 7 / 9 篇

main.ts 第五阶段:最终检查与模式执行

概述

运行时创建好之后,在进入实际模式之前,还有几个安全检查。检查通过后,程序根据 appMode 进入三种模式之一。

if (parsed.help) {
    const extensionFlags = resourceLoader
        .getExtensions()
        .extensions.flatMap((extension) => Array.from(extension.flags.values()));
    printHelp(extensionFlags);
    process.exit(0);
}

if (parsed.listModels !== undefined) {
    const searchPattern = typeof parsed.listModels === "string" ? parsed.listModels : undefined;
    await listModels(modelRuntime, searchPattern, AbortSignal.timeout(15_000));
    process.exit(0);
}

// Read piped stdin content (if any) - skip for RPC mode which uses stdin for JSON-RPC
let stdinContent: string | undefined;
if (appMode !== "rpc") {
    stdinContent = await readPipedStdin();
    if (stdinContent !== undefined && appMode === "interactive") {
        appMode = "print";
    }
}
time("readPipedStdin");

const { initialMessage, initialImages } = await prepareInitialMessage(
    parsed,
    settingsManager.getImageAutoResize(),
    stdinContent,
);
time("prepareInitialMessage");
initTheme(settingsManager.getTheme(), appMode === "interactive");
time("initTheme");

// Show deprecation warnings in interactive mode
if (appMode === "interactive" && deprecationWarnings.length > 0) {
    await showDeprecationWarnings(deprecationWarnings);
}

time("resolveModelScope");
reportDiagnostics(runtime.diagnostics);
if (runtime.diagnostics.some((diagnostic) => diagnostic.type === "error")) {
    if (runtime.diagnostics.some((diagnostic) => diagnostic.message.includes("Failed to load extension"))) {
        console.error(chalk.yellow(EXTENSION_LOAD_FAILURE_HINT));
    }
    process.exit(1);
}
time("createAgentSession");

if (appMode !== "interactive" && !session.model) {
    console.error(chalk.red(formatNoModelsAvailableMessage()));
    process.exit(1);
}

const startupBenchmark = isTruthyEnvFlag(process.env.PI_STARTUP_BENCHMARK);
if (startupBenchmark && appMode !== "interactive") {
    console.error(chalk.red("Error: PI_STARTUP_BENCHMARK only supports interactive mode"));
    process.exit(1);
}

// RPC refreshes catalogs here in the background; interactive mode starts its refresh after TUI initialization.
if (!offlineMode && appMode === "rpc") {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 15_000);
    void modelRuntime
        .refresh({ signal: controller.signal })
        .catch(() => {})
        .finally(() => clearTimeout(timeout));
}

if (appMode === "rpc") {
    printTimings();
    await runRpcMode(runtime);
} else if (appMode === "interactive") {
    const interactiveMode = new InteractiveMode(runtime, {
        migratedProviders,
        modelFallbackMessage,
        autoTrustOnReloadCwd,
        initialMessage,
        initialImages,
        initialMessages: parsed.messages,
        verbose: parsed.verbose,
        tuiMode: parsed.tuiMode,
    });
    if (startupBenchmark) {
        await interactiveMode.init();
        time("interactiveMode.init");
        // Give the TUI's stdin handler a brief chance to consume terminal query replies
        // (Kitty keyboard protocol, device attributes, cell size) before restoring the terminal.
        await new Promise((resolve) => setTimeout(resolve, 150));
        interactiveMode.stop();
        stopThemeWatcher();
        printTimings();
        if (process.stdout.writableLength > 0) {
            await new Promise<void>((resolve) => process.stdout.once("drain", resolve));
        }
        if (process.stderr.writableLength > 0) {
            await new Promise<void>((resolve) => process.stderr.once("drain", resolve));
        }
        return;
    }

    printTimings();
    await interactiveMode.run();
} else {
    printTimings();
    const exitCode = await runPrintMode(runtime, {
        mode: toPrintOutputMode(appMode),
        messages: parsed.messages,
        initialMessage,
        initialImages,
    });
    stopThemeWatcher();
    restoreStdout();
    if (exitCode !== 0) {
        process.exitCode = exitCode;
    }
    return;
}

最终检查

检查 1:--help

if (parsed.help) {
    const extensionFlags = resourceLoader
        .getExtensions()
        .extensions.flatMap((extension) => Array.from(extension.flags.values()));
    printHelp(extensionFlags);
    process.exit(0);
}

虽然 --help 在第二阶段解析时就发现了,但那时候扩展还没加载。这里等扩展加载完后,把扩展注册的额外参数也加进帮助信息里,然后打印并退出。

检查 2:--list-models

if (parsed.listModels !== undefined) {
    const searchPattern = typeof parsed.listModels === "string" ? parsed.listModels : undefined;
    await listModels(modelRuntime, searchPattern, AbortSignal.timeout(15_000));
    process.exit(0);
}

pi --list-models 或 pi --list-models sonnet。等模型运行时准备好后,列出可用模型(支持模糊搜索),然后退出。

检查 3:读取管道输入

let stdinContent: string | undefined;
if (appMode !== "rpc") {
    stdinContent = await readPipedStdin();
    if (stdinContent !== undefined && appMode === "interactive") {
        appMode = "print";
    }
}

检测有没有管道输入(比如 echo "hello" | pi)。如果有,读取内容作为初始消息。

RPC 模式跳过这步,因为 RPC 用 stdin 传输 JSON-RPC 消息,不是管道文本。

检查 4:准备初始消息

const { initialMessage, initialImages } = await prepareInitialMessage(
    parsed,
    settingsManager.getImageAutoResize(),
    stdinContent,
);

把用户输入的各种来源合并成一条初始消息:

用户输入的消息(parsed.messages)
  + 文件内容(@file.txt)
  + 图片(@image.png)
  + 管道输入(stdin)
  → initialMessage + initialImages

检查 5:初始化主题

initTheme(settingsManager.getTheme(), appMode === "interactive");

加载主题文件,设置终端的配色方案。只有 interactive 模式才完整初始化主题。

检查 6:报告诊断信息

reportDiagnostics(runtime.diagnostics);
if (runtime.diagnostics.some((diagnostic) => diagnostic.type === "error")) {
    if (runtime.diagnostics.some((diagnostic) => diagnostic.message.includes("Failed to load extension"))) {
        console.error(chalk.yellow(EXTENSION_LOAD_FAILURE_HINT));
    }
    process.exit(1);
}

把运行时创建过程中收集的所有警告/错误打印出来。如果有 error(比如扩展加载失败),退出并给出提示。

检查 7:模型是否可用

if (appMode !== "interactive" && !session.model) {
    console.error(chalk.red(formatNoModelsAvailableMessage()));
    process.exit(1);
}

非交互模式下,必须有可用模型。交互模式下可以没有模型(用户可以稍后选择),但 print/rpc 模式必须现在就有。

检查 8:刷新模型目录

if (!offlineMode && appMode === "rpc") {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 15_000);
    void modelRuntime
        .refresh({ signal: controller.signal })
        .catch(() => {})
        .finally(() => clearTimeout(timeout));
}

RPC 模式下,在后台刷新模型目录(获取最新的可用模型列表)。

模式执行

所有检查通过后,程序根据 appMode 进入三种模式之一:

模式不是在这里选择的

第五阶段不判断模式,只做两件事:①处理管道输入导致的模式切换(有管道输入时把 interactive 降级为 print),②根据已有的 appMode 值做 if-else 分发。真正的模式决策发生在第二阶段的 resolveAppMode(),根据命令行参数(--rpc → rpc,-p → print,其他 → interactive)确定初始值。

模式 1:RPC 模式

if (appMode === "rpc") {
    printTimings();
    await runRpcMode(runtime);
}

用途: 让其他程序通过 JSON-RPC 协议调用 Agent 的能力。比如 IDE 插件可以这样和 Agent 通信。

特点:

  • stdin 接收 JSON-RPC 请求
  • stdout 返回 JSON-RPC 响应
  • 无 UI,无交互
  • 永远运行,直到被杀死

典型使用场景:

  • VS Code 插件调用 Agent 的代码编辑能力
  • 其他 CLI 工具通过管道发送 JSON-RPC 请求
  • 自动化脚本调用 Agent 的文件操作能力

模式 2:Interactive 模式

} else if (appMode === "interactive") {
    const interactiveMode = new InteractiveMode(runtime, {
        migratedProviders,
        modelFallbackMessage,
        autoTrustOnReloadCwd,
        initialMessage,
        initialImages,
        initialMessages: parsed.messages,
        verbose: parsed.verbose,
        tuiMode: parsed.tuiMode,
    });

    printTimings();
    await interactiveMode.run();
}

用途: 用户在终端里直接和 Agent 对话,有完整的 UI 界面。

InteractiveMode 的参数:

参数来源作用
runtime第四阶段创建提供核心能力
migratedProviders第三阶段迁移被迁移的 Provider 列表
modelFallbackMessage运行时模型不可用时的提示
autoTrustOnReloadCwd信任计算自动信任的目录
initialMessage步骤 4初始消息
initialImages步骤 4初始图片
initialMessages参数解析用户输入的消息列表
verbose参数解析详细模式
tuiMode参数解析TUI 模式(regular/fullscreen)

InteractiveMode 做什么:

  1. 初始化 TUI(终端用户界面)
  2. 显示启动画面、模型选择器
  3. 进入对话循环:用户输入 → AI 思考 → 工具执行 → 输出结果
  4. 处理快捷键(Ctrl+P 切换模型、Ctrl+C 中断等)

模式 3:Print 模式

} else {
    printTimings();
    const exitCode = await runPrintMode(runtime, {
        mode: toPrintOutputMode(appMode),
        messages: parsed.messages,
        initialMessage,
        initialImages,
    });
    stopThemeWatcher();
    restoreStdout();
    if (exitCode !== 0) {
        process.exitCode = exitCode;
    }
    return;
}

用途: 非交互式执行,输出结果后退出。适合脚本调用。

典型用法:

# 直接提问
pi -p "列出 src/ 下所有 .ts 文件"

# 管道输入
echo "分析这段代码" | pi

# 使用文件作为输入
pi -p @prompt.txt

runPrintMode() 做什么:

  1. 发送初始消息给 AI
  2. 等待 AI 响应(可能调用工具)
  3. 把结果输出到 stdout
  4. 返回退出码(0=成功,非0=失败)

最后两行:

stopThemeWatcher();   // 停止主题文件监听
restoreStdout();      // 恢复被 takeOverStdout() 拦截的 stdout

执行流总结

用户输入: pi [args]
  │
  ├─ 第一阶段:特殊命令拦截
  │   └─ auth / install / config → 处理并退出
  │
  ├─ 第二阶段:解析参数
  │   └─ parseArgs() → --version / --export → 处理并退出
  │
  ├─ 第三阶段:基础设施初始化
  │   ├─ runMigrations()
  │   ├─ SettingsManager.create()
  │   ├─ showFirstTimeSetup()
  │   └─ createSessionManager()
  │
  ├─ 第四阶段:创建运行时环境
  │   ├─ resolveCliPaths()
  │   ├─ createRuntime() 工厂函数
  │   └─ createAgentSessionRuntime()
  │
  └─ 第五阶段:最终检查与模式执行
      ├─ --help / --list-models → 处理并退出
      ├─ rpc → JSON-RPC 服务
      ├─ interactive → TUI 交互界面
      └─ print → 输出结果,退出

整个 main.ts 的生命周期

从用户输入命令到程序退出,完整流程:

cli.ts
  │
  ├─ 设置环境(进程名、环境变量、禁用警告)
  ├─ 配置 HTTP 分发器
  └─ main(process.argv.slice(2))
       │
       ├─ 第一阶段:特殊命令拦截
       │   ├─ runAuthCommand()
       │   ├─ handlePackageCommand()
       │   └─ handleConfigCommand()
       │
       ├─ 第二阶段:解析参数
       │   ├─ parseArgs()
       │   ├─ --version / --export
       │   └─ resolveAppMode()
       │
       ├─ 第三阶段:基础设施初始化
       │   ├─ runMigrations()
       │   ├─ SettingsManager.create()
       │   ├─ showFirstTimeSetup()
       │   ├─ createSessionManager()
       │   └─ missingSessionCwdIssue
       │
       ├─ 第四阶段:创建运行时环境
       │   ├─ resolveCliPaths()
       │   ├─ createRuntime() 工厂函数
       │   │   ├─ 确定项目信任状态
       │   │   ├─ createAgentSessionServices()
       │   │   ├─ 解析模型
       │   │   └─ createAgentSessionFromServices()
       │   └─ createAgentSessionRuntime()
       │
       └─ 第五阶段:最终检查与模式执行
           ├─ --help / --list-models
           ├─ readPipedStdin()
           ├─ prepareInitialMessage()
           ├─ initTheme()
           ├─ reportDiagnostics()
           └─ 进入对应模式
               ├─ runRpcMode()
               ├─ InteractiveMode.run()
               └─ runPrintMode()