print-mode:打印模式
概述
print-mode.ts 只有 169 行,是三个模式中最简单的。它实现了一个单次执行模式:发送消息给 AI,等待响应,输出结果,退出。没有交互、没有 UI。
触发 print 模式的条件:
| 条件 | 命令示例 | 说明 |
|---|---|---|
--print 或 -p | pi -p "hello" | 显式指定非交互模式 |
--mode json | pi --mode json "hello" | JSON 输出模式(也是 print 模式) |
| stdin 不是 TTY | echo "hello" | pi | 管道输入 |
| stdout 不是 TTY | pi -p "hello" > out.txt | 重定向输出 |
典型用法:
pi -p "列出 src/ 下所有 .ts 文件" # text 模式,无上下文
pi -p -c "继续上次的讨论" # text 模式,有上下文
pi --mode json "分析这段代码" # json 模式
echo "hello" | pi # 管道输入
pi -p @prompt.txt # 文件输入
文件结构
1-13 导入
17-27 接口定义(PrintModeOptions)
33-169 核心函数 runPrintMode()
接口定义
export interface PrintModeOptions {
/** Output mode: "text" for final response only, "json" for all events */
mode: "text" | "json";
/** Array of additional prompts to send after initialMessage */
messages?: string[];
/** First message to send (may contain @file content) */
initialMessage?: string;
/** Images to attach to the initial message */
initialImages?: ImageContent[];
}
| 字段 | 类型 | 作用 |
|---|---|---|
mode | "text" | "json" | 输出模式:只输出最终回复,还是输出所有事件的 JSON 流 |
messages | string[] | 额外的消息列表 |
initialMessage | string | 第一条消息(可能包含 @file 内容) |
initialImages | ImageContent[] | 附带的图片 |
核心函数 runPrintMode()
export async function runPrintMode(runtimeHost: AgentSessionRuntime, options: PrintModeOptions): Promise<number>
- 参数:
runtimeHost是运行时对象(第四阶段创建的),options是上面的接口 - 返回值:
Promise<number>,退出码,0 表示成功,1 表示失败
整体生命周期
运行时创建完成
│
▼
runPrintMode(runtime, options)
│
├─ 1. 初始化
│ ├─ 解构参数
│ ├─ 注册信号处理器
│ └─ 设置 rebindSession 回调
│
├─ 2. 绑定会话
│ ├─ session.bindExtensions() ← 绑定扩展
│ └─ session.subscribe() ← 订阅事件(JSON 模式)
│
├─ 3. 执行消息循环
│ ├─ session.prompt(initialMessage) ← 发送初始消息
│ └─ for message in messages: ← 发送额外消息
│ └─ session.prompt(message)
│
├─ 4. 输出结果
│ ├─ JSON 模式:已经在 subscribe 里实时输出了
│ └─ Text 模式:取最后一条消息,输出文本
│
└─ 5. 清理
├─ 移除信号处理器
├─ disposeRuntime()
└─ flushRawStdout()
关键状态变量
let exitCode = 0; // 退出码,0=成功,1=失败
let session = runtimeHost.session; // 当前会话对象
let unsubscribe; // 事件订阅的取消函数
let disposed = false; // 是否已清理
const signalCleanupHandlers: Array<() => void> = []; // 信号处理器清理函数
阶段 1:初始化
解构参数
const { mode, messages = [], initialMessage, initialImages } = options;
messages = [] 是默认值语法,如果 messages 是 undefined,使用空数组。
注册信号处理器
const registerSignalHandlers = (): void => {
const signals: NodeJS.Signals[] = ["SIGTERM"];
if (process.platform !== "win32") {
signals.push("SIGHUP");
}
for (const signal of signals) {
const handler = () => {
killTrackedDetachedChildren();
void disposeRuntime().finally(() => {
process.exit(signal === "SIGHUP" ? 129 : 143);
});
};
process.on(signal, handler);
signalCleanupHandlers.push(() => process.off(signal, handler));
}
};
支持的信号:
| 信号 | 平台 | 退出码 | 含义 |
|---|---|---|---|
SIGTERM | 所有 | 143 | 终止信号(kill 默认信号) |
SIGHUP | 非 Windows | 129 | 终端关闭 |
收到信号时的处理:
killTrackedDetachedChildren(),杀掉所有跟踪的子进程(比如 bash 命令)disposeRuntime(),清理运行时资源process.exit(143/129),以对应退出码退出
清理函数的注册:
signalCleanupHandlers.push(() => process.off(signal, handler));
把移除监听器的函数存起来,最后在 finally 块中调用,防止内存泄漏。
设置 rebindSession 回调
runtimeHost.setRebindSession(async () => {
await rebindSession();
});
这是一个回调注册,当运行时需要重建会话时(比如用户切换了模型),会调用这个回调。
阶段 2:绑定会话
const rebindSession = async (): Promise<void> => {
session = runtimeHost.session;
await session.bindExtensions({
mode: mode === "json" ? "json" : "print",
commandContextActions: {
waitForIdle: () => session.waitForIdle(),
newSession: async (newSessionOptions) => runtimeHost.newSession(newSessionOptions),
fork: async (entryId, forkOptions) => {
const result = await runtimeHost.fork(entryId, forkOptions);
return { cancelled: result.cancelled };
},
navigateTree: async (targetId, navigateOptions) => {
const result = await session.navigateTree(targetId, {
summarize: navigateOptions?.summarize,
customInstructions: navigateOptions?.customInstructions,
replaceInstructions: navigateOptions?.replaceInstructions,
label: navigateOptions?.label,
});
return { cancelled: result.cancelled };
},
switchSession: async (sessionPath, switchOptions) => {
return runtimeHost.switchSession(sessionPath, switchOptions);
},
reload: async () => {
await session.reload();
},
},
onError: (err) => {
console.error(`Extension error (${err.extensionPath}): ${err.error}`);
},
});
unsubscribe?.();
unsubscribeBackpressure?.();
unsubscribe = session.subscribe((event) => {
if (mode === "json") {
writeRawStdout(`${JSON.stringify(toJsonEvent(event))}\n`);
}
});
unsubscribeBackpressure =
mode === "json"
? session.agent.subscribe(async () => {
await waitForRawStdoutBackpressure();
})
: undefined;
};
绑定扩展
await session.bindExtensions({
mode: mode === "json" ? "json" : "print",
commandContextActions: {...},
onError: (err) => {...},
});
把扩展绑定到会话,让扩展可以:
- 响应工具调用
- 注入系统提示词
- 处理自定义命令
commandContextActions 是扩展可以调用的上下文操作:
| 操作 | 作用 |
|---|---|
waitForIdle | 等待会话空闲 |
newSession | 创建新会话 |
fork | 分支会话 |
navigateTree | 在会话树中导航 |
switchSession | 切换会话 |
reload | 重新加载会话 |
订阅事件
unsubscribe = session.subscribe((event) => {
if (mode === "json") {
writeRawStdout(`${JSON.stringify(toJsonEvent(event))}\n`);
}
});
session.subscribe() 注册一个回调,每当会话状态变化时触发。
Text 模式: 回调什么都不做(mode !== "json"),事件被忽略。
JSON 模式: 把每个事件转换成 JSON 输出到 stdout。
背压处理(Backpressure)
unsubscribeBackpressure =
mode === "json"
? session.agent.subscribe(async () => {
await waitForRawStdoutBackpressure();
})
: undefined;
JSON 模式下,如果输出太快,waitForRawStdoutBackpressure() 会等待 stdout 缓冲区清空,防止内存溢出。
阶段 3:执行消息循环
try {
// 1. JSON 模式先输出会话头
if (mode === "json") {
const header = session.sessionManager.getHeader();
if (header) {
writeRawStdout(`${JSON.stringify(header)}\n`);
}
}
// 2. 绑定会话
await rebindSession();
// 3. 发送初始消息
if (initialMessage) {
await session.prompt(initialMessage, { images: initialImages });
}
// 4. 发送额外消息
for (const message of messages) {
await session.prompt(message);
}
JSON 模式的会话头
if (mode === "json") {
const header = session.sessionManager.getHeader();
if (header) {
writeRawStdout(`${JSON.stringify(header)}\n`);
}
}
JSON 模式先输出会话头,包含会话 ID、时间戳、工作目录等元信息:
{"type":"session","id":"abc123","timestamp":"2024-01-01T00:00:00Z","cwd":"/my/project"}
session.prompt() 的内部流程
这是最核心的方法。当调用 session.prompt("hello") 时:
session.prompt("hello")
│
├─ 1. 创建用户消息,存入会话历史
│ └─ session.state.messages.push({role: "user", content: "hello"})
│
├─ 2. 构建上下文(system prompt + 历史消息)
│ └─ session.buildSessionContext()
│
├─ 3. 调用 AI 模型
│ └─ modelRuntime.chat(context)
│
├─ 4. AI 返回响应(可能分多次)
│ ├─ 流式响应:逐块输出文本
│ ├─ 工具调用:执行工具,结果反馈给 AI
│ └─ 完成:设置 stopReason
│
├─ 5. 存入会话历史
│ └─ session.state.messages.push({role: "assistant", content: ...})
│
└─ 6. 触发事件
└─ session.emit("message", ...) ← subscribe 会收到
阶段 4:输出结果
Text 模式
if (mode === "text") {
const state = session.state;
const lastMessage = state.messages[state.messages.length - 1];
if (lastMessage?.role === "assistant") {
const assistantMsg = lastMessage as AssistantMessage;
if (assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") {
console.error(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
exitCode = 1;
} else {
for (const content of assistantMsg.content) {
if (content.type === "text") {
writeRawStdout(`${content.text}\n`);
}
}
}
}
}
执行流程:
取最后一条消息
│
├─ 是助手的回复?
│ │
│ ├─ 有错误?(stopReason === "error" 或 "aborted")
│ │ └─ 输出错误信息,exitCode = 1
│ │
│ └─ 正常回复
│ └─ 遍历 content,输出文本块
│
└─ 不是助手的回复?
└─ 不输出(理论上不应该发生)
content 的结构:
助手消息的 content 是一个数组,可能包含多种类型:
[
{ type: "text", text: "我可以帮你..." },
{ type: "tool_use", id: "tool_1", name: "read", input: {...} },
{ type: "text", text: "根据文件内容..." },
]
Text 模式只输出 type === "text" 的内容块。
JSON 模式
JSON 模式的输出已经在 subscribe 回调里实时完成了,这里不需要额外处理。
阶段 5:清理
} catch (error: unknown) {
console.error(error instanceof Error ? error.message : String(error));
return 1;
} finally {
for (const cleanup of signalCleanupHandlers) {
cleanup();
}
await disposeRuntime();
await flushRawStdout();
}
catch 块: 捕获所有异常,输出错误信息,返回退出码 1。
finally 块: 无论成功还是失败,都要清理:
- 移除信号处理器(防止内存泄漏)
- 销毁运行时(释放资源)
- 刷新 stdout(确保所有输出都写入)
两种输出模式的对比
| 维度 | Text 模式 | JSON 模式 |
|---|---|---|
| 输出内容 | 只输出 AI 的最终文本回复 | 输出所有会话事件的 JSON 流 |
| 实时性 | 等 AI 完成后一次性输出 | 实时输出每个事件 |
| 使用场景 | pi -p "hello",脚本调用 | pi --mode json "hello",程序化调用 |
| 错误处理 | 输出错误到 stderr | JSON 事件中包含错误信息 |
| 背压处理 | 不需要 | 需要,防止内存溢出 |
Text 模式输出示例:
我可以帮你重构这段代码。首先...
JSON 模式输出示例:
{"type":"session","id":"abc123","timestamp":"...","cwd":"/project"}
{"type":"message","message":{"role":"user","content":"hello"}}
{"type":"model_change","provider":"anthropic","modelId":"claude-3-5-sonnet"}
{"type":"message","message":{"role":"assistant","content":"你好!..."}}
信号处理的状态流转
正常运行
│
├─ 收到 SIGTERM/SIGHUP
│ │
│ ├─ killTrackedDetachedChildren() ← 杀子进程
│ ├─ disposeRuntime() ← 清理资源
│ └─ process.exit(143/129) ← 退出
│
└─ 正常结束
│
├─ finally 块执行
├─ 移除信号处理器
├─ disposeRuntime()
└─ flushRawStdout()
总结
print-mode.ts 实现了一个无状态的单次执行模式:
- 初始化,注册信号处理器,设置 rebind 回调
- 绑定,绑定扩展,订阅事件
- 执行,发送消息,等待 AI 响应
- 输出,Text 模式输出最终结果,JSON 模式实时输出事件流
- 清理,移除处理器,销毁运行时,刷新输出
没有交互、没有 UI、没有会话保存。适合脚本和自动化调用。