Pi từ mã nguồnpi v1.4.0

Chương 12Phần 3: Coding agent

Các chế độ chạy: interactive, print, JSON, RPC, SDK

Interactive, print, JSON, RPC và SDK: năm lớp vỏ quanh cùng một AgentSessionRuntime.

  • 21 phút đọc
  • 36 đoạn code
  • 17 mục
Trong chương này17 mục
  1. Vai trò: năm giao diện, một cơ chế
  2. Mô hình tư duy: mode là lớp vỏ quanh AgentSessionRuntime
  3. Từ lệnh pi tới một runtime
  4. AgentSessionRuntime và services
  5. Mỗi chế độ gắn extension theo cách riêng
  6. Print mode
  7. JSON event mode và stdout dành riêng
  8. RPC mode: giao thức JSONL hai chiều
  9. Extension UI đi qua RPC
  10. RpcClient: phía client
  11. SDK: nhúng Pi trong tiến trình
  12. Interactive mode ở mức cấu trúc
  13. Trace: một phiên RPC có hộp thoại của extension
  14. Quyết định thiết kế
  15. Cạm bẫy và trường hợp biên
  16. Đọc code ở đâu
  17. Tóm tắt

Vai trò: năm giao diện, một cơ chế#

Cùng một Pi được dùng theo năm cách. Một người gõ trong terminal (interactive). Một script chạy pi -p rồi đọc câu trả lời cuối (print). Một tiến trình đọc luồng sự kiện (event stream) có cấu trúc của một lần chạy (JSON). Một ứng dụng khác, viết bằng ngôn ngữ bất kỳ, điều khiển Pi lâu dài qua stdin/stdout (RPC). Hoặc một chương trình Node.js/Bun nhúng thẳng Pi vào tiến trình của nó (SDK). Tài liệu “How Pi Works” tóm lại quan hệ giữa chúng bằng một câu ngắn ở cuối mục Interfaces:

docs/how-pi-works.md, mục Interfaces
pi/packages/coding-agent/docs/how-pi-works.mddòng 33–39Markdown
## InterfacesInteractive mode renders session and agent events in the terminal. Print mode runs a prompt and writes the final response. JSON mode writes agent events as JSONL.RPC mode accepts JSONL commands on stdin and writes responses and events to stdout. The TypeScript SDK creates and controls agent sessions in process.All interfaces use the same agent and session mechanisms.

Câu đó đúng theo nghĩa đen trong code. Bốn chế độ CLI đều nhận cùng một đối tượng AgentSessionRuntime do main() dựng, và đều nói chuyện với cùng một AgentSession (chương AgentSession), vốn bọc cùng một Agent (chương pi-agent-core). SDK bỏ qua main() nhưng gọi đúng các hàm mà main() gọi. Khác biệt giữa các chế độ chỉ nằm ở bốn chỗ: đầu vào đi vào thế nào, sự kiện được vẽ hay tuần tự hoá ra sao, extension nhận giao diện người dùng nào, và tiến trình sống bao lâu. Chương này đi qua đúng bốn chỗ đó. Phần bên trong AgentSession thuộc về chương AgentSession, cách TUI vẽ thuộc về chương pi-tui, API của extension thuộc về chương Extensions.

Chế độKích hoạtHàm chạyVòng đờiExtension thấy
Interactivestdin và stdout đều là TTY, không có -p/--mode json|rpcnew InteractiveMode(runtime).run()tới khi người dùng thoátmode: "tui", hasUI: true
Print-p, hoặc stdin/stdout bị chuyển hướngrunPrintMode(runtime, { mode: "text" })một lần"print", hasUI: false
JSON--mode jsonrunPrintMode(runtime, { mode: "json" })một lần"json", hasUI: false
RPC--mode rpcrunRpcMode(runtime)tới khi stdin đóng"rpc", hasUI: true
SDKimport { createAgentSession }không qua main()do host quyếtmặc định "print", không UI, trừ khi host truyền vào

Mô hình tư duy: mode là lớp vỏ quanh AgentSessionRuntime#

Mọi lần chạy lệnh pi đi qua cùng một đường khởi động, và chỉ rẽ nhánh ở bước cuối:

pi [options] [@files] [messages]               cli.ts → main(args)
│
├─ lệnh con auth / install / remove / update / list / config / mcp  → chạy rồi thoát
├─ parseArgs(args) → Args                                     cli/args.ts
├─ resolveAppMode(parsed, stdin.isTTY, stdout.isTTY)
│     → "interactive" | "print" | "json" | "rpc"
├─ takeOverStdout()  nếu không phải interactive               core/output-guard.ts
├─ createSessionManager(parsed, …)   session mới, -c, -r, --session, --fork
├─ createRuntime: factory dựng services theo cwd + AgentSession
├─ createAgentSessionRuntime(createRuntime, { cwd, agentDir, sessionManager })
│
└─ rẽ nhánh theo appMode
     rpc          → runRpcMode(runtime)
     interactive  → new InteractiveMode(runtime, …).run()
     print, json  → runPrintMode(runtime, { mode: "text" | "json", … })
Đường khởi động trong main(). Ba hàm chạy ở cuối là “lớp vỏ”; mọi thứ phía trên là chung.
Mode
Đọc đầu vào, xuất sự kiện, cung cấp ExtensionUIContext. InteractiveMode, runPrintMode, runRpcMode, hoặc host SDK.
AgentSessionRuntime
Giữ session hiện tại và services theo cwd; thay session khi /new, /resume, /fork, import.
AgentSession
Một hội thoại: prompt, tin nhắn steer và follow-up, lưu session, extension, compaction (nén ngữ cảnh), retry (chương AgentSession).
Agent
Vòng lặp agent của pi-agent-core (chương pi-agent-core).
Runtime host
Tên tham số mà các mode dùng cho AgentSessionRuntime. Mode đọc lại runtimeHost.session mỗi khi session bị thay (qua callback rebind), vì đối tượng AgentSession cũ khi đó đã bị dispose.
Services
AgentSessionServices: cwd, agentDir, ModelRuntime, SettingsManager, ResourceLoader, cùng các chẩn đoán (diagnostics) thu được khi dựng.
Binding
Lời gọi session.bindExtensions(...) mà mỗi mode thực hiện để đưa cho extension một giao diện người dùng, một tên mode và các hành động như “tạo session mới”.
AgentSessionEvent
Sự kiện mà session.subscribe() phát: mọi AgentEvent của vòng lặp cộng với sự kiện cấp session như agent_settled, queue_update, compaction_start/end, auto_retry_start/end (agent-session.ts:196–237).
agent_end và agent_settled
agent_end đóng một lần chạy cấp thấp của Agent. agent_settled nghĩa là Pi không còn việc tự động nào cho lần chạy cấp session đó: không thử lại, không compaction rồi chạy tiếp, không tin nhắn chờ (json.md:48).

Từ lệnh pi tới một runtime#

File thực thi chỉ có sáu dòng:

cli.ts: điểm vào của lệnh pi
pi/packages/coding-agent/src/cli.tsdòng 1–6TypeScript
#!/usr/bin/env nodeimport { setupCli } from "./cli/setup.ts";import { main } from "./main.ts";setupCli();main(process.argv.slice(2));
setupCli() đặt tên tiến trình, biến môi trường PI_CODING_AGENT và AI_AGENT, tắt cảnh báo của Node và cấu hình HTTP dispatcher (setup.ts:4–13). Một điểm vào thứ hai, rpc-entry.ts, làm y hệt nhưng chèn sẵn --mode rpc (rpc-entry.ts:13).

main() xử lý các lệnh con trước khi đọc tuỳ chọn: pi auth ..., các lệnh gói (install, remove, update, list), pi config, pi mcp ... (main.ts:585–621). Chỉ khi không phải lệnh con, nó mới gọi parseArgs(). Bộ phân tích là một vòng for viết tay; hai tuỳ chọn quyết định chế độ trông như sau:

--mode và --print trong parseArgs()
pi/packages/coding-agent/src/cli/args.tsdòng 96–110, 177–183TypeScript
} else if (arg === "--mode") {	const mode = args[i + 1];	if (mode === undefined || mode.startsWith("-")) {		result.diagnostics.push({ type: "error", message: "--mode requires text, json, or rpc" });		continue;	}	i++;	if (mode !== "text" && mode !== "json" && mode !== "rpc") {		result.diagnostics.push({			type: "error",			message: `Invalid mode "${mode}". Valid values: text, json, rpc`,		});		continue;	}	result.mode = mode;} else if (arg === "--print" || arg === "-p") {	result.print = true;	const next = args[i + 1];	if (next !== undefined && !next.startsWith("@") && (!next.startsWith("-") || next.startsWith("---"))) {		result.messages.push(next);		i++;	}
Tuỳ chọn dài không biết tên không bị từ chối: chúng vào unknownFlags để extension đăng ký cờ riêng (args.ts:249–262); tuỳ chọn ngắn không biết tên thì báo lỗi.

Chế độ cuối cùng do một hàm nhỏ quyết định, theo thứ tự ưu tiên:

resolveAppMode(): rpc, json, rồi print nếu có -p hoặc không có TTY
pi/packages/coding-agent/src/main.tsdòng 112–123TypeScript
function resolveAppMode(parsed: Args, stdinIsTTY: boolean, stdoutIsTTY: boolean): AppMode {	if (parsed.mode === "rpc") {		return "rpc";	}	if (parsed.mode === "json") {		return "json";	}	if (parsed.print || !stdinIsTTY || !stdoutIsTTY) {		return "print";	}	return "interactive";}

Hai hệ quả đáng nhớ. Chỉ cần stdin hoặc stdout không phải terminal là Pi chọn print, nên git diff | pi "Review" và pi "..." > out.txt chạy được mà không cần -p. Và --mode text không ép chạy một lần: trong terminal, nó vẫn mở TUI (cli.md:41). Ngay sau khi chọn xong, mọi chế độ không tương tác chiếm lấy stdout:

Chiếm stdout cho các chế độ không tương tác; RPC từ chối @file
pi/packages/coding-agent/src/main.tsdòng 654–663TypeScript
let appMode = resolveAppMode(parsed, process.stdin.isTTY, process.stdout.isTTY);const shouldTakeOverStdout = appMode !== "interactive" && !isPlainRuntimeMetadataCommand(parsed);if (shouldTakeOverStdout) {	takeOverStdout();}if (parsed.mode === "rpc" && parsed.fileArgs.length > 0) {	console.error(chalk.red("Error: @file arguments are not supported in RPC mode"));	process.exit(1);}

Tiếp theo là session. createSessionManager() chọn một session trong bộ nhớ (--no-session), fork (tách nhánh) một session có sẵn (--fork), mở theo đường dẫn hoặc id (--session, có hỏi fork nếu session thuộc dự án khác), hiện bộ chọn (-r), tiếp tục session gần nhất (-c), hoặc tạo mới (main.ts:358–449). Bước này đi trước mọi service, vì chú thích ở main.ts:686–690 giải thích: session được chọn có thể thuộc một thư mục làm việc khác, và settings, resource, provider (nhà cung cấp) của dự án phải được tính theo thư mục đó.

Rồi main() định nghĩa một factory, tức một hàm dựng runtime cho một cwd và một session manager bất kỳ:

createRuntime: services cho cwd, rồi AgentSession từ services
pi/packages/coding-agent/src/main.tsdòng 733–740, 751–757, 842–864TypeScript
const createRuntime: CreateAgentSessionRuntimeFactory = async ({	cwd,	agentDir,	sessionManager,	sessionStartEvent,	projectTrustContext,}) => {	const isInitialRuntime = sessionStartEvent === undefined;	const runtimeSettingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });	const services = await createAgentSessionServices({		cwd,		agentDir,		settingsManager: runtimeSettingsManager,		modelRuntimeSignal: AbortSignal.timeout(15_000),		extensionFlagValues: parsed.unknownFlags,	const created = await createAgentSessionFromServices({		services,		sessionManager,		sessionStartEvent,		model: sessionOptions.model,		thinkingLevel: sessionOptions.thinkingLevel,		scopedModels: sessionOptions.scopedModels,		tools: sessionOptions.tools,		excludeTools: sessionOptions.excludeTools,		noTools: sessionOptions.noTools,		customTools: sessionOptions.customTools,	});	const cliThinkingOverride = parsed.thinking !== undefined || cliThinkingFromModel;	if (created.session.model && cliThinkingOverride) {		created.session.setThinkingLevel(created.session.thinkingLevel);	}	return {		...created,		services,		diagnostics,	};};

Factory đóng gói (closure) mọi đầu vào cố định của tiến trình (các cờ CLI, đường dẫn resource đã chuyển thành tuyệt đối, kho trust dự án) và mỗi lần được gọi thì: quyết định trust cho cwd đó, dựng SettingsManager, gọi createAgentSessionServices(), giải mô hình (model) và bộ tool từ cờ CLI (buildSessionOptions(), main.ts:451–554), đặt --api-key thành khoá tạm không lưu, rồi gọi createAgentSessionFromServices(), một lớp mỏng quanh createAgentSession() của SDK (agent-session-services.ts:214–233). Phần cuối của main() dùng factory đó và rẽ nhánh:

Dựng runtime, đọc stdin bị pipe, rẽ vào một trong ba mode
pi/packages/coding-agent/src/main.tsdòng 866–872, 894–901, 949–964, 985–992TypeScript
const runtime = await createAgentSessionRuntime(createRuntime, {	cwd: sessionManager.getCwd(),	agentDir,	sessionManager,});time("createAgentSessionRuntime");const { services, session, modelFallbackMessage } = runtime;// Read piped stdin content (if any) - skip for RPC mode which uses stdin for JSON-RPClet stdinContent: string | undefined;if (appMode !== "rpc") {	stdinContent = await readPipedStdin();	if (stdinContent !== undefined && appMode === "interactive") {		appMode = "print";	}}if (appMode === "rpc") {	printTimings();	await runRpcMode(runtime);} else if (appMode === "interactive") {	const interactiveMode = new InteractiveMode(runtime, {		migratedProviders,		startupDiagnostics,		modelFallbackMessage,		autoTrustOnReloadCwd,		initialMessage,		initialImages,		initialMessages: parsed.messages,		verbose: parsed.verbose,		tuiMode: parsed.tuiMode,		initialThemeSetting: parsed.useTheme,	});} else {	printTimings();	const exitCode = await runPrintMode(runtime, {		mode: toPrintOutputMode(appMode),		messages: parsed.messages,		initialMessage,		initialImages,	});

Nếu stdin bị pipe, toàn bộ nội dung được đọc trước (readPipedStdin) và nối vào đầu prompt đầu tiên cùng nội dung các file @path (initial-message.ts:20–43); một phiên đang định mở TUI sẽ chuyển sang print (dòng 898–900). RPC không đọc stdin ở bước này, vì stdin của nó chở lệnh. Trước khi rẽ nhánh, các chế độ không tương tác in chẩn đoán ra stderr, thoát với mã 1 nếu có lỗi, và đòi phải có mô hình (main.ts:915–931).

AgentSessionRuntime và services#

Vấn đề. Một session gắn với một thư mục làm việc: settings của dự án, các file AGENTS.md, extension của dự án, provider mà extension đó đăng ký, quyết định trust. Khi người dùng /resume một session của dự án khác, hoặc fork, mọi thứ đó phải được dựng lại cho thư mục mới, và các extension context cũ phải bị vô hiệu hoá. Nếu từng mode tự làm việc này, bốn chế độ sẽ có bốn cách thay session hơi khác nhau.

Giải pháp. Gom mọi thứ phụ thuộc cwd vào một gói services, và giao việc thay session cho một đối tượng duy nhất giữ factory:

AgentSessionServices: mọi thứ phụ thuộc cwd
pi/packages/coding-agent/src/core/agent-session-services.tsdòng 67–80TypeScript
/** * Coherent cwd-bound runtime services for one effective session cwd. * * This is infrastructure only. The AgentSession itself is created separately so * session options can be resolved against these services first. */export interface AgentSessionServices {	cwd: string;	agentDir: string;	modelRuntime: ModelRuntime;	settingsManager: SettingsManager;	resourceLoader: ResourceLoader;	diagnostics: AgentSessionRuntimeDiagnostic[];}

Kiểu của factory nhận cwd, agentDir, session manager, và tuỳ chọn một sessionStartEvent cho extension biết vì sao session bắt đầu (agent-session-runtime.ts:35–41). AgentSessionRuntime giữ session hiện tại, services và chính factory đó; mọi thao tác thay session đi theo cùng một khuôn:

switchSession(): hỏi extension, tháo session cũ, dựng runtime mới, rebind
pi/packages/coding-agent/src/core/agent-session-runtime.tsdòng 196–224TypeScript
async switchSession(	sessionPath: string,	options?: {		cwdOverride?: string;		withSession?: (ctx: ReplacedSessionContext) => Promise<void>;		projectTrustContextFactory?: (cwd: string) => ProjectTrustContext;	},): Promise<{ cancelled: boolean }> {	const beforeResult = await this.emitBeforeSwitch("resume", sessionPath);	if (beforeResult.cancelled) {		return beforeResult;	}	const previousSessionFile = this.session.sessionFile;	const sessionManager = SessionManager.open(sessionPath, undefined, options?.cwdOverride);	assertSessionCwdExists(sessionManager, this.cwd);	await this.teardownCurrent("resume", sessionManager.getSessionFile());	this.apply(		await this.createRuntime({			cwd: sessionManager.getCwd(),			agentDir: this.services.agentDir,			sessionManager,			sessionStartEvent: { type: "session_start", reason: "resume", previousSessionFile },			projectTrustContext: options?.projectTrustContextFactory?.(sessionManager.getCwd()),		}),	);	await this.finishSessionReplacement(options?.withSession);	return { cancelled: false };}
  1. Hỏi extensionsession_before_switch có thể huỷ thao tác.
  2. Tháo session cũteardownCurrent(): abort() để lượt (turn) đang chạy được lưu, phát session_shutdown, rồi dispose().
  3. Dựng lạiGọi factory với cwd của session đích và session_start lý do "resume".
  4. RebindGọi callback mà mode đã đăng ký bằng setRebindSession().

Bước cuối là chỗ mode và runtime gặp nhau. Mỗi mode đăng ký một hàm để đăng ký lại listener và gắn lại extension vào session mới: print mode ở print-mode.ts:70–72, RPC ở rpc-mode.ts:313–315, interactive ở interactive-mode.ts:619–622. teardownCurrent() gọi abort() trước tiên, và chú thích giải thích lý do: lượt đang dở, kể cả kết quả tool, phải được lưu vào session cũ trước khi nó bị thay (agent-session-runtime.ts:167–178).

Mỗi chế độ gắn extension theo cách riêng#

Extension không biết mình đang chạy trong chế độ nào cho tới khi mode gọi bindExtensions(). Lời gọi này nhận một uiContext, một tên mode và các hành động lệnh, rồi mới phát session_start cho extension:

bindExtensions(): mode cung cấp UI, rồi session_start mới được phát
pi/packages/coding-agent/src/core/agent-session.tsdòng 3318–3342TypeScript
async bindExtensions(bindings: ExtensionBindings): Promise<void> {	if (bindings.uiContext !== undefined) {		this._extensionUIContext = bindings.uiContext;	}	if (bindings.mode !== undefined) {		this._extensionMode = bindings.mode;	}	if (bindings.commandContextActions !== undefined) {		this._extensionCommandContextActions = bindings.commandContextActions;	}	if (bindings.abortHandler !== undefined) {		this._extensionAbortHandler = bindings.abortHandler;	}	if (bindings.shutdownHandler !== undefined) {		this._extensionShutdownHandler = bindings.shutdownHandler;	}	if (bindings.onError !== undefined) {		this._extensionErrorListener = bindings.onError;	}	this._applyExtensionBindings(this._extensionRunner);	await this._extensionRunner.emit(this._sessionStartEvent);	this._extensionRunner.reportUnhandledMcpServers();	await this.extendResourcesFromExtensions(this._sessionStartEvent.reason === "reload" ? "reload" : "startup");}

Tên mode là một trong bốn giá trị "tui" | "rpc" | "json" | "print", và extension context có thêm cờ hasUI (extensions/types.ts:323–331). Khi mode không đưa uiContext, extension runner dùng một context rỗng:

noOpUIContext: hộp thoại trả về ngay giá trị mặc định; hasUI là false
pi/packages/coding-agent/src/core/extensions/runner.tsdòng 331–345, 572–575, 628–630TypeScript
const noOpUIContext: ExtensionUIContext = {	select: async () => undefined,	confirm: async () => false,	input: async () => undefined,	notify: () => {},	onTerminalInput: () => () => {},	setStatus: () => {},	setWorkingMessage: () => {},	setWorkingVisible: () => {},	setWorkingIndicator: () => {},	setHiddenThinkingLabel: () => {},	setWidget: () => {},	setFooter: () => {},	setHeader: () => {},	setTitle: () => {},	setUIContext(uiContext?: ExtensionUIContext, mode: ExtensionMode = "print"): void {		this.uiContext = uiContext ? this.wrapUIPromptContext(uiContext) : noOpUIContext;		this.mode = mode;	}	hasUI(): boolean {		return this.uiContext !== noOpUIContext;	}
Chế độmodeuiContexthasUIctx.ui.confirm(...) trả về
Interactive"tui"hộp thoại thật trong TUI (interactive-mode.ts:2633)truelựa chọn của người dùng
RPC"rpc"giao thức con extension_ui_*truecâu trả lời của client
JSON"json"không có, dùng noOpUIContextfalsefalse ngay lập tức
Print"print"không có, dùng noOpUIContextfalsefalse ngay lập tức
SDKmặc định "print" (agent-session.ts:461)do host truyềntuỳ hosttuỳ host

Vì vậy extension viết cẩn thận luôn xét ctx.hasUI. Ví dụ permission-gate.ts hỏi người dùng trước khi chạy rm -rf hay sudo, nhưng khi không có UI thì chặn luôn thay vì im lặng cho qua (permission-gate.ts:19–29).

Print mode#

Print mode là người dùng đơn giản nhất của runtime, và cũng là nơi dễ thấy nhất rằng một mode chỉ là lớp vỏ. runPrintMode() phục vụ cả -p (text) lẫn --mode json; hai chế độ chỉ khác nhau ở chỗ sự kiện có được ghi ra hay không. Mỗi lần gắn session, nó gắn extension với tên mode tương ứng và đăng ký listener:

rebindSession() của print mode: gắn extension, ghi sự kiện nếu là JSON
pi/packages/coding-agent/src/modes/print-mode.tsdòng 74–78, 101–119TypeScript
const rebindSession = async (): Promise<void> => {	session = runtimeHost.session;	await session.bindExtensions({		mode: mode === "json" ? "json" : "print",		commandContextActions: {		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;};

Phần thân chỉ là gửi các prompt lần lượt rồi, với text, in khối văn bản của assistant message cuối cùng:

Gửi prompt, in câu trả lời cuối, chọn mã thoát
pi/packages/coding-agent/src/modes/print-mode.tsdòng 121–160TypeScript
try {	if (mode === "json") {		const header = session.sessionManager.getHeader();		if (header) {			writeRawStdout(`${JSON.stringify(header)}\n`);		}	}	await rebindSession();	if (initialMessage) {		await session.prompt(initialMessage, { images: initialImages });	}	for (const message of messages) {		await session.prompt(message);	}	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`);					}				}				if (assistantMsg.stopReason === "length") {					console.error("Response was truncated: the model reached its output token limit.");					exitCode = 1;				}			}		}	}

Mỗi await session.prompt() chỉ resolve khi lần chạy đã xong, kể cả các lần thử lại tự động (sdk.md:64), nên các prompt trong pi -p "a" "b" chạy nối tiếp nhau trên cùng một session. Với text, chỉ các khối text của message cuối được in, mỗi khối một dòng; khối thinking và lời gọi tool không bao giờ ra stdout. Mã thoát phản ánh stopReason: "error" hoặc "aborted" in lỗi ra stderr và trả 1; "length" vẫn in phần văn bản bị cụt nhưng cảnh báo và trả 1. JSON mode không đặt mã thoát theo kết quả của mô hình; người đọc phải tự xem sự kiện (cli-integration.md:46).

Phần còn lại của hàm là vệ sinh tiến trình: SIGTERM và SIGHUP giết các tiến trình con chạy tách rời mà Pi theo dõi, dispose runtime rồi thoát với mã 143 hoặc 129 (print-mode.ts:50–66); khối finally luôn dispose runtime (phát session_shutdown lý do "quit") và xả hết stdout trước khi trả về.

JSON event mode và stdout dành riêng#

pi --mode json "..." ghi một dòng header của session, rồi mỗi sự kiện session một dòng JSON, rồi thoát khi các prompt xong (json.md:9). Hình dạng sự kiện trên dây gần như y hệt AgentSessionEvent trong tiến trình, trừ một loại:

toJsonEvent(): message_update mất ảnh chụp tích luỹ
pi/packages/coding-agent/src/modes/json-event.tsdòng 4–18, 40–61TypeScript
type WithoutPartial<T> = T extends { partial: unknown } ? Omit<T, "partial"> : T;type ToJsonAssistantMessageEvent<T> = T extends { type: "toolcall_start"; partial: unknown }	? WithoutPartial<T> & { id: string; toolName: string }	: WithoutPartial<T>;type MessageUpdateEvent = Extract<AgentSessionEvent, { type: "message_update" }>;type JsonMessageUpdateEvent = {	type: "message_update";	usage: Usage;	assistantMessageEvent: ToJsonAssistantMessageEvent<MessageUpdateEvent["assistantMessageEvent"]>;};/** Session event shape emitted by the JSON and RPC stdout protocols. */export type JsonAgentSessionEvent = Exclude<AgentSessionEvent, { type: "message_update" }> | JsonMessageUpdateEvent;/** * Remove cumulative assistant snapshots from streaming wire events. * `message_start` provides the initial message, deltas build it, and * `message_end` provides the final authoritative message. Cumulative usage, * tool-call ids, and tool names remain available because their size is constant. */export function toJsonEvent(event: MessageUpdateEvent): JsonMessageUpdateEvent;export function toJsonEvent(event: AgentSessionEvent): JsonAgentSessionEvent;export function toJsonEvent(event: AgentSessionEvent): JsonAgentSessionEvent {	if (event.type !== "message_update") {		return event;	}	if (event.message.role !== "assistant") {		throw new Error("message_update message is not an assistant message");	}	return {		type: "message_update",		usage: event.message.usage,		assistantMessageEvent: toJsonAssistantMessageEvent(event.assistantMessageEvent),	};}

Vì sao. Trong tiến trình, mỗi message_update mang theo toàn bộ assistant message đang lớn dần, và mỗi sự kiện con của pi-ai còn mang partial là chính message đó. Với một câu trả lời N mảnh, tuần tự hoá nguyên si nghĩa là ghi ra tổng cộng cỡ N² ký tự. Từ 0.84.0, JSON và RPC chỉ ghi phần delta: message_start cho message ban đầu, các delta dựng dần, message_end là bản cuối có thẩm quyền (CHANGELOG.md:976). Hai thứ có kích thước cố định được giữ lại: usage tích luỹ, và id/toolName gắn vào toolcall_start vì bỏ partial thì chúng không còn chỗ nào khác.

Ví dụ minh hoạ: pi --mode json "Liệt kê file" (rút gọn)
{"type":"session","version":3,"id":"0199…","timestamp":"2026-10-22T05:00:00.000Z","cwd":"/work/app"}
{"type":"agent_start"}
{"type":"turn_start"}
{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"Liệt kê file"}],…}}
{"type":"message_end","message":{"role":"user",…}}
{"type":"message_start","message":{"role":"assistant","content":[],"stopReason":"pending",…}}
{"type":"message_update","usage":{…},"assistantMessageEvent":{"type":"toolcall_start","contentIndex":0,"id":"call_1","toolName":"bash"}}
{"type":"message_update","usage":{…},"assistantMessageEvent":{"type":"toolcall_delta","contentIndex":0,"delta":"{\"command\":\"ls\"}"}}
…
{"type":"agent_end","messages":[…],"willRetry":false}
{"type":"agent_settled","aborted":false}
Dòng đầu là header của session (version 3 là CURRENT_SESSION_VERSION, session-manager.ts:41); RPC không có dòng này.

Stdout chỉ dành cho giao thức#

Một dòng console.log lạc từ extension hay thư viện nào đó sẽ làm hỏng luồng JSONL. Vì vậy, ngay khi chọn chế độ không tương tác, main() gọi takeOverStdout():

takeOverStdout(): mọi lần ghi stdout thông thường bị chuyển sang stderr
pi/packages/coding-agent/src/core/output-guard.tsdòng 45–70TypeScript
export function takeOverStdout(): void {	if (stdoutTakeoverState) {		return;	}	const rawStdoutWrite = process.stdout.write.bind(process.stdout) as StdoutTakeoverState["rawStdoutWrite"];	const rawStderrWrite = process.stderr.write.bind(process.stderr) as StdoutTakeoverState["rawStderrWrite"];	const originalStdoutWrite = process.stdout.write;	process.stdout.write = ((		chunk: string | Uint8Array,		encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void),		callback?: (error?: Error | null) => void,	): boolean => {		if (typeof encodingOrCallback === "function") {			return rawStderrWrite(String(chunk), encodingOrCallback);		}		return rawStderrWrite(String(chunk), callback);	}) as typeof process.stdout.write;	stdoutTakeoverState = {		rawStdoutWrite,		rawStderrWrite,		originalStdoutWrite,	};}

Sau lời gọi này, process.stdout.write thực chất ghi vào stderr. Chỉ còn một đường tới stdout thật, và đường đó tuần tự hoá mọi lần ghi:

writeRawStdout() xếp hàng các lần ghi; waitForRawStdoutBackpressure() chờ hàng đợi rỗng
pi/packages/coding-agent/src/core/output-guard.tsdòng 85–103TypeScript
export function writeRawStdout(text: string): void {	if (text.length === 0) {		return;	}	rawStdoutWriteTail = rawStdoutWriteTail.then(() => writeRawStdoutChunk(text));	void rawStdoutWriteTail.catch(() => {		process.exit(1);	});}export async function waitForRawStdoutBackpressure(): Promise<void> {	while (true) {		const tail = rawStdoutWriteTail;		await tail;		if (tail === rawStdoutWriteTail) {			return;		}	}}

Mỗi bản ghi được nối vào một chuỗi promise (dòng 89), nên hai bản ghi không bao giờ xen vào nhau; lỗi tạm thời như EAGAIN hay ENOBUFS được thử lại sau 10 ms, còn lỗi thật làm tiến trình thoát (output-guard.ts:20–43). Đó là một sửa lỗi ở 0.76.0 (CHANGELOG.md:2023).

RPC mode: giao thức JSONL hai chiều#

RPC mode biến Pi thành một tiến trình con sống lâu: client ghi lệnh vào stdin, Pi ghi phản hồi và sự kiện ra stdout. Giao thức có bốn họ bản ghi (rpc.md:24–31):

HướngBản ghiKiểu trong rpc-types.ts
stdinLệnhRpcCommand
stdoutPhản hồi { type: "response" }RpcResponse
stdoutSự kiện sessionJsonAgentSessionEvent (cùng hình dạng với JSON mode), cộng extension_error
cả haiExtension UIRpcExtensionUIRequest (stdout), RpcExtensionUIResponse (stdin)

Data model của giao thức#

Phản hồi là một hợp lớn, mỗi lệnh một nhánh thành công có kiểu data riêng, cộng một nhánh lỗi chung cho mọi lệnh:

RpcResponse: đầu hợp và nhánh lỗi chung
pi/packages/coding-agent/src/modes/rpc/rpc-types.tsdòng 116–135, 236–245TypeScript
export type RpcResponse =	// Prompting	| { id?: string; type: "response"; command: "prompt"; success: true; data: { disposition: PromptDisposition } }	| { id?: string; type: "response"; command: "steer"; success: true; data: { disposition: QueuedInputDisposition } }	| {			id?: string;			type: "response";			command: "follow_up";			success: true;			data: { disposition: QueuedInputDisposition };	  }	| { id?: string; type: "response"; command: "abort"; success: true }	| {			id?: string;			type: "response";			command: "clear_queue";			success: true;			data: { steering: string[]; followUp: string[] };	  }	| { id?: string; type: "response"; command: "new_session"; success: true; data: { cancelled: boolean } }	| {			id?: string;			type: "response";			command: "get_commands";			success: true;			data: { commands: RpcSlashCommand[] };	  }	// Error response (any command can fail)	| { id?: string; type: "response"; command: string; success: false; error: string };

disposition trong phản hồi của prompt cho biết prompt đã đi đâu: "started" (bắt đầu một lần chạy), "queued" (đang chạy nên được xếp hàng làm steer hoặc follow-up), hay "handled" (một lệnh extension hoặc input handler đã tiêu thụ nó, không có lần chạy nào) (agent-session.ts:311–312, rpc-commands.md:40). Trạng thái mà get_state trả về là một ảnh chụp phẳng của session:

RpcSessionState
pi/packages/coding-agent/src/modes/rpc/rpc-types.tsdòng 96–109TypeScript
export interface RpcSessionState {	model?: Model<any>;	thinkingLevel: ThinkingLevel;	isStreaming: boolean;	isCompacting: boolean;	steeringMode: "all" | "one-at-a-time";	followUpMode: "all" | "one-at-a-time";	sessionFile?: string;	sessionId: string;	sessionName?: string;	autoCompactionEnabled: boolean;	messageCount: number;	pendingMessageCount: number;}

Khung bản ghi: JSONL nghiêm ngặt#

jsonl.ts: chỉ tách bản ghi ở LF
pi/packages/coding-agent/src/modes/rpc/jsonl.tsdòng 4–41TypeScript
/** * Serialize a single strict JSONL record. * * Framing is LF-only. Payload strings may contain other Unicode separators such as * U+2028 and U+2029. Clients must split records on `\n` only. */export function serializeJsonLine(value: unknown): string {	return `${JSON.stringify(value)}\n`;}/** * Attach an LF-only JSONL reader to a stream. * * This intentionally does not use Node readline. Readline splits on additional * Unicode separators that are valid inside JSON strings and therefore does not * implement strict JSONL framing. */export function attachJsonlLineReader(stream: Readable, onLine: (line: string) => void): () => void {	const decoder = new StringDecoder("utf8");	let buffer = "";	const emitLine = (line: string) => {		onLine(line.endsWith("\r") ? line.slice(0, -1) : line);	};	const onData = (chunk: string | Buffer) => {		buffer += typeof chunk === "string" ? chunk : decoder.write(chunk);		while (true) {			const newlineIndex = buffer.indexOf("\n");			if (newlineIndex === -1) {				return;			}			emitLine(buffer.slice(0, newlineIndex));			buffer = buffer.slice(newlineIndex + 1);		}	};

Chi tiết này nghe vụn vặt nhưng từng làm hỏng giao thức. JSON.stringify không thoát hai ký tự U+2028 (line separator) và U+2029 (paragraph separator), nên chúng có thể nằm nguyên trong một chuỗi JSON hợp lệ. readline của Node lại coi chúng là ký tự xuống dòng và cắt bản ghi làm đôi. Từ 0.57.0, cả hai phía dùng bộ đọc tự viết chỉ tách ở \n (CHANGELOG.md:3373), và tài liệu cấm client dùng readline (rpc.md:52–54).

Phía server: runRpcMode()#

runRpcMode() chiếm stdout, định nghĩa hàm output() ghi một bản ghi JSONL qua writeRawStdout, cùng hai hàm dựng phản hồi success() và error() (rpc-mode.ts:54–77). Việc gắn session giống print mode, nhưng có UI thật và một kênh báo lỗi extension:

rebindSession() của RPC: UI qua giao thức, mọi sự kiện ra stdout, áp lực ngược
pi/packages/coding-agent/src/modes/rpc/rpc-mode.tsdòng 317–322, 345–364TypeScript
const rebindSession = async (): Promise<void> => {	session = runtimeHost.session;	await session.bindExtensions({		uiContext: createExtensionUIContext(),		mode: "rpc",		commandContextActions: {		shutdownHandler: () => {			shutdownRequested = true;		},		onError: (err) => {			output({ type: "extension_error", extensionPath: err.extensionPath, event: err.event, error: err.error });		},	});	unsubscribe?.();	unsubscribeBackpressure?.();	unsubscribe = session.subscribe((event) => {		output(toJsonEvent(event));		if (event.type === "agent_settled") {			void checkShutdownRequested();		}	});	unsubscribeBackpressure = session.agent.subscribe(async () => {		await waitForRawStdoutBackpressure();	});};

Mọi sự kiện session đều được ghi ra (dòng 355–356), và agent_settled còn là điểm kiểm tra yêu cầu tắt từ extension. Bộ xử lý lệnh là một switch lớn; phần thú vị nhất là bốn lệnh đầu:

prompt trả phản hồi sau preflight; steer, follow_up, abort chờ kết quả
pi/packages/coding-agent/src/modes/rpc/rpc-mode.tsdòng 394–429TypeScript
case "prompt": {	// Start prompt handling immediately, but emit the authoritative response only after	// prompt preflight succeeds. Queued and immediately handled prompts also count as success.	let preflightSucceeded = false;	void session		.prompt(command.message, {			images: command.images,			streamingBehavior: command.streamingBehavior,			source: "rpc",			preflightResult: (disposition) => {				preflightSucceeded = true;				output(success(id, "prompt", { disposition }));			},		})		.catch((e) => {			if (!preflightSucceeded) {				output(error(id, "prompt", e.message));			}		});	return undefined;}case "steer": {	const disposition = await session.steer(command.message, command.images, { source: "rpc" });	return success(id, "steer", { disposition });}case "follow_up": {	const disposition = await session.followUp(command.message, command.images, { source: "rpc" });	return success(id, "follow_up", { disposition });}case "abort": {	await session.abort();	return success(id, "abort");}

prompt là lệnh duy nhất không chờ. Một prompt được chấp nhận có thể chạy nhiều phút, và client cần biết sớm rằng nó đã được nhận. Vì vậy session.prompt() được khởi động mà không await, và phản hồi được ghi đúng một lần qua callback preflightResult: khi lệnh extension đã xử lý, khi prompt được xếp hàng, hoặc ngay trước khi lần chạy bắt đầu. Nếu prompt bị từ chối trước bước đó (không có mô hình, thiếu API key, đang compaction), .catch ghi một phản hồi lỗi; lỗi xảy ra sau khi đã chấp nhận thì chỉ xuất hiện trong luồng sự kiện. Hành vi “một phản hồi có thẩm quyền sau preflight” có từ 0.67.3 (CHANGELOG.md:2740), còn disposition có từ 0.99.0 (CHANGELOG.md:450). abort thì ngược lại: nó chờ session rảnh hẳn rồi mới trả lời.

Mỗi dòng stdin đi qua handleInputLine():

Ba nhánh. JSON hỏng nhận một phản hồi lỗi với command: "parse" và không có id. Một extension_ui_response được chuyển cho hộp thoại đang chờ có cùng id và không sinh phản hồi nào. Mọi thứ còn lại là lệnh. Điểm quan trọng nằm ở chỗ gắn bộ đọc: mỗi dòng được xử lý bằng void handleInputLine(line), không chờ dòng trước xong (rpc-mode.ts:807–815). Nhờ vậy abort hay steer được xử lý ngay cả khi một lệnh chậm như compact đang chạy, và cũng vì vậy client phải ghép phản hồi theo id, không theo thứ tự (rpc.md:44). Khi stdin đóng, shutdown() dispose runtime, xả stdout và thoát; hàm runRpcMode() trả về một promise không bao giờ resolve để giữ tiến trình sống (rpc-mode.ts:726–743, :817–818).

Extension UI đi qua RPC#

Vấn đề. Một extension như permission-gate gọi await ctx.ui.select("Allow?", ["Yes", "No"]) ngay giữa hook tool_call, tức giữa vòng lặp agent. Trong TUI, đó là một hộp thoại. Trong RPC không có màn hình nào; người duy nhất trả lời được là client ở đầu kia của pipe. Giải pháp là một giao thức con: Pi ghi một extension_ui_request có id ngẫu nhiên, giữ promise của hộp thoại trong một bảng chờ, và resolve nó khi client gửi extension_ui_response cùng id.

Yêu cầu UI (stdout) và phản hồi UI (stdin)
pi/packages/coding-agent/src/modes/rpc/rpc-types.tsdòng 252–263, 286–297TypeScript
export type RpcExtensionUIRequest =	| { type: "extension_ui_request"; id: string; method: "select"; title: string; options: string[]; timeout?: number }	| { type: "extension_ui_request"; id: string; method: "confirm"; title: string; message: string; timeout?: number }	| {			type: "extension_ui_request";			id: string;			method: "input";			title: string;			placeholder?: string;			timeout?: number;	  }	| { type: "extension_ui_request"; id: string; method: "editor"; title: string; prefill?: string }	| { type: "extension_ui_request"; id: string; method: "setTitle"; title: string }	| { type: "extension_ui_request"; id: string; method: "set_editor_text"; text: string };// ============================================================================// Extension UI Commands (stdin)// ============================================================================/** Response to an extension UI request */export type RpcExtensionUIResponse =	| { type: "extension_ui_response"; id: string; value: string }	| { type: "extension_ui_response"; id: string; confirmed: boolean }	| { type: "extension_ui_response"; id: string; cancelled: true };

Hộp thoại có ba cách kết thúc: client trả lời, timeout hết hạn, hoặc AbortSignal của extension bị huỷ. Hai cách sau resolve bằng giá trị mặc định, nên client không cần tự đếm giờ (rpc-extension-ui.md:10). Các phương thức của ExtensionUIContext chia làm hai loại:

select, confirm, input là hộp thoại; notify chỉ ghi ra rồi thôi
pi/packages/coding-agent/src/modes/rpc/rpc-mode.tsdòng 136–161TypeScript
const createExtensionUIContext = (): ExtensionUIContext => ({	select: (title, options, opts) =>		createDialogPromise(opts, undefined, { method: "select", title, options, timeout: opts?.timeout }, (r) =>			"cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined,		),	confirm: (title, message, opts) =>		createDialogPromise(opts, false, { method: "confirm", title, message, timeout: opts?.timeout }, (r) =>			"cancelled" in r && r.cancelled ? false : "confirmed" in r ? r.confirmed : false,		),	input: (title, placeholder, opts) =>		createDialogPromise(opts, undefined, { method: "input", title, placeholder, timeout: opts?.timeout }, (r) =>			"cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined,		),	notify(message: string, type?: "info" | "warning" | "error"): void {		// Fire and forget - no response needed		output({			type: "extension_ui_request",			id: crypto.randomUUID(),			method: "notify",			message,			notifyType: type,		} as RpcExtensionUIRequest);	},
  • Hộp thoại (select, confirm, input, editor): chờ phản hồi. Riêng editor() tự dựng promise mà không qua createDialogPromise, nên không có timeout hay abort (rpc-mode.ts:254–271); kiểu yêu cầu của nó cũng không có trường timeout.
  • Ghi rồi thôi (notify, setStatus, setWidget, setTitle, setEditorText): ghi một bản ghi và không chờ gì. setWidget chỉ nhận mảng chuỗi; widget dạng component bị bỏ qua (rpc-mode.ts:195–208).
  • Không hỗ trợ: custom() trả undefined, getEditorText() trả "" vì một hàm đồng bộ không thể chờ RPC, footer, header, chỉ báo “working”, đổi theme đều là no-op (rpc-extension-ui.md:12–23). Extension cần UI thật của terminal nên kiểm tra ctx.mode === "tui".

RpcClient: phía client#

Với client TypeScript, gói xuất sẵn RpcClient: nó khởi động node <cliPath> --mode rpc với các cờ tuỳ chọn (rpc-client.ts:81–98), rồi bọc mỗi lệnh thành một phương thức có kiểu. Mọi phương thức đi qua send():

send(): gắn id req_N, chờ phản hồi cùng id, hết hạn sau 30 giây
pi/packages/coding-agent/src/modes/rpc/rpc-client.tsdòng 576–604TypeScript
const id = `req_${++this.requestId}`;const fullCommand = { ...command, id } as RpcCommand;return new Promise((resolve, reject) => {	const timeout = setTimeout(() => {		this.pendingRequests.delete(id);		reject(new Error(`Timeout waiting for response to ${command.type}. Stderr: ${this.stderr}`));	}, 30000);	this.pendingRequests.set(id, {		resolve: (response) => {			clearTimeout(timeout);			resolve(response);		},		reject: (error) => {			clearTimeout(timeout);			reject(error);		},	});	try {		stdin.write(serializeJsonLine(fullCommand));	} catch (error: unknown) {		const writeError = error instanceof Error ? error : new Error(String(error));		const pending = this.pendingRequests.get(id);		this.pendingRequests.delete(id);		pending?.reject(writeError);	}});
handleLine(): phản hồi có id thì resolve, còn lại là sự kiện
pi/packages/coding-agent/src/modes/rpc/rpc-client.tsdòng 523–543TypeScript
private handleLine(line: string): void {	try {		const data = JSON.parse(line);		// Check if it's a response to a pending request		if (data.type === "response" && data.id && this.pendingRequests.has(data.id)) {			const pending = this.pendingRequests.get(data.id)!;			this.pendingRequests.delete(data.id);			pending.resolve(data as RpcResponse);			return;		}		// Otherwise it's an event. Iterate a snapshot so listeners that unsubscribe during dispatch		// do not cause later listeners to miss this event.		for (const listener of [...this.eventListeners]) {			listener(data as JsonAgentSessionEvent);		}	} catch {		// Ignore non-JSON lines	}}

Bản ghi nào không phải phản hồi của một yêu cầu đang chờ đều được coi là sự kiện, kể cả extension_ui_request. Nhưng RpcClient ở v1.4.0 không có phương thức nào để ghi extension_ui_response: send() là private và luôn gắn id rồi chờ một phản hồi. Vì vậy ví dụ chính thức về extension UI không dùng RpcClient mà tự spawn tiến trình, tự đọc stdout và tự ghi phản hồi hộp thoại vào stdin (examples/rpc-extension-ui.ts:259–263, :540). Hai tiện ích cuối của RpcClient giải quyết một cuộc đua (race) dễ mắc:

waitForIdle() chờ agent_settled; promptAndWait() đăng ký listener trước khi gửi
pi/packages/coding-agent/src/modes/rpc/rpc-client.tsdòng 471–486, 513–517TypeScript
waitForIdle(timeout = 60000): Promise<void> {	return new Promise((resolve, reject) => {		const timer = setTimeout(() => {			unsubscribe();			reject(new Error(`Timeout waiting for agent to become idle. Stderr: ${this.stderr}`));		}, timeout);		const unsubscribe = this.onEvent((event) => {			if (event.type === "agent_settled") {				clearTimeout(timer);				unsubscribe();				resolve();			}		});	});}async promptAndWait(message: string, images?: ImageContent[], timeout = 60000): Promise<JsonAgentSessionEvent[]> {	const eventsPromise = this.collectEvents(timeout);	await this.prompt(message, images);	return eventsPromise;}

Một prompt ngắn có thể chạy xong, và agent_settled có thể tới, trước khi lời gọi prompt() kịp trả về. Nếu client chỉ đăng ký listener sau đó, nó chờ mãi. promptAndWait() tạo promise thu sự kiện trước rồi mới gửi lệnh (rpc.md:71). Ví dụ chính thức dùng đúng mẫu đó:

examples/rpc-client.ts: một prompt qua tiến trình con RPC
pi/packages/coding-agent/examples/rpc-client.tsdòng 15–35TypeScript
const client = new RpcClient({	cliPath: join(exampleDirectory, "../dist/cli.js"),	args: ["--no-session"],});const unsubscribe = client.onEvent((event) => {	if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {		process.stdout.write(event.assistantMessageEvent.delta);	} else if (event.type === "tool_execution_start") {		process.stderr.write(`\n[tool: ${event.toolName}]\n`);	}});try {	await client.start();	await client.promptAndWait(prompt);	process.stdout.write("\n");} finally {	unsubscribe();	await client.stop();}

SDK: nhúng Pi trong tiến trình#

SDK không phải một chế độ CLI. Nó là chính gói @earendil-works/pi-coding-agent được import như thư viện, cho chương trình Node.js hay Bun truy cập thẳng agent, session, tool và mô hình (cli-integration.md:7). Hàm trung tâm là createAgentSession(), đúng hàm mà factory của CLI gọi qua createAgentSessionFromServices(). Ví dụ tối giản trong repo:

examples/sdk/01-minimal.ts
pi/packages/coding-agent/examples/sdk/01-minimal.tsdòng 8–26TypeScript
import { createAgentSession } from "@earendil-works/pi-coding-agent";const { session } = await createAgentSession();try {	session.subscribe((event) => {		if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {			process.stdout.write(event.assistantMessageEvent.delta);		}	});	await session.prompt("What files are in the current directory?");	session.state.messages.forEach((msg) => {		console.log(msg);	});	console.log();} finally {	session.dispose();}

Không truyền gì, hàm dùng thư mục hiện tại, settings đã lưu, credential đã cấu hình và resource được phát hiện. Mỗi ranh giới đều thay được qua options:

CreateAgentSessionOptions (đầu và cuối)
pi/packages/coding-agent/src/core/sdk.tsdòng 48–62, 93–106TypeScript
export interface CreateAgentSessionOptions {	/** Working directory for project-local discovery. Default: process.cwd() */	cwd?: string;	/** Global config directory. Default: ~/.pi/agent */	agentDir?: string;	/** Canonical model/auth runtime. Defaults to a runtime using agentDir/auth.json and models.json. */	modelRuntime?: ModelRuntime;	/** Model to use. Default: from settings, else first available */	model?: Model<any>;	/** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */	thinkingLevel?: ThinkingLevel;	/** Models available for cycling (Ctrl+P in interactive mode) */	scopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;	/** Custom tools to register (in addition to built-in tools). */	customTools?: ToolDefinition[];	/** Resource loader. When omitted, DefaultResourceLoader is used. */	resourceLoader?: ResourceLoader;	/** Session manager. Default: SessionManager.create(cwd) */	sessionManager?: SessionManager;	/** Settings manager. Default: SettingsManager.create(cwd, agentDir) */	settingsManager?: SettingsManager;	/** Session start event metadata for extension runtime startup. */	sessionStartEvent?: SessionStartEvent;}

Bên trong, hàm dựng ModelRuntime, SettingsManager, SessionManager, DefaultResourceLoader, rồi một Agent và một AgentSession bọc nó (sdk.ts:191–207, :453–470); cách Agent được cấu hình có ở chương pi-agent-core, còn AgentSession ở chương AgentSession. Điều quan trọng với người nhúng là những gì SDK không làm so với main():

  • Không chiếm stdout và không chọn mode: host tự quyết cách hiển thị.
  • Không nạp các extension dựng sẵn của CLI (codemode, tool_search, MCP); host phải tự thêm vào extensionFactories của DefaultResourceLoader (sdk.md:116).
  • Không gọi bindExtensions(), nên extension chưa nhận session_start và chạy với mode "print", không UI, cho tới khi host tự gắn. MCP chỉ kết nối server ở session_start, nên quên bước này nghĩa là không có tool MCP.

Host nào cần /new, resume hay fork thì dựng AgentSessionRuntime với một factory, đúng như main(), và gắn lại listener sau mỗi lần thay session:

examples/sdk/13-session-runtime.ts: factory, runtime, rebind sau mỗi lần thay
pi/packages/coding-agent/examples/sdk/13-session-runtime.tsdòng 20–50TypeScript
const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {	const services = await createAgentSessionServices({ cwd });	return {		...(await createAgentSessionFromServices({			services,			sessionManager,			sessionStartEvent,		})),		services,		diagnostics: services.diagnostics,	};};const runtime = await createAgentSessionRuntime(createRuntime, {	cwd: process.cwd(),	agentDir: getAgentDir(),	sessionManager: SessionManager.create(process.cwd()),});let unsubscribe: (() => void) | undefined;async function bindSession() {	unsubscribe?.();	const session = runtime.session;	await session.bindExtensions({});	unsubscribe = session.subscribe((event) => {		if (event.type === "queue_update") {			console.log("Queued:", event.steering.length + event.followUp.length);		}	});	return session;}

Sau khi runtime thay session, mọi subscription cũ thuộc về một AgentSession đã dispose (sdk.md:60), đúng vấn đề mà setRebindSession() giải quyết cho các mode CLI. Gói còn xuất chính các mode (InteractiveMode, runPrintMode, runRpcMode, index.ts:417–434) và cả main() với MainOptions.extensionFactories. pi nạp extension dựng sẵn của chính nó qua đúng đường đó (main.ts:570–578), nên một host có thể phát hành CLI riêng kèm extension inline mà không phải sửa Pi.

Chọn SDK hay RPC? Tài liệu tóm tắt: SDK chạy trong cùng tiến trình và có toàn bộ API TypeScript; RPC chạy trong tiến trình con, điều khiển bằng JSONL, hợp với ngôn ngữ khác, IDE, hay khi muốn cô lập tiến trình (rpc.md:7–10).

Interactive mode ở mức cấu trúc#

interactive-mode.ts dài hơn 7.000 dòng, nhưng phần lớn là giao diện: bộ chọn, hộp thoại, phím tắt, cách vẽ từng loại message (chương pi-tui). Ở mức cấu trúc, nó là một mode như mọi mode khác: nhận runtime, gắn extension, đăng ký sự kiện, và đẩy đầu vào của người dùng vào session.prompt().

  1. constructorLưu runtime, đăng ký setRebindSession và setBeforeSessionInvalidate, dựng TUI và cây component (:610).
  2. init()Gắn component vào TUI, ui.start() trước extension để session_start dùng được hộp thoại, cài phím, rồi rebindCurrentSession() (:999–1105).
  3. bindbindExtensions({ uiContext, mode: "tui", ... }) và subscribeToAgent() (:2092–2118).
  4. run()Kiểm tra phiên bản, cập nhật gói, danh mục mô hình chạy nền; gửi prompt khởi đầu; rồi vòng lặp chính.
run(): prompt khởi đầu, rồi vòng lặp đọc đầu vào mãi mãi
pi/packages/coding-agent/src/modes/interactive/interactive-mode.tsdòng 1232–1262TypeScript
// Process initial messagesif (initialMessage) {	try {		await this.session.prompt(initialMessage, { images: initialImages });	} catch (error: unknown) {		const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";		this.showError(errorMessage);	}}if (initialMessages) {	for (const message of initialMessages) {		try {			await this.session.prompt(message);		} catch (error: unknown) {			const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";			this.showError(errorMessage);		}	}}// Main interactive loopwhile (true) {	const userInput = await this.getUserInput();	try {		await this.session.prompt(userInput);	} catch (error: unknown) {		const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";		this.showError(errorMessage);	}}

Trong lúc một lần chạy diễn ra, vòng lặp chính đang đứng ở await this.session.prompt(...). Nếu người dùng nhấn Enter lúc đó, bộ xử lý submit của editor không đi qua vòng lặp này mà gửi thẳng một prompt có streamingBehavior: "steer"; tổ hợp phím follow-up (mặc định Alt+Enter) gửi "followUp" (:4460–4468):

Enter khi đang chạy là steer; khi rảnh thì trả cho vòng lặp chính
pi/packages/coding-agent/src/modes/interactive/interactive-mode.tsdòng 3333–3354, 3357–3361TypeScript
		// If streaming, use prompt() with steer behavior		// This handles extension commands (execute immediately), prompt template expansion, and queueing		if (this.session.isStreaming) {			this.editor.addToHistory?.(text);			this.editor.setText("");			await this.session.prompt(text, { streamingBehavior: "steer" });			this.updatePendingMessagesDisplay();			this.ui.requestRender();			return;		}		// Normal message submission		// First, move any pending bash components to chat		this.flushPendingBashComponents();		if (this.onInputCallback) {			this.onInputCallback(text);		} else {			this.pendingUserInputs.push(text);		}		this.editor.addToHistory?.(text);	};private subscribeToAgent(): void {	this.unsubscribe = this.session.subscribe(async (event) => {		await this.handleEvent(event);	});}

Trước nhánh đó, bộ xử lý submit đã tự xử lý các lệnh dựng sẵn như /settings, /model, /resume, /quit và lệnh bash !/!! (:3156–3319). Mọi thứ còn lại, kể cả lệnh của extension, đi vào session.prompt(), cùng hàm mà RPC và print gọi. Chiều ngược lại, handleEvent() biến mỗi AgentSessionEvent thành một thay đổi trên cây component rồi xin vẽ lại:

message_start của assistant tạo một AssistantMessageComponent đang stream
pi/packages/coding-agent/src/modes/interactive/interactive-mode.tsdòng 3457–3479TypeScript
case "message_start":	if (event.message.role === "custom") {		this.addMessageToChat(event.message);		this.ui.requestRender();	} else if (event.message.role === "user") {		this.addMessageToChat(event.message);		this.updatePendingMessagesDisplay();		this.ui.requestRender();	} else if (event.message.role === "assistant") {		this.streamingComponent = new AssistantMessageComponent(			undefined,			this.hideThinkingBlock,			this.getMarkdownThemeWithSettings(),			this.hiddenThinkingLabel,			this.outputPad,			this.getMarkdownTransformers(),		);		this.streamingMessage = event.message;		this.chatContainer.addChild(this.streamingComponent);		this.streamingComponent.updateContent(this.streamingMessage, true);		this.ui.requestRender();	}	break;
Sự kiệnInteractive mode làm gì
message_start (user, custom, assistant)Thêm message vào khung chat; với assistant, tạo component đang stream
message_updateCập nhật component; mỗi khối toolCall mới sinh một ToolExecutionComponent ngay khi đối số còn đang stream (:3486–3511)
message_endChốt message; nếu lỗi hoặc bị huỷ, đánh dấu lỗi cho các tool đang chờ
tool_execution_*Cập nhật đúng component theo toolCallId; lời gọi lồng nhau (parentToolCallId) hiện bên trong dòng của tool cha (:3563–3608)
queue_updateVẽ lại danh sách tin steer/follow-up đang chờ
compaction_*, auto_retry_*Chỉ báo trạng thái
agent_settledKiểm tra yêu cầu tắt từ extension

Trace: một phiên RPC có hộp thoại của extension#

Kịch bản: một client khởi động pi --mode rpc --no-session -e ./permission-gate.ts, gửi một prompt khiến mô hình gọi bash với rm -rf build. Extension permission-gate chặn lại hỏi ý kiến; client đồng ý. Mũi tên → là dòng client ghi vào stdin, ← là dòng Pi ghi ra stdout.

Ví dụ minh hoạ: trao đổi JSONL (các trường dài được rút gọn bằng …)
→ {"id":"req-1","type":"prompt","message":"Xoá thư mục build rồi báo lại"}
← {"id":"req-1","type":"response","command":"prompt","success":true,"data":{"disposition":"started"}}
← {"type":"agent_start"}
← {"type":"turn_start"}
← {"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"Xoá thư mục build rồi báo lại"}],…}}
← {"type":"message_end","message":{"role":"user",…}}
← {"type":"message_start","message":{"role":"assistant","content":[],"stopReason":"pending",…}}
← {"type":"message_update","usage":{…},"assistantMessageEvent":{"type":"toolcall_start","contentIndex":0,"id":"call_1","toolName":"bash"}}
← {"type":"message_update","usage":{…},"assistantMessageEvent":{"type":"toolcall_delta","contentIndex":0,"delta":"{\"command\":\"rm -rf build\"}"}}
← {"type":"message_update","usage":{…},"assistantMessageEvent":{"type":"toolcall_end","contentIndex":0,"toolCall":{…}}}
← {"type":"message_end","message":{"role":"assistant","stopReason":"toolUse",…}}
← {"type":"tool_execution_start","toolCallId":"call_1","toolName":"bash","args":{"command":"rm -rf build"}}
← {"type":"extension_ui_request","id":"5f0c…","method":"select","title":"… rm -rf build … Allow?","options":["Yes","No"]}
     vòng lặp agent đứng chờ trong beforeToolCall; các lệnh khác vẫn được xử lý
→ {"type":"extension_ui_response","id":"5f0c…","value":"Yes"}
← {"type":"tool_execution_end","toolCallId":"call_1","toolName":"bash","result":{"content":[{"type":"text","text":"(no output)"}],…},"isError":false,"durationMs":14}
← {"type":"message_start","message":{"role":"toolResult","toolCallId":"call_1",…}}
← {"type":"message_end","message":{"role":"toolResult",…}}
← {"type":"turn_end","message":{…},"toolResults":[{…}]}
← {"type":"turn_start"}
← {"type":"message_start","message":{"role":"assistant",…}}
← {"type":"message_update","usage":{…},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"Đã xoá thư mục build."}}
← {"type":"message_end","message":{"role":"assistant","stopReason":"stop",…}}
← {"type":"turn_end","message":{…},"toolResults":[]}
← {"type":"agent_end","messages":[…],"willRetry":false}
← {"type":"agent_settled","aborted":false}
→ {"id":"req-2","type":"get_state"}
← {"id":"req-2","type":"response","command":"get_state","success":true,"data":{"isStreaming":false,…}}
Một lần chạy, hai lượt, một hộp thoại. Phản hồi của req-1 tới trước agent_start; agent_settled mới là tín hiệu xong.

Đi lại từng chặng, kèm chỗ code chịu trách nhiệm:

  1. handleInputLine() parse dòng đầu và gọi handleCommand(). Nhánh prompt khởi động session.prompt() mà không await. AgentSession kiểm tra đã có mô hình và API key, chuẩn bị lần chạy, rồi gọi preflightResult("started") ngay trước agent.prompt() (agent-session.ts:1873–1899). Vì mọi bản ghi đi qua cùng một hàng đợi writeRawStdout, phản hồi luôn đứng trước agent_start.
  2. Các sự kiện từ agent_start tới message_end của assistant là chuỗi sự kiện của vòng lặp agent (trace ở chương pi-agent-core), đi qua toJsonEvent() nên message_update chỉ còn delta, và toolcall_start mang id, toolName.
  3. tool_execution_start được phát trước khi chuẩn bị lời gọi; ngay sau đó vòng lặp gọi beforeToolCall, tức hook tool_call của extension. permission-gate thấy hasUI là true và gọi ctx.ui.select(), mà trong RPC là createDialogPromise(): một extension_ui_request với UUID mới được ghi ra, và promise nằm trong bảng chờ. Không có timeout vì extension không truyền.
  4. Client ghi extension_ui_response. Vì mỗi dòng stdin được xử lý độc lập, dòng này không phải chờ req-1. handleInputLine() tìm thấy id trong bảng chờ và resolve promise với "Yes". Hook trả về undefined, bash chạy, kết quả đi qua afterToolCall rồi ra thành tool_execution_end và message toolResult.
  5. Lượt 2 trả lời bằng văn bản, không gọi tool; vòng lặp kết thúc với agent_end. AgentSession kiểm tra xem có cần thử lại, compaction hay tin chờ không; không có gì nên nó phát agent_settled (agent-session.ts:1899–1917).

Hai biến thể đáng thử trong đầu. Nếu client trả "No", permission-gate trả { block: true, reason: "Blocked by user" }; vòng lặp tạo một kết quả lỗi mang đúng lý do đó, nên tool_execution_end có isError: true và mô hình đọc được lý do ở lượt 2. Nếu chạy cùng kịch bản bằng pi -p, hasUI là false và extension chặn ngay với lý do “Dangerous command blocked (no UI for confirmation)”, không có bản ghi UI nào. Còn nếu trong lúc bash chạy client gửi {"id":"req-3","type":"steer","message":"..."}, Pi trả {"disposition":"queued"} và phát queue_update; khi tin đó được chèn sau batch tool, AgentSession gỡ nó khỏi danh sách chờ và phát queue_update mới trước khi chuyển tiếp message_start của tin nhắn user đó (agent-session.ts:1120–1135).

Quyết định thiết kế#

Một runtime, nhiều lớp vỏ#

“All four modes use the same agent, sessions, resources, and tools. The mode determines how input enters Pi, how output is exposed, and whether the process remains available for more commands” (cli-integration.md:5). Lợi ích thấy ngay trong code: compaction, retry, extension, session tree được viết một lần trong AgentSession và chạy ở mọi chế độ; một client RPC có cùng hành vi steer/follow-up như người gõ trong TUI. Cái giá là mọi tính năng UI phải có đường suy biến khi không có màn hình, do đó có noOpUIContext, hasUI và ctx.mode.

Chọn chế độ theo TTY#

Chọn print khi stdin hoặc stdout không phải terminal làm Pi khớp với thói quen Unix: pipe vào, pipe ra, không cần cờ. Đánh đổi là một chút bất ngờ cho người chuyển hướng output mà vẫn tưởng sẽ thấy TUI, và nhu cầu phân biệt rõ --print (chạy một lần) với --mode (định dạng đầu ra) (cli.md:41).

Bảo vệ stdout thay vì tin vào code khác#

Extension là module TypeScript chạy ngay trong tiến trình Pi (how-pi-works.md:43), nên không thể giả định không ai gọi console.log. Thay vì đòi mọi extension tuân thủ, Pi chuyển hướng process.stdout.write sang stderr và giữ cho mình một lối ghi riêng. Thiết kế này đổi một lớp lỗi khó gỡ (luồng JSONL hỏng giữa chừng) lấy một quy ước đơn giản: stdout chỉ có bản ghi giao thức, stderr chứa mọi thứ khác.

Giao thức tuyến tính, có áp lực ngược#

Ba lựa chọn cùng hướng: JSONL chỉ tách ở LF (0.57.0), message_update chỉ mang delta để kích thước tuyến tính (0.84.0), và áp lực ngược từ stdout tới tận vòng lặp agent nhờ listener được await. Kết quả là một client chậm làm Pi chậm lại thay vì làm Pi phình bộ nhớ, và một câu trả lời dài không làm luồng sự kiện phình theo bình phương.

Tách “được nhận”, “xong một lần chạy” và “hết việc”#

Một client cần ba tín hiệu khác nhau, và giao thức tách chúng ra. Phản hồi của prompt nói prompt đã được nhận và đi đâu (disposition). agent_end nói một lần chạy cấp thấp đã xong. agent_settled nói Pi không còn tự làm gì nữa, vì một lần chạy cấp session có thể gồm nhiều lần chạy cấp thấp: thử lại sau lỗi provider, compaction khi tràn rồi chạy lại, follow-up (agent-session.ts:1899–1909). agent_settled xuất hiện ở 0.80.4 (CHANGELOG.md:1484) và có thêm cờ aborted ở 1.1.0 (CHANGELOG.md:186).

UI của extension là một giao thức con, suy biến có kiểm soát#

Thay vì cấm extension dùng hộp thoại ngoài TUI, RPC chuyển hộp thoại thành cặp yêu cầu và phản hồi có id, có timeout phía Pi, và biến những gì không thể làm qua dây (component tuỳ biến, footer, đọc editor đồng bộ) thành no-op thay vì lỗi. Nhờ vậy cùng một extension chạy được trong TUI, trong IDE dùng RPC và trong script, miễn nó xét hasUI và mode.

Factory theo cwd thay vì một session cố định#

Vì services phụ thuộc cwd, Pi không dựng chúng một lần lúc khởi động mà giữ một factory để dựng lại khi session đổi thư mục. Chú thích của AgentSessionRuntime nói rõ thứ tự: tháo runtime cũ trước, rồi dựng và áp dụng runtime mới; nếu dựng thất bại, lỗi được ném lên cho caller (agent-session-runtime.ts:67–73).

Cạm bẫy và trường hợp biên#

  • --mode text trong terminal vẫn mở TUI. Muốn chạy một lần, dùng -p.
  • Pipe vào stdin âm thầm chuyển sang print. echo "..." | pi không mở TUI (main.ts:898–900).
  • RPC từ chối @file. Gửi ảnh qua trường images của lệnh prompt.
  • JSON mode không đặt mã thoát theo kết quả mô hình. Một câu trả lời lỗi vẫn cho mã 0; phải xem stopReason trong message_end của assistant.
  • Đăng ký listener trước khi gửi prompt, nếu không một lần chạy ngắn có thể xong trước khi bạn kịp nghe.
  • Ghép phản hồi theo id. Lệnh được xử lý đồng thời, nên thứ tự phản hồi không theo thứ tự lệnh; lỗi parse không có id.
  • Ngừng đọc stdout là làm Pi đứng. Áp lực ngược đi tới tận vòng lặp agent.
  • Đừng dùng readline cho luồng của Pi; nó tách sai ở U+2028/U+2029.
  • Hộp thoại không có UI trả về ngay giá trị mặc định. Trong print, JSON và SDK chưa gắn UI, confirm() trả false và select() trả undefined ngay lập tức. Extension không xét hasUI có thể đi nhầm nhánh mà không ai thấy.
  • Listener của session.subscribe() không phải rào chắn. Chúng được gọi mà không await; muốn chặn vòng lặp cho tới khi việc async xong (như áp lực ngược của các mode), phải đăng ký trên session.agent, và khi đó chỉ nhận AgentEvent.
  • SDK sau khi thay session: listener cũ thuộc về session đã dispose; phải đăng ký lại và gọi lại bindExtensions().
  • Chú thích JSDoc của createAgentSession() còn ví dụ cũ createAgentSession({ continueSession: true }) (sdk.ts:171–174), nhưng CreateAgentSessionOptions ở v1.4.0 không có trường đó. Cách đúng là truyền sessionManager: SessionManager.continueRecent(cwd), như main() làm cho -c (main.ts:432–434).
  • pi -c -p không kèm prompt không gửi gì cho mô hình; theo code, print mode vẫn đọc message cuối của transcript đã khôi phục, nên nếu message cuối là của assistant, nó in lại câu trả lời cuối của session gần nhất (print-mode.ts:139–143).
  • RpcClient không trả lời được hộp thoại của extension. Nếu extension của bạn gọi ctx.ui.select() hay confirm() không kèm timeout hay signal, một client chỉ dùng RpcClient sẽ để vòng lặp agent chờ mãi; hãy tự quản lý tiến trình như examples/rpc-extension-ui.ts, hoặc đặt timeout cho hộp thoại.
  • RpcClient mặc định tìm dist/cli.js tương đối với thư mục hiện tại của client (rpc-client.ts:81); truyền cliPath rõ ràng như ví dụ chính thức.

Đọc code ở đâu#

Thứ tựFileĐọc để hiểu
1how-pi-works.md, cli-integration.mdCác chế độ là gì và chọn chế độ nào.
2src/cli.ts, src/main.ts:574Đường khởi động: lệnh con, resolveAppMode(), session, factory, rẽ nhánh.
3src/cli/args.ts, docs/cli.mdMọi tuỳ chọn dòng lệnh và cách chúng được phân tích.
4agent-session-runtime.ts, agent-session-services.tsServices theo cwd, factory, thay session và rebind.
5src/core/sdk.ts:191, docs/sdk.mdcreateAgentSession(); sau đó sang chương AgentSession.
6print-mode.ts, json-event.ts, output-guard.tsChế độ một lần, định dạng trên dây, bảo vệ stdout; tham chiếu sự kiện ở docs/json.md.
7rpc-types.ts, jsonl.tsData model và khung bản ghi của giao thức.
8rpc-mode.ts, docs/rpc.md, rpc-commands.md, rpc-extension-ui.mdServer RPC, từng lệnh, giao thức con của extension UI.
9rpc-client.ts, examples/rpc-client.ts, examples/rpc-extension-ui.tsPhía client, kể cả cách trả lời hộp thoại.
10interactive-mode.ts:945init(), run() (1148), submit (3156), handleEvent() (3363); phần vẽ ở chương pi-tui.
11examples/sdk/, permission-gate.tsMười bốn ví dụ SDK được typecheck cùng repo; một extension mẫu xử lý cả có và không có UI.

Tóm tắt#

  • main() dựng một AgentSessionRuntime duy nhất qua một factory theo cwd, rồi giao nó cho một trong ba lớp vỏ: InteractiveMode, runPrintMode() (text và JSON), runRpcMode(). SDK gọi cùng các hàm đó trong tiến trình của host.
  • Chế độ được chọn theo thứ tự: --mode rpc, --mode json, rồi print nếu có -p hoặc stdin/stdout không phải TTY, cuối cùng là interactive.
  • Mỗi mode khác nhau ở bốn điểm: đầu vào, cách xuất sự kiện, ExtensionUIContext đưa cho extension, và vòng đời tiến trình.
  • Các chế độ không tương tác chuyển console.log sang stderr và ghi giao thức qua một hàng đợi riêng, có áp lực ngược tới vòng lặp agent.
  • JSON và RPC dùng JSONL chỉ tách ở LF, và message_update trên dây chỉ mang delta.
  • RPC: lệnh có id tuỳ chọn và được xử lý đồng thời; prompt trả lời một lần sau preflight kèm disposition; agent_settled là tín hiệu hết việc; hộp thoại của extension là cặp extension_ui_request/extension_ui_response.
  • Khi session bị thay (/new, resume, fork, import), runtime tháo session cũ, dựng lại services và gọi callback rebind của mode.

Mục lục

Giao diện