/** * Pure event-detection for `genieEventChat`. Given two * `GenieMessage` snapshots (current + prior) and the surrounding * `space_id`, derive the semantic deltas (status transitions, new * attachments, new thoughts, SQL emission, warehouse submission, * row-count progress, follow-up suggestions, text deltas) and * yield them as typed {@link GenieChatEvent}s. * * Detectors are built through a single typed factory that ties * each event's name to its scope (whole-message vs per-attachment) * and payload shape, so a misnamed event or a payload that doesn't * match its event fails to compile. An orchestrator walks the * snapshot diff and yields every detector that fires in a stable * order (status first, then per-attachment field deltas) so a * subscriber that logs events as they arrive sees a sensible * sequence. * * The module is intentionally pure: no `EventEmitter`, no `this`, * no I/O, no module-level mutable state. The `message` and * `result` events are NOT derived here - they belong to the chat * lifecycle layer (the `genieEventChat` driver) because they track * per-yield / per-turn-completion semantics rather than a * field-level snapshot diff. * * @module */ import { type GenieAttachment, type GenieChatEvent, type GenieChatEventFields, type GenieChatEventType, type GenieChatLocation, type GenieMessage } from "./genie-model.ts"; /** * What a single detector call returns: zero (`undefined`), one * (fields object), or many (`fields[]`) events of the same type. * Each result is the variant's payload fields **without** the * `type` discriminator - the orchestrator stamps `type` when it * yields the event. */ type DetectorResult = GenieChatEventFields | GenieChatEventFields[] | undefined; /** * Where in the wire shape a given event is derived from. Drives * which arguments `detect` receives. `"message"` events watch * `GenieMessage` itself; `"attachment"` events watch one slot of * `message.attachments[]`. `"lifecycle"` events (`message`, * `result`) are emitted by the chat driver directly and can't be * built with {@link eventDetector} - they have no diff signature. */ interface DetectorScope { status: "message"; attachment: "attachment"; thinking: "attachment"; text: "attachment"; query: "attachment"; statement: "attachment"; rows: "attachment"; suggested_questions: "attachment"; question: "lifecycle"; message: "lifecycle"; result: "lifecycle"; } /** * `detect` callback signature for a given event type. Resolved * from {@link DetectorScope}: `"message"` events get the top-level * snapshot triple, `"attachment"` events get the per-slot quad, * `"lifecycle"` events resolve to `never` (no diff-based detector * exists for them). */ type DetectFn = DetectorScope[T] extends "message" ? (current: GenieMessage, previous: GenieMessage | undefined, space_id: string) => DetectorResult : DetectorScope[T] extends "attachment" ? (current: GenieAttachment, previous: GenieAttachment | undefined, location: GenieChatLocation, index: number) => DetectorResult : never; /** * Typed detector for one event in the {@link GenieChatEvent} * union. The `type` field is the event name; `detect`'s signature * is picked from {@link DetectorScope} based on `T`. */ interface EventDetector { readonly type: T; detect: DetectFn; } /** * Build an {@link EventDetector}. Pass the event name as the * literal first arg and the matching `detect` callback as the * second. TS infers `T` from the literal, narrows `detect`'s * signature accordingly, and types the return as * `EventDetector`. * * Build-time guarantees: * * - `eventDetector("status2", ...)` fails - the name isn't in * {@link GenieChatEvent}. * - `eventDetector("status", attachmentArgsCallback)` fails - * `"status"` is message-scoped, so `detect` must take * `(GenieMessage, GenieMessage | undefined, string)`. * - Returning a `ThinkingEvent`-shaped fields object from a * `"status"` detector fails - the return type is constrained * to `DetectorResult<"status">`. * * Lifecycle event names (`"message"`, `"result"`) resolve `detect` * to `never` and won't compile, which is intentional: those have * no diff signature and are emitted directly by the chat driver. */ export declare function eventDetector(type: T, detect: DetectFn): EventDetector; /** Top-level `message.status` transitioned. */ export declare const detectStatus: EventDetector<"status">; /** First time we see an attachment slot. */ export declare const detectAttachmentAdded: EventDetector<"attachment">; /** * One emit per new `(thought_type, content)` tuple on a query * attachment. Value-based set diff: Genie can mutate existing * thought slots in place (e.g. re-typing index 0 from * `DATA_SOURCING` to `DESCRIPTION` while re-appending the * original at index 1), so positional / append-only diff would * miss re-types and double-count re-orders. */ export declare const detectThinking: EventDetector<"thinking">; /** Text-attachment `content` appeared or changed. */ export declare const detectText: EventDetector<"text">; /** SQL transitioned undefined -> string, or changed. */ export declare const detectQuery: EventDetector<"query">; /** Warehouse-statement id assigned. */ export declare const detectStatement: EventDetector<"statement">; /** * `row_count` changed - fires on every transition including the * initial `undefined -> 0` and the post-execution `0 -> N`. * Carries the statement id when available for correlation. */ export declare const detectRows: EventDetector<"rows">; /** * Follow-up suggested-questions array appeared or changed. * Compares structurally so a length-preserving content rewrite still fires. */ export declare const detectSuggestedQuestions: EventDetector<"suggested_questions">; /** * Walk the diff between `current` and `previous` and yield every * derived event the snapshot produced. Detector order mirrors * Genie's wire ordering (status first, then per-attachment field * deltas) so a subscriber that simply logs events as they arrive * sees them in a sensible sequence. * * Caller responsibilities (not handled here): * * - Yield `{ type: "message", message: current }` BEFORE * calling this, once per poll yield. * - Yield `{ type: "result", ... }` AFTER calling this when * `isTerminalStatus(current.status)` - per-turn lifecycle, * not a per-snapshot field diff. * - Decide what counts as a "fresh turn" and pass `undefined` * for `previous` on turn boundaries, so anonymous-attachment * state from a prior turn doesn't bleed in. * * Sync generator: the diff is pure CPU work, no awaits. Use * `yield*` from an async generator to splice the events into a * stream. */ export declare function eventsFromMessage(current: GenieMessage, previous: GenieMessage | undefined, space_id: string): Generator; export {};