import { OpenCodeAdapter } from './opencode-stdio-adapter.js'; import type { AgentSendOpts, ChatMessage } from '../../../core/types.js'; import { type OpencodeServeManager } from './serve-manager.js'; import { type ApprovalBus } from '../../../core/approval-bus.js'; /** Shape of an event arriving on opencode's /event SSE stream. Every field * is optional because we are deliberately conservative — opencode's bus is * shared across many event types and we touch only the slice we need. */ interface OpencodePart { id?: string; type?: string; text?: string; tokens?: { input?: number; output?: number; total?: number; reasoning?: number; }; cost?: number; time?: { start?: number; end?: number; }; sessionID?: string; } /** Shape returned by POST /session/:id/message on opencode 1.14.45+ where the * /event SSE stream silently drops bus events. The endpoint blocks until the * assistant message is complete and returns the full message body. We use it * as the authoritative source of text + usage when SSE yields nothing. */ interface OpencodeMessageResponse { info?: { id?: string; sessionID?: string; cost?: number; tokens?: { input?: number; output?: number; total?: number; reasoning?: number; }; }; parts?: OpencodePart[]; } interface OpencodeEvent { type: string; properties?: { sessionID?: string; info?: { id?: string; status?: { type?: string; }; sessionID?: string; }; part?: OpencodePart; status?: { type?: string; }; error?: unknown; /** permission.asked carries an id at properties.id (Permission.Request). */ id?: string; /** Permission name on permission.asked, e.g. "external_directory" / "bash". */ permission?: string; /** Patterns the agent wants to access. */ patterns?: string[]; /** Tool-specific metadata that may help the user decide. */ metadata?: Record; }; } export interface HttpAdapterOptions { /** Override the singleton serve manager. Tests inject a mock. */ serve?: OpencodeServeManager; /** Override fetch. Tests inject a mock. */ fetchImpl?: typeof fetch; /** Override the IM approval bus. Defaults to the process singleton; pass * `null` to disable the bridge entirely (used by tests that want to lock * in the no-bus fallback path). */ approvalBus?: ApprovalBus | null; /** Register Agim's MCP sidecar in opencode's own config before starting * `opencode serve`. Tests inject a no-op to avoid touching user config. */ ensureMcp?: () => Promise; } export declare class OpenCodeHttpAdapter extends OpenCodeAdapter { private readonly serve; private readonly fetchImpl; private readonly ensureMcp; /** null when the bridge is intentionally disabled. */ private readonly approvalBus; /** Set of opencode session ids whose ruleset we've already PATCHed in this * process lifetime. Lets us re-apply the gate policy when an agim * restart resurfaces an old session, without duplicating rules every * turn. Cleared only by agim restart — that's fine since the rules * the previous run wrote are still in opencode's session DB. */ private readonly rulesetApplied; constructor(opts?: HttpAdapterOptions); /** * Translate a single SSE event into adapter-side effects: * - sessionID capture → opts.onAgentSessionId * - step-finish cost/tokens → opts.onUsage * - text-part with finished `time.end` → returned as a chunk (with * part id when known, so the caller can dedupe against the POST-body * fallback for SSE-broken builds — see drainResponseBody). * Pure: tests drive this directly without spinning up a daemon. */ inspectHttpEvent(event: OpencodeEvent, sessionID: string, opts: AgentSendOpts): { text?: string; partId?: string; }; /** * `session.status` event with status.type === 'idle' for our session is * the canonical "stop" signal in opencode (cf. run.ts:532). */ isIdleEvent(event: OpencodeEvent, sessionID: string): boolean; isErrorEvent(event: OpencodeEvent, sessionID: string): { error: string; } | null; buildHttpContextualPrompt(prompt: string, history?: ChatMessage[]): string; sendPrompt(_sessionId: string, prompt: string, history?: ChatMessage[], opts?: AgentSendOpts): AsyncGenerator; /** * Extract assistant text + usage from the POST /session/:id/message response * body, skipping any text part already emitted via SSE (matched by part id). * Used as a fallback for opencode builds whose /event stream is broken. */ drainResponseBody(body: OpencodeMessageResponse, alreadyYielded: Set, opts: AgentSendOpts): string[]; /** * Decide how to handle a `permission.asked` SSE event: * - If `this.approvalBus` is set AND has a notifier installed AND the * call carries enough IM context (threadId + platform) → register a * synthetic pending so the user gets an IM card; the bus's resolve * callback POSTs the decision back to opencode via /permission/.../reply. * - Otherwise → fall back to `once`. Without an IM channel there's no * human to ask, and a plain `reject` would surface as a tool-call * failure mid-conversation which is worse UX than allowing. * * Fire-and-forget: the SSE loop must keep draining other events while the * bridge waits for the user. The bus enforces its own timeout (5 min by * default) so we never deadlock the SSE loop. */ private routeAsk; /** * Build the session-level permission ruleset to append to opencode's * agent defaults. See createSession's docstring for policy rationale. * * The returned rules are flat `{permission, pattern, action}` objects * matching opencode's `Permission.Rule` schema. */ private buildSessionRuleset; /** * PATCH a resumed session with our gate ruleset so subsequent tool calls * surface as `permission.asked` events for the bridge. opencode's update * endpoint merges (current + payload) — payload comes last and wins under * findLast semantics. Idempotent across agim restarts but NOT within a * single process: the caller (sendPrompt) gates on rulesetApplied. */ private applyRuleset; private createSession; private postPrompt; /** * Poll `GET /permission` as the fallback channel for permission.asked when * opencode's SSE bus is broken (1.14.45+). Every tick lists pending * permissions for the daemon, filters to this session, and feeds new * entries through routeAsk — the same bridge the SSE path would have used. * * Local `seen` Set keeps a turn from re-routing the same reqId on every * tick. The bus also dedupes by reqId in registerSyntheticPending, so a * future opencode build that fixes SSE won't cause double-routing. * * The first tick fires immediately so a permission already pending at * sendPrompt entry (e.g. one persisted from a previous opencode serve * lifetime) surfaces without waiting an interval. * * Returns a stop function. Caller MUST invoke it in finally{} to prevent * the interval from leaking once the turn ends. */ private startPermissionPoller; private replyPermission; private openEventStream; } export declare class EventStream implements AsyncIterable { private reader; private decoder; private buf; private closed; constructor(body: NonNullable); close(): void; [Symbol.asyncIterator](): AsyncIterator; } export type { OpencodeEvent }; //# sourceMappingURL=opencode-http-adapter.d.ts.map