Pi từ mã nguồnpi v1.4.0

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

AgentSession: vòng đời một prompt

Từ prompt() tới file session: mở rộng đầu vào, chạy, ghi sự kiện, thử lại và đổi mô hình.

  • 23 phút đọc
  • 51 đoạn code
  • 18 mục
Trong chương này18 mục
  1. Vai trò
  2. Mô hình tư duy
  3. Data model: cấu hình, sự kiện, message
  4. Dựng một phiên: services, SDK và runtime
  5. Bước 1: prompt() xử lý đầu vào
  6. Bước 2: chuẩn bị và chạy một lần chạy
  7. Bước 3: sự kiện và ghi session
  8. Lần theo một prompt có một lời gọi tool
  9. Tự động thử lại khi provider lỗi
  10. Nơi compaction được kích hoạt
  11. Đổi mô hình và mức suy nghĩ
  12. Steer, follow-up, abort và tin nhắn tuỳ biến
  13. Lệnh ! của người dùng
  14. Các dịch vụ xung quanh: settings, auth, model, trust, đường dẫn
  15. Quyết định thiết kế
  16. Cạm bẫy và trường hợp biên
  17. Đọc code ở đâu
  18. Tóm tắt

Vai trò#

Agent của pi-agent-core chỉ biết chạy vòng lặp agent (agent loop) trên một mảng message nằm trong bộ nhớ: gửi yêu cầu, nhận luồng (stream) phản hồi, chạy tool, lặp lại cho đến khi mô hình thôi gọi tool (xem chương pi-agent-core). Nó không biết tệp session, settings, extension hay lệnh /compact. Một coding agent dùng hằng ngày cần nhiều hơn: mỗi message phải được ghi xuống đĩa ngay khi hoàn tất, prompt phải được mở rộng (prompt template, skill), lỗi provider (nhà cung cấp) tạm thời phải được thử lại, ngữ cảnh gần đầy phải được nén, đổi mô hình (model) phải được ghi lại để lần mở phiên sau khôi phục đúng.

AgentSession trong packages/coding-agent/src/core/agent-session.ts là lớp làm tất cả những việc đó. Chú thích đầu tệp liệt kê đúng phạm vi của nó:

Chú thích đầu tệp agent-session.ts
pi/packages/coding-agent/src/core/agent-session.tsdòng 1–14TypeScript
/** * AgentSession - Core abstraction for agent lifecycle and session management. * * This class is shared between all run modes (interactive, print, rpc). * It encapsulates: * - Agent state access * - Event subscription with automatic session persistence * - Model and thinking level management * - Compaction (manual and auto) * - Bash execution * - Session switching and branching * * Modes use this class and add their own I/O layer on top. */
Lớp này dùng chung cho mọi chế độ chạy; mỗi chế độ chỉ thêm lớp vào/ra của riêng mình.

Ai gọi nó? Mọi chế độ chạy trong chương các chế độ: interactive gọi session.prompt(text) khi người dùng nhấn Enter (vòng lặp chính ở interactive-mode.ts:1253), RPC mode chuyển lệnh JSON thành các lời gọi tương tự, print mode gửi một prompt rồi thoát, còn SDK trả thẳng đối tượng AgentSession cho mã của bạn. Ba cộng sự cốt lõi được giữ dưới dạng trường readonly: agent, sessionManager, settingsManager (agent-session.ts:379).

Ngoài ba trường đó, nó giữ ModelRuntime (tra mô hình và credential), ResourceLoader (extension, skill, prompt template, context file), ExtensionRunner (phát sự kiện cho extension) và sổ đăng ký tool. Vị trí của nó trong cả hệ thống:

Chế độ chạy
interactive, print, JSON, RPC, SDK: đọc đầu vào, vẽ sự kiện ra màn hình (chương các chế độ, pi-tui).
AgentSessionRuntime
Sở hữu phiên hiện tại và các dịch vụ gắn với cwd; thay phiên khi /new, /resume, /fork.
AgentSession
Chương này: mở rộng prompt, ghi session, retry, kích hoạt compaction, đổi mô hình, hàng đợi, lệnh !.
Agent (pi-agent-core)
Vòng lặp agent: lượt, lời gọi tool, hàng đợi steer/follow-up (chương pi-agent-core).
ModelRuntime → pi-ai
Giải quyết credential, gửi yêu cầu tới provider (chương pi-ai).

Bên cạnh trục dọc đó là SessionManager (tệp JSONL, chương Session), SettingsManager, ExtensionRunner (chương Extensions) và các tool cùng system prompt (chương Công cụ).

Mô hình tư duy#

Có một câu đủ để giữ trong đầu suốt chương: tệp session là bản gốc, mảng message của Agent chỉ là bản chiếu. AgentSession nối hai bên theo hai chiều. Chiều đi lên: nó đăng ký nghe mọi sự kiện (event) của Agent và ghi từng message đã hoàn tất thành một entry trong session. Chiều đi xuống: nó cài các hook vào Agent để ngay trước mỗi yêu cầu gửi tới mô hình, ngữ cảnh được dựng lại từ session, chứ không lấy mảng trong bộ nhớ của Agent.

   Chế độ chạy ── prompt(text) / steer / followUp / abort / executeBash
        │
┌───────▼──────────────┐   subscribe(_handleAgentEvent)   ┌──────────────────────┐
│     AgentSession     │◄──────── sự kiện của Agent ──────│        Agent         │
│                      │                                   │   (pi-agent-core)    │
│  hook cài lúc khởi   │── prepareRequest ────────────────►│  vòng lặp agent:     │
│  tạo (constructor):  │── prepareNextTurnWithContext ────►│  lượt → tool → lượt  │
│                      │── finishTurn ────────────────────►│                      │
│                      │── beforeToolCall / afterToolCall ►│                      │
│                      │── transformContext (2 lớp) ──────►│                      │
└──┬────────┬───────┬──┘                                   └──────────┬───────────┘
   │append*()│emit()  │get*()                                          │ streamFn
   ▼        ▼       ▼                                                 ▼
SessionManager  ExtensionRunner  SettingsManager              ModelRuntime → provider
 (tệp JSONL)
AgentSession nghe sự kiện để ghi session, và cài hook để mỗi yêu cầu được dựng từ session.

Các hook được cài đúng một lần trong constructor. Chúng đọc this._extensionRunner lúc chạy, nên /reload thay runner mới mà không phải cài lại hook:

Constructor: đăng ký nghe sự kiện và cài hook
pi/packages/coding-agent/src/core/agent-session.tsdòng 511–525TypeScript
// Always subscribe to agent events for internal handling// (session persistence, extensions, auto-compaction, retry logic)this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);this._installAgentToolHooks();this._installAgentNextTurnRefresh();this._installAgentRequestProjection();this._installAgentBoundaryHooks();this._installHiddenDeclarationsProjection();this._installAgentForcedPromptProjection();this._buildRuntime({	activeToolNames: this._initialActiveToolNames,	includeAllExtensionTools: true,});if (this._usesDefaultTools) this._restoreToolsFromTranscript(this._defaultToolModifiers);
_buildRuntime() tạo định nghĩa tool, ExtensionRunner và sổ đăng ký tool; _restoreToolsFromTranscript() khôi phục bộ tool mà nhánh session đã ghi lại.

Từ vựng dùng trong chương:

lần chạy (run)
Từ _startRun() đến sự kiện agent_settled. Một lần chạy có thể gọi agent.prompt() một lần rồi agent.continue() nhiều lần (sau retry, sau compaction, khi còn tin nhắn xếp hàng).
lượt (turn)
Một phản hồi assistant cùng các kết quả tool của nó, kết thúc bằng sự kiện turn_end.
entry
Một dòng JSON trong tệp session, có id và parentId (chương Session).
projection
Kết quả sessionManager.buildSessionProjection(): danh sách message mô hình sẽ thấy, đi từ gốc tới lá (leaf) của nhánh đang hoạt động, đã áp dụng compaction và context edit, mỗi message kèm entry nguồn.
boundary
Hai điểm dừng turn_end và agent_before_settle, nơi extension có thể ghi thêm entry và xin chạy tiếp.

Data model: cấu hình, sự kiện, message#

AgentSessionConfig: những gì cần để dựng một phiên#

Constructor nhận một đối tượng cấu hình. Các trường bắt buộc cho thấy những cộng sự chính đều đến từ bên ngoài: Agent, SessionManager, SettingsManager, ResourceLoader và ModelRuntime do bên gọi (thường là createAgentSession() trong sdk.ts) truyền vào. Chỉ ExtensionRunner và sổ đăng ký tool được dựng bên trong, trong _buildRuntime() (agent-session.ts:3666), vì /reload phải dựng lại chúng.

AgentSessionConfig (lược phần cấu hình tool)
pi/packages/coding-agent/src/core/agent-session.tsdòng 252–268, 296–300TypeScript
export interface AgentSessionConfig {	agent: Agent;	sessionManager: SessionManager;	settingsManager: SettingsManager;	cwd: string;	/** Models to cycle through with Ctrl+P (from --models flag) */	scopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;	/** Resource loader for extensions, skills, prompts, themes, context files, and system prompt */	resourceLoader: ResourceLoader;	/** SDK custom tools registered outside extensions */	customTools?: ToolDefinition[];	/** Canonical model/auth runtime used by coding-agent internals. */	modelRuntime: ModelRuntime;	/** Keeps the prompt cache entry of the last session request warm. */	cacheWarmer?: Pick<CacheWarmer, "cancel" | "status" | "onAgentSettled" | "onModeChanged" | "onWarmed">;	/** Initial active built-in tool names. Default: [read, bash, edit, write] */	initialActiveToolNames?: string[];	/** Mutable ref used by Agent to access the current ExtensionRunner */	extensionRunnerRef?: { current?: ExtensionRunner };	/** Session start event metadata emitted when extensions bind to this runtime. */	sessionStartEvent?: SessionStartEvent;}
Phần bị lược (dòng 269 đến 295) là các tuỳ chọn lọc tool: usesDefaultTools, allowedToolNames, excludedToolNames, baseToolsOverride; chúng thuộc chương Công cụ.

AgentSessionEvent: sự kiện công khai#

Người dùng của AgentSession (TUI, RPC, SDK) đăng ký bằng session.subscribe(listener). Họ nhận mọi sự kiện của Agent cộng với các sự kiện riêng của phiên:

AgentSessionEvent (lược các sự kiện retry của bước tóm tắt)
pi/packages/coding-agent/src/core/agent-session.tsdòng 196–222, 237TypeScript
export type AgentSessionEvent =	| WithParentToolCallId<Exclude<AgentEvent, { type: "agent_end" }>>	| {			type: "agent_end";			messages: AgentMessage[];			willRetry: boolean;	  }	| { type: "agent_settled"; aborted: boolean }	| {			type: "queue_update";			steering: readonly string[];			followUp: readonly string[];	  }	| { type: "compaction_start"; reason: "manual" | "threshold" | "overflow" }	| { type: "entry_appended"; entry: SessionEntry }	| { type: "session_info_changed"; name: string | undefined }	| { type: "thinking_level_changed"; level: ThinkingLevel }	| {			type: "compaction_end";			reason: "manual" | "threshold" | "overflow";			result: CompactionResult | undefined;			aborted: boolean;			willRetry: boolean;			errorMessage?: string;	  }	| { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }	| { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }	| { type: "bash_execution_update"; id?: string; delta: string };
agent_end được bọc lại để thêm willRetry: UI biết lần dừng này là tạm thời. agent_settled mới là điểm phiên thực sự rảnh. entry_appended báo các entry ghi ngoài luồng message (context edit, trạng thái router, entry của extension).

Sự kiện được phát cho extension trước, rồi mới tới listener công khai (agent-session.ts:1142). Thứ tự này có lý do, sẽ thấy ở bước 3.

PromptOptions#

Tuỳ chọn của prompt()
pi/packages/coding-agent/src/core/agent-session.tsdòng 314–326TypeScript
/** Options for AgentSession.prompt() */export interface PromptOptions {	/** Whether to dispatch extension commands and expand skill commands and prompt templates (default: true) */	expandPromptTemplates?: boolean;	/** Image attachments */	images?: ImageContent[];	/** When streaming, how to queue the message: "steer" (interrupt) or "followUp" (wait). Required if streaming. */	streamingBehavior?: "steer" | "followUp";	/** Source of input for extension input event handlers. Defaults to "interactive". */	source?: InputSource;	/** Internal hook used by RPC mode to observe how an accepted prompt was dispatched. Not called if the prompt is rejected. */	preflightResult?: (disposition: PromptDisposition) => void;}
streamingBehavior là bắt buộc khi agent đang chạy: không có nó, prompt() ném lỗi thay vì đoán người dùng muốn steer hay follow-up.

Bốn role message riêng của coding agent#

pi-ai chỉ biết bốn role mà provider hiểu: system, user, assistant, toolResult. Coding agent cần thêm các loại message mà UI hiển thị khác đi nhưng vẫn phải vào ngữ cảnh. Nó khai báo chúng bằng declaration merging trên interface CustomAgentMessages của pi-agent-core (docs/message-types.md):

BashExecutionMessage và CustomMessage
pi/packages/coding-agent/src/core/messages.tsdòng 26–53TypeScript
/** * Message type for bash executions via the ! command. */export interface BashExecutionMessage {	role: "bashExecution";	command: string;	output: string;	exitCode: number | undefined;	cancelled: boolean;	truncated: boolean;	fullOutputPath?: string;	timestamp: number;	/** If true, this message is excluded from LLM context (!! prefix) */	excludeFromContext?: boolean;}/** * Message type for extension-injected messages via sendMessage(). * These are custom messages that extensions can inject into the conversation. */export interface CustomMessage<T = unknown> {	role: "custom";	customType: string;	content: string | (TextContent | ImageContent)[];	display: boolean;	details?: T;	timestamp: number;}
bashExecution sinh ra từ lệnh ! của người dùng; custom do extension gửi qua pi.sendMessage(). display chỉ điều khiển việc vẽ; details không bao giờ gửi cho mô hình.
Hai message tóm tắt và phần mở rộng union
pi/packages/coding-agent/src/core/messages.tsdòng 55–77TypeScript
export interface BranchSummaryMessage {	role: "branchSummary";	summary: string;	fromId: string | null;	timestamp: number;}export interface CompactionSummaryMessage {	role: "compactionSummary";	summary: string;	tokensBefore: number;	timestamp: number;}// Extend CustomAgentMessages via declaration mergingdeclare module "@earendil-works/pi-agent-core" {	interface CustomAgentMessages {		bashExecution: BashExecutionMessage;		custom: CustomMessage;		branchSummary: BranchSummaryMessage;		compactionSummary: CompactionSummaryMessage;	}}
Hai role tóm tắt không bao giờ được ghi thành entry message; chúng được sinh lại từ entry compaction và branch_summary mỗi lần dựng ngữ cảnh (chương Session).

Trước khi gửi, convertToLlm() hạ cấp các role riêng về user. Hàm này được truyền vào Agent trong sdk.ts và được vòng lặp agent gọi một lần cho mỗi yêu cầu:

convertToLlm(): hạ các role riêng về user
pi/packages/coding-agent/src/core/messages.tsdòng 148–183TypeScript
export function convertToLlm(messages: AgentMessage[]): Message[] {	return messages		.map((m): Message | undefined => {			switch (m.role) {				case "bashExecution":					// Skip messages excluded from context (!! prefix)					if (m.excludeFromContext) {						return undefined;					}					return {						role: "user",						content: [{ type: "text", text: bashExecutionToText(m) }],						timestamp: m.timestamp,					};				case "custom": {					const content = typeof m.content === "string" ? [{ type: "text" as const, text: m.content }] : m.content;					return {						role: "user",						content,						timestamp: m.timestamp,					};				}				case "branchSummary":					return {						role: "user",						content: [{ type: "text" as const, text: BRANCH_SUMMARY_PREFIX + m.summary + BRANCH_SUMMARY_SUFFIX }],						timestamp: m.timestamp,					};				case "compactionSummary":					return {						role: "user",						content: [							{ type: "text" as const, text: COMPACTION_SUMMARY_PREFIX + m.summary + COMPACTION_SUMMARY_SUFFIX },						],						timestamp: m.timestamp,					};
Lệnh !! đặt excludeFromContext nên bị bỏ hẳn. Các nhánh còn lại (dòng 184 đến 188) trả nguyên system, user, assistant, toolResult.

Tóm tắt compaction và tóm tắt nhánh được bọc trong thẻ <summary> với câu dẫn cố định (messages.ts:11). sdk.ts còn bọc thêm một lớp: khi setting images.blockImages bật, mọi ảnh trong message user và toolResult được thay bằng dòng chữ "Image reading is disabled." (sdk.ts:296), và setting được đọc lại mỗi lần nên đổi giữa phiên có hiệu lực ngay.

Dựng một phiên: services, SDK và runtime#

Việc dựng một phiên chia làm ba tầng, mỗi tầng một tệp. Tách như vậy vì có những thứ phụ thuộc vào thư mục làm việc (cwd): settings dự án, extension dự án, provider do extension đăng ký. Khi /resume mở một session của dự án khác, cwd đổi, và mọi thứ gắn với cwd phải được tạo lại.

  1. Services theo cwdcreateAgentSessionServices() tạo ModelRuntime, SettingsManager, DefaultResourceLoader cho một cwd.
  2. PhiêncreateAgentSession() trong sdk.ts chọn mô hình, mức suy nghĩ (thinking level), tạo Agent và AgentSession.
  3. RuntimeAgentSessionRuntime giữ phiên hiện tại và thay nó khi người dùng đổi phiên.

Tầng 1: services gắn với cwd#

Bộ services cho một 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[];}
createAgentSessionServices(): auth.json, models.json, settings, resource loader
pi/packages/coding-agent/src/core/agent-session-services.tsdòng 135–154, 194–205TypeScript
export async function createAgentSessionServices(	options: CreateAgentSessionServicesOptions,): Promise<AgentSessionServices> {	const cwd = resolvePath(options.cwd);	const agentDir = options.agentDir ? resolvePath(options.agentDir) : getAgentDir();	const modelRuntime =		options.modelRuntime ??		(await ModelRuntime.create({			authPath: join(agentDir, "auth.json"),			modelsPath: join(agentDir, "models.json"),			signal: options.modelRuntimeSignal,		}));	const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);	const resourceLoader = new DefaultResourceLoader({		...(options.resourceLoaderOptions ?? {}),		cwd,		agentDir,		settingsManager,	});	await resourceLoader.reload(options.resourceLoaderReloadOptions);	await modelRuntime.refresh({ allowNetwork: false });	diagnostics.push(...applyExtensionFlagValues(resourceLoader, options.extensionFlagValues));	return {		cwd,		agentDir,		modelRuntime,		settingsManager,		resourceLoader,		diagnostics,	};}
Phần lược đăng ký các provider, native provider và virtual model mà extension khai báo trong lúc nạp. Danh mục mô hình được làm mới với allowNetwork: false: khởi động không chờ mạng.

Tầng 2: createAgentSession() khôi phục và tạo Agent#

Nếu SessionManager đã có dữ liệu (đang --continue hay --resume), mô hình được khôi phục từ nhánh session, với điều kiện provider của nó vẫn có credential:

sdk.ts: khôi phục mô hình từ session
pi/packages/coding-agent/src/core/sdk.tsdòng 209–232TypeScript
// Check if session has existing data to restoreconst existingSession = sessionManager.buildSessionContext();const hasExistingSession = existingSession.messages.length > 0;const hasThinkingEntry = sessionManager.getBranch().some((entry) => entry.type === "thinking_level_change");let model = options.model;let modelFallbackMessage: string | undefined;// Assistant messages name the physical model that answered, so a virtual selection is only in// model_change entries.const sessionModel = getBranchSelection(sessionManager.getBranch(), (provider, modelId) =>	modelRuntime.getModel(provider, modelId),);// If session has data, try to restore model from itif (!model && hasExistingSession && sessionModel) {	const restoredModel = modelRuntime.getModel(sessionModel.provider, sessionModel.modelId);	if (restoredModel && modelRuntime.hasConfiguredAuth(restoredModel.provider)) {		model = restoredModel;	}	if (!model) {		modelFallbackMessage = `Could not restore model ${sessionModel.provider}/${sessionModel.modelId}`;	}}
Nếu không khôi phục được, findInitialModel() chọn theo settings rồi theo provider có credential (mục dịch vụ). Mức suy nghĩ đi theo thứ tự tương tự ở sdk.ts:253, và cuối cùng bị kẹp (clamp) theo khả năng của mô hình.

Rồi Agent được tạo. Bốn điểm nối quan trọng nằm gọn trong lời gọi này:

sdk.ts: Agent nhận message từ session và các callback của coding agent
pi/packages/coding-agent/src/core/sdk.tsdòng 403–438TypeScript
const agent = new Agent({	initialState: {		systemPrompt: "",		model,		thinkingLevel,		tools: [],		messages: existingSession.messages,	},	convertToLlm: convertToLlmWithBlockImages,	streamFn: async (model, context, options) => {		const requestOptions = buildRequestOptions(options);		// Compaction and summaries use their own routing ids; only session requests		// replace the cache entry, so warming restarts from them. Keep warming while		// the current transcript still extends the request's prefix. Agent state may		// shallow-copy the messages array or refresh the model object without changing		// the provider request, so top-level object identity is not a valid cache key.		if (options?.sessionId === sessionManager.getSessionId()) {			cacheWarmer.start({ model, context, options: requestOptions }, cacheContextIsCurrent(model));		}		return modelRuntime.streamSimple(model, context, requestOptions);	},	onPayload: transformProviderPayload,	onResponse: handleProviderResponse,	onProviderStreamEvent: handleProviderStreamEvent,	sessionId: sessionManager.getSessionId(),	transformContext: async (messages) => {		const runner = extensionRunnerRef.current;		if (!runner) return messages;		return runner.emitContext(messages);	},	steeringMode: settingsManager.getSteeringMode(),	followUpMode: settingsManager.getFollowUpMode(),	transport: settingsManager.getTransport(),	thinkingBudgets: settingsManager.getThinkingBudgets(),	maxRetryDelayMs: settingsManager.getProviderRetrySettings().maxRetryDelayMs,});
systemPrompt để trống: ở v1.4.0 system prompt nằm trong transcript dưới dạng message system (bước 2). messages lấy từ buildSessionContext(). streamFn đi qua ModelRuntime.streamSimple(), nơi credential được giải quyết cho từng yêu cầu. transformContext là sự kiện context của extension.

Một phiên mới được ghi sẵn mô hình và mức suy nghĩ ban đầu, để lần resume sau khôi phục đúng:

sdk.ts: entry đầu tiên của một phiên mới
pi/packages/coding-agent/src/core/sdk.tsdòng 440–451TypeScript
// Restore missing settings metadata for older sessions.if (hasExistingSession) {	if (!hasThinkingEntry) {		sessionManager.appendThinkingLevelChange(thinkingLevel);	}} else {	// Save initial model and thinking level for new sessions so they can be restored on resume	if (model) {		sessionManager.appendModelChange(model.provider, model.id);	}	sessionManager.appendThinkingLevelChange(thinkingLevel);}

Hai entry này chưa chạm đĩa: SessionManager chỉ tạo tệp khi có message user hoặc assistant đầu tiên (chương Session). Mở pi rồi thoát ngay không để lại tệp rỗng.

Tầng 3: AgentSessionRuntime thay phiên#

AgentSession không tự chuyển sang tệp khác. Đổi phiên nghĩa là huỷ phiên cũ và dựng phiên mới bằng một factory do main.ts cung cấp (main.ts:733):

Huỷ phiên cũ rồi dựng phiên mới khi resume
pi/packages/coding-agent/src/core/agent-session-runtime.tsdòng 167–178, 209–224TypeScript
private async teardownCurrent(reason: SessionShutdownEvent["reason"], targetSessionFile?: string): Promise<void> {	// Settle any active response first so the aborted turn (including tool	// results) is persisted to the outgoing session before it is replaced.	await this.session.abort();	await emitSessionShutdownEvent(this.session.extensionRunner, {		type: "session_shutdown",		reason,		targetSessionFile,	});	this.beforeSessionInvalidate?.();	this.session.dispose();}	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 };}
abort() chạy trước để lượt đang dở (kể cả kết quả tool) được ghi vào phiên cũ trước khi nó bị thay. Sau đó extension nhận session_shutdown, phiên cũ bị dispose(), và factory tạo services cho cwd của session đích.

newSession(), fork() và importFromJsonl() đi theo cùng khuôn đó (agent-session-runtime.ts:226); chi tiết fork nằm ở chương Session. Mọi ctx mà extension giữ từ phiên cũ trở thành stale: dispose() gọi invalidate() với thông báo hướng dẫn dùng withSession (agent-session.ts:1423).

Bước 1: prompt() xử lý đầu vào#

Phần còn lại của chương lần theo một prompt từ lúc người dùng gõ đến lúc phiên rảnh trở lại. Bước đầu tiên xảy ra trước cả AgentSession, và chỉ áp dụng cho dòng lệnh.

Đối số @file trên dòng lệnh#

Với pi @src/app.ts "giải thích tệp này", main.ts đọc tệp trước khi tạo prompt đầu tiên. Tệp văn bản được nhúng nguyên văn trong thẻ <file>; tệp ảnh trở thành ImageContent đính kèm, còn văn bản chỉ giữ một thẻ <file> rỗng (hoặc chứa gợi ý về việc xử lý ảnh) làm chỗ tham chiếu:

processFileArguments(): tệp thành văn bản hoặc ảnh
pi/packages/coding-agent/src/cli/file-processor.tsdòng 49–78TypeScript
const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);if (mimeType) {	// Handle image file	const content = await readFile(absolutePath);	const processed = await processImage(content, mimeType, { autoResizeImages });	if (!processed.ok) {		text += `<file name="${absolutePath}">${processed.message}</file>\n`;		continue;	}	const attachment: ImageContent = {		type: "image",		mimeType: processed.mimeType,		data: processed.data,	};	images.push(attachment);	// Add text reference to image with optional processing hints	if (processed.hints.length > 0) {		text += `<file name="${absolutePath}">${processed.hints.join("\n")}</file>\n`;	} else {		text += `<file name="${absolutePath}"></file>\n`;	}} else {	// Handle text file	try {		const content = stripBom(await readFile(absolutePath, "utf-8"));		text += `<file name="${absolutePath}">\n${content}\n</file>\n`;
main.ts gọi hàm này với autoResizeImages: false (main.ts:222): việc thu nhỏ ảnh để AgentSession làm sau, khi extension đã có cơ hội đổi mô hình.

Trong trình soạn thảo của interactive mode, gõ @ chỉ mở gợi ý đường dẫn và chèn chuỗi @path vào văn bản (autocomplete.ts:443); nội dung tệp không được đọc sẵn, mô hình tự dùng tool read nếu cần.

prompt(): bốn cửa trước khi chạy#

Đi theo từng cửa:

  1. Đang phát agent_settled?Nếu một listener của agent_settled gọi prompt(), lời gọi được hoãn vào _deferredSettledActions và chạy sau khi sự kiện phát xong (dòng 2093 đến 2096).
  2. Lệnh extensionVăn bản bắt đầu bằng / được tra trong các lệnh extension. Tìm thấy thì chạy handler với command context và dừng; lỗi của handler được báo qua emitError chứ không ném ra (agent-session.ts:2180).
  3. Sự kiện inputExtension có thể trả handled (nuốt input), transform (thay văn bản, ảnh) hoặc để nguyên (agent-session.ts:2041).
  4. Skill và prompt template/skill:tên args được thay bằng nội dung SKILL.md; /tên args khớp một prompt template được thay bằng template đã điền tham số.
Mở rộng /skill:tên thành một khối skill
pi/packages/coding-agent/src/core/agent-session.tsdòng 2211–2225TypeScript
private _expandSkillCommand(text: string): string {	if (!text.startsWith("/skill:")) return text;	const spaceIndex = text.indexOf(" ");	const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);	const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();	const skill = this.resourceLoader.getSkills().skills.find((s) => s.name === skillName);	if (!skill) return text; // Unknown skill, pass through	try {		const content = readFileSync(skill.filePath, "utf-8");		const body = stripFrontmatter(content).trim();		const skillBlock = `<skill name="${skill.name}" location="${skill.filePath}">\nReferences are relative to ${skill.baseDir}.\n\n${body}\n</skill>`;		return args ? `${skillBlock}\n\n${args}` : skillBlock;
Frontmatter bị bỏ, phần thân được bọc trong thẻ <skill> có đường dẫn tệp. TUI nhận ra khối này nhờ parseSkillBlock() (agent-session.ts:177) để vẽ gọn lại. Skill không tồn tại thì văn bản đi qua nguyên vẹn.

Prompt template đơn giản hơn: expandPromptTemplate() tách /tên khỏi phần tham số, tìm template trùng tên, và nếu có thì trả về nội dung template đã thay $1, $@, ${1:-mặc định}... bằng các tham số (prompt-templates.ts:309, substituteArgs()). Không khớp template nào thì văn bản giữ nguyên. Cách khai báo template thuộc chương Extensions.

Nửa sau quyết định giữa xếp hàng và bắt đầu một lần chạy:

prompt(), nửa sau: xếp hàng hoặc bắt đầu chạy
pi/packages/coding-agent/src/core/agent-session.tsdòng 2136–2175TypeScript
	// If streaming, queue via steer() or followUp() based on option	if (this.isStreaming) {		if (!options?.streamingBehavior) {			throw new Error(				"Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.",			);		}		if (options.streamingBehavior === "followUp") {			await this._queueFollowUp(expandedText, currentImages);		} else {			await this._queueSteer(expandedText, currentImages);		}		preflightResult?.("queued");		return;	}	// Validate model	if (!this.model) {		throw new Error(formatNoModelSelectedMessage());	}	const hasConfiguredAuth =		this._modelRuntime.hasConfiguredAuth(this.model.provider) ||		(await this._modelRuntime.checkAuth(this.model.provider)) !== undefined;	if (!hasConfiguredAuth) {		const isOAuth = this._modelRuntime.isUsingOAuth(this.model.provider);		if (isOAuth) {			throw new Error(				`Authentication failed for "${this.model.provider}". ` +					`Credentials may have expired or network is unavailable. ` +					`Run '/login ${this.model.provider}' to re-authenticate.`,			);		}		throw new Error(formatNoApiKeyFoundMessage(this.model.provider));	}	await this._startRun({ messageType: "user", prompt: expandedText, images: currentImages }, () =>		preflightResult?.("started"),	);}
Khi agent đang chạy, văn bản đã mở rộng đi vào hàng đợi steer hoặc follow-up (mục hàng đợi). Khi rảnh, phiên kiểm tra có mô hình và có credential cho provider rồi mới gọi _startRun().

Bước 2: chuẩn bị và chạy một lần chạy#

_startRun() là khung của mọi lần chạy, dù khởi đầu từ prompt của người dùng hay từ sendCustomMessage(..., { triggerTurn: true }) của extension. Nửa đầu chuẩn bị:

_startRun(), nửa đầu: đánh dấu bận, chuẩn bị message
pi/packages/coding-agent/src/core/agent-session.tsdòng 1873–1897TypeScript
private async _startRun(trigger: BeforeAgentStartTrigger, onStarted?: () => void): Promise<void> {	this._isAgentRunActive = true;	this._agentRunAbortRequested = false;	let messages: AgentMessage[];	try {		messages = await this._prepareRun(trigger);	} catch (error) {		this._isAgentRunActive = false;		this._flushPendingBashMessages();		this._flushPendingCustomMessages();		this._resolveIdleWaitIfIdle();		throw error;	}	// Messages held while the run was starting go before its starting messages, as they would	// have while idle. No run exists yet, so they cannot split a tool call from its result.	this._flushPendingBashMessages();	this._flushPendingCustomMessages();	onStarted?.();	// Compaction before the prompt may have scheduled a retry; the new prompt replaces it.	this._failedResponse = undefined;	this._recordSelection();	// The run records the loadout in the transcript; restored tools that did not register by now	// are dropped, so a tool that never registers does not stay pending.	this._pendingToolNames.clear();
Phiên bị coi là bận ngay từ dòng 1874, trước cả before_agent_start. Nhờ vậy một tin nhắn gửi tới trong lúc handler của extension đang chạy được xếp vào lần chạy này thay vì mở lần chạy thứ hai, và một lệnh abort lúc đó sẽ huỷ lần chạy ngay khi nó tồn tại.

Nửa sau là vòng lặp sau run: agent.prompt() chạy vòng lặp agent cho tới agent_end; rồi AgentSession tự hỏi có cần chạy tiếp không.

_startRun(), nửa sau: vòng lặp sau run và agent_settled
pi/packages/coding-agent/src/core/agent-session.tsdòng 1898–1917TypeScript
try {	await this.agent.prompt(messages);	while (!this._agentRunAbortRequested) {		if (await this._handlePostAgentRun()) {			if (this._agentRunAbortRequested) break;			await this.agent.continue();			continue;		}		if (this._agentRunAbortRequested || !(await this._runBeforeSettleBoundary())) break;		if (this._agentRunAbortRequested) break;		await this.agent.continue();	}} finally {	if (this._agentRunAbortRequested) this._finishCancelledRetry();	this._failedResponse = undefined;	this._runSystemPromptOptions = undefined;	this._flushPendingBashMessages();	this._flushPendingCustomMessages();	await this._emitAgentSettled();}
agent.continue() tiếp tục từ transcript hiện có, không thêm message mới. Khối finally luôn đổ các message bash và custom còn treo rồi phát agent_settled, kể cả khi lần chạy bị abort.

_prepareRun(): before_agent_start và message khởi đầu#

_prepareRun(), phần đầu: compaction trước prompt, before_agent_start
pi/packages/coding-agent/src/core/agent-session.tsdòng 1926–1942TypeScript
private async _prepareRun(trigger: BeforeAgentStartTrigger): Promise<AgentMessage[]> {	// Check if we need to compact before sending (catches aborted responses).	// The starting message is sent below, so do not call agent.continue() here.	const lastAssistant = this._findLastAssistantMessage();	if (lastAssistant) {		await this._checkCompaction(lastAssistant, false);	}	const selectedToolsBefore = this._baseSystemPromptOptions.selectedTools;	const result = await this._extensionRunner.emitBeforeAgentStart(trigger, this._baseSystemPromptOptions);	// Handlers may edit event.systemPromptOptions.selectedTools or call setActiveTools(),	// which updates the live loadout instead. An explicit edit wins; otherwise the live	// loadout is authoritative, so a setActiveTools() call is not undone here.	const handlerEditedTools =		result.systemPromptOptions.selectedTools.length !== selectedToolsBefore.length ||		result.systemPromptOptions.selectedTools.some((name, index) => name !== selectedToolsBefore[index]);	if (!handlerEditedTools) result.systemPromptOptions.selectedTools = this.getActiveToolNames();
_checkCompaction(lastAssistant, false) xét cả phản hồi bị abort: nếu lượt trước bị người dùng ngắt khi ngữ cảnh đã gần đầy, compaction chạy trước khi gửi prompt mới. Handler before_agent_start nhận bản sao các tuỳ chọn system prompt và có thể sửa chúng hoặc chèn message (chương Extensions).
_prepareRun(), phần sau: message user, nextTurn, custom, bản vá system
pi/packages/coding-agent/src/core/agent-session.tsdòng 1944–1976TypeScript
const messages: AgentMessage[] = [];if (trigger.messageType === "custom") {	messages.push(trigger.message);} else {	// Normalize images after before_agent_start so extension-driven model selection	// determines the resize profile used for the request and history.	const normalized = await this._normalizePromptImages(trigger.images);	const text =		normalized.hints.length > 0 ? `${trigger.prompt}\n\n${normalized.hints.join("\n")}` : trigger.prompt;	messages.push({		role: "user",		content: [{ type: "text", text }, ...normalized.images],		timestamp: Date.now(),	});}// Inject any pending "nextTurn" messages as context alongside the starting messagemessages.push(...this._pendingNextTurnMessages);this._pendingNextTurnMessages = [];for (const msg of result.messages) {	messages.push({		role: "custom",		customType: msg.customType,		// Untyped extensions can pass null/missing content; normalize at ingestion.		content: msg.content ?? [],		display: msg.display,		details: msg.details,		timestamp: Date.now(),	});}const updateMessage = this._preparePromptAndToolLoadout(result.systemPromptOptions);this._runSystemPromptOptions = result.systemPromptOptions;if (updateMessage) messages.unshift(updateMessage);return messages;
Ảnh được chuẩn hoá ở đây, sau before_agent_start, vì extension có thể đã đổi mô hình và giới hạn kích thước ảnh đi theo mô hình. Lỗi xử lý ảnh không chặn prompt: nó thành dòng gợi ý nối vào văn bản.

Kết quả là một mảng theo thứ tự: [bản vá system nếu prompt thay đổi], message user, các message "nextTurn" đang chờ, các message custom do handler trả về.

System prompt là message trong transcript#

Ở v1.4.0 system prompt không phải một chuỗi riêng truyền theo mỗi yêu cầu. Nó được chia thành các phần có tên (sections) và ghi vào transcript dưới dạng message system. Message đầu tiên mang mọi phần; các message sau chỉ mang phần đã đổi (docs/session-format.md:80). _preparePromptAndToolLoadout() tạo bản vá đó:

Bản vá system prompt = hiệu giữa các phần đã phát lại và các phần mới
pi/packages/coding-agent/src/core/agent-session.tsdòng 1758–1770TypeScript
private _preparePromptAndToolLoadout(	options: NormalizedBuildSystemPromptOptions,	messages: AgentMessage[] = this.agent.state.messages,): SystemMessage | undefined {	options.selectedTools = this._applyToolLoadout(options.selectedTools).map((tool) => tool.name);	// The tool list and rules must match the declarations the request carries.	options.hiddenTools = [...this._hiddenDeclarations];	const sections = diffSystemPromptSections(		getCurrentSystemMessage(messages)?.sections ?? {},		buildSystemPromptSections(options),	);	return sections ? { role: "system", content: "", sections, timestamp: Date.now() } : undefined;}
getCurrentSystemMessage() của pi-ai phát lại (replay) mọi message system trong transcript thành một trạng thái hiện tại (transcript.ts:73); diffSystemPromptSections() trả undefined khi không có gì đổi (system-prompt.ts:217). Nội dung từng phần (preamble, tools, skills, context file) thuộc chương Công cụ và system prompt.

Tool đi theo cùng cơ chế. Ngay trước khi phát các message khởi đầu, vòng lặp agent so bộ tool đã khai báo trong transcript với bộ tool thực thi được và ghi chênh lệch vào toolsAdded/toolsRemoved của message system (agent-loop.ts:342). Vì vậy prompt đầu tiên của một phiên mới ghi một message system chứa mọi phần prompt và mọi khai báo tool; prompt thứ hai, nếu không có gì đổi, không ghi message system nào.

Một ngoại lệ: handler before_agent_start trả về systemPrompt muốn đúng chuỗi đó đứng đầu yêu cầu. Chuỗi ép buộc này không ghi vào transcript; nó được chiếu lên yêu cầu bằng một lớp transformContext gộp mọi message system thành một đầu duy nhất (agent-session.ts:1807). Lớp còn lại AgentSession cài lên transformContext bỏ các khai báo tool mà hook prepareLoadout muốn ẩn (agent-session.ts:1787). Cả hai chạy sau sự kiện context của extension, lớp do sdk.ts cài.

_handlePostAgentRun(): có chạy tiếp không#

Quyết định sau agent_end: retry, compaction, hàng đợi
pi/packages/coding-agent/src/core/agent-session.tsdòng 1979–2015TypeScript
private async _handlePostAgentRun(): Promise<boolean> {	const message = this._lastAssistantMessage;	this._lastAssistantMessage = undefined;	if (this._agentRunAbortRequested) {		this._finishCancelledRetry();		return false;	}	if (!message) return this.agent.hasQueuedMessages();	if (this._isRetryableError(message) && (await this._prepareRetry(message))) {		if (this._agentRunAbortRequested) this._finishCancelledRetry();		this._failedResponse = message;		return !this._agentRunAbortRequested;	}	if (this._agentRunAbortRequested) {		this._finishCancelledRetry();		return false;	}	if (message.stopReason === "error" && this._retryAttempt > 0) {		this._emit({			type: "auto_retry_end",			success: false,			attempt: this._retryAttempt,			finalError: message.errorMessage,		});		this._retryAttempt = 0;	}	if (await this._checkCompaction(message)) {		return !this._agentRunAbortRequested;	}	// The low-level loop drains both queues before agent_end. Messages queued by	// agent_end handlers require a fresh run before pre-settlement handlers fire.	return !this._agentRunAbortRequested && this.agent.hasQueuedMessages();}
Ba lý do để gọi agent.continue(): lỗi có thể thử lại (retry), compaction do tràn ngữ cảnh cần thử lại lượt (compaction), hoặc còn tin nhắn xếp hàng do handler agent_end thêm vào.

Khi không còn lý do nào, _runBeforeSettleBoundary() phát boundary agent_before_settle: extension có thể ghi entry (một custom message, một context edit, thậm chí một compaction) và xin chạy tiếp. Lời xin chỉ được chấp nhận khi ngữ cảnh sau khi ghi còn chạy tiếp được: có message ngoài system và message cuối không phải assistant, hoặc vẫn còn message đang chờ (hàng đợi steer/follow-up, custom message bị hoãn) (agent-session.ts:999).

Bước 3: sự kiện và ghi session#

Trong lúc vòng lặp agent chạy, mọi sự kiện đi qua một hàm duy nhất, _handleAgentEvent. Phần đầu của nó làm ba việc trước khi ghi bất cứ thứ gì:

_handleAgentEvent(), phần đầu: abort trễ, gỡ khỏi hàng đợi, phát sự kiện
pi/packages/coding-agent/src/core/agent-session.tsdòng 1103–1106, 1120–1144TypeScript
private _handleAgentEvent = async (event: AgentEvent): Promise<void> => {	// An abort requested while the run was starting, e.g. during before_agent_start, stops it	// here. agent_start is awaited before the first request.	if (event.type === "agent_start" && this._agentRunAbortRequested) this.agent.abort();	// When a user message starts, check if it's from either queue and remove it BEFORE emitting	// This ensures the UI sees the updated queue state	if (event.type === "message_start" && event.message.role === "user") {		this._overflowRecoveryAttempted = false;		const messageText = contentText(event.message.content, "");		if (messageText) {			// Check steering queue first			const steeringIndex = this._steeringMessages.indexOf(messageText);			if (steeringIndex !== -1) {				this._steeringMessages.splice(steeringIndex, 1);				this._emitQueueUpdate();			} else {				// Check follow-up queue				const followUpIndex = this._followUpMessages.indexOf(messageText);				if (followUpIndex !== -1) {					this._followUpMessages.splice(followUpIndex, 1);					this._emitQueueUpdate();				}			}		}	}	// Emit to extensions first, then notify public listeners.	await this._emitExtensionEvent(event);	this._emit(event.type === "agent_end" ? { ...event, willRetry: this._willRetryAfterAgentEnd(event) } : event);
Khi một message user bắt đầu, văn bản của nó được gỡ khỏi danh sách steer hoặc follow-up đang hiển thị, rồi mới phát sự kiện, để UI thấy hàng đợi đã đúng. Phần lược (1107 đến 1119) gắn bản ghi các lời gọi tool lồng nhau (ctx.executeTool()) vào message toolResult.

Extension được phát trước listener công khai vì handler message_end của extension có quyền thay message:

message_end: extension có thể thay message trước khi nó được ghi
pi/packages/coding-agent/src/core/agent-session.tsdòng 1334–1352TypeScript
} else if (event.type === "message_end") {	const extensionEvent: MessageEndEvent = {		type: "message_end",		message: event.message,	};	const replacement = await this._extensionRunner.emitMessageEnd(extensionEvent);	if (replacement) {		// Untyped extension handlers can return messages with null/missing content;		// normalize so it never enters agent state or session history.		const normalized =			(replacement.role === "user" ||				replacement.role === "assistant" ||				replacement.role === "toolResult" ||				replacement.role === "custom") &&			replacement.content == null				? ({ ...replacement, content: [] } as AgentMessage)				: replacement;		this._replaceMessageInPlace(event.message, normalized);	}
_replaceMessageInPlace() xoá mọi khoá của đối tượng cũ rồi gán khoá của đối tượng mới (agent-session.ts:1286). Agent state, các listener và lệnh ghi session ngay sau đó đều giữ cùng một tham chiếu, nên cả ba thấy bản đã thay.

Rồi tới phần ghi. Chỉ message_end dẫn tới ghi; message_start và message_update (từng mẩu văn bản đang stream) chỉ để vẽ.

_handleAgentEvent(), phần ghi: message_end thành entry
pi/packages/coding-agent/src/core/agent-session.tsdòng 1146–1168, 1190–1197TypeScript
// Handle session persistenceif (event.type === "message_end") {	let entryId: string | undefined;	// Check if this is a custom message from extensions	if (event.message.role === "custom") {		// Persist as CustomMessageEntry		entryId = this.sessionManager.appendCustomMessageEntry(			event.message.customType,			event.message.content,			event.message.display,			event.message.details,		);	} else if (		event.message.role === "system" ||		event.message.role === "user" ||		event.message.role === "assistant" ||		event.message.role === "toolResult"	) {		// Regular LLM message - persist as SessionMessageEntry		entryId = this.sessionManager.appendMessage(event.message);	}	if (entryId) this._entryIdsByMessage.set(event.message, entryId);	// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere// A turn ends after its assistant message and every tool result has been appended,// so this is the first point in the run where a context-only custom message can be// inserted without landing between a tool call and its result. Flushing after the// extension and listener dispatch above also picks up messages that turn_end// handlers queued.if (event.type === "turn_end") {	this._flushPendingCustomMessages();}
Message custom thành entry custom_message; bốn role của provider thành entry message. bashExecution và hai role tóm tắt không bao giờ đi qua đây. Phần lược (1170 đến 1187) đặt lại bộ đếm retry khi có một phản hồi assistant thành công.

Ghi xong không có nghĩa là xong. Các hook cài trên Agent bảo đảm yêu cầu kế tiếp nhìn thấy đúng nội dung tệp session, không phải mảng message trong bộ nhớ. Trước mỗi yêu cầu, prepareRequest thay toàn bộ context.messages bằng projection của session:

prepareRequest: mỗi yêu cầu được dựng lại từ projection của session
pi/packages/coding-agent/src/core/agent-session.tsdòng 787–814TypeScript
private _installAgentRequestProjection(): void {	const previousPrepareRequest = this.agent.prepareRequest;	this.agent.prepareRequest = async (request, signal) => {		const failed = this._failedResponse;		this._failedResponse = undefined;		const prepare = async () => {			const projection = this.sessionManager.buildSessionProjection();			const canonicalContext = {				...request.context,				messages: projection.messages,				// Messages declare the provider-visible loadout; context.tools keeps executable implementations.				tools: this.agent.state.tools.slice(),			};			const previous = await previousPrepareRequest?.(				{					...request,					context: canonicalContext,					model: this.agent.state.model,					thinkingLevel: this.agent.state.thinkingLevel,				},				signal,			);			return { previous, context: previous?.context ?? canonicalContext, projection };		};		let { previous, context, projection } = await prepare();		const model = previous?.model ?? this.agent.state.model;		const thinkingLevel = previous?.thinkingLevel ?? this.agent.state.thinkingLevel;		if (!isVirtualModel(model)) return { ...previous, context, model, thinkingLevel };
Nếu extension đã ghi một context_edit hay một compaction giữa hai lượt, yêu cầu sau phản ánh ngay. Phần sau dòng 814 chỉ chạy khi mô hình đang chọn là virtual model: router chọn mô hình vật lý cho riêng yêu cầu này, có thể ghi trạng thái router thành entry custom và có thể kích hoạt compaction theo cửa sổ ngữ cảnh của mô hình được chọn.

Giữa hai lượt trong cùng một lần chạy, prepareNextTurnWithContext làm thêm hai việc: kiểm tra ngưỡng compaction và tính lại bản vá system prompt (ví dụ một tool vừa được bật):

prepareNextTurnWithContext: compaction giữa lượt và bản vá system
pi/packages/coding-agent/src/core/agent-session.tsdòng 904–931TypeScript
this.agent.prepareNextTurnWithContext = async (turn, signal) => {	const context = await this._compactBeforeNextAssistantResponse(turn.context);	const previousSnapshot = await previousPrepareNextTurnWithContext?.({ ...turn, context }, signal);	const nextContext = previousSnapshot?.context ?? context;	const runOptions = this._runSystemPromptOptions ?? this._baseSystemPromptOptions;	const options = normalizeBuildSystemPromptOptions({		...runOptions,		selectedTools: this.getActiveToolNames(),		toolSnippets: { ...this._baseSystemPromptOptions.toolSnippets, ...runOptions.toolSnippets },		toolGuidelines: { ...this._baseSystemPromptOptions.toolGuidelines, ...runOptions.toolGuidelines },	});	const updateMessage = this._preparePromptAndToolLoadout(options, nextContext.messages);	// Keep session.systemPrompt and ctx.getSystemPrompt() in step with what the provider sees.	this._runSystemPromptOptions = options;	return {		...previousSnapshot,		context: {			...nextContext,			tools: this.agent.state.tools.slice(),		},		messages: updateMessage			? [...(previousSnapshot?.messages ?? []), updateMessage]			: previousSnapshot?.messages,		model: this.agent.state.model,		thinkingLevel: this.agent.state.thinkingLevel,	};};
Bản vá được trả trong messages; vòng lặp agent phát nó như message mới trước phản hồi kế tiếp, nên nó cũng đi qua message_end và được ghi thành entry.

Ngược lại, khi AgentSession tự ghi entry ngoài luồng sự kiện (sau compaction, retry, điều hướng cây), nó gọi _refreshFinalizedContext() để chép projection mới vào agent.state.messages và nhớ entry nguồn của từng message (agent-session.ts:938).

Entry nào được ghi khi nào#

Thời điểmEntryNơi ghi
Tạo phiên mới (SDK)model_change, thinking_level_changesdk.ts:440
--name lúc khởi độngsession_infomain.ts:716
message_end của system, user, assistant, toolResultmessageagent-session.ts:1158
message_end của customcustom_messageagent-session.ts:1150
Custom message gửi khi rảnh hoặc hoãn tới cuối lượtcustom_messageagent-session.ts:2395
Lệnh ! khi rảnh, hoặc sau lần chạymessage (role bashExecution)agent-session.ts:3957, 3989
Đổi hoặc xoay vòng mô hìnhmodel_changeagent-session.ts:2549, 2616, 2651
Mức suy nghĩ thực sự thay đổithinking_level_changeagent-session.ts:2691
Retry hoặc phục hồi sau tràn ngữ cảnhcontext_edit với replacement: nullagent-session.ts:1267
Compaction thủ công hoặc tự độngcompactionagent-session.ts:2911, 3237
Router của virtual model đổi trạng tháicustom (pi.virtual-model-state)agent-session.ts:829
Extension gọi pi.appendEntry()customagent-session.ts:3466
Extension ghi ở boundarycustom, custom_message, context_edit, compactionagent-session.ts:946
Đặt tên phiênsession_infoagent-session.ts:4003
Gắn nhãnlabelagent-session.ts:3480
/tree có tóm tắt nhánhbranch_summaryagent-session.ts:4170
Làm ấm prompt cacheusage (kind: "cache_warm")cache-warmer.ts:342

Cấu trúc của từng loại entry nằm ở chương Session.

Lần theo một prompt có một lời gọi tool#

Kịch bản: một phiên mới, mô hình thường (không phải virtual model), không extension nào can thiệp. Người dùng gõ "package.json khai báo version nào?". Mô hình gọi tool read một lần rồi trả lời. Sơ đồ dưới đây ghép ba bước ở trên thành một dòng thời gian; mũi tên trái là sự kiện hoặc lời gọi hook từ Agent, mũi tên phải là lời gọi của AgentSession.

TUI          AgentSession                       Agent (pi-agent-core)         SessionManager
 │                 │                                     │                           │
 ├─ prompt(text) ─►│                                     │                           │
 │                 │ lệnh extension? input? skill?       │                           │
 │                 │ template? đang chạy? credential?    │                           │
 │                 │ _startRun() → _prepareRun():        │                           │
 │                 │ compaction trước prompt?            │                           │
 │                 │ before_agent_start                  │                           │
 │                 ├─ agent.prompt([system, user]) ─────►│                           │
 │                 │                                     │ declareToolChanges()      │
 │                 │◄─ agent_start, turn_start ──────────┤                           │
 │                 │◄─ message_end(system) ──────────────┤                           │
 │                 ├─ appendMessage(system) ─────────────┼──────────────────────────►│ ① message
 │                 │◄─ message_end(user) ────────────────┤                           │
 │                 ├─ appendMessage(user) ───────────────┼──────────────────────────►│ ② message, tạo tệp
 │                 │◄─ hook prepareRequest ──────────────┤                           │
 │                 ├─ buildSessionProjection() ──────────┼──────────────────────────►│
 │                 ├─ context.messages = projection ────►│                           │
 │                 │                                     │ transformContext()        │
 │                 │                                     │ convertToLlm()            │
 │                 │                                     │ streamFn → provider       │
 │◄────────────────┼── message_start, message_update ────┤                           │
 │                 │◄─ message_end(assistant: toolCall) ─┤                           │
 │                 ├─ appendMessage(assistant) ──────────┼──────────────────────────►│ ③ message
 │                 │◄─ hook beforeToolCall: tool_call ───┤                           │
 │                 │                                     │ tool_execution_start      │
 │                 │                                     │ chạy tool read            │
 │                 │                                     │ tool_execution_end        │
 │                 │◄─ hook afterToolCall: tool_result ──┤                           │
 │                 │◄─ message_end(toolResult) ──────────┤                           │
 │                 ├─ appendMessage(toolResult) ─────────┼──────────────────────────►│ ④ message
 │                 │◄─ hook finishTurn: turn_end ────────┤                           │
 │                 │◄─ turn_end ─────────────────────────┤                           │
 │                 │◄─ hook prepareNextTurnWithContext ──┤                           │
 │                 │ ngưỡng compaction, bản vá system    │                           │
 │                 │◄─ hook prepareRequest (lần 2) ──────┤                           │
 │                 │                                     │ streamFn → provider       │
 │                 │◄─ message_end(assistant: văn bản) ──┤                           │
 │                 ├─ appendMessage(assistant) ──────────┼──────────────────────────►│ ⑤ message
 │                 │◄─ turn_end, agent_end ──────────────┤                           │
 │                 │ _handlePostAgentRun(): không        │                           │
 │                 │ retry, không compaction, hàng       │                           │
 │                 │ đợi rỗng; agent_before_settle       │                           │
 │◄─ agent_settled ┤                                     │                           │
Một prompt, hai yêu cầu tới provider, năm entry message. Mỗi mũi tên từ Agent được await: Agent cập nhật state của nó rồi chờ từng listener xong mới đi tiếp (agent.ts:575), nên mỗi entry đã nằm trên đĩa trước khi vòng lặp bước sang việc kế tiếp.

Đọc theo thứ tự:

  1. Đầu vàoprompt() không gặp lệnh extension, không có handler input, văn bản không phải skill hay template. Agent đang rảnh, mô hình có credential: gọi _startRun().
  2. Chuẩn bịPhiên mới chưa có assistant nào nên không kiểm tra compaction. before_agent_start không đổi gì. Transcript chưa có message system nào, nên bản vá chứa mọi phần prompt; declareToolChanges() gắn khai báo của bốn tool mặc định vào nó.
  3. Hai message đầumessage_end(system) rồi message_end(user). Entry ① chỉ nằm trong bộ nhớ cùng model_change và thinking_level_change mà sdk.ts ghi lúc tạo phiên; khi entry ② (message user) được ghi, SessionManager mới tạo tệp và viết header cùng mọi entry đang có (chương Session).
  4. Yêu cầu thứ nhấtprepareRequest thay context bằng projection của session. Ba lớp transformContext chạy (sự kiện context, ẩn khai báo, prompt ép buộc), convertToLlm() hạ role, streamFn gửi qua ModelRuntime. TUI vẽ dần từ message_update.
  5. Lời gọi toolPhản hồi kết thúc với stopReason: "toolUse"; entry ③ được ghi. Hook beforeToolCall phát tool_call cho extension (có thể chặn), tool read chạy, hook afterToolCall phát tool_result (có thể sửa) và thu nhỏ ảnh nếu có. Entry ④ là kết quả tool.
  6. Hết lượt 1finishTurn phát boundary turn_end cho extension; sự kiện turn_end đổ các custom message bị hoãn (không có).
  7. Lượt 2prepareNextTurnWithContext kiểm tra ngưỡng compaction và tính bản vá system (không đổi, nên không có message system mới). Yêu cầu thứ hai trả về văn bản với stopReason: "stop"; entry ⑤.
  8. Kết thúcKhông còn lời gọi tool, không có tin nhắn xếp hàng: agent_end với willRetry: false. _handlePostAgentRun() trả false, boundary agent_before_settle không xin chạy tiếp, khối finally phát agent_settled.

Ví dụ minh hoạ: tệp session sau kịch bản này có header và bảy entry, nối nhau thành một chuỗi qua parentId.

Thứ tựEntryNội dung chínhGhi bởi
0session (header)version: 3, id, cwdSessionManager.newSession()
1model_changeprovider, modelId ban đầusdk.ts
2thinking_level_changemức suy nghĩ ban đầusdk.ts
3 ①message, role systemsections đầy đủ, toolsAdded bốn tool_handleAgentEvent
4 ②message, role uservăn bản prompt_handleAgentEvent
5 ③message, role assistantkhối toolCall gọi read, usage, stopReason: "toolUse"_handleAgentEvent
6 ④message, role toolResulttoolCallId, nội dung tệp_handleAgentEvent
7 ⑤message, role assistantcâu trả lời, stopReason: "stop"_handleAgentEvent

Dạng JSON của từng dòng có ở ví dụ JSONL trong chương Session.

Tự động thử lại khi provider lỗi#

Vấn đề: giữa chừng một lượt, provider trả "overloaded" (HTTP 529) hoặc kết nối bị đứt. Vòng lặp agent không ném lỗi; nó kết thúc lần chạy bằng một message assistant có stopReason: "error" và errorMessage. Không có retry, người dùng phải gõ "tiếp tục". Pi thử lại ở hai tầng:

  • Tầng provider: SDK của provider tự thử lại một yêu cầu trước khi có phản hồi. AgentSession chỉ truyền tham số vào từ nhóm setting retry.provider (timeoutMs, maxRetries, maxRetryDelayMs) qua buildRequestOptions (sdk.ts:340).
  • Tầng phiên: AgentSession xét phản hồi lỗi sau agent_end và chạy lại cả lượt. Phần này nằm trọn trong agent-session.ts.
Lỗi nào được thử lại
pi/packages/coding-agent/src/core/agent-session.tsdòng 3765–3773TypeScript
/** * Check if an error is retryable (overloaded, rate limit, server errors). * Context overflow errors are NOT retryable (handled by compaction instead). */private _isRetryableError(message: AssistantMessage): boolean {	// Context overflow is handled by compaction, not retry.	if (isContextOverflow(message, (this._modelForMessage(message) ?? this.model)?.contextWindow ?? 0)) return false;	return isRetryableAssistantError(message);}
Tràn cửa sổ ngữ cảnh (context window) không được thử lại vì gửi lại y nguyên sẽ tràn tiếp; nó đi sang nhánh compaction. Phần còn lại do pi-ai phân loại bằng biểu thức chính quy trên errorMessage: "overloaded", "rate limit", mã 429/500/502/503/504, lỗi mạng... (retry.ts:33), trừ các lỗi hết hạn mức hoặc hết tiền, loại lỗi không tự khỏi sau vài giây (retry.ts:266).
_prepareRetry(): đếm lần, chờ lùi dần, loại lần hỏng khỏi ngữ cảnh
pi/packages/coding-agent/src/core/agent-session.tsdòng 3822–3861TypeScript
private async _prepareRetry(message: AssistantMessage): Promise<boolean> {	const settings = this.settingsManager.getRetrySettings();	if (!settings.enabled) {		return false;	}	this._retryAttempt++;	if (this._retryAttempt > settings.maxRetries) {		// Preserve the completed attempt count so post-run handling can emit the final failure.		this._retryAttempt--;		return false;	}	const delayMs = retryDelayMs(settings, this._retryAttempt);	this._emit({		type: "auto_retry_start",		attempt: this._retryAttempt,		maxAttempts: settings.maxRetries,		delayMs,		errorMessage: message.errorMessage || "Unknown error",	});	// Keep the failed attempt in raw history while durably omitting it from model projection.	this._omitRecoveryAttempt(message);	// Wait with exponential backoff (abortable)	this._retryAbortController = new AbortController();	try {		await sleep(delayMs, this._retryAbortController.signal);	} catch {		// Aborted during sleep - emit end event so UI can clean up		this._finishCancelledRetry();		return false;	} finally {		this._retryAbortController = undefined;	}	return true;
Độ trễ là baseDelayMs * 2^(attempt-1), chặn trên bởi maxAgentDelayMs (retry.ts:142). Mặc định maxRetries: 3, baseDelayMs: 2000, tức 2, 4, 8 giây (settings-defaults.ts:18). Thời gian chờ dùng một AbortController riêng, nên Esc huỷ được ngay trong lúc chờ.

Dòng 3847 là phần đáng học nhất. Lần thử hỏng không bị xoá khỏi tệp, vì session chỉ ghi thêm. Thay vào đó AgentSession ghi một entry context_edit với replacement: null cho message lỗi và cho mọi kết quả tool đi kèm nó:

_omitRecoveryAttempt(): bỏ lần thử khỏi projection, giữ trong lịch sử thô
pi/packages/coding-agent/src/core/agent-session.tsdòng 1239–1272TypeScript
/** * Persistently omit a failed attempt and its tool results from model context. Results are * found in the projected context, so the pair stays together whether the attempt comes from * the current run or a resumed session. An unpaired result fails the next request (#11227). */private _omitRecoveryAttempt(message: AssistantMessage): void {	const toolCallIds = new Set(message.content.flatMap((block) => (block.type === "toolCall" ? [block.id] : [])));	// A context edit replaces the projected copy, so match it by source entry as well.	const entryId = this._findPersistedMessageEntryId(message);	const messages = this.agent.state.messages;	const index = messages.findIndex(		(m) => m === message || (entryId !== undefined && this._entryIdsByMessage.get(m) === entryId),	);	const toolResults: AgentMessage[] = [];	for (const m of index < 0 ? [] : messages.slice(index + 1)) {		if (m.role === "assistant") break;		if (m.role === "toolResult" && toolCallIds.has(m.toolCallId)) toolResults.push(m);	}	const targets = [message, ...toolResults];	const targetIds = targets.map((target) => this._findPersistedMessageEntryId(target));	const unresolvedProjectedTarget = targets.some(		(target, index) => targetIds[index] === undefined && this.agent.state.messages.includes(target),	);	if (unresolvedProjectedTarget) {		throw new Error("Cannot persist recovery omission because a projected message has no source entry");	}	for (const targetId of targetIds) {		if (!targetId) continue;		const editId = this.sessionManager.appendContextEdit(targetId, null);		const entry = this.sessionManager.getEntry(editId);		if (entry) this._emit({ type: "entry_appended", entry });	}	this._refreshFinalizedContext();}
Kết quả tool được tìm trong ngữ cảnh đã chiếu, nên cặp lời gọi và kết quả luôn bị bỏ cùng nhau, kể cả khi lần thử nằm trong một phiên vừa resume. Một kết quả tool mồ côi khiến mọi yêu cầu sau bị provider từ chối: đó là lỗi #11227 được sửa ở 1.4.0 (CHANGELOG.md:45).

Sau khi _prepareRetry() trả true, _handlePostAgentRun() lưu phản hồi hỏng vào _failedResponse rồi _startRun() gọi agent.continue(). Yêu cầu kế tiếp không còn thấy lần hỏng trong ngữ cảnh; nếu mô hình đang chọn là virtual model, router nhận nó qua failed với lý do "retry" (agent-session.ts:790). Khi một phản hồi assistant thành công đến, bộ đếm về 0 và auto_retry_end báo thành công (agent-session.ts:1177). Khi hết lượt thử, auto_retry_end mang success: false và lỗi cuối. UI biết trước có thử lại hay không nhờ trường willRetry trên agent_end, tính bằng cùng các điều kiện (agent-session.ts:1200).

Nơi compaction được kích hoạt#

Thuật toán compaction (nén ngữ cảnh): chọn điểm cắt, giữ khoảng 20.000 token gần nhất, viết bản tóm tắt; tất cả thuộc chương Compaction. Mục này chỉ trả lời câu hỏi: AgentSession gọi nó ở đâu và khi nào. Có năm điểm:

ĐiểmHàmLý doChạy lại lượt?
Trước prompt mới_prepareRun() gọi _checkCompaction(lastAssistant, false)tràn hoặc vượt ngưỡng, kể cả khi lượt trước bị abortkhông
Sau agent_end_handlePostAgentRun() gọi _checkCompaction(message)overflow hoặc thresholdcó, nếu phản hồi lỗi vì tràn
Giữa hai lượt của một lần chạy_compactBeforeNextAssistantResponse()thresholdkhông cần: lượt chưa bắt đầu
Trước yêu cầu của virtual modelprepareRequestthreshold, theo mô hình được routekhông cần
Thủ côngcompact() (/compact, RPC, ctx.compact())manualkhông bao giờ
_checkCompaction(): ba trường hợp tự động và các điều kiện bỏ qua
pi/packages/coding-agent/src/core/agent-session.tsdòng 2994–3000, 3011–3035TypeScript
 * Automatic cases: * 1. Overflow with retry: a context-overflow error or recoverable length stop; *    remove the failed assistant message, compact, and retry the turn once. * 2. Overflow without retry: a successful response exceeded the configured *    context window; compact but preserve the completed response. * 3. Threshold without retry: valid or estimated context usage crossed the *    configured threshold; compact without retrying the completed response.private async _checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise<boolean> {	const settings = this.settingsManager.getCompactionSettings(this.model);	if (!settings.enabled) return false;	// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false	if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return false;	// Skip overflow check if the message came from a different model.	// This handles the case where user switched from a smaller-context model (e.g. opus)	// to a larger-context model (e.g. codex) - the overflow error from the old model	// shouldn't trigger compaction for the new model. Under a virtual selection, the	// physical model that produced the message supplies the limits.	const messageModel = this._modelForMessage(assistantMessage);	const sameModel = messageModel !== undefined;	const contextWindow = (messageModel ?? this.model)?.contextWindow ?? 0;	// Skip compaction checks if this assistant message is older than the latest	// compaction boundary. This prevents a stale pre-compaction usage/error	// from retriggering compaction on the first prompt after compaction.	const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());	const assistantIsFromBeforeCompaction =		compactionEntry !== null && assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime();	if (assistantIsFromBeforeCompaction) {		return false;	}
Ngoài setting compaction.enabled, có ba phép lọc trước khi xét: phản hồi bị người dùng abort (trừ lần kiểm tra trước prompt); phản hồi của một mô hình khác (đổi từ mô hình cửa sổ nhỏ sang mô hình cửa sổ lớn thì lỗi tràn cũ không còn nghĩa); phản hồi cũ hơn compaction gần nhất (usage của nó phản ánh ngữ cảnh trước khi nén).
Tràn ngữ cảnh: compact rồi thử lại lượt đúng một lần
pi/packages/coding-agent/src/core/agent-session.tsdòng 3070–3106TypeScript
if (contextOverflow || recoverableLength) {	const willRetry = assistantMessage.stopReason !== "stop";	// Case 2: the response completed successfully. Compact, but do not retry because	// agent.continue() cannot continue from a completed assistant response.	if (!willRetry) {		return await this._runAutoCompaction("overflow", false);	}	if (this._overflowRecoveryAttempted) {		const errorMessage = contextOverflow			? "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model."			: "Truncated response recovery failed after one compact-and-retry attempt.";		this._emit({			type: "compaction_end",			reason: "overflow",			result: undefined,			aborted: false,			willRetry: false,			errorMessage,		});		await this._emitSessionCompactFailed({			reason: "overflow",			errorMessage,			aborted: false,			willRetry: false,			fromExtension: false,		});		return false;	}	// Persistently omit the selected final attempt before post-run recovery compaction.	this._overflowRecoveryAttempted = true;	this._omitRecoveryAttempt(assistantMessage);	const retry = await this._runAutoCompaction("overflow", willRetry);	if (retry) this._failedResponse = assistantMessage;	return retry;
Nếu phản hồi đã hoàn tất (stopReason: "stop") mà vẫn vượt cửa sổ, chỉ nén, không thử lại: không thể continue() từ một phản hồi assistant đã xong. Nếu phản hồi lỗi vì tràn, lần thử hỏng bị loại khỏi ngữ cảnh bằng _omitRecoveryAttempt() như khi retry, rồi nén và chạy lại. _overflowRecoveryAttempted bảo đảm chỉ một lần; lần tràn thứ hai báo lỗi gợi ý giảm ngữ cảnh hoặc đổi mô hình lớn hơn.

Trường hợp ngưỡng (agent-session.ts:3109) dùng số token ngữ cảnh từ usage của phản hồi; khi phản hồi lỗi hoặc usage bằng 0, nó ước lượng từ phản hồi hợp lệ gần nhất, và khi có context edit trên nhánh, nó ước lượng trên projection. Điều kiện cuối cùng đơn giản: contextTokens > contextWindow - reserveTokens (compaction.ts:267), với reserveTokens mặc định 16.384.

Điểm giữa hai lượt quan trọng với các lần chạy dài nhiều tool: không có nó, một chuỗi hai mươi lời gọi read có thể làm tràn ngữ cảnh trước khi lần chạy kết thúc.

Nén giữa hai lượt, trước phản hồi assistant kế tiếp
pi/packages/coding-agent/src/core/agent-session.tsdòng 776–785TypeScript
private async _compactBeforeNextAssistantResponse(context: AgentContext): Promise<AgentContext> {	const projection = this.sessionManager.buildSessionProjection();	// A virtual selection is checked in prepareRequest, against the model the request is routed to.	const model = this.model;	if (!model || isVirtualModel(model) || !this._exceedsCompactionThreshold(model, projection)) {		return { ...context, messages: projection.messages };	}	await this._runAutoCompaction("threshold", false);	return { ...context, messages: this.sessionManager.buildSessionProjection().messages };}

Mọi đường tự động đều đi qua _runAutoCompaction(): phát compaction_start, cho extension cơ hội huỷ hoặc tự cung cấp bản tóm tắt qua session_before_compact, ghi entry compaction, làm mới context, phát compaction_end (agent-session.ts:3157). Nó trả true (gọi agent.continue()) khi cần chạy lại lượt hoặc khi còn tin nhắn xếp hàng. Compaction thủ công thì khác ở một điểm: compact() gọi abort() trước tiên, nên lượt đang chạy bị dừng và không được tiếp tục (agent-session.ts:2828).

Đổi mô hình và mức suy nghĩ#

Đổi mô hình bằng /model hay xoay vòng bằng Ctrl+P là thao tác trên phiên, không phải trên settings. Mặc định nó chỉ có hiệu lực trong phiên hiện tại; tuỳ chọn persist mới ghi thêm vào settings toàn cục (agent-session.ts:328). Điều phiên cần là ghi lại lựa chọn vào transcript, để resume khôi phục đúng mô hình mà không đụng tới mặc định cho phiên mới.

setModel(): kiểm tra credential, ghi model_change, áp mức suy nghĩ
pi/packages/coding-agent/src/core/agent-session.tsdòng 2541–2561TypeScript
async setModel(model: Model<any>, options: ModelMutationOptions = {}): Promise<void> {	if (!(await this._modelRuntime.checkAuth(model.provider))) {		throw new Error(`No API key for ${model.provider}/${model.id}`);	}	const previousModel = this.model;	const thinkingLevel = this._getThinkingLevelForModelSwitch(model);	this.agent.state.model = model;	this.sessionManager.appendModelChange(model.provider, model.id);	if (options.persist) {		this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);		this._addPersistedDefaultToNonEmptyScope(model);	}	// Apply thinking level for the new model.	// Per-model thinking level overrides take priority over the global default.	// Model persistence does not implicitly rewrite the global thinking default.	this.setThinkingLevel(thinkingLevel);	await this._emitModelSelect(model, previousModel, "set");}
Mô hình mới vào agent.state.model. Nếu đổi khi đang chạy, nó có hiệu lực từ yêu cầu kế tiếp: cả prepareRequest lẫn prepareNextTurnWithContext đọc lại agent.state.model trước mỗi yêu cầu (agent-session.ts:928).

Mức suy nghĩ khi đổi mô hình được chọn theo thứ tự: mức ghi kèm trong pattern --models (ví dụ sonnet:high), rồi mức riêng của mô hình trong modelThinkingLevels, rồi defaultThinkingLevel, rồi mức hiện tại (agent-session.ts:2733). cycleModel() xoay trong danh sách --models nếu có, nếu không thì trong mọi mô hình có credential (agent-session.ts:2583); cả hai nhánh cũng ghi model_change.

setThinkingLevel(): kẹp theo mô hình, chỉ ghi khi thực sự đổi
pi/packages/coding-agent/src/core/agent-session.tsdòng 2676–2699TypeScript
setThinkingLevel(level: ThinkingLevel, options: ModelMutationOptions = {}): void {	const availableLevels = this.getAvailableThinkingLevels();	const effectiveLevel = availableLevels.includes(level) ? level : this._clampThinkingLevel(level, availableLevels);	// Only persist if actually changing	const previousLevel = this.agent.state.thinkingLevel;	const isChanging = effectiveLevel !== previousLevel;	this.agent.state.thinkingLevel = effectiveLevel;	if (options.persist) {		this.settingsManager.setDefaultThinkingLevel(level);	}	if (isChanging) {		this.sessionManager.appendThinkingLevelChange(effectiveLevel);		this._emit({ type: "thinking_level_changed", level: effectiveLevel });		void this._extensionRunner.emit({			type: "thinking_level_select",			level: effectiveLevel,			previousLevel,		});	}}
Mức được kẹp theo khả năng của mô hình (clampThinkingLevel của pi-ai). Entry thinking_level_change chỉ được ghi khi mức hiệu lực thay đổi, nên chuyển qua lại giữa hai mô hình không làm phình tệp. Khi persist, settings nhận mức người dùng yêu cầu, không phải mức đã kẹp.

Lúc mở lại phiên, hai giá trị được đọc ngược từ nhánh: mức suy nghĩ lấy từ thinking_level_change cuối cùng, mô hình lấy từ model_change cuối cùng hoặc từ message assistant gần nhất (session-manager.ts:418). Virtual model là ngoại lệ: message assistant chỉ ghi tên mô hình vật lý đã trả lời, nên lựa chọn ảo chỉ tìm thấy trong model_change. _recordSelection() chạy trước mỗi lần chạy: khi lựa chọn hiện tại hoặc lựa chọn mà nhánh đang ghi là một virtual model và hai lựa chọn khác nhau, nó ghi thêm một model_change lên nhánh hiện tại, chẳng hạn sau khi /tree đưa lá sang một nhánh mà model_change cuối nằm ở nhánh kia (agent-session.ts:621).

Steer, follow-up, abort và tin nhắn tuỳ biến#

Người dùng không phải chờ agent làm xong mới gõ tiếp. Nhấn Enter khi agent đang chạy gửi một tin nhắn steer (chen vào lượt đang chạy); Alt+Enter (phím mặc định của app.message.followUp, keybindings.ts:67) gửi một follow-up (xếp hàng sau) (interactive-mode.ts:3335, 4462). Hai hàng đợi thật nằm trong Agent: vòng lặp agent lấy tin steer sau mỗi lượt, khi kết quả tool đã vào ngữ cảnh và trước yêu cầu kế tiếp; tin follow-up chỉ được lấy khi agent sắp dừng (agent-loop.ts:303, chi tiết ở chương pi-agent-core). Chế độ one-at-a-time (mặc định) lấy từng tin một, all lấy hết (agent.ts:165); AgentSession đọc chế độ từ setting steeringMode, followUpMode.

Xếp hàng: một bản văn bản cho UI, một message thật cho Agent
pi/packages/coding-agent/src/core/agent-session.tsdòng 2302–2327TypeScript
private async _queueSteer(text: string, images?: ImageContent[]): Promise<void> {	this._steeringMessages.push(text);	this._emitQueueUpdate();	const content: (TextContent | ImageContent)[] = [{ type: "text", text }];	if (images) {		content.push(...images);	}	this.agent.steer({		role: "user",		content,		timestamp: Date.now(),	});}/** * Internal: Queue a follow-up message (already expanded, no extension command check). */private async _queueFollowUp(text: string, images?: ImageContent[]): Promise<void> {	this._followUpMessages.push(text);	this._emitQueueUpdate();	const content: (TextContent | ImageContent)[] = [{ type: "text", text }];	if (images) {		content.push(...images);	}	this.agent.followUp({ role: "user", content, timestamp: Date.now() });}
AgentSession giữ bản sao văn bản trong _steeringMessages/_followUpMessages chỉ để hiển thị và phát queue_update. Bản sao được gỡ khi message user tương ứng bắt đầu, khớp theo nội dung văn bản (agent-session.ts:1120).

Văn bản được mở rộng skill và template trước khi xếp hàng, nhưng một lệnh extension thì không xếp hàng được: steer() và followUp() ném lỗi nếu gặp nó (agent-session.ts:2332). Đi qua prompt() thì khác: lệnh extension được chạy ngay, cả khi agent đang bận.

Abort#

abort(): dừng mọi thứ đang chạy rồi chờ phiên rảnh
pi/packages/coding-agent/src/core/agent-session.tsdòng 2498–2508TypeScript
async abort(): Promise<void> {	if (this._isAgentRunActive) {		this._agentRunAbortRequested = true;	}	this.abortRetry();	this.abortCompaction();	this.abortBranchSummary();	if (this._isBeforeSettle) this._abortDuringBeforeSettle = true;	this.agent.abort();	await this.waitForIdle();}
Cờ _agentRunAbortRequested chặn vòng lặp sau run trong _startRun() gọi agent.continue() thêm lần nào. Phản hồi đang stream kết thúc với stopReason: "aborted" và vẫn được ghi như mọi message khác.

Ở interactive mode, Esc không chỉ gọi abort(): nó rút mọi tin đang xếp hàng bằng clearQueue() và đặt chúng lại vào trình soạn thảo, nối trước văn bản đang gõ, để người dùng sửa rồi gửi lại (interactive-mode.ts:4711).

Tin nhắn tuỳ biến của extension#

Extension gửi message vào ngữ cảnh bằng pi.sendMessage(), đi tới sendCustomMessage(). Hàm này phải chọn giữa năm cách giao, tuỳ phiên đang chạy hay rảnh:

sendCustomMessage(): nextTurn, steer/follow-up, chạy mới, hoãn, ghi ngay
pi/packages/coding-agent/src/core/agent-session.tsdòng 2357–2393TypeScript
async sendCustomMessage<T = unknown>(	message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,	options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },): Promise<void> {	const appMessage = {		role: "custom" as const,		customType: message.customType,		// Untyped extensions can pass null/missing content; normalize at ingestion.		content: message.content ?? [],		display: message.display,		details: message.details,		timestamp: Date.now(),	} satisfies CustomMessage<T>;	if (options?.deliverAs === "nextTurn") {		this._pendingNextTurnMessages.push(appMessage);	} else if (this.isStreaming && options?.triggerTurn !== false) {		if (options?.deliverAs === "followUp") {			this.agent.followUp(appMessage);		} else {			this.agent.steer(appMessage);		}	} else if (options?.triggerTurn) {		if (this._isEmittingAgentSettled) {			this._deferredSettledActions.push(async () => await this.sendCustomMessage(message, options));			return;		}		await this._startRun({ messageType: "custom", message: appMessage });	} else if (this.isStreaming) {		// Appending now would put the message between an assistant tool call and its		// result, which providers that validate message order reject on replay. Defer		// to the end of the turn. Nothing is emitted yet: message events must not		// describe messages the session tree does not contain.		this._pendingCustomMessages.push(appMessage);	} else {		this._appendCustomMessage(appMessage);	}}
nextTurn: đính kèm vào prompt kế tiếp của người dùng. Đang chạy và được phép kích hoạt lượt: xếp vào hàng steer hoặc follow-up. Rảnh và triggerTurn: mở một lần chạy mới bắt đầu từ message này. Đang chạy nhưng không kích hoạt lượt: hoãn tới cuối lượt. Rảnh và không kích hoạt: ghi ngay.

Trường hợp hoãn có lý do cụ thể. Nếu ghi ngay khi một lượt đang chạy, message custom có thể chen giữa message assistant chứa lời gọi tool và message toolResult của nó. Provider kiểm tra thứ tự message sẽ từ chối transcript đó khi phát lại. Vì vậy message nằm trong _pendingCustomMessages cho tới turn_end, thời điểm đầu tiên mà mọi kết quả tool của lượt đã được ghi (agent-session.ts:1190). Kết quả lệnh ! đi theo cùng nguyên tắc, như mục sau cho thấy. sendUserMessage() của extension thì đi qua prompt() với source: "extension" và mặc định không mở rộng template (agent-session.ts:2429).

Lệnh ! của người dùng#

Gõ !git status trong trình soạn thảo chạy lệnh ngay trên máy, không qua mô hình, rồi đưa kết quả vào ngữ cảnh để lượt sau mô hình biết. !!git status chạy y hệt nhưng không cho mô hình thấy. Interactive mode tách hai tiền tố:

Interactive mode: ! và !!
pi/packages/coding-agent/src/modes/interactive/interactive-mode.tsdòng 3303–3319TypeScript
// Handle bash command (! for normal, !! for excluded from context)if (text.startsWith("!")) {	const isExcluded = text.startsWith("!!");	const command = isExcluded ? text.slice(2).trim() : text.slice(1).trim();	if (command) {		if (this.session.isBashRunning) {			this.showWarning("A bash command is already running. Press Esc to cancel it first.");			this.editor.setText(text);			return;		}		this.editor.addToHistory?.(text);		await this.handleBashCommand(command, isExcluded);		this.isBashMode = false;		this.updateEditorBorderColor();		return;	}}
Mỗi lúc chỉ một lệnh ! chạy. Trước khi chạy, extension nhận sự kiện user_bash và có thể tự trả kết quả hoặc cung cấp BashOperations khác, ví dụ để chạy qua SSH hay trong container (interactive-mode.ts:6960).
executeBash(): tiền tố shell, chạy, stream từng mẩu, ghi kết quả
pi/packages/coding-agent/src/core/agent-session.tsdòng 3901–3933TypeScript
async executeBash(	command: string,	onChunk?: (chunk: string) => void,	options?: { excludeFromContext?: boolean; id?: string; operations?: BashOperations },): Promise<BashResult> {	const abortController = new AbortController();	this._bashAbortControllers.add(abortController);	// Apply command prefix if configured (e.g., "shopt -s expand_aliases" for alias support)	const prefix = this.settingsManager.getShellCommandPrefix();	const shellPath = this.settingsManager.getShellPath();	const resolvedCommand = prefix ? `${prefix}\n${command}` : command;	try {		const result = await executeBashWithOperations(			resolvedCommand,			this.sessionManager.getCwd(),			options?.operations ?? createLocalBashOperations({ shellPath }),			{				onChunk: (delta) => {					onChunk?.(delta);					this._emit({ type: "bash_execution_update", id: options?.id, delta });				},				signal: abortController.signal,			},		);		this.recordBashResult(command, result, options);		return result;	} finally {		this._bashAbortControllers.delete(abortController);	}}
Setting shellCommandPrefix (ví dụ shopt -s expand_aliases) được nối trước lệnh; mỗi mẩu output đi ra listener qua sự kiện bash_execution_update. RPC mode dùng cùng hàm này cho lệnh bash của nó.

Việc chạy thật nằm trong bash-executor.ts. Nó phải chịu được lệnh in ra hàng trăm MB mà không giữ hết trong RAM:

Bộ đệm cuộn, tệp tạm, cắt phần đuôi
pi/packages/coding-agent/src/core/bash-executor.tsdòng 83–98, 130–145TypeScript
	// Start writing to temp file if exceeds threshold	if (totalBytes > DEFAULT_MAX_BYTES) {		ensureTempFile();	}	if (tempFileStream) {		tempFileStream.write(text);	}	// Keep rolling buffer	outputChunks.push(text);	outputBytes += text.length;	while (outputBytes > maxOutputBytes && outputChunks.length > 1) {		const removed = outputChunks.shift()!;		outputBytes -= removed.length;	}flushOutput();const fullOutput = outputChunks.join("");const truncationResult = truncateTail(fullOutput);if (truncationResult.truncated) {	ensureTempFile();}tempFileStream?.end();const cancelled = options?.signal?.aborted ?? false;return {	output: truncationResult.truncated ? truncationResult.content : fullOutput,	exitCode: cancelled ? undefined : (exitCode ?? undefined),	cancelled,	truncated: truncationResult.truncated,	fullOutputPath: tempFilePath,};
Output được làm sạch (bỏ mã ANSI, thay byte nhị phân, bỏ \r), giữ trong một bộ đệm cuộn khoảng 100 KB (DEFAULT_MAX_BYTES * 2, dòng 56). Quá 50 KB thì toàn bộ output còn được ghi ra tệp tạm. Cuối cùng truncateTail() giữ phần đuôi trong giới hạn 2.000 dòng hoặc 50 KB, và fullOutputPath trỏ tới tệp đầy đủ.

Kết quả trở thành một BashExecutionMessage. Điểm tinh tế: nếu agent đang chạy, message bị hoãn, cùng lý do với custom message ở mục trước:

recordBashResult(): ghi ngay khi rảnh, hoãn khi đang chạy
pi/packages/coding-agent/src/core/agent-session.tsdòng 3939–3960TypeScript
recordBashResult(command: string, result: BashResult, options?: { excludeFromContext?: boolean }): void {	const bashMessage: BashExecutionMessage = {		role: "bashExecution",		command,		output: result.output,		exitCode: result.exitCode,		cancelled: result.cancelled,		truncated: result.truncated,		fullOutputPath: result.fullOutputPath,		timestamp: Date.now(),		excludeFromContext: options?.excludeFromContext,	};	// If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering	if (this.isStreaming) {		// Queue for later - will be flushed on agent_end		this._pendingBashMessages.push(bashMessage);	} else {		this.sessionManager.appendMessage(bashMessage);		this._refreshFinalizedContext();	}}
Message hoãn được đổ ra trong khối finally của _startRun() (agent-session.ts:3985). Hàm này là public: một extension tự chạy lệnh trong user_bash vẫn gọi nó để kết quả vào session.

Entry được ghi là một entry message thường với role bashExecution. Khi dựng yêu cầu, convertToLlm() biến nó thành message user với văn bản do bashExecutionToText() tạo: dòng "Ran `lệnh`", output trong khối code, mã thoát khác 0 nếu có, và đường dẫn output đầy đủ khi bị cắt (messages.ts:82). Đây không phải một kết quả tool: mô hình không gọi nó, nên không có toolCallId nào để ghép.

Các dịch vụ xung quanh: settings, auth, model, trust, đường dẫn#

AgentSession dựa vào các dịch vụ dưới đây. Mỗi dịch vụ đủ lớn để có tài liệu riêng; mục này chỉ nêu điều cần để hiểu vòng đời một prompt.

Settings: toàn cục và dự án#

Có hai tệp: ~/.pi/agent/settings.json (toàn cục) và <cwd>/.pi/settings.json (dự án) (settings-manager.ts:186). Settings hiệu lực là hai tệp trộn sâu, tệp dự án thắng:

Trộn settings: object trộn đệ quy, giá trị khác bị thay
pi/packages/coding-agent/src/core/settings-manager.tsdòng 61–82, 142–147TypeScript
function isMergeableObject(value: unknown): value is Record<string, unknown> {	return typeof value === "object" && value !== null && !Array.isArray(value);}function deepMergeObjects(base: Record<string, unknown>, overrides: Record<string, unknown>): Record<string, unknown> {	const result = { ...base };	for (const key of Object.keys(overrides)) {		const overrideValue = overrides[key];		if (overrideValue === undefined) {			continue;		}		const baseValue = base[key];		result[key] =			isMergeableObject(baseValue) && isMergeableObject(overrideValue)				? deepMergeObjects(baseValue, overrideValue)				: overrideValue;	}	return result;}/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */function deepMergeSettings(base: Settings, overrides: Settings): Settings {	const merged = deepMergeObjects(base as Record<string, unknown>, overrides as Record<string, unknown>) as Settings;	const defaultTools = mergeDefaultTools(base.defaultTools, overrides.defaultTools);	return defaultTools === undefined ? merged : { ...merged, defaultTools };}
Object lồng nhau như compaction được trộn từng khoá, nên dự án có thể chỉ đặt compaction.reserveTokens mà vẫn giữ compaction.enabled toàn cục. Mảng bị thay nguyên, trừ defaultTools: một danh sách chỉ gồm +tên/-tên được nối vào danh sách kế thừa thay vì thay nó (settings-manager.ts:122).

Ba chi tiết vận hành đáng biết. Thứ nhất, khi dự án chưa được tin cậy, tệp dự án không được đọc (settings-manager.ts:366) và mọi lệnh ghi vào nó bị từ chối. Thứ hai, các setter như setDefaultModelAndProvider() ghi vào phạm vi toàn cục; lúc ghi, tệp được đọc lại dưới file lock và chỉ những trường mà tiến trình này đã sửa được ghi đè, nên hai tiến trình pi đổi hai setting khác nhau không xoá thay đổi của nhau (settings-manager.ts:598). Thứ ba, lỗi ghi được ghi nhận vào danh sách lỗi chứ không ném ra giữa một lượt (settings-manager.ts:576). Kiểu Settings sinh từ schema TypeBox trong settings-schema.ts (settings-schema.ts:479), cùng schema mà settings.json tham chiếu qua $schema để editor gợi ý (docs/settings.md:5); giá trị mặc định nằm ở settings-defaults.ts.

Credential: auth.json và làm mới OAuth#

AuthStorage cài đặt interface CredentialStore của pi-ai trên tệp ~/.pi/agent/auth.json, mỗi provider một credential: hoặc api_key, hoặc oauth với access, refresh, expires (auth/types.ts:17). Tệp được tạo với quyền 0600, thư mục cha 0700 (auth-storage.ts:37), mọi lần đọc ghi đi qua proper-lockfile. Khi đọc một api_key, giá trị được giải qua resolveConfigValue(): $NAME đọc biến môi trường, !lệnh chạy lệnh shell (auth-storage.ts:442).

Interface CredentialStore chỉ có một đường ghi, modify(provider, fn) (ngoài delete để đăng xuất): fn nhận credential hiện tại bên trong lock (auth/types.ts:78). Thiết kế đó phục vụ đúng một việc khó, làm mới OAuth khi nhiều tiến trình pi cùng chạy:

pi-ai: làm mới OAuth với khoá kiểm tra hai lần
pi/packages/ai/src/auth/resolve.tsdòng 119–154TypeScript
export async function refreshStoredOAuthCredential(	credentials: CredentialStore,	providerId: string,	oauth: OAuthAuth,	needsRefresh: (credential: OAuthCredential) => boolean,	signal: AbortSignal,): Promise<OAuthCredential | undefined> {	let post: Credential | undefined;	const lockWait = new AbortController();	const cancelLockWait = () => lockWait.abort(signal.reason);	signal.addEventListener("abort", cancelLockWait, { once: true });	if (signal.aborted) cancelLockWait();	try {		post = await credentials.modify(			providerId,			async (current) => {				signal.removeEventListener("abort", cancelLockWait);				signal.throwIfAborted();				if (current?.type !== "oauth") return undefined; // logged out meanwhile				if (!needsRefresh(current)) return undefined; // another process/request refreshed				try {					return await oauth.refresh(current, AbortSignal.timeout(DEFAULT_OAUTH_REFRESH_TIMEOUT_MS));				} catch (error) {					throw new ModelsError("oauth", `OAuth refresh failed for ${providerId}`, { cause: error });				}			},			{ signal: lockWait.signal },		);	} catch (error) {		if (error instanceof ModelsError) throw error;		signal.throwIfAborted();		throw new ModelsError("auth", `Credential store modify failed for ${providerId}`, { cause: error });	} finally {		signal.removeEventListener("abort", cancelLockWait);	}	return post?.type === "oauth" ? post : undefined;
Token còn dưới năm phút được làm mới. Điều kiện cần làm mới được kiểm tra lại bên trong lock: nếu một tiến trình khác vừa làm mới xong, fn trả undefined và giữ nguyên. Lời gọi refresh bỏ qua tín hiệu huỷ của người gọi, chỉ bị giới hạn bởi timeout 15 giây, vì provider có thể đã xoay vòng refresh token: huỷ giữa chừng sẽ làm mất token hợp lệ duy nhất.

Thứ tự nguồn credential: --api-key lúc chạy, rồi auth.json, rồi apiKey trong models.json, cuối cùng biến môi trường của provider (docs/models.md:23). Các bản cài cũ dùng oauth.json và apiKeys trong settings; migrateAuthToAuthJson() chuyển chúng sang auth.json một lần lúc khởi động (migrations.ts:21).

Mô hình: ModelRuntime, ModelRegistry và model-resolver#

ModelRuntime là bộ sưu tập Models của pi-ai đã cấu hình cho coding agent (model-runtime.ts:173): provider dựng sẵn với danh mục mô hình có thể cập nhật từ pi.dev, provider khai báo trong models.json, provider và virtual model do extension đăng ký, credential từ AuthStorage. Mỗi lần streamSimple(), nó giải credential cho đúng yêu cầu đó rồi mới gọi provider (model-runtime.ts:659), nên một token OAuth hết hạn giữa phiên được làm mới trong suốt. Định tuyến virtual model, mô hình ảnh và classifier thuộc chương Pi 1.0. ModelRegistry chỉ là một mặt tiền đồng bộ bọc ModelRuntime cho extension (model-registry.ts:44).

--model nhận một pattern, không cần id chính xác. resolveCliModel() xét trên mọi mô hình, kể cả mô hình chưa có credential, để --api-key dùng được cho lần cài đặt đầu:

resolveCliModel(): --provider, provider/id, mờ, hậu tố thinking
pi/packages/coding-agent/src/core/model-resolver.tsdòng 459–486TypeScript
// Important: use *all* models here, not just models with pre-configured auth.// This allows "--api-key" to be used for first-time setup.const models = [...modelRuntime.getModels()];if (models.length === 0) {	return {		model: undefined,		warning: undefined,		error: "No models available. Check your installation or add models to models.json.",	};}const provider = cliProvider	? models.find((m) => m.provider.toLowerCase() === cliProvider.toLowerCase())?.provider	: undefined;if (cliProvider && !provider) {	return {		model: undefined,		warning: undefined,		error: `Unknown provider "${cliProvider}". Use --list-models to see available providers/models.`,	};}const result = resolveModelReference(	{ text: cliModel, provider, thinkingSuffix: "strict", fuzzy: true },	models,	// An inline key goes to whichever provider is selected, so stored credentials must not choose it	inlineApiKey ? () => false : (providerId) => modelRuntime.hasConfiguredAuth(providerId),);
Quy tắc trong resolveModelReference(): provider/id chỉ là gợi ý vì chính chuỗi đó có thể là id trên provider khác; khớp chính xác thắng khớp mờ; khớp mờ so chuỗi con trên id và tên, ưu tiên alias hơn bản có ngày (claude-sonnet-4-5 hơn claude-sonnet-4-5-20250929) (model-resolver.ts:286); hậu tố :high chỉ được tách khi cả chuỗi không khớp mô hình nào. Khớp ở nhiều provider có credential thì báo mơ hồ thay vì đoán; không khớp nhưng provider đã rõ thì dùng id tuỳ biến kèm cảnh báo.

Khi không có --model và session không có gì để khôi phục, findInitialModel() chọn mô hình khởi đầu:

findInitialModel(): mặc định trong settings, rồi mô hình mặc định của provider có credential
pi/packages/coding-agent/src/core/model-resolver.tsdòng 576–606TypeScript
// 2. Try saved default from settings if auth is configured.if (defaultProvider && defaultModelId) {	const found = modelRuntime.getModel(defaultProvider, defaultModelId);	if (found && modelRuntime.hasConfiguredAuth(found.provider)) {		model = found;		const perModel = modelThinkingLevels?.[`${defaultProvider}/${defaultModelId}`];		if (perModel) {			thinkingLevel = perModel;		} else if (defaultThinkingLevel) {			thinkingLevel = defaultThinkingLevel;		}		return { model, thinkingLevel, fallbackMessage: undefined };	}}// 3. Try first available model with valid API keyconst availableModels = [...modelRuntime.getAvailableSnapshot()];if (availableModels.length > 0) {	// Try to find a default model from known providers	for (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) {		const defaultId = defaultModelPerProvider[provider];		const match = availableModels.find((m) => m.provider === provider && m.id === defaultId);		if (match) {			return { model: match, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };		}	}	// If no default found, use first available	return { model: availableModels[0], thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined };}
Bước 1 (lược) dùng mô hình đầu tiên của --models khi không phải đang tiếp tục một phiên. Ở main.ts, --models được xử lý trước: nếu mô hình mặc định trong settings nằm trong phạm vi thì dùng nó, không thì dùng mô hình đầu tiên (main.ts:500).

Trust: quyết định trước khi nạp tài nguyên dự án#

Một thư mục lạ có thể chứa .pi/extensions/ là mã TypeScript chạy trong tiến trình pi. Vì vậy quyết định tin cậy dự án phải có trước khi những thứ đó được nạp (docs/how-pi-works.md:49). Chỉ những tài nguyên có thể thực thi hoặc đổi cấu hình mới đòi quyết định: .pi/settings.json, .pi/mcp.json, các thư mục extensions, skills, prompts, themes, SYSTEM.md, APPEND_SYSTEM.md, và .agents/skills ở thư mục hiện tại hoặc thư mục cha (trust-manager.ts:186). Thứ tự nạp trong ResourceLoader.reload() cho thấy cách làm:

Nạp extension không có tài nguyên dự án, hỏi trust, rồi mới nạp đầy đủ
pi/packages/coding-agent/src/core/resource-loader.tsdòng 505–528TypeScript
async loadProjectTrustExtensions(): Promise<LoadExtensionsResult> {	// Force untrusted project settings for the bootstrap pass. This keeps project-local	// extensions/packages out while still loading user/global and temporary CLI extensions.	this.settingsManager.setProjectTrusted(false);	await this.settingsManager.reload();	return this.loadCurrentExtensionSet({ includeInlineFactories: true });}async reload(options?: ResourceLoaderReloadOptions): Promise<void> {	resetTimings("extensions");	if (this.loaded) {		clearExtensionCache();	}	let preTrustExtensions: LoadExtensionsResult | undefined;	if (options?.resolveProjectTrust) {		preTrustExtensions = await this.loadProjectTrustExtensions();		const projectTrusted = await options.resolveProjectTrust({ extensionsResult: preTrustExtensions });		this.settingsManager.setProjectTrusted(projectTrusted);	}	// reload() preserves SettingsManager.projectTrusted and reloads settings for that trust state.	await this.settingsManager.reload();
Lượt nạp đầu ép trạng thái chưa tin cậy, nên chỉ extension dựng sẵn, extension của người dùng và extension truyền từ dòng lệnh được chạy. Chúng có thể trả lời sự kiện project_trust.
Thứ tự quyết định trust
pi/packages/coding-agent/src/core/project-trust.tsdòng 47–85TypeScript
export async function resolveProjectTrusted(options: ResolveProjectTrustedOptions): Promise<boolean> {	if (options.trustOverride !== undefined) {		return options.trustOverride;	}	if (!hasTrustRequiringProjectResources(options.cwd)) {		return true;	}	if (options.extensionsResult) {		const { result, errors } = await emitProjectTrustEvent(			options.extensionsResult,			{ type: "project_trust", cwd: options.cwd },			options.projectTrustContext,		);		for (const error of errors) {			options.onExtensionError?.(`Extension "${error.extensionPath}" project_trust error: ${error.error}`);		}		if (result) {			const trusted = result.trusted === "yes";			if (result.remember === true) {				options.trustStore.set(options.cwd, trusted);			}			return trusted;		}	}	const decision = options.trustStore.get(options.cwd);	if (decision !== null) {		return decision;	}	switch (options.defaultProjectTrust ?? SETTINGS_DEFAULTS.defaultProjectTrust) {		case "always":			return true;		case "never":			return false;		case "ask":			break;	}
--approve/--no-approve thắng tất cả. Sau đó là extension, rồi quyết định đã lưu cho thư mục này hoặc thư mục cha gần nhất trong ~/.pi/agent/trust.json, rồi setting defaultProjectTrust. Với "ask", interactive mode hiện hộp chọn; các chế độ không có UI coi như không tin cậy.

Trust không phải hộp cát: tool vẫn chạy với quyền hệ điều hành của tiến trình, và context file như AGENTS.md vẫn được nạp dù không tin cậy (docs/security.md:27). Tài liệu cũng nói rõ một kẽ hở: setting sessionDir của dự án được đọc lúc chọn session, trước khi trust được giải quyết (docs/security.md:31). Trong code, main.ts dùng một SettingsManager mở ở cwd khởi động để lấy sessionDir, rồi mới dựng runtime với trạng thái trust đã quyết định (main.ts:686).

Đường dẫn: config.ts#

Thư mục agent: ~/.pi/agent, đổi được bằng PI_CODING_AGENT_DIR
pi/packages/coding-agent/src/config.tsdòng 604–611TypeScript
/** Get the agent config directory (e.g., ~/.pi/agent/) */export function getAgentDir(): string {	const envDir = process.env[ENV_AGENT_DIR];	if (envDir) {		return expandTildePath(envDir);	}	return join(homedir(), CONFIG_DIR_NAME, "agent");}
Tên biến môi trường suy ra từ tên ứng dụng (config.ts:584), nên một bản phân phối đổi tên qua piConfig trong package.json có thư mục và biến riêng. Quy tắc của repo: mọi đường dẫn tới tài nguyên của gói phải đi qua helper trong config.ts, không dùng __dirname, vì pi chạy được từ mã nguồn, từ gói npm và từ binary độc lập (AGENTS.md:22).
Đường dẫnNội dung
~/.pi/agent/settings.jsonsettings toàn cục
~/.pi/agent/auth.jsoncredential, quyền 0600
~/.pi/agent/models.jsonprovider và mô hình tuỳ biến
~/.pi/agent/trust.jsonquyết định trust theo thư mục
~/.pi/agent/sessions/tệp session, một thư mục con cho mỗi cwd (chương Session)
~/.pi/agent/prompts/, themes/, bin/prompt template, theme tuỳ biến, fd/rg do pi quản lý
<cwd>/.pi/settings.json, mcp.json, extensions/, skills/, prompts/, themes/, SYSTEM.md, APPEND_SYSTEM.md của dự án (cần trust)

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

Tệp session là bản gốc, mỗi yêu cầu được dựng lại từ nó#

Cách đơn giản hơn là để Agent giữ lịch sử trong bộ nhớ và thỉnh thoảng lưu ra đĩa. Pi không làm vậy vì nhiều thứ ghi vào session mà không đi qua mảng message của Agent: extension ghi context_edit hay compaction ở boundary, retry loại một lần thử hỏng, /tree dời lá sang nhánh khác. Nếu yêu cầu kế tiếp đọc từ bộ nhớ, nó sẽ khác cái mà một lần resume dựng ra từ tệp. Dựng lại từ projection trước mọi yêu cầu (prepareRequest) loại bỏ khả năng lệch đó. Bài "The Session You Cannot Take With You" của Earendil nói thẳng lập trường này: nhật ký sự kiện cục bộ là bản chuẩn (earendil.com/posts/session-portability).

System prompt là bản vá trong transcript#

Prompt cache của provider khớp theo tiền tố token chính xác: đổi system prompt hay danh sách tool ở đầu yêu cầu làm trượt cache cho toàn bộ phần sau (Earendil, "Prompt Caching In Agents"). Ghi thay đổi thành một message system mới ở cuối transcript giữ nguyên tiền tố cũ trên các mô hình hỗ trợ message system giữa hội thoại, và vì nó là một entry, thay đổi đó sống sót qua resume và qua điều hướng cây. Changelog 0.86.0 ghi đúng hai lý do này (CHANGELOG.md:609). Đổi lại, bản thân "system prompt hiện tại" không còn là một trường; muốn biết nó phải phát lại các message system, việc getCurrentSystemMessage() làm.

Không bao giờ chen message giữa lời gọi tool và kết quả#

Custom message và kết quả lệnh ! đến lúc agent đang chạy đều bị hoãn (agent-session.ts:2385). Cái giá là người dùng thấy output của ! ở vùng chờ cho tới cuối lần chạy, và mô hình chỉ biết tới nó ở lần chạy sau. Cái được là transcript luôn hợp lệ khi phát lại cho bất kỳ provider nào, kể cả provider khắt khe về thứ tự.

Bỏ khỏi ngữ cảnh thay vì xoá#

Retry và phục hồi sau tràn ngữ cảnh ghi context_edit thay vì sửa entry cũ. Lịch sử thô, UI, bản xuất HTML và thống kê token vẫn giữ lần thử hỏng (docs/session-format.md:141); getSessionStats() cộng usage trên mọi entry, kể cả phần đã bị compaction, để con số phản ánh đúng số tiền đã trả (agent-session.ts:4238). Context edit được thêm ở 0.87.0 (CHANGELOG.md:549).

Retry ở tầng phiên, tách khỏi retry của SDK#

SDK của provider chỉ thử lại được trước khi có phản hồi. Một luồng đứt giữa chừng đã sinh ra nửa message assistant, có thể cả lời gọi tool; thử lại đúng nghĩa là bỏ nửa đó khỏi ngữ cảnh rồi chạy lại cả lượt. Chỉ tầng biết về session mới làm được việc này, nên nó nằm ở AgentSession chứ không ở pi-ai.

Extension trước, listener sau#

Handler message_end của extension được phép thay cả message, chẳng hạn để che một khoá API lỡ xuất hiện trong nội dung. Phát cho extension trước và sửa đối tượng tại chỗ bảo đảm UI, agent state và tệp session cùng thấy một phiên bản (agent-session.ts:1287).

Thay cả phiên thay vì sửa phiên tại chỗ#

Settings dự án, extension dự án, provider do extension đăng ký đều gắn với cwd. Khi /resume mở một session của dự án khác, sửa từng thứ tại chỗ dễ sót. AgentSessionRuntime chọn cách thô hơn nhưng chắc chắn: abort để lượt dở được ghi vào phiên cũ, huỷ, rồi dựng lại toàn bộ services cho cwd mới (agent-session-runtime.ts:168). Cái giá là extension phải hiểu rằng ctx cũ đã chết.

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

  • Compaction thủ công chặn prompt. Trong lúc /compact chạy, prompt() ném lỗi (agent-session.ts:2110); interactive mode tự xếp tin nhắn chờ thay vì gọi nó (interactive-mode.ts:3321). SDK và RPC phải tự xử lý.
  • isIdle khác !isStreaming. Compaction và tóm tắt nhánh đang chạy làm phiên không rảnh dù không có lần chạy nào (agent-session.ts:1478).
  • @ trong trình soạn thảo không đính kèm tệp. Chỉ đối số @file trên dòng lệnh được đọc vào prompt; ở interactive mode, @path chỉ là văn bản.
  • sendUserMessage() của extension không mở rộng template trừ khi truyền expandPromptTemplates: true (agent-session.ts:2427).
  • Lỗi trong hook thành một phản hồi lỗi. Một ngoại lệ trong lúc chạy (credential hỏng, router của virtual model từ chối) không lan ra ngoài prompt(): Agent tạo một message assistant với stopReason: "error" và đi qua message_end như mọi message (agent.ts:542). Nó được ghi vào session, và được thử lại nếu thông điệp lỗi khớp mẫu.
  • Phục hồi tràn ngữ cảnh chỉ một lần. Nếu ngữ cảnh sau compaction vẫn tràn, phiên dừng với thông báo lỗi thay vì lặp (agent-session.ts:3079).
  • Mức suy nghĩ lưu vào settings là mức yêu cầu. setThinkingLevel("xhigh", { persist: true }) trên một mô hình chỉ có high ghi thinking_level_change: high vào session nhưng defaultThinkingLevel: xhigh vào settings.
  • Phiên cũ được vá metadata. Mở một session không có thinking_level_change nào sẽ ghi thêm một entry như vậy ngay lúc tạo phiên (sdk.ts:440), nên tệp thay đổi dù người dùng chưa gõ gì.
  • ctx cũ của extension. Sau ctx.newSession(), ctx.fork(), ctx.switchSession() hay ctx.reload(), mọi lời gọi qua ctx cũ ném lỗi stale; phần việc sau khi thay phiên phải nằm trong withSession.

Đọc code ở đâu#

Thứ tự đề xuất: dựng phiên trước, rồi lần theo một prompt, rồi các nhánh phụ.

#Tệp và vùngĐọc để hiểu
1sdk.ts:191Khôi phục mô hình, mức suy nghĩ; tạo Agent với convertToLlm, streamFn, transformContext.
2agent-session.ts:379Trạng thái của lớp và các hook cài trong constructor.
3agent-session.ts:2083prompt(): lệnh extension, input, skill, template, hàng đợi, kiểm tra auth.
4agent-session.ts:1863_startRun(), _prepareRun(), _handlePostAgentRun(), boundary trước khi rảnh.
5agent-session.ts:1102Ghi session từ sự kiện.
6agent-session.ts:766prepareRequest, finishTurn, prepareNextTurnWithContext.
7agent-loop.ts:172Các hook trên được gọi ở đâu trong vòng lặp agent.
8agent-session.ts:3761, 1239Retry và context_edit loại lần thử hỏng.
9agent-session.ts:2990Các điểm kích hoạt compaction tự động.
10agent-session.ts:2517Đổi mô hình, mức suy nghĩ, chế độ hàng đợi.
11agent-session.ts:2299Hàng đợi, custom message, abort.
12packages/coding-agent/src/core/bash-executor.ts, agent-session.ts:3888Lệnh !.
13packages/coding-agent/src/core/messages.ts, packages/coding-agent/docs/message-types.mdRole riêng và convertToLlm().
14packages/coding-agent/src/core/agent-session-services.ts, packages/coding-agent/src/core/agent-session-runtime.tsServices theo cwd, thay phiên.
15packages/coding-agent/src/core/settings-manager.ts, packages/coding-agent/src/core/settings-schema.ts, packages/coding-agent/docs/settings.mdHai tầng settings, trộn, ghi an toàn.
16packages/coding-agent/src/core/auth-storage.ts, packages/ai/src/auth/resolve.tsauth.json, lock, làm mới OAuth.
17packages/coding-agent/src/core/model-resolver.ts, packages/coding-agent/src/core/model-runtime.ts, packages/coding-agent/docs/models.mdPattern --model, mô hình khởi đầu, credential theo yêu cầu.
18packages/coding-agent/src/core/project-trust.ts, packages/coding-agent/src/core/trust-manager.ts, packages/coding-agent/docs/security.mdTrust trước khi nạp tài nguyên dự án.
19config.ts:577Các đường dẫn dưới ~/.pi/agent.

Tóm tắt#

  • AgentSession bọc Agent của pi-agent-core và dùng chung cho mọi chế độ chạy; nó nghe mọi sự kiện để ghi session và cài hook để mỗi yêu cầu được dựng lại từ projection của session.
  • Một prompt đi qua: lệnh extension, sự kiện input, skill, prompt template, rồi hoặc vào hàng đợi (nếu đang chạy) hoặc mở một lần chạy: compaction trước prompt, before_agent_start, bản vá system prompt, agent.prompt(), vòng lặp sau run.
  • Chỉ message_end dẫn tới ghi: bốn role của provider thành entry message, custom thành custom_message; các entry khác (mô hình, mức suy nghĩ, compaction, context edit, nhãn) được ghi bởi lời gọi tương ứng.
  • System prompt và bộ tool sống trong transcript dưới dạng message system chứa bản vá, để giữ prompt cache và sống sót qua resume.
  • Retry ở tầng phiên: lỗi tạm thời được thử lại tối đa 3 lần với độ trễ 2, 4, 8 giây; lần hỏng bị loại khỏi ngữ cảnh bằng context_edit, không bị xoá.
  • Compaction được kích hoạt ở năm điểm: trước prompt, sau run, giữa hai lượt, trước yêu cầu của virtual model, và thủ công.
  • Custom message và output ! không bao giờ chen giữa lời gọi tool và kết quả: chúng được hoãn tới cuối lượt hoặc cuối lần chạy.
  • Xung quanh là settings hai tầng (dự án thắng, trộn sâu), auth.json có lock và làm mới OAuth kiểm tra hai lần, bộ giải pattern mô hình, và quyết định trust có trước khi nạp tài nguyên dự án.

Mục lục

Giao diện