/** Service contracts and everything that crosses the wire. */ import type { HarnessEvent, LaneSnapshot } from "@earendil-works/pi-agent-core"; import type { AuthEvent, AuthPrompt } from "@earendil-works/pi-ai"; /** Commands answer with data, never exceptions, exactly like a remote call would. */ export type CommandResult = { ok: true } | { ok: false; error: string }; /** Durable model identity. Nothing outside a worker holds a `Model` object. */ export interface ModelRef { provider: string; modelId: string; } export interface ModelSummary extends ModelRef { name: string; } export interface ProviderAccount { id: string; name: string; authType: "oauth" | "api_key"; configured: boolean; /** Where the credential came from, for display: "stored", "environment", an env var name. */ source?: string; /** False for ambient credentials pi cannot collect itself, such as AWS profiles or env vars. */ interactive: boolean; methodName?: string; } export interface ModelsState { readonly models: readonly ModelSummary[]; readonly accounts: readonly ProviderAccount[]; readonly refreshing: boolean; } /** An auth prompt without its `AbortSignal`: the part that can cross a transport. */ export type AuthPromptRequest = AuthPrompt extends infer Prompt ? Prompt extends unknown ? Omit : never : never; /** A service seen from the other side of a connection: every method returns a promise. */ export type Remote = { [K in keyof T]: T[K] extends (...args: infer A) => infer R ? (...args: A) => Promise> : never; }; /** Names one service and carries its call and event types. Names are globally unique. */ export interface ServiceToken { readonly name: string; /** Phantom, never read: keeps the types attached to the token. */ readonly types?: (api: TApi, event: TEvent) => void; } export function defineService(name: string): ServiceToken { return { name }; } export interface SessionSummary { id: string; path: string; cwd: string; createdAt: number; } export interface SessionSnapshot { sessionId: string; cwd: string; sessionPath: string; /** Carries the lane configuration, queues, and stats: no side-channel replication. */ lane: LaneSnapshot; models: ModelsState; } /** Everything the `Models` service publishes. */ export type ModelsEvent = | { type: "state"; state: ModelsState } // Login runs the wrong way round: the request is an event, the answer is an ordinary call. | { type: "prompt"; requestId: string; request: AuthPromptRequest } | { type: "notice"; notice: AuthEvent }; /** The login half, for whatever drives the dialog. */ export type AuthEventPayload = Exclude; /** One presentation's subscription: a `lane.watch()` in the worker, named so its events can be filtered. */ export interface LaneSubscription { subscriptionId: string; snapshot: SessionSnapshot; } /** Lane events are addressed to the subscription whose watch produced them. */ export interface LaneEvent { subscriptionId: string; event: HarnessEvent; } export interface LaneServiceApi { /** * Capture a snapshot and open a subscription for one presentation. Its events are addressed to * `presentationId`, so the server routes them instead of broadcasting. Buffered until `start`. */ watch(presentationId: string): Promise; /** Begin delivery, draining everything buffered since the snapshot. */ start(subscriptionId: string): Promise; unwatch(subscriptionId: string): Promise; prompt(text: string): Promise; steer(text: string): Promise; followUp(text: string): Promise; compact(): Promise; abort(): Promise; setModel(ref: ModelRef): Promise; } export interface ModelsServiceApi { refresh(): Promise; login(providerId: string, authType: "oauth" | "api_key"): Promise; authReply(requestId: string, answer: string | null): Promise; } /** Provided by a worker so the server can identify the session it opened, without naming lane methods. */ export interface WorkerServiceApi { describe(): Promise<{ sessionId: string }>; } export interface SessionsServiceApi { list(): Promise; attach(sessionId: string | null, cwd: string, presentationId: string): Promise; } /** Provided by the worker. One subscription per presentation; `watch` again to rebase. */ export const Lane = defineService("lane"); /** Provided by the worker. Small enough to publish whole. */ export const Models = defineService("models"); /** Provided by the worker, consumed only by the server. */ export const Worker = defineService("worker"); /** Provided by the server. */ export const Sessions = defineService("sessions");