import type { AgentClient } from "@skaile/workspaces/client"; import type { AgentCommand, AgentEvent, ConnectorRequestCommand, ConnectorResponseEvent, Mention, ReplyRef } from "@skaile/workspaces/types"; import type { Message, MessageStore, SubscriberTransport } from "./types.js"; /** * Construction options for {@link SessionDispatcher}. * * All three infrastructure objects (`client`, `store`, `transport`) are * injected by the caller so the dispatcher remains testable and framework-agnostic. * * @docLink packages/session/api-reference#session-dispatcher */ export interface SessionDispatcherOptions { /** Unique identifier for the session this dispatcher manages. */ sessionId: string; /** Connected agent client used to send commands and receive events. */ client: AgentClient; /** Persistence layer for all non-transient messages. */ store: MessageStore; /** Fan-out transport for broadcasting events to connected frontends. */ transport: SubscriberTransport; /** * When true, the dispatcher prepends conversation history to every prompt * command sent to the agent. Enable for agents without native session * continuity (e.g. mock mode). Agents with native multi-turn support * (Claude SDK, OMP) should set this to false to avoid duplicate context. * Default: false. */ injectConversationHistory?: boolean; /** Max conversation history messages to include in enriched prompts (default: 50). */ historyLimit?: number; /** * Optional callback to transform prompt text before forwarding to the agent. * The original (untransformed) prompt is persisted and broadcast to subscribers. * Applied after conversation history enrichment. Use this to inject context * (e.g. UI state) that the agent should see but subscribers should not. */ transformPrompt?: (prompt: string, senderId: string | undefined, replyTo?: ReplyRef) => string; } /** * Bridges a single agent session between an {@link AgentClient} and one or more * frontend subscribers. * * Responsibilities: * - Persists every non-transient command and event as a {@link Message} with a * monotonic sequence number via the injected {@link MessageStore}. * - Fans agent events out to all connected frontends via the injected * {@link SubscriberTransport}. * - Optionally enriches prompt commands with conversation history so agents * without native multi-turn support maintain context across turns. * * **Lifecycle:** call {@link init} before any other method, and {@link disconnect} * when the session ends. * * @example * ```ts * const dispatcher = new SessionDispatcher({ * sessionId: 'sess-123', * client: agentClient, * store: prismaMessageStore, * transport: trpcTransport, * }) * await dispatcher.init() * const history = await dispatcher.subscribe('ws-conn-abc', { limit: 50 }) * const msg = await dispatcher.sendCommand({ type: 'prompt', prompt: 'Hello' }, 'user-42') * await dispatcher.disconnect() * ``` * * @docLink packages/session/api-reference#session-dispatcher */ export declare class SessionDispatcher { /** The session identifier this dispatcher is bound to. */ readonly sessionId: string; private client; private store; private transport; private subscribers; private subscriberMeta; private seq; private _availableResources; /** Whether to inject conversation history into prompt commands. */ private readonly injectConversationHistory; /** Maximum number of history messages to include in enriched prompts. */ private readonly historyLimit; /** Optional prompt transformer applied before forwarding (after persistence). */ private _transformPrompt?; /** Whether the dispatcher is currently in compaction mode. */ private _compacting; /** Accumulated text content during compaction. */ private _compactionText; /** Idle watcher changes waiting for the next agent-visible delivery. */ private readonly workspaceChanges; /** V1 turn boundary: delivery starts a turn; finished/fatal error ends it. */ private agentTurnActive; /** True only during the synchronous AgentClient.send call. */ private agentDeliveryInFlight; /** Failed idle deliveries retain synchronous watcher events for retry. */ private deliveryStartedWhileIdle; /** Prevents a synchronous terminal event from being overwritten after send. */ private deliveryReachedTerminal; /** Idle changes emitted after a synchronous terminal survive a successful send. */ private postTerminalWorkspaceChanges; /** * One-shot waiters registered via {@link onceNextFinished}. Each is invoked * exactly once with the text of the next `finished` assistant event, then * the list is cleared. Backs the A2A router's synchronous `ask` answer * capture. */ private nextFinishedWaiters; /** * Text streamed by the agent since the last `finished` event. Used as the * fallback answer text for {@link onceNextFinished} when a `finished` event * carries an empty `summary` (drivers that stream `text` chunks and leave * the summary blank). */ private _turnText; /** * @param opts - Dispatcher configuration — see {@link SessionDispatcherOptions}. */ constructor(opts: SessionDispatcherOptions); /** * Replace the prompt transformer at runtime. * * Useful when additional context (e.g. a UI state cache) becomes available * after the dispatcher is constructed but before the first prompt is sent. */ set transformPrompt(fn: ((prompt: string, senderId: string | undefined, replyTo?: ReplyRef) => string) | undefined); /** * Initialize the dispatcher for this session. * * Restores the monotonic sequence counter from the store and wires up the * agent event listener. Must be called once before any other method. * * @throws If {@link MessageStore.getLatestSeq} rejects. */ init(): Promise; /** * Register a frontend subscriber and return recent message history. * * Subsequent agent events are pushed to this subscriber via the * {@link SubscriberTransport} until {@link unsubscribe} is called. * * @param subscriberId - Opaque connection ID (e.g. WebSocket session ID). * @param opts - Optional subscription options. * @param opts.limit - Number of recent messages to return (default: 50). * @returns Recent messages ordered by seq ascending. */ subscribe(subscriberId: string, opts?: { limit?: number; userId?: string; roles?: string[]; }): Promise; /** * Remove a frontend subscriber and release transport resources. * * @param subscriberId - The ID passed to {@link subscribe}. */ unsubscribe(subscriberId: string): void; /** * Send a command from a frontend to the agent. * * Persistent commands are wrapped in a Message envelope and stored before * forwarding. Transient commands (resource RPC) are forwarded directly * without persistence — returns null in that case. * * For `prompt` commands, the original message is persisted, but an enriched * version (with conversation history prepended) is sent to the agent so it * maintains context across turns. * * @param command - The command to dispatch to the agent (a prompt or tool result). * @param senderId - ID of the user sending the command, if known. * @param mentions - Resolved mention objects extracted from the prompt text. * @param visibility - Optional platform visibility envelope (F-23). When * `visibilityMode` is `HumansOnly`, or `Private` without the `"__agent__"` * sentinel in `privateRecipientIds` (B-30), the message is persisted and * broadcast to subscribers but NOT forwarded to the agent. * @param outgoingPrefix - Optional text prepended to the outgoing command's * user-text field ({@link command.prompt} / `command.answer` / * `command.content`) AFTER persistence but BEFORE history enrichment and * {@link transformPrompt}. The persisted {@link Message} envelope is built * from the original `command` and never sees the prefix. Use this to * inject one-shot wake-time context (Option B1 pending restoration * prompt) that the agent must see but UI subscribers must not. * @returns The persisted Message envelope, or null for transient command types. */ sendCommand(command: AgentCommand, senderId?: string, mentions?: Mention[], visibility?: NonNullable, outgoingPrefix?: string): Promise; /** * Persist a synthetic event (not received from the agent) and broadcast it. * * Used by the platform to inject events like a partial-text `finished` record * before a cancel command. * * @param event - The agent event to persist. * @param mentions - Resolved mentions from the originating command. */ persistEvent(event: AgentEvent, mentions?: Mention[]): Promise; /** * Load paginated message history for this session. * * @param opts.before - Return messages with seq strictly less than this value. * @param opts.limit - Maximum number of messages to return. */ getMessages(opts?: { before?: number; limit?: number; }): Promise; /** * Available mounts and connectors captured from the most recent * `resources_available` event emitted by the agent on connect. * * Empty until the agent emits the event or {@link seedResources} is called. */ get availableResources(): { mounts: unknown[]; connectors: unknown[]; }; /** * Seed the available resources from an external cache. * * This is needed when the dispatcher is created after the agent has already * emitted `resources_available` on WebSocket connect. Without seeding, the * dispatcher would report an empty resource list until the next reconnect. * * @param resources - Resource handles to register before the session starts. */ seedResources(resources: { mounts: unknown[]; connectors: unknown[]; }): void; /** * Deliver a prompt to the agent that triggers a turn but is NOT persisted. * * Unlike {@link sendCommand}, this never touches the {@link MessageStore}: * the prompt is forwarded straight to the agent client. The caller owns * persistence of whatever conversation rows should represent this turn. * * Used by the platform's A2A router to deliver a `from-peer` prompt to a * target session: the router writes the authoritative A2A `Message` rows * itself, so persisting the framed prompt here too would create an extra, * un-attributed plain `prompt` row. The dispatcher's job for this prompt is * delivery only. * * The agent still produces a normal turn, so an A2A `ask` round-trip can * capture the answer via {@link onceNextFinished}. * * @param prompt - The fully-formed prompt text to deliver to the agent. */ deliverPrompt(prompt: string): void; /** * Send a resource request and await the correlated response. * * This is a convenience method for request/response resource RPC. * The request is transient (not persisted). A one-shot event listener * waits for the matching `resource_response` by `requestId`. * * @param command - A ConnectorRequestCommand (caller must set requestId). * @param timeoutMs - How long to wait before rejecting (default: 30s). */ requestResource(command: ConnectorRequestCommand, timeoutMs?: number): Promise; /** * Broadcast a transient event to all subscribers without persisting it. * * Used for ephemeral signals like typing indicators or user-command * echoes that other subscribers need to see in real time. */ broadcast(event: AgentEvent): void; /** Disconnect the agent client and clean up all subscribers. */ disconnect(): Promise; /** * Enter compaction mode. While active, events from the agent are captured * but NOT persisted. Text content is accumulated for the snapshot. */ beginCompaction(): void; /** * Exit compaction mode and return the accumulated text. * The caller is responsible for creating and emitting the snapshot event. */ endCompaction(): string; /** Whether the dispatcher is currently in compaction mode. */ get isCompacting(): boolean; /** * Register a one-shot callback fired with the text of this session's next * finished assistant turn. Returns an unsubscribe function. * * Semantics: * - The callback fires **at most once** — on the first `finished` event * observed after registration. Later finished turns do not re-fire it. * - A waiter registered after a `finished` event waits for the *following* * one. The hook captures the *next* finished turn, not a past one. * - The answer text is the `finished` event's `summary`; when that is * empty (drivers that stream `text` and leave the summary blank), the * text accumulated since the previous finished turn is used instead. * - Calling the returned function removes the waiter; if it has already * fired this is a no-op. * * Used by the platform's A2A router (`A2ARouterService.ask`) to capture a * peer session's answer to a `from-peer` question. v1 captures the first * finished turn after delivery — see the busy-target notes in the A2A * design spec for the known turn-matching limitation. * * @param cb - Invoked once with the next finished turn's text. * @returns Unsubscribe function that removes this waiter. */ onceNextFinished(cb: (text: string) => void): () => void; /** * Build an enriched prompt by prepending conversation history. * * Loads recent persisted messages, formats user/assistant turns, and * wraps them in a `` block. The current user * message (just persisted) is excluded since it is already the prompt. * * Each turn is rendered as a self-closing `` element * rather than a bare `[User]:` / `[Assistant]:` transcript line. A plain * role-prefixed transcript invites the model to *continue* it — completing * the next `[User]:` turn as plaintext inside its own answer (a fabricated * follow-up that swallows the real next message). XML-tagged, explicitly * read-only turns remove that completion affordance while preserving the * same information. * * Returns the original prompt unchanged if there is no prior history. */ private enrichPromptWithHistory; private handleAgentEvent; private shouldPersist; private isAgentVisibleCommand; private sendAgentVisible; private prependWorkspaceChanges; private createMessage; } //# sourceMappingURL=dispatcher.d.ts.map