import type { ChannelAttachmentDto, RoutingDecisionInfo } from '@mosaicstack/types'; /** * The single chat execution strategy resolved by {@link ChatRuntimeRouter}. * * Exactly one runtime is live per process. There is no union that lets a * `pi-rpc` deployment silently fall back to embedded execution: an unmet * `pi-rpc` precondition is a typed startup failure, never a downgrade. */ export type ChatRuntimeMode = 'legacy' | 'pi-rpc'; export type ChatRuntimeKind = 'embedded' | 'harness'; /** The resolved runtime. Slice Zero exposes only its immutable {@link ChatRuntimeKind}. */ export interface ChatRuntime { readonly kind: ChatRuntimeKind; } /** Why the `pi-rpc` runtime could not be made ready. Both are hard startup failures. */ export type ChatRuntimeUnavailableReason = | 'adapter_unavailable' | 'conversation_service_unavailable'; /** * Raised at module init when `pi-rpc` mode is selected but its preconditions are * unmet. Carries only fixed, browser-safe text — never a raw exception message, * stack, or provider detail — and reports the frozen ack code `runtime_unsupported`. */ export class ChatRuntimeUnavailableError extends Error { readonly code = 'runtime_unsupported' as const; readonly reason: ChatRuntimeUnavailableReason; constructor(reason: ChatRuntimeUnavailableReason) { super( reason === 'adapter_unavailable' ? 'The pi-rpc chat runtime is unavailable: no "pi" harness adapter is registered.' : 'The pi-rpc chat runtime is unavailable: the harness conversation service is not bound.', ); this.name = 'ChatRuntimeUnavailableError'; this.reason = reason; } } /** * Resolves the process-wide chat runtime mode from the environment. Anything other * than the exact opt-in token `pi-rpc` keeps the legacy embedded runtime. */ export function resolveChatRuntimeMode( env: Record = process.env, ): ChatRuntimeMode { return env['CHAT_HARNESS_RUNTIME'] === 'pi-rpc' ? 'pi-rpc' : 'legacy'; } // --------------------------------------------------------------------------- // Transitional embedded chat port (Task Five). // // The legacy embedded browser behaviour is moved behind this exact interface so // neither the controller nor the gateway retains AgentService, RoutingEngine, // session, `piSession`, metric, listener, or channel access. `EmbeddedChatRuntime` // implements the port; `ChatRuntimeRouter` exposes the same narrowly named // operations and returns `runtime_unsupported` before touching Embedded for legacy // browser operations when the mode is `pi-rpc`. // // The names are frozen (spec jarvis-brain@1c629b06). Legacy REST completion, // legacy Socket streaming, P3 harness turns, and verified Discord are distinct // transport/trust capabilities — there is deliberately no generic // `sendConversationTurn` nor an AgentService-shaped mirror on the router. // --------------------------------------------------------------------------- /** * Phantom brand keeping {@link OwnedConversationContext} nominally distinct so browser * DTOs are never structurally assignable to it. The factory that mints one may be called * only after authentication with `scopeFromUser(...)`, never with payload authority fields. */ declare const ownedConversationContextBrand: unique symbol; /** Gateway-only ownership context. Embedded rechecks owner+tenant on every operation. */ export interface OwnedConversationContext { readonly [ownedConversationContextBrand]: true; readonly conversationId: string; readonly scope: Readonly<{ userId: string; tenantId: string }>; } /** * Every non-`ok` legacy runtime outcome. Missing, foreign, and no-longer-owned * conversations all collapse to `conversation_unavailable`. Ownership/mode/validation * failures are total results and never throw. */ export type LegacyRuntimeFailure = | { readonly ok: false; readonly code: 'runtime_unsupported'; readonly retryable: false } | { readonly ok: false; readonly code: 'conversation_unavailable'; readonly retryable: false } | { readonly ok: false; readonly code: 'request_invalid'; readonly retryable: false } | { readonly ok: false; readonly code: 'thinking_level_invalid'; readonly retryable: false; readonly availableThinkingLevels: readonly string[]; } | { readonly ok: false; readonly code: 'runtime_unavailable'; readonly retryable: true } | { readonly ok: false; readonly code: 'turn_already_dispatched'; readonly retryable: false } | { readonly ok: false; readonly code: 'operation_failed'; readonly retryable: boolean } | { readonly ok: false; readonly code: 'timeout'; readonly retryable: true }; /** Total result: an `ok` value or one of the fixed {@link LegacyRuntimeFailure} codes. */ export type LegacyRuntimeResult = | { readonly ok: true; readonly value: T } | LegacyRuntimeFailure; /** User-facing session projection. Carries no session object, handle, or credential path. */ export interface LegacySessionPresentation { readonly provider: string; readonly modelId: string; readonly thinkingLevel: string; readonly availableThinkingLevels: readonly string[]; readonly agentName?: string; readonly routingDecision?: RoutingDecisionInfo; } /** Terminal usage stats, normalized by Embedded from AgentService metrics. */ export interface LegacyUsage { readonly provider: string; readonly modelId: string; readonly thinkingLevel: string; readonly tokens: Readonly<{ input: number; output: number; cacheRead: number; cacheWrite: number; total: number; }>; readonly cost: number; readonly context: Readonly<{ percent: number | null; window: number }>; } /** * Normalized stream event. Exposes no `AgentSession`, `piSession`, native handle, raw * exception, tool arguments, or credential-bearing path — the gateway sees only these. */ export type LegacyRuntimeEvent = | { readonly type: 'started' } | { readonly type: 'text_delta'; readonly text: string } | { readonly type: 'thinking_delta'; readonly text: string } | { readonly type: 'tool_started'; readonly toolCallId: string; readonly toolName: string; } | { readonly type: 'tool_finished'; readonly toolCallId: string; readonly toolName: string; readonly isError: boolean; } | { readonly type: 'settled'; readonly usage?: LegacyUsage }; /** Legacy browser message input. Authority fields are advisory only; scope comes from the context. */ export interface LegacyBrowserMessagePayload { readonly content: string; readonly provider?: string; readonly modelId?: string; readonly agentId?: string; readonly attachments?: readonly ChannelAttachmentDto[]; } /** A prepared-but-not-yet-dispatched legacy socket turn. */ export interface LegacySocketTurnLease { readonly presentation: LegacySessionPresentation; /** * Atomically one-shot and scope-rechecking. A second call returns * `turn_already_dispatched` and performs zero prompt/tool effects. */ dispatch(): Promise>; /** Idempotent, non-throwing. Removes listener and channel, including partial setup. */ dispose(): Promise; } /** * Phantom brand for {@link VerifiedDiscordIngressContext}. Minted only after service-token * auth plus signature, allowlist, binding, expected-route, replay, configured-agent, * forced-scope, and attachment-normalization checks. */ declare const verifiedDiscordIngressContextBrand: unique symbol; /** Fully-verified Discord ingress. Contains no socket, envelope, signature, token, or escape hatch. */ export interface VerifiedDiscordIngressContext { readonly [verifiedDiscordIngressContextBrand]: true; readonly conversationId: string; readonly scope: Readonly<{ userId: string; tenantId: string }>; readonly configuredAgent: Readonly<{ agentConfigId: string; instanceId: string }>; readonly content: string; readonly attachments?: readonly ChannelAttachmentDto[]; readonly correlationId: string; readonly discordMessageId: string; readonly discordUserId: string; } /** Verified-Discord turn lease. Same atomic one-shot dispatch and idempotent dispose rules. */ export interface VerifiedDiscordTurnLease { readonly presentation: LegacySessionPresentation; dispatch(): Promise>; dispose(): Promise; } /** Server-owned egress projection the runtime pushes normalized events into. */ export interface LegacyRuntimeStream { /** Server-derived, e.g. `websocket:`. Never client-supplied. */ readonly channelId: string; onEvent(event: LegacyRuntimeEvent): void; } /** * The exact transitional port. `EmbeddedChatRuntime` implements it; `ChatRuntimeRouter` * mirrors the operation names and fails closed with `runtime_unsupported` for legacy * browser operations under `pi-rpc`. */ export interface LegacyEmbeddedChatPort { completeLegacyRestTurn( context: OwnedConversationContext, input: Readonly<{ content: string }>, ): Promise< LegacyRuntimeResult> >; prepareLegacySocketTurn( context: OwnedConversationContext, input: LegacyBrowserMessagePayload, stream: LegacyRuntimeStream, ): Promise>; setLegacyThinking( context: OwnedConversationContext, level: string, ): LegacyRuntimeResult; abortLegacyTurn(context: OwnedConversationContext): Promise>; applyLegacyModelOverride( context: OwnedConversationContext, modelId: string, ): LegacyRuntimeResult; readLegacySessionPresentation( context: OwnedConversationContext, ): LegacyRuntimeResult; dispatchVerifiedDiscordIngress( context: VerifiedDiscordIngressContext, stream: LegacyRuntimeStream, ): Promise>; } /** * Mints an {@link OwnedConversationContext} from a server-derived scope. Callers must pass * a scope produced by `scopeFromUser(...)` after authentication — never a client-supplied * authority field. The brand is phantom, so this is the only way to obtain the branded type. */ export function ownConversation( conversationId: string, scope: Readonly<{ userId: string; tenantId: string }>, ): OwnedConversationContext { return { conversationId, scope } as unknown as OwnedConversationContext; } /** * Mints a {@link VerifiedDiscordIngressContext}. Callers must have already completed every * ingress check (service-token auth, signature, allowlist, binding, expected-route, replay, * configured-agent, forced-scope, attachment normalization) before calling this. */ export function verifyDiscordIngress( fields: Omit, ): VerifiedDiscordIngressContext { return { ...fields } as unknown as VerifiedDiscordIngressContext; }