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.
Trong chương này18 mục
- Vai trò
- Mô hình tư duy
- Data model: cấu hình, sự kiện, message
- Dựng một phiên: services, SDK và runtime
- Bước 1: prompt() xử lý đầu vào
- Bước 2: chuẩn bị và chạy một lần chạy
- Bước 3: sự kiện và ghi session
- Lần theo một prompt có một lời gọi tool
- Tự động thử lại khi provider lỗi
- Nơi compaction được kích hoạt
- Đổi mô hình và mức suy nghĩ
- Steer, follow-up, abort và tin nhắn tuỳ biến
- Lệnh ! của người dùng
- Các dịch vụ xung quanh: settings, auth, model, trust, đường dẫn
- Quyết định thiết kế
- Cạm bẫy và trường hợp biên
- Đọc code ở đâu
- 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ó:
/** * 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. */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:
/new, /resume, /fork.!.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)
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:
// 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ệnagent_settled. Một lần chạy có thể gọiagent.prompt()một lần rồiagent.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ó
idvà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_endvà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.
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;}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:
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#
/** 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):
/** * 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.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; }}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:
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, };!! đặ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.
- Services theo cwd
createAgentSessionServices()tạoModelRuntime,SettingsManager,DefaultResourceLoadercho một cwd. - Phiên
createAgentSession()trongsdk.tschọn mô hình, mức suy nghĩ (thinking level), tạoAgentvàAgentSession. - Runtime
AgentSessionRuntimegiữ 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#
/** * 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[];}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, };}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:
// 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}`; }}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:
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:
// 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):
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:
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#
async prompt(text: string, options?: PromptOptions): Promise<void> { if (this._isEmittingAgentSettled) { this._deferredSettledActions.push(async () => await this.prompt(text, options)); return; } const expandPromptTemplates = options?.expandPromptTemplates ?? true; const preflightResult = options?.preflightResult; // Handle extension commands first (execute immediately, even during streaming) // Extension commands manage their own LLM interaction via pi.sendMessage() if (expandPromptTemplates && text.startsWith("/")) { const handled = await this._tryExecuteExtensionCommand(text); if (handled) { // Extension command executed, no prompt to send preflightResult?.("handled"); return; } } if (this._compactionAbortController !== undefined) { throw new Error( "Cannot submit a prompt while compaction is in progress. Wait for compaction to finish and retry.", ); } // Emit input event for extension interception (before skill/template expansion) const processedInput = await this._runInputHandlers( text, options?.images, options?.source ?? "interactive", this.isStreaming ? options?.streamingBehavior : undefined, ); if (!processedInput) { preflightResult?.("handled"); return; } const { text: currentText, images: currentImages } = processedInput; // Expand skill commands (/skill:name args) and prompt templates (/template args) let expandedText = currentText; if (expandPromptTemplates) { expandedText = this._expandSkillCommand(expandedText); expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]); }/tên do pi.registerCommand đăng ký) chạy ngay cả khi agent đang bận, trước kiểm tra compaction. Sự kiện input cho extension xem văn bản thô, trước khi skill và template được mở rộng.Đi theo từng cửa:
- Đang phát agent_settled?Nếu một listener của
agent_settledgọiprompt(), lời gọi được hoãn vào_deferredSettledActionsvà chạy sau khi sự kiện phát xong (dòng 2093 đến 2096). - 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 quaemitErrorchứ không ném ra (agent-session.ts:2180). - 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). - Skill và prompt template
/skill:tên argsđược thay bằng nội dung SKILL.md;/tên argskhớp một prompt template được thay bằng template đã điền tham số.
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;<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:
// 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"), );}_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ị:
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();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.
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#
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).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;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á đó:
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#
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();}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ì:
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);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:
} 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ẽ.
// 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();}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:
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 };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):
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, };};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ểm | Entry | Nơi ghi |
|---|---|---|
| Tạo phiên mới (SDK) | model_change, thinking_level_change | sdk.ts:440 |
--name lúc khởi động | session_info | main.ts:716 |
message_end của system, user, assistant, toolResult | message | agent-session.ts:1158 |
message_end của custom | custom_message | agent-session.ts:1150 |
| Custom message gửi khi rảnh hoặc hoãn tới cuối lượt | custom_message | agent-session.ts:2395 |
Lệnh ! khi rảnh, hoặc sau lần chạy | message (role bashExecution) | agent-session.ts:3957, 3989 |
| Đổi hoặc xoay vòng mô hình | model_change | agent-session.ts:2549, 2616, 2651 |
| Mức suy nghĩ thực sự thay đổi | thinking_level_change | agent-session.ts:2691 |
| Retry hoặc phục hồi sau tràn ngữ cảnh | context_edit với replacement: null | agent-session.ts:1267 |
| Compaction thủ công hoặc tự động | compaction | agent-session.ts:2911, 3237 |
| Router của virtual model đổi trạng thái | custom (pi.virtual-model-state) | agent-session.ts:829 |
Extension gọi pi.appendEntry() | custom | agent-session.ts:3466 |
| Extension ghi ở boundary | custom, custom_message, context_edit, compaction | agent-session.ts:946 |
| Đặt tên phiên | session_info | agent-session.ts:4003 |
| Gắn nhãn | label | agent-session.ts:3480 |
/tree có tóm tắt nhánh | branch_summary | agent-session.ts:4170 |
| Làm ấm prompt cache | usage (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 ┤ │ │
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ự:
- Đầu vào
prompt()không gặp lệnh extension, không có handlerinput, văn bản không phải skill hay template. Agent đang rảnh, mô hình có credential: gọi_startRun(). - Chuẩn bịPhiên mới chưa có assistant nào nên không kiểm tra compaction.
before_agent_startkhông đổi gì. Transcript chưa có messagesystemnà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ó. - Hai message đầu
message_end(system)rồimessage_end(user). Entry ① chỉ nằm trong bộ nhớ cùngmodel_changevàthinking_level_changemàsdk.tsghi lúc tạo phiên; khi entry ② (messageuser) được ghi,SessionManagermới tạo tệp và viết header cùng mọi entry đang có (chương Session). - Yêu cầu thứ nhất
prepareRequestthay context bằng projection của session. Ba lớptransformContextchạy (sự kiệncontext, ẩn khai báo, prompt ép buộc),convertToLlm()hạ role,streamFngửi quaModelRuntime. TUI vẽ dần từmessage_update. - Lời gọi toolPhản hồi kết thúc với
stopReason: "toolUse"; entry ③ được ghi. HookbeforeToolCallpháttool_callcho extension (có thể chặn), toolreadchạy, hookafterToolCallpháttool_result(có thể sửa) và thu nhỏ ảnh nếu có. Entry ④ là kết quả tool. - Hết lượt 1
finishTurnphát boundaryturn_endcho extension; sự kiệnturn_endđổ các custom message bị hoãn (không có). - Lượt 2
prepareNextTurnWithContextkiểm tra ngưỡng compaction và tính bản vá system (không đổi, nên không có messagesystemmới). Yêu cầu thứ hai trả về văn bản vớistopReason: "stop"; entry ⑤. - Kết thúcKhông còn lời gọi tool, không có tin nhắn xếp hàng:
agent_endvớiwillRetry: false._handlePostAgentRun()trảfalse, boundaryagent_before_settlekhông xin chạy tiếp, khốifinallyphátagent_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ự | Entry | Nội dung chính | Ghi bởi |
|---|---|---|---|
| 0 | session (header) | version: 3, id, cwd | SessionManager.newSession() |
| 1 | model_change | provider, modelId ban đầu | sdk.ts |
| 2 | thinking_level_change | mức suy nghĩ ban đầu | sdk.ts |
| 3 ① | message, role system | sections đầy đủ, toolsAdded bốn tool | _handleAgentEvent |
| 4 ② | message, role user | văn bản prompt | _handleAgentEvent |
| 5 ③ | message, role assistant | khối toolCall gọi read, usage, stopReason: "toolUse" | _handleAgentEvent |
| 6 ④ | message, role toolResult | toolCallId, nội dung tệp | _handleAgentEvent |
| 7 ⑤ | message, role assistant | câ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) quabuildRequestOptions(sdk.ts:340). - Tầng phiên: AgentSession xét phản hồi lỗi sau
agent_endvà chạy lại cả lượt. Phần này nằm trọn trongagent-session.ts.
/** * 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);}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).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;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ó:
/** * 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();}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ểm | Hàm | Lý do | Chạ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ị abort | không |
Sau agent_end | _handlePostAgentRun() gọi _checkCompaction(message) | overflow hoặc threshold | có, 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() | threshold | không cần: lượt chưa bắt đầu |
| Trước yêu cầu của virtual model | prepareRequest | threshold, theo mô hình được route | không cần |
| Thủ công | compact() (/compact, RPC, ctx.compact()) | manual | không bao giờ |
* 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; }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).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;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.
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.
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");}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(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, }); }}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.
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() });}_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#
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();}_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:
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ố:
// 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; }}! 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).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); }}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:
// 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,};\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(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(); }}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:
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 };}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:
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;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:
// 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),);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:
// 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 };}--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:
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();project_trust.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#
/** 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");}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ẫn | Nội dung |
|---|---|
~/.pi/agent/settings.json | settings toàn cục |
~/.pi/agent/auth.json | credential, quyền 0600 |
~/.pi/agent/models.json | provider và mô hình tuỳ biến |
~/.pi/agent/trust.json | quyế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
/compactchạ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ý. isIdlekhá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ố@filetrên dòng lệnh được đọc vào prompt; ở interactive mode,@pathchỉ là văn bản.sendUserMessage()của extension không mở rộng template trừ khi truyềnexpandPromptTemplates: 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():Agenttạo một messageassistantvớistopReason: "error"và đi quamessage_endnhư 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óhighghithinking_level_change: highvào session nhưngdefaultThinkingLevel: xhighvào settings. - Phiên cũ được vá metadata. Mở một session không có
thinking_level_changenà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()hayctx.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 trongwithSession.
Đọ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óm tắt#
AgentSessionbọcAgentcủ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_enddẫn tới ghi: bốn role của provider thành entrymessage,customthànhcustom_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
systemchứ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.jsoncó 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.