/** * InProcessTransport — wraps a pi-agent-core {@link Agent} behind the * {@link SessionTransport} interface (D-P2-4, scope §3). * * `Agent.subscribe` listeners are bridged onto a {@link SessionEvent} stream * via the pure {@link mapAgentEvent} mapping. The transport owns `seq` * assignment (monotonic from 1) and `sessionId` injection. A bounded async * queue backs `events()` so a slow consumer never blocks the agent loop. * * Provider lane: OpenRouter through pi-ai (D-P2-2). The default model * catalogue comes from `../credentials.js` `defaultModels()` — builtinModels * backed by the persistent credential store. Without an injected `models` * collection the transport fails fast with a named error * ({@link MissingApiKeyError}) if the OpenRouter key is missing before any * network call. * * P4: the transport accepts an injected {@link Models} collection + provider * id so the TUI (and the faux-provider golden-frame tests) can drive the same * loop without forking it. When `models` is supplied the OpenRouter key * requirement is skipped — the caller owns provider auth (faux needs none). * The default path (no `models`) is unchanged so `openkai chat` is * byte-for-byte identical. */ import type { AgentMessage, AgentTool } from "@earendil-works/pi-agent-core"; import type { Model } from "@earendil-works/pi-ai"; import type { Api } from "@earendil-works/pi-ai"; import type { Models } from "@earendil-works/pi-ai"; import type { CastConfig } from "../fusion/casts.js"; import { SessionPermissionGate } from "./permission-gate.js"; import type { SessionEvent, SessionTransport, SessionTransportOptions } from "./transport.js"; /** Default model: cheap tool-calling OpenRouter model from the bundled catalogue. */ export declare const DEFAULT_MODEL_ID = "nvidia/nemotron-3-nano-30b-a3b:free"; /** Error thrown when OPENROUTER_API_KEY is missing — caught by the CLI for a named exit. */ export declare class MissingApiKeyError extends Error { readonly name = "MissingApiKeyError"; constructor(provider: string, envVar: string); } /** Options for constructing an {@link InProcessTransport}. */ export interface InProcessTransportOptions extends SessionTransportOptions { /** Override the read-only tool set (default: the P2 trio bound to cwd). */ tools?: AgentTool[]; /** * Injected {@link Models} collection (P4). When supplied the transport * resolves the model from it via `getModel(provider, modelId)` and skips * the OpenRouter API-key requirement — the caller owns provider auth. Used * by the faux-provider golden-frame tests; production paths leave this * unset and use the built-in OpenRouter catalogue. */ models?: Models; /** * Provider id to resolve the model under (default: `openrouter`). Ignored * when `models` is unset. Pairs with {@link models} for injection. */ provider?: string; /** * Prior message entries to seed the agent transcript (P4 session resume). * Passed to the Agent as `initialState.messages` so a resumed session has * model context. Empty by default (fresh session). */ initialMessages?: AgentMessage[]; /** * P4b: enable the permission gate. When true the transport owns a * {@link SessionPermissionGate} and exposes the gated tool set * (write_file / edit_file / bash) behind {@link SessionTransport.respond}. * When false (default — the `openkai chat` v1-compat path) the transport * uses the read-only trio and `respond()` throws (no approval channel). */ enablePermissions?: boolean; /** * Activity sink: every session event is also offered here (for the live * activity feed behind `openkai tail`). Fire-and-forget; never awaited. */ onActivity?: (event: SessionEvent) => void; /** * Extra tools merged INTO the built-in set — never a replacement for it. * With the gate enabled they go through {@link gatedTools}' extraTools * slot; without it they append to the read-only set. This fixes the * "MCP replaces everything" bug: built-ins are always present. * For post-construction injection (e.g. gate-wired MCP proxies) use * {@link InProcessTransport.addExtraTools}. */ extraTools?: AgentTool[]; /** Operator cast config (~/.openkai/config.json "casts") for the task tool's stage→model resolution. */ castConfig?: CastConfig; } /** * In-process transport over a pi-agent-core {@link Agent}. The agent runs in * the same Node process; events flow through a bounded queue to the consumer. */ export declare class InProcessTransport implements SessionTransport { readonly sessionId: string; private currentModelId; private currentThinkingLevel; private readonly agent; private readonly queue; private seq; private closed; /** P4b permission gate (undefined when permissions are disabled — v1 path). */ private readonly gateInstance; private readonly shadow; private readonly cwd; private readonly mutationHooks; private readonly onActivity; /** Stored tool sets for plan/act mode switching (E010). */ private readonly fullTools; private readonly readOnlySet; private _planMode; /** The models collection the agent streams through (kept for compaction). */ private readonly modelsCollection; /** Stamp + push a permission_request event onto the session queue. */ private emitPermissionEvent; constructor(options: InProcessTransportOptions); /** Whether plan mode is active (read-only tools only). */ get planMode(): boolean; /** The session permission gate (undefined when permissions are disabled). */ get gate(): SessionPermissionGate | undefined; /** * Toggle plan mode: swaps the agent's tool set between full and read-only, * AND flips the gate's plan-mode refusal so an in-flight turn whose tool * snapshot predates the toggle is still refused at the gate (fail-closed). */ setPlanMode(on: boolean): void; /** * Append tools to the live set post-construction (gate-wired MCP proxies — * the gate only exists after the transport is constructed, so discovery * runs second). Updates the agent's live tool set unless plan mode is on * (the swap back out of plan mode picks them up via {@link fullTools}). */ addExtraTools(tools: AgentTool[]): void; /** The active model id (mutable: `/model` switches mid-session). */ get modelId(): string; /** * Switch the model for future turns (pi-agent-core: state.model is * forward-looking by contract). The picker resolves the Model from the * catalogue; this just applies it. */ setModel(model: Model): void; /** Set the reasoning effort for future turns (off…max). */ setThinkingLevel(level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"): void; get thinkingLevel(): string; /** Set the autonomy axis (no-op when the gate is disabled). */ setAutonomy(level: "off" | "low" | "med" | "high"): void; get autonomyLevel(): string; /** * Run a shell command through the SAME gate as model-driven bash (TUI * bash-mode, droid's `!` toggle): identical overlay, consent, and floor — * the operator's keystroke and the model's tool call are one trust path. * Throws when the gate is disabled (print mode). */ runBash(command: string): Promise<{ text: string; isError: boolean; }>; prompt(text: string): Promise; steer(text: string): void; abort(): void; /** * P4b: answer a {@link permission_request}. Implemented on this transport * only — the trust boundary (scope §2). When the permission gate is not * enabled (the v1-compat `openkai chat` path) this throws rather than * silently no-op'ing, so the "remote approval injection banned" guarantee is * an explicit refusal instead of being dropped. */ respond(requestId: string, decision: "once" | "always" | "reject"): void; events(): AsyncIterable; getMessages(): AgentMessage[]; setMessages(messages: AgentMessage[]): void; getContextWindow(): number; /** * LLM-summarising compaction (E017 contract #1 — replaces the naive * head+last-pair elision). The conversation before pi-agent-core's * `findCutPoint` cut (their `keepRecentTokens` discipline, splitting at a * turn boundary) is summarised by `generateSummaryWithUsage` — the * structured Goal/Progress/Decisions/Next-Steps checkpoint, or its * incremental UPDATE when `previousSummary` is passed. The context becomes * `[summaryMessage, ...retainedTail]`: the summary travels as a user-role * message mirroring pi's `createCompactionSummaryMessage` wire text, so * this transport's role-filtering `convertToLlm` keeps it. * * Returns the raw summary (persist it and pass it back next call for the * incremental path) plus estimated context tokens before/after. Returns * `undefined` when there is nothing worth compacting — fewer than two * messages, or a cut that would summarise nothing / elide nothing. */ compactSession(previousSummary?: string): Promise<{ summary: string; before: number; after: number; } | undefined>; /** * Close the session: abort any active run, reject all pending approvals, * shut down MCP servers and the LSP client, then close the event queue. * A closed session never leaves a language server or MCP child running. */ close(): Promise; /** * Undo the most recent gated mutation: restore the work tree to the * previous shadow snapshot. Throws when permissions are disabled (no * shadow repo exists) or there is nothing to undo. */ undoLastMutation(): Promise; } //# sourceMappingURL=local-transport.d.ts.map