import { type AgentTool, type ToolExecuteResult } from "@kenkaiiii/gg-agent"; import { type Message, type MessageProvenance, type Provider, type Usage, type ThinkingLevel } from "@kenkaiiii/gg-ai"; import { EventBus } from "./event-bus.js"; import { type CompletionReview } from "./completion-review.js"; import { SlashCommandRegistry } from "./slash-commands.js"; import { type CustomCommand } from "./custom-commands.js"; import { type BranchInfo, type KenTurnPayload, type AutopilotMarkerPayload, type AppMarkerPayload, type RunOutcome, type TurnMetricPayload } from "./session-manager.js"; import { type Skill } from "./skills.js"; import type { BackgroundProcess } from "./process-manager.js"; import { type ChildModelPick, type SubAgentSnapshot } from "./subagent-manager.js"; import type { MCPElicitHandler } from "./mcp/index.js"; import { type ContextLimits } from "./context-limits.js"; import { type ImportForeignTranscriptResult } from "./foreign-session-import.js"; import { type CacheExpiryStatus } from "./cache-expiry.js"; import { type AgentDefinition } from "./agents.js"; import { type EnhanceResult } from "../utils/prompt-enhancer.js"; import type { VerificationEvidence } from "./verification-evidence.js"; /** A chat attachment (image / video / other file) prepared for the model. The * raw base64 `data` rides native blocks; `path` (when persisted to disk) lets * the agent's tools open the file directly. */ export interface SessionAttachment { kind: "image" | "video" | "file"; mediaType: string; data: string; name: string; path?: string; } /** Per-prompt run controls. */ export interface PromptRunOptions { /** Offer the model no tools for this prompt. */ disableTools?: boolean; /** Hold reasoning effort at the plan-mode ceiling for this prompt. */ capThinking?: boolean; } export interface AgentSessionOptions { provider: Provider; model: string; cwd: string; baseUrl?: string; /** Replaces the whole system prompt — nothing else is rendered. */ systemPrompt?: string; /** * A sub-agent definition's body, COMPOSED with the standard scaffolding * (Tools, project context, return contract, Environment) instead of replacing * it — see `buildSubAgentSystemPrompt`. * * Prefer this over `systemPrompt` for delegated children: a bare replacement * leaves the child with no Tools section and no Environment facts, which is * precisely how a sub-agent ends up misusing tools it was never told it had. * Ignored when `systemPrompt` is set. */ agentPrompt?: string; /** Whether `agentPrompt` composition includes project instruction files. Default `"project"`. */ agentContext?: "project" | "none"; /** * Who `agentPrompt` speaks to. `"subagent"` (default) appends the delegated * child's return contract; `"primary"` is a user-facing specialist (Motion) * that keeps the Tools/Environment scaffolding but answers the user directly. */ agentRole?: "subagent" | "primary"; /** Synchronous volatile prompt suffix, refreshed immediately before every run. */ getSystemPromptTail?: () => string; sessionId?: string; continueRecent?: boolean; maxTokens?: number; maxTurns?: number; /** * How many times the turn budget may be extended when the agent is still * making progress. Defaults to the agent-loop default (2); sub-agents pass a * stricter cap because a child's extensions multiply against the parent's * own budget. */ maxTurnExtensions?: number; thinkingLevel?: ThinkingLevel; signal?: AbortSignal; /** Prefix used for provider prompt-cache routing keys. */ promptCacheKeyPrefix?: string; /** * Explicit prompt-cache routing key. When set, overrides the * `${promptCacheKeyPrefix}:${sessionId}` default so spawned sub-agents can * inherit a stable parent-scoped key — without this, each sub-agent process * generates a fresh sessionId and starts with a cold cache. */ promptCacheKey?: string; /** * If true, this session does NOT create a `.jsonl` session file or persist * any messages. Used by subagent spawns (`--json` mode) so their transcripts * don't leak into `ggcoder continue` for the parent project. Subagent runs * are one-shot, NDJSON-streamed to the parent over stdout, and have no * resumable identity. */ transient?: boolean; /** * If true, `initialize()` returns WITHOUT waiting for MCP servers to connect — * the connection runs in the background and tools are appended when ready. * Hosts whose readiness is gated on `initialize()` (the gg-app sidecar, which * can't emit its listening handshake until init resolves) set this so a slow * or hanging stdio MCP server (e.g. a first-run `npx -y …` download) can't * delay the session from becoming usable. Default (false) keeps the CLI's * connect-before-ready behavior so MCP tools are present on the first turn. */ backgroundMcpConnect?: boolean; /** * Handler for a server-initiated MCP `elicitation/create` — a request for user * input in the middle of a tool call. Hosts that can render a form (the * gg-app sidecar) supply this; without it the session declares no elicitation * capability and servers fall back to their no-input behavior. */ onMcpElicit?: MCPElicitHandler; /** * If true, an over-context restored session is NOT compacted inline during * `loadExistingSession()` — the existing pre-run auto-compaction in * `runLoop()` handles it on the first prompt instead (with proper * compaction_start/_end events). The inline load compaction makes a summary * LLM call with a 30s timeout, and hosts whose readiness is gated on * `initialize()` (the gg-app sidecar: waitForReady blocks the whole webview) * would freeze the UI for that entire call. Default (false) keeps the * compact-on-load behavior for CLI resume/`ggcoder continue`. */ deferLoadCompaction?: boolean; /** * Plan-mode callbacks. When provided, the `enter_plan`/`exit_plan` tools are * registered and the session manages plan-mode restrictions + system-prompt * rebuilds. Hosts (e.g. the gg-app sidecar) use these to surface plan-mode * UI. Omitted by callers that don't want plan mode (CLI wires its own). */ onEnterPlan?: (reason?: string) => void | Promise; /** * Built-in tools to keep in the core set instead of the deferred * tool_search catalog: for a specialist whose job is a deferred tool (GG * Goals orchestrates with spawn_agent and friends). Fixed for the session. */ coreTools?: readonly string[]; /** * Default provider/model for the workers this session spawns, when their * agent names no model of its own. Read at spawn time. */ getChildModel?: () => ChildModelPick | undefined; /** * Wrap each built-in tool once, at registration. Must keep the tool's name, * description and parameters (only `execute` may change), so the tool block * stays prompt-cache stable. Used by GG Goals to gate its own editing. */ wrapTool?: (tool: AgentTool) => AgentTool; /** * Built-in tools this session never has, removed at registration (before * tiering) and left out of the system prompt's Tools section. Unlike * `allowedTools`, the rest of the tool set and its tiering are untouched. * Fixed for the session, so the cached tool block never changes. Used by * GG Goals, which never edits or runs anything itself. */ withoutTools?: readonly string[]; /** * A fixed team of sub-agents, replacing agent discovery (project, global * and bundled): the only agents spawn_agent and subagent can start. Used by * GG Goals, whose workers follow their own rules (goals-agent/goal-team.ts). */ subAgents?: readonly AgentDefinition[]; /** * Start every spawned child with this session's extra workspace roots too * (less the folder it works in), so it can read and change all of them. * GG Goals' workers work across all of a goal's attached folders. */ shareRootsWithSubAgents?: boolean; /** The sub-agent a spawned child is when the spawn names none (one of `subAgents`). */ defaultSubAgent?: string; onExitPlan?: (planPath: string) => Promise; /** * If provided, the session's tool set is filtered to ONLY these tool names * after `createTools()` runs, and the system prompt's Tools section lists only * them. Used by read-only advisory sessions (e.g. the Ken mentor agent) to * register a safe subset — excluded mutating tools (write/edit/bash/…) are * never registered, so a hallucinated call can't change the repo. Default * (undefined) = all tools, preserving every existing caller's behavior. */ allowedTools?: string[]; /** * MCP server names whose tools are allowed in an allow-listed session. Only * meaningful alongside `allowedTools`. With it set, the session connects ONLY * these named MCP servers (not the full configured set) and every tool they * expose (`mcp____*`) passes the allow-list, so a read-only agent can * be handed one research server while still being barred from every mutating * tool. Empty/undefined → an allow-listed session skips * MCP entirely (its dynamic tool names could never match a fixed allow-list). */ allowedMcpServers?: string[]; /** * Force 1-h prompt-cache TTL + pre-warm regardless of the user's global * `speedProfile` setting. Bursty read-only advisory sessions (the Ken * mentor + autopilot reviewer) call the same static system prompt on a * schedule that routinely exceeds the default 5-min cache window — a * dropped cache there resends the whole cached prefix at full price right * when it matters most, independent of whatever the user picked for the * main build session. Default (undefined) = follow `speedProfile`. */ forceLongCacheRetention?: boolean; /** Hidden persistent subagent workers omit the async orchestration tool suite. */ subagentWorker?: boolean; /** Session storage root override. Chat agents use a dedicated namespace. */ sessionRootDir?: string; /** Register GG Coder built-in/prompt/custom slash commands. Defaults to true. */ coderSlashCommands?: boolean; /** Enable loop-break and re-grounding hooks. Defaults to true. */ selfCorrectionHooks?: boolean; /** Override the semantic-loop judge LLM call (tests). Receives the finished * prompt, returns the model's raw reply. Default: one-shot `stream()` call * on the session's ACTIVE model. */ semanticLoopJudge?: (prompt: string) => Promise; /** Load project skills/agents and create local .gg directories. Defaults to true. */ projectCustomization?: boolean; /** * Use exactly this skill set instead of discovering bundled/global/project * skills. A mode with a private skill bundle (Motion) passes its own list so * its skills stay invisible to every other mode, and vice versa. */ skills?: readonly Skill[]; /** * Mode-specific defaults for prompt byte budgets. The user's `contextLimits` * setting still overrides these. Motion raises its skill-catalog budget * because its skill set is a fixed, bundled one (never untrusted files). */ contextLimits?: Partial; /** Register global + bundled subagents without loading project customization. */ globalSubagents?: boolean; /** Load GG Coder extensions. Defaults to true. */ loadExtensions?: boolean; /** Inject GG Coder's model-specific subagent orchestration prompt. Defaults to true. */ orchestrationPrompt?: boolean; /** Host-provided tools appended to this session only (for example, chat delegation). */ additionalTools?: AgentTool[]; /** Mode-owned completion policy; absent in Coder/chat/worker sessions. */ completionReview?: CompletionReview; /** Remind the agent to submit its plan (exit_plan) when a run is about to * stop still in plan mode. Desktop only: an unsubmitted plan there leaves * nothing to review. See plan-submission-gate.ts. */ planSubmissionGate?: boolean; } export { resolveSessionToolResultCharLimit, resolveSessionTurnToolResultCharLimit, } from "./agent-session/helpers.js"; export interface AgentSessionState { provider: Provider; model: string; cwd: string; sessionId: string; sessionPath: string; messageCount: number; planMode: boolean; /** accountId from the most recently resolved credentials, if any — lets * callers compute the transport-specific context window (e.g. OpenAI Codex * OAuth) without re-resolving credentials. */ accountId?: string; } export declare class AgentSession { readonly eventBus: EventBus; readonly slashCommands: SlashCommandRegistry; private settingsManager; private authStorage; private sessionManager; private extensionLoader; private messages; private readonly markers; private readonly cacheDiagnostics; /** Internal-only (GG_INTERNAL): live per-session cost/reliability recorder. * Absent entirely in public builds — see core/internal-diagnostics.ts. */ private diagnosticsRecorder?; private tools; /** Rebuilds the read tool for a new model (video byte cap is baked in at * creation). Called from switchModel so video-capable models get the * read-tool's native-video path after a mid-session model change. */ private rebuildReadTool; /** Forgets every file read; called whenever the conversation is replaced or * rewound, so the model must re-read a file before changing it. */ private clearReadTracker; private recordBashReads; private skills; private cacheKeyLogged; private readonly loopMonitor; private regroundingInjected; /** * The environment as the cached system prompt currently describes it. * Re-recorded on every prompt build, so a rebuild (e.g. `/add-dir`) needs no * delta; anything that changes WITHOUT one is caught by the hook below. */ private renderedEnvironment; /** Wall-clock start of the current run; scopes the background-process gate. */ private runStartedAt; /** Gate injections spent this run, capped by MAX_PROCESS_GATE_INJECTIONS. */ private processGateInjected; /** Plan-submission reminders spent this run (plan-submission-gate.ts). */ private planSubmissionNudges; /** Verification evidence: code edited this run, and what has proved it since. * Passive tracking only — it feeds run status and autopilot, never a turn. */ private readonly verificationGate; /** Host-observed evidence (edited files, in-flight and background checks) feeding the gate. */ private readonly verificationTracker; /** Mirror of the last `hook_armed` value, so the event fires only on an edge. */ private preFinalArmed; private compactionOccurred; /** * Re-grounding carry-over for post-turn compaction. `resetHookState` clears * `compactionOccurred` at run start — before the injection point — so a * background compaction that finished after the final response parks its * "re-ground next turn" signal here for pickup at the next run start. */ private compactionArmedForNextRun; /** In-flight post-turn (background) compaction, if any. */ private postTurnCompaction?; private lastCompactionCompacted; private compactionRetryAfter; /** * SoL-Pi plan-step compaction bookkeeping (see compaction/plan-step-policy.ts). * Step progress comes from `[DONE:n]` markers in assistant text — the same * contract the approved-plan UI tracks. */ private planStepState; /** A restored oversized checkpoint must be canonicalized before its first prompt is persisted. */ private deferredCompactionPending; /** Latest provider count, anchored to the assistant response it measured. */ private providerContext; private originalRequest; private userQueue; private queueSeq; /** Instant interrupt: the running loop's preempt listeners, fired on queueMessage. */ private steeringListeners; private processManager?; private lspManager?; private debugManager?; private subAgentManager?; /** * Out-of-band push notifications (finished children, background-process * progress). Producers enqueue; `getHookSteeringMessages` drains into the * live turn so the agent never has to spend a turn asking. */ private readonly notifications; private managerAbortSignal?; private readonly managerAbortHandler; /** MCP client, deferred tool_search catalog and cached-stub reconcile state. */ private readonly mcp; /** Resolved prompt-injection byte budgets (contextLimits setting). */ private contextLimits; /** * Built-in tools held in the catalog instead of the live toolset. Their names * still render as one-line hints in the prompt's Tools section, so the model * can discover and promote them; a promoted name drops out of this list. */ private deferredBuiltinToolNames; private provider; private model; private cwd; /** accountId from the most recently resolved credentials — cached so sync * callers (e.g. the app-sidecar's context-window footer stat) can reflect * transport-specific windows (e.g. OpenAI Codex OAuth's smaller window) * without re-resolving credentials on every poll. */ private lastAccountId?; private baseUrl?; private maxTokens; private thinkingLevel?; private customSystemPrompt?; /** Sub-agent definition body composed into the standard prompt scaffolding. */ private agentPrompt?; /** Stable prompt prefix retained separately from the volatile uncached tail. */ private baseSystemPrompt; /** * Project languages whose style packs and verify commands are in the * standard prompt. Only ever grows within a session (same policy as the * terminal UI): a pack disappearing mid-task would rewrite the cached prefix * for no benefit. */ private activeLanguages; /** Languages the model has already been given guidance for, via the system * prompt or an appended language update. */ private announcedLanguages; /** Shared with the tool layer so plan-mode restrictions read live state. */ private planModeRef; /** Path of the approved plan currently being implemented, or undefined. When * set, the system prompt carries the `[DONE:n]` progress contract so the * model emits step-completion markers the UI's plan-progress widget reads. */ private approvedPlanPath?; /** Extra workspace roots added with `/add-dir` (resolved, de-duplicated). */ private additionalRoots; private sessionId; private checkpointGeneration; /** Stable identity shared by compaction and approved-plan checkpoint files. */ private conversationId; /** Original user-authored prompt, retained when internal messages replace history. */ private sessionPreview; /** Runtime conversation identity for provider transport headers. Transient * children need one even though they intentionally have no persisted session. */ private readonly transportSessionId; private sessionPath; private lastPersistedIndex; /** * The array `agentLoop` is currently mutating, while a run is in flight. * * Normally identical to `this.messages`, but a mid-loop compaction rebinds * `this.messages` to the compacted result while the loop keeps appending to * its own array. Step-boundary flushes must follow the loop's array or they * would silently persist nothing for the rest of the run. */ private activeLoopMessages; /** * Number of non-system messages guaranteed to be in the session file — the * anchor base for transcript markers (Ken turns, autopilot verdicts, app * markers). `this.messages` can run ahead of the file: the agent loop * appends assistant/tool/steering messages in place but they are only * persisted when the run SUCCEEDS, so after a failed run the in-memory list * carries an unpersisted tail. Markers anchored against that tail point past * their real position on resume — the row (notably an error) renders lower * in the transcript than it happened, bunching at the bottom, or gets * dropped as out-of-range. Anchoring to the persisted prefix keeps resume * placement 1:1 with where the row appeared live. */ private persistedTranscriptCount; /** Current leaf entry ID in the session DAG — used to chain parentIds for branching. */ private currentLeafId; private opts; constructor(options: AgentSessionOptions); /** * Derive the output-token cap for a model. Follows the active model's * `maxOutputTokens` so a session booted on a large-output model (e.g. Kimi's * 256K) doesn't carry that cap to a smaller one (e.g. Opus's 128K) after a * model switch — that mismatch surfaces from the provider as * `max_tokens: 262144 > 128000, which is the maximum allowed …`. An explicit * `maxTokens` override is honored but clamped to the model's ceiling. */ private resolveMaxTokens; initialize(): Promise; /** * Whether a tool name is permitted for this session. With no `allowedTools` * everything passes (default behavior). Otherwise a tool is allowed when its * name is in `allowedTools`, OR it's an MCP tool (`mcp____`) * whose `` is in `allowedMcpServers`. The MCP-prefix rule lets a * whitelisted research server expose all its tools * without hard-coding each one, while every other tool stays blocked. */ private isToolAllowed; /** * Connect all configured MCP servers and append their tools to `this.tools`. * Resolves the GLM api key first (Z.AI's bundled servers need it). Never * throws — a failed connect is logged and skipped — so it is safe to either * `await` (CLI: tools ready before the first turn) or fire-and-forget * (sidecar: `backgroundMcpConnect`, so a slow stdio server can't stall * startup). Tools are pushed onto the live array the agent loop reads each * turn, so background-connected servers become available on the next prompt. */ private connectMcpServers; /** Persist `cwd` as a trusted project for project-scope MCP. Called by the * sidecar's `/mcp/add` handler when a user adds a project-scope server via * the MCP modal — the explicit add is itself the trust signal. The next * session load connects its `.gg/mcp.json` servers. */ trustProject(cwd: string): Promise; /** * Route freshly connected MCP tools: deferred into the tool_search catalog * (default — keeps ~8k tokens of schema out of every cache-miss turn) or * pushed eagerly when the user opted out. * Allow-listed sessions (Ken) always get the eager path — their fixed tool * expectations predate the catalog, and tool_search isn't allow-listed. */ private addMcpTools; /** * Deferred follow-up tools whose need is certain once a trigger succeeds. * `wait_agent` follows nearly every `spawn_agent`, and loading it through * `tool_search` cost a whole turn (bench 41: ~6 s per fan-out). Likewise a * background `bash` is always followed by `task_output` (and sometimes * `task_send`/`task_stop`). Promote them as soon as the trigger succeeds: the * tool list grows exactly as it would after that `tool_search`, one turn * earlier, and sessions that never trigger keep the smaller prefix. * Call before tier partitioning so a deferred trigger is wrapped too. */ private promoteFollowUpTools; /** Append a deferred built-in to the live tool list (append-only). */ private promoteDeferredBuiltin; private wrapToPromote; /** * Publish cached tool definitions into the deferred catalog so `tool_search` * answers correctly on turn 1 (see {@link McpToolRegistry.seedFromCache}). */ private seedMcpCatalogFromCache; /** * Resolve a `/name [args]` input to the prompt template it expands into, or * null when it isn't a prompt-template command for THIS session (an ordinary * message, a registry/action command, or any slash input on a non-coder * agent). Shared by {@link prompt} and {@link willExpandPromptTemplate} so * callers can't drift from the expansion that actually happens. */ private resolveSlashInput; /** * Whether {@link prompt} would expand this input into a template body and * persist it as a user message. Hosts use it to record the typed `/name` for * transcript restore — gating on anything looser risks tagging an unrelated * message when the command turns out NOT to expand. */ willExpandPromptTemplate(content: string): Promise; /** * Process user input. Handles slash commands or runs agent loop. * `capThinking` holds reasoning effort at the plan-mode ceiling for this * prompt — for turns that must answer quickly from what is already known. */ prompt(content: string, provenance?: MessageProvenance, options?: PromptRunOptions): Promise; /** * Prompt with multimodal attachments (images / videos) alongside optional * text. Images and videos become native content blocks the model can see; * non-media files are surfaced as a text note with their saved path so the * agent can open them with its tools. Slash-command parsing is skipped — * attachments are always a direct conversational turn. */ promptWithAttachments(text: string, attachments: SessionAttachment[]): Promise; /** * Build the native content blocks (text + image/video notes + file notes) for * a user message with attachments. Shared by {@link promptWithAttachments} and * the mid-run steering drain so queued media is delivered identically. */ private buildAttachmentParts; /** * Reset per-run self-correction hook state. Mirrors the TUI's run_start * resets so each run evaluates the hooks from a clean slate. `originalRequest` * is the verbatim user ask, pinned for post-compaction re-grounding. */ private resetHookState; /** * Fold one agent event into the hook stat accumulators. Pure bookkeeping — * the same signals the TUI's useAgentLoop collects, so loop-break decisions * match across the CLI and the app. */ private trackHookEvent; /** * Count full-file `cat` output from the step that just finished as reads, so * an edit after `cat` does not cost a second read. Uses the step's results as * stored in the transcript (after per-turn trimming): only bytes the model * actually received count. */ private creditBashReads; /** * Append every message added since the last flush to the session file. * * Safe to call mid-run: `agentLoop` mutates its message array in place, so * the slice from `lastPersistedIndex` is always exactly the new tail. * Transient sessions (subagent spawns) have no session file and * `persistMessage` no-ops for them. * * Persistence failures must never take down a live run — the post-loop flush * retries the same range. */ private flushPendingMessages; /** * Mid-loop steering hook: delivers pushed notifications and queued user * steering, fires the loop-breaker when the agent looks stuck, then * post-compaction re-grounding. At most one loop-break/re-grounding per run. * Mirrors the TUI's getSteeringMessages ordering. */ private getHookSteeringMessages; /** Fire the semantic loop judge when deterministic evidence is suspicious but * the deterministic breaker stayed quiet — the syntactic blind spot where * every retry differs slightly and the run still makes no progress. * Fire-and-forget: the call runs while the next turn streams, and a finished * verdict is consumed by the NEXT steering poll — never blocking a turn. */ private maybeStartSemanticLoopCheck; /** Fresh image-only critique context, using the active transport and auth routing. */ private callCompletionReviewer; /** One-shot judge call on the session's ACTIVE model — deliberately not a * cheaper routing: judging a model's own failure patterns with a weaker * model swaps false negatives for false positives. */ private callSemanticLoopJudge; /** * Turn-budget extension gate. The loop consults this instead of stopping * mid-task when it exhausts `maxTurns`. Grant ONLY on evidence of progress — * handing more turns to a spinning agent just buys it more tokens to spin * with — so reuse the same stuck signals that drive the loop-breaker. */ private shouldExtendTurnBudget; /** Would a stop right now inject a pre-final follow-up? Only queued LSP * diagnostics (real errors injected below) or a mode-owned completion * review can, so clients hold the candidate answer only then. */ private wouldInjectBeforeFinal; /** Broadcast pre-final hook arming on change. Both edges matter: armed=false * after the hook fires is what lets a client stream the final answer live * again. Callable before `initialize()`, when no manager exists yet. */ private refreshHookArming; /** * Pre-stop follow-ups: LSP errors, unread child agents and background * processes, and a mode-owned completion review. Verification evidence is * tracked passively for run status; it never forces another turn. */ private getHookFollowUpMessages; /** Wraps the real run: cancels an in-flight cache prewarm and tracks run * activity / last real request time for {@link prewarm}. */ private runLoop; /** Auto-compact if needed, run agent loop with auth retry, and persist messages. */ private runLoopInner; switchModel(provider: string, model: string): Promise; /** * Record the switch as its own trailing message at the point it happened, * rather than by rewriting the system prompt. Two reasons: the system prompt * is the cached prefix and rewriting it costs a full cache write, and a * resumed transcript can only attribute output to the right model if the * switch sits in message order. * * Skipped before the conversation starts (nothing to disambiguate) and while * an assistant turn has unresolved tool calls, where inserting a user message * would break tool_use/tool_result pairing. */ private appendModelSwitchNote; private adoptCompactionCheckpoint; /** Canonicalize a deferred restore before a new prompt can fork stale history. */ private adoptDeferredCheckpointBeforePrompt; private persistCompactionCheckpoint; /** * Wait out any in-flight post-turn background compaction before a new * entry point mutates `this.messages` (prompt, attachments, model switch). * The background compact() snapshots and then REPLACES the array, so a * message pushed while it runs would be silently dropped from the live * history — the pre-run await in runLoop() sits after the push and cannot * protect it. */ private settlePostTurnCompaction; /** * Advance plan-step bookkeeping over the messages added since the last * observation and, when a plan step was newly completed (`[DONE:n]`), run * the SoL-Pi cost rule. Called between model steps (in-flight) and once * after the run; `scanIndex` and `doneSteps` make each message and each * step count once across both paths. Returns undefined when no step * completed since the last observation. */ private observePlanStepProgress; /** * Plan-step bookkeeping after ANY successful compaction (as in the bench: * every compaction leaves a cache-write cost to be repaid by later savings). */ private recordPlanStepCompaction; /** * Post-turn compaction: once the final response has been delivered, compact * in the background while the user reads the answer, instead of making the * next prompt pay the summarizer latency up front (the pre-run path stays as * the backstop, so a skipped or failed attempt costs nothing). Mirrors the * Codex `model_post_turn_compact_threshold_percent` guards: skip when user * input is already queued (it would race the next turn), when the run was * aborted, or during the failure cooldown — and never let a compaction * error surface in the completed turn. Besides the size trigger, a newly * completed approved-plan step may compact when the cache-cost rule in * compaction/plan-step-policy.ts says the shrink pays for itself. */ private maybeCompactPostTurn; compact(existingCredentials?: { accessToken: string; accountId?: string; projectId?: string; baseUrl?: string; }, mode?: "manual" | "automatic" | "forced", /** User-stated focus (`/compact `): what must survive verbatim. */ focus?: string): Promise; newSession(preserveConversation?: boolean): Promise; loadSession(sessionPath: string): Promise; /** * Create a branch at a specific point in the conversation. * Rewinds the message history to the given entry and sets the leaf * so new messages fork from that point. * * @param stepsBack Number of messages to rewind (default: 2 — backs up past last assistant + tool) */ branch(stepsBack?: number): Promise<{ branchedFrom: number; messagesKept: number; }>; /** * List all branches in the current session. */ listBranches(): Promise; getState(): AgentSessionState; /** * Tokens currently in context and the window they are measured against. * * Uses the same accounting as the compaction decision: authoritative provider * usage when we have it (it includes the system prompt and tool schemas), * plus a local estimate of anything appended after that sample. A client * reading this right after a compaction sees the drop, because compaction * clears the retained provider sample along with the messages it measured. * * `costUsd` is present only when EVERY recorded turn has an authoritative * price; a partial sum would read as a full session cost and understate it. */ getContextUsage(): { used: number; size: number; costUsd?: number; }; /** * Whether the provider's prompt cache has likely lapsed since the last * successful request, and how many tokens the next message would re-read at * full price. Null when the route has no known TTL or the chat is empty. */ getCacheExpiryStatus(now?: number): CacheExpiryStatus | null; getPlanMode(): boolean; /** Queue a user message (optionally with attachments) to be injected mid-run * as steering. Returns the new queue length. No-op semantics are the caller's * concern. */ queueMessage(text: string, attachments?: SessionAttachment[]): number; /** Pending queued messages (id + text), oldest first, for client display. */ listQueuedMessages(): Array<{ id: string; text: string; }>; /** Cancel one pending message by id. Returns true if it was still queued. * A false return is the normal race rather than an error: the message drained * into the run between the client rendering the cancel affordance and the * click arriving. */ cancelQueuedMessage(id: string): boolean; /** Number of messages currently queued. */ getQueuedCount(): number; /** Remove and return the oldest queued message (text + attachments), or null. * Used by the sidecar to run a message that queued while autopilot was * reviewing (no run in flight to steer it into) — unlike {@link drainQueue}, * attachments survive so queued media isn't silently dropped. */ takeNextQueuedMessage(): { text: string; attachments: SessionAttachment[]; } | null; /** Clear the queue, returning the combined text (to restore to the composer). * Queued attachments are dropped on cancel — the composer only restores text. */ drainQueue(): string; /** * Tell the agent a new fact at its next turn, as a short hidden message * appended after the cached prompt (the same channel as a finished worker), * so a mid-session change reaches it without re-rendering the prompt. A * later note with the same id replaces one not yet delivered. */ notify(id: string, text: string): void; /** This session's sub-agents (workers), newest-state, including ones restored after a restart. */ listSubAgents(): SubAgentSnapshot[]; /** Snapshot of background processes (bash run_in_background), newest-state. */ listBackgroundProcesses(): BackgroundProcess[]; /** Stop a background process by id. Returns a human-readable status string. */ killBackgroundProcess(id: string): Promise; /** * Force-stop every background process tree, synchronously. Background * commands run in their own process group, so the daemon's group kill on * quit never reaches them: this is the only thing that does. Callers on a * shutdown deadline run it before awaiting anything that can hang. */ stopBackgroundProcesses(): void; /** Replace a host-owned system prompt in place without resetting conversation history. */ setCustomSystemPrompt(systemPrompt: string, promptCacheKeyPrefix?: string): void; /** * Toggle plan mode: flips the shared ref (so tools enforce read-only * restrictions) and rebuilds the system prompt in place so the model is told * about the mode change on its next turn. No-op when a custom system prompt * is in force (the host owns the prompt then). */ setPlanMode(active: boolean): Promise; /** * Bake an approved plan into the system prompt so the model is told to emit * `[DONE:n]` markers as it completes each step (the contract the UI's * plan-progress widget reads). Pass `undefined` to clear it. No-op when a * custom system prompt is in force (the host owns the prompt then). */ setApprovedPlan(approvedPlanPath: string | undefined): Promise; /** Extra workspace roots added with `/add-dir`, in the order added. */ getAdditionalRoots(): string[]; /** * The custom `.gg/commands` slash commands this session can run: the * project's, then those of every added root (a goal's attached repos too). * Read fresh, so a new command file is picked up without a restart. */ loadCustomCommands(): Promise; /** * Add another workspace root. Tools already accept absolute paths, so this * only widens the write guard and tells the model the root exists. Rebuilding * the system prompt costs one cache-miss turn — the alternative (an uncached * suffix) would drift from the tool behaviour it describes. * * @returns the resolved root, or an error message for the user. */ addDirectory(dir: string): Promise<{ ok: true; root: string; } | { ok: false; error: string; }>; /** Remove an exact root previously added with `/add-dir`. */ removeDirectory(dir: string): Promise<{ ok: true; root: string; } | { ok: false; error: string; }>; /** * Name a root change in the prompt only while there is no conversation yet. * Mid-conversation the env-delta note tells the model instead: the roots sit * in the Environment section, and rewriting it would invalidate every cached * message after it (a goal attaching a folder re-sent ~75k tokens). */ private refreshPromptForRoots; /** * Names to advertise as available-on-demand. A tool the model already * promoted lives in `this.tools` and carries its own schema, so it drops out * of the index rather than being listed twice. */ private deferredToolNamesForPrompt; /** * The environment about to be rendered into a prompt, remembered as the * truth the model has been told. Pairs with the env-delta hook: whatever the * prompt states, the model is only corrected when reality moves away from it. */ private recordRenderedEnvironment; /** Environment facts that vary per session rather than per host. */ private promptEnvironment; /** * Build the stable system-prompt prefix for the current tool set and state. * * Three modes, in precedence order: a full replacement (`systemPrompt`), a * composed sub-agent prompt (`agentPrompt` — agent body plus Tools, project * context, return contract and Environment), or the standard prompt. */ private buildBasePrompt; /** * Bring the system prompt to the exact state the next provider request will * send. Shared by real runs and {@link prewarm} so both see one prefix. * * Languages are re-detected at each task boundary. Before the first reply * nothing is cached worth keeping, so a new language simply rebuilds the * prompt. After that the prompt is a cached prefix and is left alone: the * new language is announced by {@link announceNewLanguages} instead. */ private prepareSystemPromptForRequest; private hasReply; /** * Give the model style packs for languages that appeared after its prompt * was cached (e.g. the previous task scaffolded a TypeScript project), as a * hidden message appended to the history rather than a prompt re-render that * would throw away the whole cache. Runs just before a new user message is * added, so the user's request stays the last thing the model reads and the * update is saved in order with the rest of the conversation. */ private announceNewLanguages; /** * Whether this session renders language packs: the standard prompt, and a * code-writing child (a sub-agent prompt with project context). A verbatim * custom prompt never does, nor do user-facing specialist agents (Motion, * goals) or recon children without project context. */ private languagesApply; /** Add newly detected project languages. Returns true when the set grew. */ private refreshActiveLanguages; /** Rebuild messages[0] from current plan-mode + approved-plan state. */ private rebuildSystemPromptInPlace; /** * Compose the system message: stable prefix, then everything volatile behind * the `` marker that providers split on for cache control. * * Anything that varies with the live model/provider/thinking level belongs in * the tail. Putting it in the prefix means every model switch rewrites cached * bytes and the next turn pays a full cache write instead of a read. */ private withSystemPromptTail; /** * Sol/Terra async-orchestration guidance for the CURRENT model and thinking * level. Per-switch volatile by definition, so it is rendered as a tail part * rather than spliced into the cached prefix. */ private orchestrationPolicyTail; /** * Re-render the uncached tail of messages[0] in place. The cached prefix is * byte-identical afterwards, so this is safe to call on every model / * thinking-level change. */ private refreshSystemPromptTail; getMessages(): Message[]; getTurnMetrics(): TurnMetricPayload[]; private persistTurnMetric; /** Ken Kai (mentor) turns recorded against this session, in record order. Used * by the host to interleave Ken's advisory exchanges back into the transcript * on resume. Never part of the LLM message history. */ getKenTurns(): KenTurnPayload[]; /** Autopilot verdict markers recorded against this session, in record order. * Used by the host to interleave the auto-review loop's markers back into * the transcript on resume, mirroring `getKenTurns`. */ getAutopilotMarkers(): AutopilotMarkerPayload[]; /** Non-system messages that are actually on disk. Transcript markers anchor * against this (not the in-memory list, which can run ahead after a failed * run), so hosts computing marker-derived values must use the same base. */ getPersistedTranscriptCount(): number; /** * Rebase every transcript anchor (Ken turns, autopilot verdicts, app markers) * onto a freshly compacted message list. Called right after `this.messages` * is replaced and before the markers are re-persisted into the continuation * file, so the new file carries positions that match its own transcript. */ private remapMarkerAnchors; /** * Record one Ken Kai (mentor agent) turn against this build session: the * user's question + Ken's reply. Kept in memory for the live transcript and * persisted as a `custom` entry (parentId null, so it's never on the message * DAG and never seen by the LLM, and can't race the build session's leaf while * Ken runs concurrently). `afterMessageCount` anchors it among the messages so * the host can interleave it chronologically. No-op persistence for transient * sessions (kept in memory only). Best-effort: a write failure is swallowed by * appendEntry's own handling. */ persistKenTurn(question: string, reply: string): Promise; /** * Record one autopilot verdict marker (prompted / done / human / capped) * against this build session. Kept in memory for the live transcript and * persisted as a `custom` entry (parentId null, same as Ken turns) so a * resumed session renders the exact same Ken bubble the live run showed * instead of dropping the marker or falling back to a raw verdict string. * No-op persistence for transient sessions (kept in memory only). */ getVerificationEvidence(): VerificationEvidence[]; /** Current request activity, separate from persistent workspace verification debt. */ getRunVerificationActivity(): { changed: boolean; checked: boolean; evidence: VerificationEvidence[]; }; getVerificationProblem(): string | null; private persistCompletionReviewState; private persistVerificationState; persistAutopilotMarker(phase: AutopilotMarkerPayload["phase"], extra?: { reason?: string; body?: string; }): Promise; /** App transcript markers recorded against this session, in record order. * Used by the host to interleave display-only rows (plan banner, task * header, errors, user-bubble hints) back into the transcript on resume. */ getAppMarkers(): AppMarkerPayload[]; /** * Record one app transcript marker (display-only row) against this session. * Same treatment as autopilot markers: kept in memory for the live * transcript, persisted as a `custom` entry (parentId null, never on the * message DAG) so a resumed session shows the identical row. `anchorOffset` * shifts the recorded `afterMessageCount` — pass +1 for a marker that should * attach to the user message about to be pushed by the imminent prompt. * No-op persistence for transient sessions. */ persistAppMarker(kind: AppMarkerPayload["kind"], data: Record, anchorOffset?: number): Promise; /** * Open the run journal for one `RunLifecycle` generation. * * Never throws and never blocks the run: a session with no file (transient * children) writes nothing, and a write failure just means the run isn't * journalled — which is strictly better than failing the run over it. */ persistRunStarted(generation: number): Promise; /** Close the run journal. Its absence is what marks a run as crashed. */ persistRunFinished(generation: number, outcome: RunOutcome): Promise; /** * Rewrite a draft prompt into a tighter, terminology-correct version using * the ACTIVE provider/model. A stateless one-off LLM call (no agent loop, no * tools, no session mutation) — safe to run even mid-run. Returns the plain * enhanced text plus typed segments marking each corrected term. Errors throw * so the caller can surface them. */ enhancePrompt(text: string): Promise; /** Current reasoning/thinking level, or undefined when thinking is off. */ getThinkingLevel(): ThinkingLevel | undefined; /** Set the reasoning/thinking level (undefined turns thinking off). Takes * effect on the next prompt, since the in-flight loop reads it at start. */ setThinkingLevel(level: ThinkingLevel | undefined): void; /** Replace the abort signal (e.g. after cancellation). */ setSignal(signal: AbortSignal): void; private bindManagerCancellation; private runLoopDepth; private lastRealRequestAt; private lastPrewarmAt; private prewarmController; /** * Best-effort Anthropic prompt-cache prewarm before the user's next turn * (desktop app calls this on the first keystroke after opening a chat or an * idle pause). Sends the exact request prefix the next real turn will use — * same system/tools/thinking/cache options — with `max_tokens: 1`, so the * first real reply is a cache read instead of a cold write. */ prewarm(signal?: AbortSignal): Promise<{ ok: boolean; reason: string; usage?: Usage; }>; private codexShapeSetting; /** True when speedProfile is "optimized" (1-h cache TTL + pre-warm), or the * session was constructed with `forceLongCacheRetention` (Ken sessions). */ private isSpeedOptimized; /** * Ordered auth-storage keys the current (provider, model) pair tries, first * match wins. Almost always just the provider id; Xiaomi models can prefer * one endpoint and fall back to another the user configured instead (e.g. * `mimo-v2.6-pro` prefers the Token Plan, falls back to API Credits; the * API-only `mimo-v2.6-pro-ultraspeed` has no fallback). */ private currentAuthStorageKeys; private getPromptCacheKey; /** Stable cache-routing key for downstream sub-agent processes. */ getCurrentCacheKey(): string | undefined; dispose(): Promise; private setSessionPath; private createNewSession; private loadExistingSession; /** * Surface runs that died mid-flight, without resuming them. * * Deliberately NOT auto-resumed: the dead run's tools already wrote files, * ran commands and made commits. Replaying it would duplicate those effects. * The user gets a transcript row and decides. * * Each detected run is also closed as `aborted`, so reopening the session * reports it once rather than on every load. */ private recordInterruptedRuns; private prepareDynamicContext; private persistMessage; private createSlashCommandContext; /** * Import a Claude Code / Codex / Cursor transcript as a resumable GG Coder * session in this session's sessions directory. Never throws — a bad path or * an unrecognized format comes back as `{ ok: false, error }` so both the CLI * and the desktop app can show it verbatim. */ importForeignTranscript(filePath: string, opts?: { cwd?: string; }): Promise; }