/** * 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. */ import { type Agent, type AgentEvent, type AgentMessage, type AgentState, type AgentTool, ThinkingLevel } from "@oh-my-pi/pi-agent-core"; import { type CompactionResult } from "@oh-my-pi/pi-agent-core/compaction"; import type { AssistantMessage, Effort, ImageContent, Message, MessageAttribution, Model, ProviderSessionState, ServiceTier, SimpleStreamOptions, TextContent, ToolChoice, UsageReport } from "@oh-my-pi/pi-ai"; import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async"; import type { Rule } from "../capability/rule"; import { type ModelRegistry } from "../config/model-registry"; import { type ResolvedModelRoleValue } from "../config/model-resolver"; import { type PromptTemplate } from "../config/prompt-templates"; import type { Settings, SkillsSettings } from "../config/settings"; import { RawSseDebugBuffer } from "../debug/raw-sse-buffer"; import { type PythonResult } from "../eval/py/executor"; import { type BashResult } from "../exec/bash-executor"; import type { TtsrManager } from "../export/ttsr"; import type { LoadedCustomCommand } from "../extensibility/custom-commands"; import type { CustomTool } from "../extensibility/custom-tools/types"; import type { ExtensionRunner } from "../extensibility/extensions"; import type { CompactOptions, ContextUsage } from "../extensibility/extensions/types"; import type { Skill, SkillWarning } from "../extensibility/skills"; import { type FileSlashCommand } from "../extensibility/slash-commands"; import { GoalRuntime } from "../goals/runtime"; import type { Goal, GoalModeState } from "../goals/state"; import type { HindsightSessionState } from "../hindsight/state"; import { type DiscoverableMCPSearchIndex, type DiscoverableMCPTool } from "../mcp/discoverable-tool-metadata"; import type { PlanModeState } from "../plan-mode/state"; import { type AgentRegistry } from "../registry/agent-registry"; import { type SecretObfuscator } from "../secrets/obfuscator"; import { type DiscoverableTool, type DiscoverableToolSearchIndex } from "../tool-discovery/tool-index"; import type { CheckpointState } from "../tools/checkpoint"; import { type TodoItem, type TodoPhase } from "../tools/todo-write"; import type { ClientBridge } from "./client-bridge"; import { type CustomMessage } from "./messages"; import type { BranchSummaryEntry, NewSessionOptions, SessionContext, SessionManager } from "./session-manager"; import { ToolChoiceQueue } from "./tool-choice-queue"; import { YieldQueue } from "./yield-queue"; /** Session-specific events that extend the core AgentEvent */ export type AgentSessionEvent = AgentEvent | { type: "auto_compaction_start"; reason: "threshold" | "overflow" | "idle"; action: "context-full" | "handoff"; } | { type: "auto_compaction_end"; action: "context-full" | "handoff"; result: CompactionResult | undefined; aborted: boolean; willRetry: boolean; errorMessage?: string; /** True when compaction was skipped for a benign reason (no model, no candidates, nothing to compact). */ skipped?: boolean; } | { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string; } | { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string; } | { type: "retry_fallback_applied"; from: string; to: string; role: string; } | { type: "retry_fallback_succeeded"; model: string; role: string; } | { type: "ttsr_triggered"; rules: Rule[]; } | { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number; } | { type: "todo_auto_clear"; } | { type: "irc_message"; message: CustomMessage; } | { type: "notice"; level: "info" | "warning" | "error"; message: string; source?: string; } | { type: "thinking_level_changed"; thinkingLevel: ThinkingLevel | undefined; } | { type: "goal_updated"; goal: Goal | null; state?: GoalModeState; }; /** Listener function for agent session events */ export type AgentSessionEventListener = (event: AgentSessionEvent) => void; export type AsyncJobSnapshotItem = Pick; export interface AsyncJobSnapshot { running: AsyncJobSnapshotItem[]; recent: AsyncJobSnapshotItem[]; delivery: AsyncJobDeliveryState; } export interface AgentSessionConfig { agent: Agent; sessionManager: SessionManager; settings: Settings; /** Models to cycle through with Ctrl+P (from --models flag) */ scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel; }>; /** Initial session thinking selector. */ thinkingLevel?: ThinkingLevel; /** Prompt templates for expansion */ promptTemplates?: PromptTemplate[]; /** File-based slash commands for expansion */ slashCommands?: FileSlashCommand[]; /** Extension runner (created in main.ts with wrapped tools) */ extensionRunner?: ExtensionRunner; /** Loaded skills (already discovered by SDK) */ skills?: Skill[]; /** Skill loading warnings (already captured by SDK) */ skillWarnings?: SkillWarning[]; /** Custom commands (TypeScript slash commands) */ customCommands?: LoadedCustomCommand[]; skillsSettings?: SkillsSettings; /** Model registry for API key resolution and model discovery */ modelRegistry: ModelRegistry; /** Tool registry for LSP and settings */ toolRegistry?: Map; /** Current session pre-LLM message transform pipeline */ transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise; /** Provider payload hook used by the active session request path */ onPayload?: SimpleStreamOptions["onPayload"]; /** Provider response hook used by the active session request path */ onResponse?: SimpleStreamOptions["onResponse"]; /** Raw SSE hook used by the active session request path */ onSseEvent?: SimpleStreamOptions["onSseEvent"]; /** Per-session raw SSE diagnostic buffer */ rawSseDebugBuffer?: RawSseDebugBuffer; /** Current session message-to-LLM conversion pipeline */ convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise; /** System prompt builder that can consider tool availability. Returns ordered provider-facing blocks. */ rebuildSystemPrompt?: (toolNames: string[], tools: Map) => Promise<{ systemPrompt: string[]; }>; /** Rebuild the SSH tool from current capability discovery results. */ reloadSshTool?: () => Promise; requestedToolNames?: ReadonlySet; /** * Optional accessor for live MCP server instructions. Read by the session's * `rebuildSystemPrompt`-skip optimization to detect server-side instruction * changes (e.g. an MCP server upgrade) that would otherwise pass the tool-set * signature comparison and silently keep a stale prompt cached. */ getMcpServerInstructions?: () => Map | undefined; /** Enable hidden-by-default MCP tool discovery for this session. */ mcpDiscoveryEnabled?: boolean; /** MCP tool names to activate for the current session when discovery mode is enabled. */ initialSelectedMCPToolNames?: string[]; /** Whether constructor-provided MCP defaults should be persisted immediately. */ persistInitialMCPToolSelection?: boolean; /** MCP server names whose tools should seed discovery-mode sessions whenever those servers are connected. */ defaultSelectedMCPServerNames?: string[]; /** MCP tool names that should seed brand-new sessions created from this AgentSession. */ defaultSelectedMCPToolNames?: string[]; /** TTSR manager for time-traveling stream rules */ ttsrManager?: TtsrManager; /** Secret obfuscator for deobfuscating streaming edit content */ obfuscator?: SecretObfuscator; /** Logical owner for retained Python kernels created by this session. */ evalKernelOwnerId?: string; /** * AsyncJobManager that this session installed as the process-global instance. * Only set for top-level sessions; subagents inherit the parent's manager and * **MUST NOT** dispose it on their own teardown. */ ownedAsyncJobManager?: AsyncJobManager; /** Agent identity (registry id like "0-Main" or "3-Alice") used for IRC routing. */ agentId?: string; /** Shared agent registry (for forwarding IRC observations to the main session UI). */ agentRegistry?: AgentRegistry; /** * Override the provider-facing session ID for all API requests from this session. * When absent, `sessionManager.getSessionId()` is used. Needed when benchmark or * SDK callers issue probes / prewarming with an explicit `--provider-session-id` * so that credential sticky selection is consistent with the session's streaming calls. */ providerSessionId?: string; } /** Options for AgentSession.prompt() */ export interface PromptOptions { /** Whether to expand file-based prompt templates (default: true) */ expandPromptTemplates?: boolean; /** Image attachments */ images?: ImageContent[]; /** When streaming, how to queue the message: "steer" (interrupt) or "followUp" (wait). */ streamingBehavior?: "steer" | "followUp"; /** Optional tool choice override for the next LLM call. */ toolChoice?: ToolChoice; /** Send as developer/system message instead of user. Providers that support it use the developer role; others fall back to user. */ synthetic?: boolean; /** Explicit billing/initiator attribution for the prompt. Defaults to user prompts as `user` and synthetic prompts as `agent`. */ attribution?: MessageAttribution; /** Skip pre-send compaction checks for this prompt (internal use for maintenance flows). */ skipCompactionCheck?: boolean; } /** Result from a handoff operation. */ export interface HandoffResult { document: string; savedPath?: string; } export interface SessionHandoffOptions { autoTriggered?: boolean; signal?: AbortSignal; } /** Result from cycleModel() */ export interface ModelCycleResult { model: Model; thinkingLevel: ThinkingLevel | undefined; /** Whether cycling through scoped models (--models flag) or all available */ isScoped: boolean; } /** Result from cycleRoleModels() */ export interface RoleModelCycleResult { model: Model; thinkingLevel: ThinkingLevel | undefined; role: string; } /** Session statistics for /session command */ export interface SessionStats { sessionFile: string | undefined; sessionId: string; userMessages: number; assistantMessages: number; toolCalls: number; toolResults: number; totalMessages: number; tokens: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number; }; premiumRequests: number; cost: number; } export declare class AgentSession { #private; readonly agent: Agent; readonly sessionManager: SessionManager; readonly settings: Settings; readonly yieldQueue: YieldQueue; readonly configWarnings: string[]; readonly rawSseDebugBuffer: RawSseDebugBuffer; constructor(config: AgentSessionConfig); /** Model registry for API key resolution and model discovery */ get modelRegistry(): ModelRegistry; /** Advance the tool-choice queue and return the next directive for the upcoming LLM call. */ nextToolChoice(): ToolChoice | undefined; /** * Force the next model call to target a specific active tool, then terminate * the agent loop. Pushes a two-step sequence [forced, "none"] so the model * calls exactly the forced tool once and then cannot call another. */ setForcedToolChoice(toolName: string): void; /** The tool-choice queue: forces forthcoming tool invocations and carries handlers. */ get toolChoiceQueue(): ToolChoiceQueue; /** Peek the in-flight directive's invocation handler for use by the resolve tool. */ peekQueueInvoker(): ((input: unknown) => Promise | unknown) | undefined; peekStandingResolveHandler(): ((input: unknown) => Promise | unknown) | undefined; setStandingResolveHandler(handler: ((input: unknown) => Promise | unknown) | null): void; /** Provider-scoped mutable state store for transport/session caches. */ get providerSessionState(): Map; getHindsightSessionState(): HindsightSessionState | undefined; setHindsightSessionState(state: HindsightSessionState | undefined): HindsightSessionState | undefined; /** TTSR manager for time-traveling stream rules */ get ttsrManager(): TtsrManager | undefined; /** Whether a TTSR abort is pending (stream was aborted to inject rules) */ get isTtsrAbortPending(): boolean; /** Whether the plan-mode → compaction transition's expected internal abort is * pending. Consumed by `#handleAgentEvent` to stamp `SILENT_ABORT_MARKER` * on the next aborted assistant message_end; cleared unconditionally by * `InteractiveMode.#approvePlan`'s `finally` block. */ get isPlanCompactAbortPending(): boolean; /** Arm the silent-abort marker for the next aborted assistant message_end. * Caller MUST clear via `clearPlanCompactAbortPending()` in a `finally` * to guarantee no leak. */ markPlanCompactAbortPending(): void; /** Unconditionally clear the silent-abort flag. Idempotent: safe when the * flag was never set OR was already consumed by `#handleAgentEvent`. */ clearPlanCompactAbortPending(): void; /** Register a compact display string for a custom message that the caller is * about to dispatch via `promptCustomMessage` / `sendCustomMessage`. * Returns a stable tag the caller MUST embed in * `CustomMessage.details.__pendingDisplayTag` so the agent-side * `message_start` handler can remove the matching display entry when the * queued message is consumed. * * Does NOT push to the agent's steering/followUp queue — that happens * separately inside `sendCustomMessage`. */ enqueueCustomMessageDisplay(text: string, mode: "steer" | "followUp"): string; getAsyncJobSnapshot(options?: { recentLimit?: number; }): AsyncJobSnapshot | null; /** * Emit a UI-only notice to the session. Surfaces in interactive mode as a * `showWarning` / `showError` / `showStatus` line; non-interactive modes * receive the event through the normal subscribe stream. * * Notices are NOT added to agent state and never reach the LLM — use this * for out-of-band conditions the user should see but the model shouldn't * react to (e.g. background queue flush failures). */ emitNotice(level: "info" | "warning" | "error", message: string, source?: string): void; /** * Subscribe to agent events. * Session persistence is handled internally (saves messages on message_end). * Multiple listeners can be added. Returns unsubscribe function for this listener. */ subscribe(listener: AgentSessionEventListener): () => void; /** * Remove all listeners, flush pending writes, and disconnect from agent. * Call this when completely done with the session. */ dispose(): Promise; /** Full agent state */ get state(): AgentState; /** Current model (may be undefined if not yet selected) */ get model(): Model | undefined; /** Current thinking level */ get thinkingLevel(): ThinkingLevel | undefined; get serviceTier(): ServiceTier | undefined; /** Whether agent is currently streaming a response */ get isStreaming(): boolean; /** Wait until streaming and deferred recovery work are fully settled. */ waitForIdle(): Promise; drainAsyncJobDeliveriesForAcp(options?: { timeoutMs?: number; }): Promise; /** Most recent assistant message in agent state. */ getLastAssistantMessage(): AssistantMessage | undefined; /** Current effective system prompt blocks (includes any per-turn extension modifications) */ get systemPrompt(): string[]; /** Current retry attempt (0 if not retrying) */ get retryAttempt(): number; /** * Get the names of currently active tools. * Returns the names of tools currently set on the agent. */ getActiveToolNames(): string[]; /** Whether the edit tool is registered in this session. */ get hasEditTool(): boolean; /** * Get a tool by name from the registry. */ getToolByName(name: string): AgentTool | undefined; /** * Get all configured tool names (built-in via --tools or default, plus custom tools). */ getAllToolNames(): string[]; isMCPDiscoveryEnabled(): boolean; /** @deprecated Use {@link getDiscoverableTools} with `{ source: "mcp" }` instead. * Preserves the legacy `description`-bearing MCP shape for back-compat callers. */ getDiscoverableMCPTools(): DiscoverableMCPTool[]; /** @deprecated Use {@link getDiscoverableToolSearchIndex} instead. * Returns the legacy MCP search index whose documents expose `tool.description`. */ getDiscoverableMCPSearchIndex(): DiscoverableMCPSearchIndex; getSelectedMCPToolNames(): string[]; activateDiscoveredMCPTools(toolNames: string[]): Promise; isToolDiscoveryEnabled(): boolean; getDiscoverableTools(filter?: { source?: DiscoverableTool["source"]; }): DiscoverableTool[]; getDiscoverableToolSearchIndex(): DiscoverableToolSearchIndex; getSelectedDiscoveredToolNames(): string[]; activateDiscoveredTools(toolNames: string[]): Promise; /** * Reload the SSH tool from disk-backed capability discovery and make the * refreshed definition visible to the next model call without restarting. */ refreshSshTool(options?: { activateIfAvailable?: boolean; }): Promise; /** * Set active tools by name. * Only tools in the registry can be enabled. Unknown tool names are ignored. * Also rebuilds the system prompt to reflect the new tool set. * Changes take effect before the next model call. */ setActiveToolsByName(toolNames: string[]): Promise; /** Rebuild the base system prompt using the current active tool set. */ refreshBaseSystemPrompt(): Promise; /** * Replace MCP tools in the registry and recompute the visible MCP tool set immediately. * This allows /mcp add/remove/reauth to take effect without restarting the session. */ refreshMCPTools(mcpTools: CustomTool[]): Promise; /** * Replace RPC host-owned tools and refresh the active tool set before the next model call. */ refreshRpcHostTools(rpcTools: AgentTool[]): Promise; /** Whether auto-compaction is currently running */ get isCompacting(): boolean; /** All messages including custom types like BashExecutionMessage */ get messages(): AgentMessage[]; buildDisplaySessionContext(): SessionContext; /** Convert session messages using the same pre-LLM pipeline as the active session. */ convertMessagesToLlm(messages: AgentMessage[], signal?: AbortSignal): Promise; /** Apply session-level stream hooks to a direct side request. */ prepareSimpleStreamOptions(options: SimpleStreamOptions, provider?: string): SimpleStreamOptions; /** Current steering mode */ get steeringMode(): "all" | "one-at-a-time"; /** Current follow-up mode */ get followUpMode(): "all" | "one-at-a-time"; /** Current interrupt mode */ get interruptMode(): "immediate" | "wait"; /** Current session file path, or undefined if sessions are disabled */ get sessionFile(): string | undefined; /** Current session ID */ get sessionId(): string; /** Current session display name, if set */ get sessionName(): string | undefined; /** Scoped models for cycling (from --models flag) */ get scopedModels(): ReadonlyArray<{ model: Model; thinkingLevel?: ThinkingLevel; }>; /** Prompt templates */ getPlanModeState(): PlanModeState | undefined; setPlanModeState(state: PlanModeState | undefined): void; getGoalModeState(): GoalModeState | undefined; setGoalModeState(state: GoalModeState | undefined): void; get goalRuntime(): GoalRuntime; markPlanReferenceSent(): void; setPlanReferencePath(path: string): void; get clientBridge(): ClientBridge | undefined; setClientBridge(bridge: ClientBridge | undefined): void; getCheckpointState(): CheckpointState | undefined; setCheckpointState(state: CheckpointState | undefined): void; /** * Inject the plan mode context message into the conversation history. */ sendPlanModeContext(options?: { deliverAs?: "steer" | "followUp" | "nextTurn"; }): Promise; sendGoalModeContext(options?: { deliverAs?: "steer" | "followUp" | "nextTurn"; }): Promise; resolveRoleModel(role: string): Model | undefined; /** * Resolve a role to its model AND thinking level. * Unlike resolveRoleModel(), this preserves the thinking level suffix * from role configuration (e.g., "anthropic/claude-sonnet-4-5:xhigh"). */ resolveRoleModelWithThinking(role: string): ResolvedModelRoleValue; get promptTemplates(): ReadonlyArray; /** Replace file-based slash commands used for prompt expansion. */ setSlashCommands(slashCommands: FileSlashCommand[]): void; /** Custom commands (TypeScript slash commands and MCP prompts) */ get customCommands(): ReadonlyArray; /** Update the MCP prompt commands list. Called when server prompts are (re)loaded. */ setMCPPromptCommands(commands: LoadedCustomCommand[]): void; /** * Send a prompt to the agent. * - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming * - Expands file-based prompt templates by default * - During streaming, queues via steer() or followUp() based on streamingBehavior option * - Validates model and API key before sending (when not streaming) * @throws Error if streaming and no streamingBehavior specified * @throws Error if no model selected or no API key available (when not streaming) */ prompt(text: string, options?: PromptOptions): Promise; promptCustomMessage(message: Pick, "customType" | "content" | "display" | "details" | "attribution">, options?: Pick): Promise; /** * Queue a steering message to interrupt the agent mid-run. */ steer(text: string, images?: ImageContent[]): Promise; /** * Queue a follow-up message to process after the agent would otherwise stop. */ followUp(text: string, images?: ImageContent[]): Promise; queueDeferredMessage(message: CustomMessage): void; /** * Send a custom message to the session. Creates a CustomMessageEntry. * * Handles three cases: * - Streaming: queue as steer/follow-up or store for next turn * - Not streaming + triggerTurn: appends to state/session, starts new turn unless the client cannot own it * - Not streaming + no trigger: appends to state/session, no turn */ sendCustomMessage(message: Pick, "customType" | "content" | "display" | "details" | "attribution">, options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn"; }): Promise; /** * Send a user message to the agent. Always triggers a turn. * When the agent is streaming, use deliverAs to specify how to queue the message. * * @param content User message content (string or content array) * @param options.deliverAs Delivery mode when streaming: "steer" or "followUp" */ sendUserMessage(content: string | (TextContent | ImageContent)[], options?: { deliverAs?: "steer" | "followUp"; }): Promise; /** * Clear queued messages and return them. * Useful for restoring to editor when user aborts. */ clearQueue(): { steering: string[]; followUp: string[]; }; /** Number of pending messages (includes steering, follow-up, and next-turn messages) */ get queuedMessageCount(): number; /** Get pending messages (read-only). Returns the public text-only view; * internal `{text, tag?}` records are mapped to `.text` so callers * (`updatePendingMessagesDisplay`, `restoreQueuedMessagesToEditor`) see * the unchanged historical shape. */ getQueuedMessages(): { steering: readonly string[]; followUp: readonly string[]; }; /** * Pop the last queued message (steering first, then follow-up). * Used by dequeue keybinding to restore messages to editor one at a time. * Returns the popped entry's `.text`; the tag (if any) dies with the * record — no orphan state can outlive the queue entry. */ popLastQueuedMessage(): string | undefined; get skillsSettings(): SkillsSettings | undefined; /** Skills loaded by SDK (empty if --no-skills or skills: [] was passed) */ get skills(): readonly Skill[]; /** Skill loading warnings captured by SDK */ get skillWarnings(): readonly SkillWarning[]; getTodoPhases(): TodoPhase[]; setTodoPhases(phases: TodoPhase[]): void; /** * Abort current operation and wait for agent to become idle. */ abort(options?: { goalReason?: "interrupted" | "internal"; }): Promise; /** * Start a new session, optionally with initial messages and parent tracking. * Clears all messages and starts a new session. * Listeners are preserved and will continue receiving events. * @param options - Optional initial messages and parent session path * @returns true if completed, false if cancelled by hook */ newSession(options?: NewSessionOptions): Promise; /** * Set a display name for the current session. */ setSessionName(name: string, source?: "auto" | "user"): Promise; /** * Fork the current session, creating a new session file with the exact same state. * Copies all entries and artifacts to the new session. * Unlike newSession(), this preserves all messages in the agent state. * @returns true if completed, false if cancelled by hook or not persisting */ fork(): Promise; /** * Set model directly. * Validates API key, saves to session and settings. * @throws Error if no API key available for the model */ setModel(model: Model, role?: string, options?: { selector?: string; thinkingLevel?: ThinkingLevel; }): Promise; /** * Set model temporarily (for this session only). * Validates API key, saves to session log but NOT to settings. * @throws Error if no API key available for the model */ setModelTemporary(model: Model, thinkingLevel?: ThinkingLevel): Promise; /** * Cycle to next/previous model. * Uses scoped models (from --models flag) if available, otherwise all available models. * @param direction - "forward" (default) or "backward" * @returns The new model info, or undefined if only one model available */ cycleModel(direction?: "forward" | "backward"): Promise; /** * Cycle through configured role models in a fixed order. * Skips missing roles. * @param roleOrder - Order of roles to cycle through (e.g., ["slow", "default", "smol"]) * @param options - Optional settings: `temporary` to not persist to settings */ cycleRoleModels(roleOrder: readonly string[], options?: { temporary?: boolean; }): Promise; /** * Get all available models with valid API keys. */ getAvailableModels(): Model[]; /** * Set thinking level. * Saves the effective metadata-clamped level to session and settings only if it changes. */ setThinkingLevel(level: ThinkingLevel | undefined, persist?: boolean): void; /** * Cycle to next thinking level. * @returns New level, or undefined if model doesn't support thinking */ cycleThinkingLevel(): ThinkingLevel | undefined; /** * True when *any* fast-mode-granting service tier is configured, regardless * of whether the active model's provider actually realizes it. Used by the * toggle (`/fast on|off`) so re-toggling a scoped tier (`openai-only`, * `claude-only`) doesn't silently broaden it to unscoped `priority`. * * For "is fast mode actually applied to the next request?" use * {@link isFastModeActive} instead — that one respects the model's provider. */ isFastModeEnabled(): boolean; /** * True when the configured `serviceTier` resolves to `"priority"` for the * *currently selected model's provider*. Returns false for scoped tiers * that don't match (e.g. `"openai-only"` on an anthropic model) and when * no model is selected. */ isFastModeActive(): boolean; setServiceTier(serviceTier: ServiceTier | undefined): void; setFastMode(enabled: boolean): void; toggleFastMode(): boolean; /** * Get available thinking levels for current model. */ getAvailableThinkingLevels(): ReadonlyArray; /** * Set steering mode. * Saves to settings. */ setSteeringMode(mode: "all" | "one-at-a-time"): void; /** * Set follow-up mode. * Saves to settings. */ setFollowUpMode(mode: "all" | "one-at-a-time"): void; /** * Set interrupt mode. * Saves to settings. */ setInterruptMode(mode: "immediate" | "wait"): void; /** * Manually compact the session context. * Aborts current agent operation first. * @param customInstructions Optional instructions for the compaction summary * @param options Optional callbacks for completion/error handling */ compact(customInstructions?: string, options?: CompactOptions): Promise; /** * Cancel in-progress context maintenance (manual compaction, auto-compaction, or auto-handoff). */ abortCompaction(): void; /** Trigger idle compaction through the auto-compaction flow (with UI events). */ runIdleCompaction(): Promise; /** * Cancel in-progress branch summarization. */ abortBranchSummary(): void; /** * Cancel in-progress handoff generation. */ abortHandoff(): void; /** * Check if handoff generation is in progress. */ get isGeneratingHandoff(): boolean; /** * Generate a handoff document with a oneshot LLM call, then start a new session with it. * * @param customInstructions Optional focus for the handoff document * @param options Handoff execution options * @returns The handoff document text, or undefined if cancelled/failed */ handoff(customInstructions?: string, options?: SessionHandoffOptions): Promise; /** * Toggle auto-compaction setting. */ setAutoCompactionEnabled(enabled: boolean): void; /** Whether auto-compaction is enabled */ get autoCompactionEnabled(): boolean; /** * Cancel in-progress retry. */ abortRetry(): void; /** Whether auto-retry is currently in progress */ get isRetrying(): boolean; /** Whether auto-retry is enabled */ get autoRetryEnabled(): boolean; /** * Toggle auto-retry setting. */ setAutoRetryEnabled(enabled: boolean): void; /** * Manually retry the last failed assistant turn. * Removes the error message from agent state and re-attempts with a fresh retry budget. * @returns true if retry was initiated, false if no failed turn to retry or agent is busy */ retry(): Promise; /** * Execute a bash command. * Adds result to agent context and session. * @param command The bash command to execute * @param onChunk Optional streaming callback for output * @param options.excludeFromContext If true, command output won't be sent to LLM (!! prefix) */ executeBash(command: string, onChunk?: (chunk: string) => void, options?: { excludeFromContext?: boolean; }): Promise; /** * Record a bash execution result in session history. * Used by executeBash and by extensions that handle bash execution themselves. */ recordBashResult(command: string, result: BashResult, options?: { excludeFromContext?: boolean; }): void; /** * Cancel running bash command. */ abortBash(): void; /** Whether a bash command is currently running */ get isBashRunning(): boolean; /** Whether there are pending bash messages waiting to be flushed */ get hasPendingBashMessages(): boolean; /** * Execute Python code in the shared kernel. * Uses the same kernel session as eval's Python backend, allowing collaborative editing. * @param code The Python code to execute * @param onChunk Optional streaming callback for output * @param options.excludeFromContext If true, execution won't be sent to LLM ($$ prefix) */ executePython(code: string, onChunk?: (chunk: string) => void, options?: { excludeFromContext?: boolean; }): Promise; assertEvalExecutionAllowed(): void; /** * Track Python work started outside AgentSession.executePython so dispose can await and abort it too. */ trackEvalExecution(execution: Promise, abortController: AbortController): Promise; /** * Record a Python execution result in session history. */ recordPythonResult(code: string, result: PythonResult, options?: { excludeFromContext?: boolean; }): void; /** * Cancel running Python execution. */ abortEval(): void; /** Whether a Python execution is currently running */ get isEvalRunning(): boolean; /** Whether there are pending Python messages waiting to be flushed */ get hasPendingPythonMessages(): boolean; /** * Generate an ephemeral reply to a background message (e.g. an IRC ping from * another agent) using this session's current model + system prompt + history. * * The reply is computed via a side-channel `streamSimple` call (analogous to * `/btw`) so it never blocks on the recipient's in-flight tool calls. After * the reply is generated, both the incoming question and the auto-reply are * queued for injection into the recipient's persisted history so the model * sees the exchange on its next turn. Injection happens immediately when the * session is idle, otherwise it is deferred until streaming ends. */ respondAsBackground(args: { from: string; message: string; awaitReply?: boolean; signal?: AbortSignal; }): Promise<{ replyText: string | null; }>; /** * Emit an IRC relay observation event on this session for UI rendering only. * Does not persist the record to history. Public so other sessions can forward. */ emitIrcRelayObservation(record: CustomMessage): void; /** * Run a single ephemeral side-channel turn against this session's current * model + system prompt + history. No tools are used; the side request * does not block on, or interfere with, any in-flight main turn. The * session's history and persisted state are NOT modified by this call. * * Used by `respondAsBackground` (IRC) and `BtwController` (`/btw`) to share * the snapshot + stream pipeline. The snapshot includes any in-flight * streaming assistant text so the model sees the half-finished response * rather than missing context. */ runEphemeralTurn(args: { promptText: string; onTextDelta?: (delta: string) => void; signal?: AbortSignal; }): Promise<{ replyText: string; assistantMessage: AssistantMessage; }>; /** * Reload the current session from disk. * * Intended for extension commands and headless modes to re-read the current session * file and re-emit session_switch hooks. */ reload(): Promise; /** * Switch to a different session file. * Aborts current operation, loads messages, restores model/thinking. * Listeners are preserved and will continue receiving events. * @returns true if switch completed, false if cancelled by hook */ switchSession(sessionPath: string): Promise; /** * Create a branch from a specific entry. * Emits before_branch/branch session events to hooks. * * @param entryId ID of the entry to branch from * @returns Object with: * - selectedText: The text of the selected user message (for editor pre-fill) * - cancelled: True if a hook cancelled the branch */ branch(entryId: string): Promise<{ selectedText: string; cancelled: boolean; }>; /** * Navigate to a different node in the session tree. * Unlike branch() which creates a new session file, this stays in the same file. * * @param targetId The entry ID to navigate to * @param options.summarize Whether user wants to summarize abandoned branch * @param options.customInstructions Custom instructions for summarizer * @returns Result with editorText (if user message) and cancelled status */ navigateTree(targetId: string, options?: { summarize?: boolean; customInstructions?: string; }): Promise<{ editorText?: string; cancelled: boolean; aborted?: boolean; summaryEntry?: BranchSummaryEntry; /** Raw session context built during navigation — pass to renderInitialMessages to skip a second O(N) walk. */ sessionContext?: SessionContext; }>; /** * Get all user messages from session for branch selector. */ getUserMessagesForBranching(): Array<{ entryId: string; text: string; }>; /** * Get session statistics. */ getSessionStats(): SessionStats; /** * Get current context usage statistics. * Uses the last assistant message's usage data when available, * otherwise estimates tokens for all messages. */ getContextUsage(): ContextUsage | undefined; fetchUsageReports(signal?: AbortSignal): Promise; /** * Export session to HTML. * @param outputPath Optional output path (defaults to session directory) * @returns Path to exported file */ exportToHtml(outputPath?: string): Promise; /** * Get text content of last assistant message. * Useful for /copy command. * @returns Text content, or undefined if no assistant message exists */ getLastAssistantText(): string | undefined; hasCopyCandidateAssistantMessage(): boolean; /** * Get text content of the most recent visible handoff message. * Fresh handoff sessions store the handoff context as a custom message, not * an assistant message, so callers that copy the "last" message can use this * as a fallback before the new session has an assistant response. */ getLastVisibleHandoffText(): string | undefined; /** * Format the entire session as plain text for clipboard export. * Includes user messages, assistant text, thinking blocks, tool calls, and tool results. */ formatSessionAsText(): string; /** * Format the conversation as compact context for subagents. * Includes only user messages and assistant text responses. * Excludes: system prompt, tool definitions, tool calls/results, thinking blocks. */ formatCompactContext(): string; /** * Check if extensions have handlers for a specific event type. */ hasExtensionHandlers(eventType: string): boolean; /** * Get the extension runner (for setting UI context and error handlers). */ get extensionRunner(): ExtensionRunner | undefined; }