import { type Socket } from 'node:net'; import { type AgentSessionRuntime, type AgentSessionServices, type CreateAgentSessionResult, type ExtensionUIContext, type PromptOptions } from '@earendil-works/pi-coding-agent'; import { type ImageContent } from '@earendil-works/pi-ai'; import { type BrokerSdkConfig } from './launch.js'; import { type BrokerEngine } from './broker-sdk.js'; import { type ManagedProviderRotationPolicyControl } from './managed-provider-cooling.js'; import { type KnownStreamWaitControl } from './stream-watchdog.js'; import { FrameDecoder, type BrokerSnapshot, type BrokerToClient, type ClientRole, type RpcExtensionUIRequest, type RpcExtensionUIResponse } from './broker-protocol.js'; /** * The pinned pi SDK never yields `session.model === undefined` for a model-less * boot: Agent.createMutableAgentState() falls back to a hardcoded * `{ provider:'unknown', id:'unknown', api:'unknown', … }` stub (DEFAULT_MODEL) * whenever no model is supplied at construction, and `session.model` returns it * verbatim. This mirrors the SDK's own (non-exported) interactive-mode.js * `isUnknownModel()` so the broker recognizes that stub as "no model" wherever it * guards the model-less launch path (kickoff deferral, reload_auth re-drive) and * refuses to record/broadcast it as a real `unknown/unknown` model. */ export declare function isUnknownModel(model: { provider?: string; id?: string; api?: string; } | null | undefined): boolean; /** * Route a controller `prompt`/`follow_up` frame against the LIVE session state. * The controller picks its frame type off a possibly-STALE `isStreaming` * snapshot, so the broker is authoritative and the client's choice is a HINT: * * - m-B (streaming-safe prompt): a `prompt` arriving mid-stream needs * PromptOptions.streamingBehavior ("Required if streaming"; prompt() throws * without it). "steer" mirrors pi interactive-mode's own * Enter-while-streaming submit (`prompt(text, { streamingBehavior: "steer" })`). * - m-C (idle-safe follow_up): pi's followUp() only enqueues into a RUNNING * agent loop — on an idle session nothing ever drains the queue and the text * is silently lost. An idle `follow_up` therefore runs as a prompt now. * * driveEngine calls this synchronously with `session.isStreaming`, so the * routed state cannot go stale between the check and the engine call. * C1: images ride as a field on PromptOptions (pi: `prompt(text, { images })`). */ export declare function resolveEngineRoute(frame: { type: 'prompt' | 'follow_up'; images?: ImageContent[]; }, isStreaming: boolean): { call: 'prompt'; options: PromptOptions; } | { call: 'followUp'; }; /** * True iff `text` would be consumed by pi's OWN leading-name preflight before * `session.prompt/steer/followUp` ever sees it as prose — an extension command * (`session.extensionRunner.getCommand`), a `/skill:name` skill, or a `/name` * prompt template (mirrors `_tryExecuteExtensionCommand` / `_expandSkillCommand` * / `expandPromptTemplate` in the pinned SDK verbatim, including the exact * name-parsing substrings). Inline memory-reference guidance must NEVER fire * for these — the leading token routes to the engine's own command surface, * not prose referencing a memory doc. Each lookup is independently wrapped so * an engine missing one accessor (the fake, or a degraded session) degrades * that check to "not a match" rather than aborting the whole classification — * matching `buildCommandList`'s existing "no live extensionRunner" tolerance. */ export declare function isLeadingEngineCommand(text: string, session: { extensionRunner?: { getCommand(name: string): unknown; }; resourceLoader?: { getSkills(): { skills: Array<{ name: string; }>; }; }; promptTemplates?: ReadonlyArray<{ name: string; }>; }): boolean; /** * Enumerate every leading-token STRING this session's engine would * independently consume as a leading command/skill/template dispatch BEFORE * inline memory-reference guidance ever applies — the read-op sibling of * `isLeadingEngineCommand`'s point-lookup, gathered as a full enumeration for * viewer-side suppression instead of a per-submission membership check. Same * three sources, same shape of the leading match `isLeadingEngineCommand` * checks (bare extension-command invocation names and prompt-template names, * `skill:` for skills), so `/${token}` round-trips through * `isLeadingEngineCommand(text, session)` for every emitted token (locked by * a test). Kept STRICTLY separate from `buildCommandList` (command * completion) and the ref inventory (`RefMeta` rows) — this is neither. */ export declare function engineLeadingCommandTokens(session: { extensionRunner?: { getRegisteredCommands(): Array<{ invocationName: string; }>; }; resourceLoader?: { getSkills(): { skills: Array<{ name: string; }>; }; }; promptTemplates?: ReadonlyArray<{ name: string; }>; }): string[]; /** * Choose how the hidden `crtr-memory-refs` guidance custom message rides * alongside a human frame's own engine call (design Flow B). Pure over the * frame's routing outcome — never reads live session state itself, so a * caller can decide the delivery mode from the exact same `route` * `resolveEngineRoute` already computed for the human call. * * - `steer` frame -> 'steer' (always; no route call exists). * - a TRUE mid-stream follow_up (`routeCall === 'followUp'`) -> 'followUp'. * - every other `prompt`/`follow_up` routed as `routeCall === 'prompt'`: * idle -> 'nextTurn' (the ONLY case guidance precedes the human call — * pi's `prompt()` reads+clears its pending-nextTurn queue synchronously * into the SAME turn as the new user message); streaming (m-B * steer-while-streaming) -> 'steer', never 'nextTurn' (that would attach * to a later, unrelated turn). */ export declare function chooseGuidanceDeliveryMode(frameType: 'prompt' | 'follow_up' | 'steer', routeCall: 'prompt' | 'followUp' | undefined, isStreaming: boolean): 'steer' | 'followUp' | 'nextTurn'; /** * Deliver a `nextTurn` guidance custom message ATOMICALLY around the idle- * prompt call it's meant to ride alongside. Pi's `_pendingNextTurnMessages` * queue (the only public seam for `deliverAs:'nextTurn'`) is drained ONLY deep * inside `session.prompt()`, well after several await points — extension- * command interception, the `input` extension transform, a streaming reroute, * model/auth validation, pre-turn compaction — that can return or throw BEFORE * the drain ever runs. `sendCustomMessage`'s nextTurn branch pushes * synchronously and unconditionally, so any of those early exits strands the * pushed guidance in the queue to attach itself to whichever LATER, unrelated * prompt happens to reach the drain first. * * There is no public SDK seam to observe or cancel a pending nextTurn message * (`_pendingNextTurnMessages` is TS-`private` only — a plain runtime field, not * `#private`), so this reaches into that one field and removes EXACTLY the * entry this call pushed, identified by object identity via a locally- * generated marker riding the message's own `details` field (never surfaced to * the model, and irrelevant if the message WAS consumed normally — `details` * is just carried through like any other field once folded into a real turn). * Runs UNCONDITIONALLY after the human call settles, success or failure/early- * return alike — a "successful" `prompt()` promise can just as easily be an * early return that skipped the drain (an intercepted extension command, an * `input` transform, a streaming reroute) as a genuine consumed turn, so only * checking `.catch` is not enough. */ export declare function promptWithNextTurnGuidance(session: { sendCustomMessage(message: { customType: string; content: unknown; display?: boolean; details?: unknown; }, options?: { deliverAs?: 'steer' | 'followUp' | 'nextTurn'; }): Promise; }, guidance: string, runPrompt: () => Promise): Promise; /** True iff a parsed JSONL branch-entry line is a hidden (`display:false`) * custom message — the general display boundary crossed if left in an * exported branch (not just `crtr-memory-refs` guidance; any hidden custom * content). Covers both persisted shapes a branch entry can take: a * `type:'custom_message'` entry (display at the entry's own top level) and a * `type:'message'` entry wrapping a `role:'custom'` message (display nested * under `.message`). */ export declare function isHiddenJsonlBranchEntry(entry: unknown): boolean; /** Rewrite an already-exported JSONL file in place, dropping every hidden * (`display:false`) branch entry while re-chaining `parentId` so the * remaining entries stay a single valid linear branch — applying the general * display:false boundary to `session.exportToJsonl`'s own output, which * serializes every entry verbatim regardless of display. The header line * (line 1) is copied through untouched. */ export declare function stripHiddenEntriesFromJsonlExport(filePath: string): void; interface BrokerClient { id: string; role: ClientRole; socket: Socket; decoder: FrameDecoder; helloed: boolean; /** Unflushed outbound bytes handed to `socket.write` but not yet flushed to the * OS (M1 backpressure accounting). Incremented before each write, decremented * in that write's completion callback. */ pendingBytes: number; /** Outbound frames written but not yet flushed (the queue-depth half of the * high-water mark). */ queuedFrames: number; } /** A blocking dialog awaiting the controller's response, the broker-side default * timeout, or the engine's abort. */ interface PendingDialog { /** The original request (T4) — retained so `welcome.pending_dialog` and the * re-route-on-become-controller path can re-deliver a still-pending dialog to * a (new) controller. The Wave-0 shape stored only the resolver. */ request: RpcExtensionUIRequest; /** The controller answered — resolve with its parsed response (also clears the * broker-side timeout and removes the entry from the registry). */ resolve: (response: RpcExtensionUIResponse) => void; } /** Dispose the live engine session if one exists (idempotent). Called by * broker-cli's fatal handlers before exit so a crash never orphans detached * bash children. No-op before the session is built or after a clean dispose. */ export declare function disposeActiveSession(): void; export declare function runBroker(nodeId: string, startupAt?: bigint, startupTs?: string): Promise; export declare function snapshotMessages(session: CreateAgentSessionResult['session']): BrokerSnapshot['messages']; export declare function buildBrokerSession(engine: BrokerEngine, cfg: BrokerSdkConfig): Promise<{ session: CreateAgentSessionResult['session']; services: AgentSessionServices; resuming: boolean; /** The session-replacement runtime, present iff the engine exposes * createAgentSessionRuntime (real SDK yes, fake-engine no). The broker wires * its new_session/switch_session/fork ops + rebind through it. */ runtime?: AgentSessionRuntime; }>; /** Broker-side hooks the UI context needs to route (or noOp) extension dialogs. */ export interface BrokerDialogDeps { /** The controller client, or null when ZERO viewers are attached. */ controller: () => BrokerClient | null; /** Forward a dialog request to the (non-null) controller. */ forward: (client: BrokerClient, request: RpcExtensionUIRequest) => void; /** Pending-dialog registry, keyed by request id (answered via extension_ui_response). */ pending: Map; /** Broadcast a non-blocking display frame (setStatus/setWidget/setTitle) to ALL * viewers — the relay path for pi's fire-and-forget extension-UI surface. */ broadcast: (frame: BrokerToClient) => void; /** Live provider-fallback policy. This is a callback because an explicit * set_model updates the running broker's pin state without a revive. */ providerRotationPolicy?: ManagedProviderRotationPolicyControl; /** Optional broker-owned control for a deliberate, deadline-bounded provider * wait: pause the dead-stream watchdog while the provider extension sleeps to * a known finite future deadline, returning a release fn that resumes it. Put * on the UI context under `KNOWN_STREAM_WAIT_UI` so the (jiti-loaded) provider * extension reaches it. Absent for non-broker/test UI contexts (plain no-op). */ onKnownStreamWait?: KnownStreamWaitControl; } export declare function makeBrokerUiContext(deps: BrokerDialogDeps): ExtensionUIContext; export {};