import type { Message, MessageProvenance, Provider, ThinkingLevel } from "@kenkaiiii/gg-ai"; import { type ToolExecuteResult } from "@kenkaiiii/gg-agent"; import { AgentSession, type SessionAttachment } from "../core/agent-session.js"; import type { ChildModelPick, SubAgentSnapshot } from "../core/subagent-manager.js"; import { createAskUserTool } from "../tools/ask-user.js"; import { type GoalBriefExtras, type GoalRecord } from "../core/goals-store.js"; import type { EventBus } from "../core/event-bus.js"; import type { AutopilotMarkerPayload } from "../core/session-manager.js"; import { type AcpKenReviewer } from "./acp-autopilot.js"; /** The ACP major version this mode implements. Bumped only for breaking changes. */ export declare const ACP_PROTOCOL_VERSION = 1; /** * The slice of {@link AgentSession} this mode uses. * * Narrowed to an interface so a test can drive the whole protocol without a * provider, credentials or a network — the transport is what is under test, * and a real model would make the frames non-deterministic. */ export interface AcpAgentSession { readonly eventBus: Pick; initialize(): Promise; /** `provenance` marks GG Goals' hidden kickoff/resume instructions. */ prompt(content: string, provenance?: MessageProvenance): Promise; /** Image prompts (`promptCapabilities.image`); without it images are ignored. */ promptWithAttachments?(text: string, attachments: SessionAttachment[]): Promise; /** * Mid-run steering (`_gg/queue`). Optional so a test double or alternative * session need not support it; `_gg/queue` then answers -32602. */ queueMessage?(text: string): number; listQueuedMessages?(): Array<{ id: string; text: string; }>; cancelQueuedMessage?(id: string): boolean; drainQueue?(): string; /** GG Goals' workers, for `_gg/goal_state`. */ listSubAgents?(): SubAgentSnapshot[]; /** Adds a goal's attached folder to the workspace. */ addDirectory?(dir: string): Promise<{ ok: true; } | { ok: false; error: string; }>; /** Tell the agent a fact at its next turn (goal settings changed). */ notify?(id: string, text: string): void; getState(): { sessionId: string; provider: Provider; model: string; }; /** * Context accounting for the ACP `usage_update` notification. Optional so a * test double or an alternative session implementation need not carry token * accounting — a session without it simply reports no usage. */ getContextUsage?(): { used: number; size: number; costUsd?: number; }; dispose(): Promise; /** Replace the turn-cancellation signal so the session remains reusable. */ setSignal(signal: AbortSignal): void; /** Replay a session file from disk into this session. */ loadSession(sessionPath: string): Promise; /** * Ken's verdict markers (desktop `persistAutopilotMarker`). Optional so a * test double need not persist; without them a reopened session has no * Ken bubbles. */ persistAutopilotMarker?(phase: AutopilotMarkerPayload["phase"], extra?: { reason?: string; body?: string; }): Promise; getAutopilotMarkers?(): AutopilotMarkerPayload[]; /** The conversation as restored, used to replay history to the client. */ getMessages(): Message[]; switchModel(provider: string, model: string): Promise; getThinkingLevel(): ThinkingLevel | undefined; setThinkingLevel(level: ThinkingLevel | undefined): void; /** Plan mode is GG's session mode: read-only research until exit_plan. */ getPlanMode(): boolean; setPlanMode(active: boolean): Promise; /** Bake an approved plan into the prompt so [DONE:n] progress markers work. */ setApprovedPlan(planPath: string | undefined): Promise; /** Host verification evidence for Ken's digest; optional for test doubles. */ getVerificationEvidence?(): AgentSession["getVerificationEvidence"] extends () => infer R ? R : never; getVerificationProblem?(): string | null; /** Fresh context for an accepted plan (desktop /plan/accept). Optional: * without it an accept keeps the current context and sends no reset. */ newSession?(preserveConversation?: boolean): Promise; /** * Registry commands (/model, /compact, …), including any an extension * registered during `initialize`. Optional so a test double need not carry a * registry — a session without one simply advertises no registry commands. */ readonly slashCommands?: { getAll(): { name: string; aliases: string[]; description: string; usage: string; }[]; }; } /** * The plan-mode callbacks the agent loop calls when the model uses the * `enter_plan` / `exit_plan` tools. * * Handed to the session factory rather than built inside it so a test double * can drive plan mode without a model: approving a plan is what turns it into * the client's to-do list, and that path is otherwise unreachable. */ export interface AcpPlanHooks { onEnterPlan: () => Promise; /** Returns the instruction handed back to the model after approval. */ onExitPlan: (planPath: string) => Promise; } /** * What a session is being built for, beyond the plan hooks. * * A third argument rather than a change to the first two so existing session * factories (tests) keep working unchanged. */ export interface AcpSessionContext { /** The session's working directory: the goal folder for a goal session. */ cwd: string; /** Set when the cwd is a goal folder: build a GG Goals coordinator. */ goal: { dir: string; record: GoalRecord; } | null; /** The goal's brief extras (memory, attached projects' rules), read once. */ goalExtras?: GoalBriefExtras; /** Read live: true once the user has started the goal. */ isGoalActive: () => boolean; /** Read live at each worker spawn. */ getChildModel: () => ChildModelPick | undefined; /** * The `ask_user` tool, present only when the client opted in with * `clientCapabilities._meta.gg.askUser`. Goes in `additionalTools` at * construction (the tool array is append-only for the prompt cache). */ askUserTool?: ReturnType; /** * The client reviews plans itself (`clientCapabilities._meta.gg.planReview`): * exit_plan parks the plan for a human decision instead of auto-approving, * and the session gets the desktop's `planSubmissionGate`. */ planReview?: boolean; } /** * A plan submitted via exit_plan that awaits a decision (desktop parity: * app-sidecar.ts pendingPlanPath/planGeneration/offeredPlanGeneration). One * object so the autopilot cycle can read and settle it. */ export interface AcpPendingPlanState { planPath: string | null; content: string; /** Bumped on every set/clear; a stale decision is refused. */ generation: number; /** Generation last offered to the client; -1 when none. */ offeredGeneration: number; } export interface AcpModeOptions { provider: Provider; model: string; cwd: string; version: string; baseUrl?: string; systemPrompt?: string; thinkingLevel?: ThinkingLevel; /** Defaults to `process.stdin` / `process.stdout`. */ input?: NodeJS.ReadableStream; output?: NodeJS.WritableStream; /** * Builds the session for `session/new`. Overridden by tests; production * always gets a real {@link AgentSession}. */ createSession?: (signal: AbortSignal, hooks: AcpPlanHooks, context: AcpSessionContext) => AcpAgentSession; /** `~/.gg` by default; where goals (`goals/`) and sessions live. */ agentDir?: string; /** * `ask_user` soft deadline (default: the desktop's interactive 10 min). Past * it the model hears "no answer yet" and the client's question stays open. */ askUserDeadlineMs?: number; /** * Builds Autopilot Ken's reviewer for a session's project. Overridden by * tests; production gets {@link createAcpKenReviewer}. */ createKenReviewer?: (cwd: string, buildModel: () => { provider: Provider; model: string; }) => AcpKenReviewer; } /** The selectors this agent exposes. Ids are part of the wire contract. */ export declare const MODEL_CONFIG_ID = "model"; export declare const THINKING_CONFIG_ID = "thinking"; export declare const MODE_CONFIG_ID = "mode"; /** * Turn a restored conversation into the `session/update` stream a client needs * to draw it. * * ACP has no "here is the transcript" response: a loaded session is replayed as * the same notifications a live turn produces, so the client needs no second * rendering path. Thinking is deliberately NOT replayed — it is transient by * design, and a wall of stale reasoning above a resumed conversation buries the * thing the user came back for. * * Each replayed chunk carries a `messageId` so a client can group chunks into * the messages they came from; ids are per-replay and positional, which is all * the protocol needs of them. */ export declare function historyUpdates(messages: readonly Message[]): Record[]; /** A replayed Ken marker, as `_gg/autopilot_event` params (sans sessionId). */ export interface HistoryAutopilotEvent { phase: string; reason?: string; body?: string; history: true; } /** * {@link historyUpdates} with Ken's verdict markers interleaved at their * `afterMessageCount` (non-system messages, as the desktop history route * counts them). Marker phase `plan_approved` replays as ACP `plan_accepted`. */ export declare function historyFrames(messages: readonly Message[], markers: readonly AutopilotMarkerPayload[]): Array<{ update: Record; } | { autopilot: HistoryAutopilotEvent; }>; /** * Serve ACP on stdio until the input stream ends. * * Resolves when the client disconnects; the caller owns process exit. */ export declare function runAcpMode(options: AcpModeOptions): Promise; /** * CLI entry point: serve ACP on the real stdio, then exit. * * Split from {@link runAcpMode} so the protocol can be tested without the * process-level side effects (signal handlers, logger teardown, exit codes). */ export declare function runAcpModeCli(options: AcpModeOptions): Promise;