@dbx-tools/genie-shared 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +32 -0
- package/dist/index.js +32 -0
- package/dist/src/event.d.ts +161 -0
- package/dist/src/event.js +257 -0
- package/dist/src/protocol.d.ts +855 -0
- package/dist/src/protocol.js +418 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/index.ts +33 -0
- package/package.json +38 -0
- package/src/event.ts +376 -0
- package/src/protocol.ts +519 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dbx-tools/genie-shared`: pure-types + sync-helpers surface of
|
|
3
|
+
* the `@dbx-tools/genie` package. Safe to import from browser
|
|
4
|
+
* bundles (no `node:*`, no `WorkspaceClient`, no I/O).
|
|
5
|
+
*
|
|
6
|
+
* What lives here:
|
|
7
|
+
*
|
|
8
|
+
* - {@link ./src/protocol.js}: wire-format zod schemas + types
|
|
9
|
+
* extending the generated `@dbx-tools/sdk-shared` Genie shapes
|
|
10
|
+
* (`GenieMessageSchema`, `GenieAttachmentSchema`,
|
|
11
|
+
* `GenieQueryAttachmentSchema`, `GenieThoughtSchema`,
|
|
12
|
+
* `messageStatusSchema`, ...) plus the high-level event
|
|
13
|
+
* vocabulary the `genieEventChat` driver emits
|
|
14
|
+
* (`GenieChatEvent`, `GenieChatLocation`, per-variant payload
|
|
15
|
+
* interfaces) and terminal-status / attachment-discriminator
|
|
16
|
+
* helpers (`TERMINAL_STATUSES`, `isTerminalStatus`,
|
|
17
|
+
* `detectAttachmentType`, `tagAttachment`).
|
|
18
|
+
* - {@link ./src/event.js}: pure sync detectors
|
|
19
|
+
* (`detectStatus`, `detectThinking`, `detectAttachmentAdded`,
|
|
20
|
+
* `detectText`, `detectQuery`, `detectStatement`,
|
|
21
|
+
* `detectRows`, `detectSuggestedQuestions`) and the
|
|
22
|
+
* `eventsFromMessage` orchestrator generator. Used by
|
|
23
|
+
* `genieEventChat` server-side; also reusable from the
|
|
24
|
+
* browser when consumers want to derive UI events from
|
|
25
|
+
* `GenieMessage` snapshots themselves.
|
|
26
|
+
*
|
|
27
|
+
* Server-only chat driving (`genieChat`, `genieEventChat`) lives
|
|
28
|
+
* in `@dbx-tools/genie` and pulls these types in. Frontends only
|
|
29
|
+
* need this package.
|
|
30
|
+
*/
|
|
31
|
+
export * from "./src/event.js";
|
|
32
|
+
export * from "./src/protocol.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dbx-tools/genie-shared`: pure-types + sync-helpers surface of
|
|
3
|
+
* the `@dbx-tools/genie` package. Safe to import from browser
|
|
4
|
+
* bundles (no `node:*`, no `WorkspaceClient`, no I/O).
|
|
5
|
+
*
|
|
6
|
+
* What lives here:
|
|
7
|
+
*
|
|
8
|
+
* - {@link ./src/protocol.js}: wire-format zod schemas + types
|
|
9
|
+
* extending the generated `@dbx-tools/sdk-shared` Genie shapes
|
|
10
|
+
* (`GenieMessageSchema`, `GenieAttachmentSchema`,
|
|
11
|
+
* `GenieQueryAttachmentSchema`, `GenieThoughtSchema`,
|
|
12
|
+
* `messageStatusSchema`, ...) plus the high-level event
|
|
13
|
+
* vocabulary the `genieEventChat` driver emits
|
|
14
|
+
* (`GenieChatEvent`, `GenieChatLocation`, per-variant payload
|
|
15
|
+
* interfaces) and terminal-status / attachment-discriminator
|
|
16
|
+
* helpers (`TERMINAL_STATUSES`, `isTerminalStatus`,
|
|
17
|
+
* `detectAttachmentType`, `tagAttachment`).
|
|
18
|
+
* - {@link ./src/event.js}: pure sync detectors
|
|
19
|
+
* (`detectStatus`, `detectThinking`, `detectAttachmentAdded`,
|
|
20
|
+
* `detectText`, `detectQuery`, `detectStatement`,
|
|
21
|
+
* `detectRows`, `detectSuggestedQuestions`) and the
|
|
22
|
+
* `eventsFromMessage` orchestrator generator. Used by
|
|
23
|
+
* `genieEventChat` server-side; also reusable from the
|
|
24
|
+
* browser when consumers want to derive UI events from
|
|
25
|
+
* `GenieMessage` snapshots themselves.
|
|
26
|
+
*
|
|
27
|
+
* Server-only chat driving (`genieChat`, `genieEventChat`) lives
|
|
28
|
+
* in `@dbx-tools/genie` and pulls these types in. Frontends only
|
|
29
|
+
* need this package.
|
|
30
|
+
*/
|
|
31
|
+
export * from "./src/event.js";
|
|
32
|
+
export * from "./src/protocol.js";
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure event-detection for `genieEventChat`. Given two
|
|
3
|
+
* `GenieMessage` snapshots (current + prior) and the surrounding
|
|
4
|
+
* `space_id`, derive the semantic deltas (status transitions, new
|
|
5
|
+
* attachments, new thoughts, SQL emission, warehouse submission,
|
|
6
|
+
* row-count progress, follow-up suggestions, text deltas) and
|
|
7
|
+
* yield them as typed {@link GenieChatEvent}s.
|
|
8
|
+
*
|
|
9
|
+
* Architecture:
|
|
10
|
+
*
|
|
11
|
+
* - Each detector is built with {@link eventDetector}, which
|
|
12
|
+
* takes the event name as a literal string and a `detect`
|
|
13
|
+
* callback. TS infers `T extends GenieChatEventType` from the
|
|
14
|
+
* literal, looks up its scope in {@link DetectorScope}, and
|
|
15
|
+
* resolves `detect`'s parameter list (message vs
|
|
16
|
+
* per-attachment) and return type
|
|
17
|
+
* ({@link DetectorResult}<T>) accordingly. Pass an unknown
|
|
18
|
+
* name (`"status2"`) and the call fails to compile; pass a
|
|
19
|
+
* payload shape that doesn't match the named event and the
|
|
20
|
+
* return fails to compile.
|
|
21
|
+
* - {@link eventsFromMessage}: sync generator. Walks the
|
|
22
|
+
* snapshot diff and yields flat `{type, ...fields}` events
|
|
23
|
+
* for every detector that fires, in a stable order (status
|
|
24
|
+
* first, then per-attachment field deltas) so a subscriber
|
|
25
|
+
* that simply logs events as they arrive sees them in a
|
|
26
|
+
* sensible sequence.
|
|
27
|
+
* - Private helpers (`matchPrevAttachment`, `thoughtKey`):
|
|
28
|
+
* diff plumbing shared across detectors.
|
|
29
|
+
*
|
|
30
|
+
* The module is intentionally pure: no `EventEmitter`, no `this`,
|
|
31
|
+
* no I/O, no module-level mutable state. The `message` and
|
|
32
|
+
* `result` events are NOT derived here - they belong to the chat
|
|
33
|
+
* lifecycle layer (`chat.ts`, `genieEventChat`) because they track
|
|
34
|
+
* per-yield / per-turn-completion semantics rather than a
|
|
35
|
+
* field-level snapshot diff.
|
|
36
|
+
*/
|
|
37
|
+
import { type GenieAttachment, type GenieChatEvent, type GenieChatEventFields, type GenieChatEventType, type GenieChatLocation, type GenieMessage } from "./protocol.js";
|
|
38
|
+
/**
|
|
39
|
+
* What a single detector call returns: zero (`undefined`), one
|
|
40
|
+
* (fields object), or many (`fields[]`) events of the same type.
|
|
41
|
+
* Each result is the variant's payload fields **without** the
|
|
42
|
+
* `type` discriminator - the orchestrator stamps `type` when it
|
|
43
|
+
* yields the event.
|
|
44
|
+
*/
|
|
45
|
+
type DetectorResult<T extends GenieChatEventType> = GenieChatEventFields<T> | GenieChatEventFields<T>[] | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Where in the wire shape a given event is derived from. Drives
|
|
48
|
+
* which arguments `detect` receives. `"message"` events watch
|
|
49
|
+
* `GenieMessage` itself; `"attachment"` events watch one slot of
|
|
50
|
+
* `message.attachments[]`. `"lifecycle"` events (`message`,
|
|
51
|
+
* `result`) are emitted by `chat.ts` directly and can't be built
|
|
52
|
+
* with {@link eventDetector} - they have no diff signature.
|
|
53
|
+
*/
|
|
54
|
+
interface DetectorScope {
|
|
55
|
+
status: "message";
|
|
56
|
+
attachment: "attachment";
|
|
57
|
+
thinking: "attachment";
|
|
58
|
+
text: "attachment";
|
|
59
|
+
query: "attachment";
|
|
60
|
+
statement: "attachment";
|
|
61
|
+
rows: "attachment";
|
|
62
|
+
suggested_questions: "attachment";
|
|
63
|
+
question: "lifecycle";
|
|
64
|
+
message: "lifecycle";
|
|
65
|
+
result: "lifecycle";
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* `detect` callback signature for a given event type. Resolved
|
|
69
|
+
* from {@link DetectorScope}: `"message"` events get the top-level
|
|
70
|
+
* snapshot triple, `"attachment"` events get the per-slot quad,
|
|
71
|
+
* `"lifecycle"` events resolve to `never` (no diff-based detector
|
|
72
|
+
* exists for them).
|
|
73
|
+
*/
|
|
74
|
+
type DetectFn<T extends GenieChatEventType> = DetectorScope[T] extends "message" ? (current: GenieMessage, previous: GenieMessage | undefined, space_id: string) => DetectorResult<T> : DetectorScope[T] extends "attachment" ? (current: GenieAttachment, previous: GenieAttachment | undefined, location: GenieChatLocation, index: number) => DetectorResult<T> : never;
|
|
75
|
+
/**
|
|
76
|
+
* Typed detector for one event in the {@link GenieChatEvent}
|
|
77
|
+
* union. The `type` field is the event name; `detect`'s signature
|
|
78
|
+
* is picked from {@link DetectorScope} based on `T`.
|
|
79
|
+
*/
|
|
80
|
+
interface EventDetector<T extends GenieChatEventType> {
|
|
81
|
+
readonly type: T;
|
|
82
|
+
detect: DetectFn<T>;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build an {@link EventDetector}. Pass the event name as the
|
|
86
|
+
* literal first arg and the matching `detect` callback as the
|
|
87
|
+
* second. TS infers `T` from the literal, narrows `detect`'s
|
|
88
|
+
* signature accordingly, and types the return as
|
|
89
|
+
* `EventDetector<T>`.
|
|
90
|
+
*
|
|
91
|
+
* Build-time guarantees:
|
|
92
|
+
*
|
|
93
|
+
* - `eventDetector("status2", ...)` fails - the name isn't in
|
|
94
|
+
* {@link GenieChatEvent}.
|
|
95
|
+
* - `eventDetector("status", attachmentArgsCallback)` fails -
|
|
96
|
+
* `"status"` is message-scoped, so `detect` must take
|
|
97
|
+
* `(GenieMessage, GenieMessage | undefined, string)`.
|
|
98
|
+
* - Returning a `ThinkingEvent`-shaped fields object from a
|
|
99
|
+
* `"status"` detector fails - the return type is constrained
|
|
100
|
+
* to `DetectorResult<"status">`.
|
|
101
|
+
*
|
|
102
|
+
* Lifecycle event names (`"message"`, `"result"`) resolve `detect`
|
|
103
|
+
* to `never` and won't compile, which is intentional: those have
|
|
104
|
+
* no diff signature and are emitted directly by `chat.ts`.
|
|
105
|
+
*/
|
|
106
|
+
export declare function eventDetector<T extends GenieChatEventType>(type: T, detect: DetectFn<T>): EventDetector<T>;
|
|
107
|
+
/** Top-level `message.status` transitioned. */
|
|
108
|
+
export declare const detectStatus: EventDetector<"status">;
|
|
109
|
+
/** First time we see an attachment slot. */
|
|
110
|
+
export declare const detectAttachmentAdded: EventDetector<"attachment">;
|
|
111
|
+
/**
|
|
112
|
+
* One emit per new `(thought_type, content)` tuple on a query
|
|
113
|
+
* attachment. Value-based set diff: Genie can mutate existing
|
|
114
|
+
* thought slots in place (e.g. re-typing index 0 from
|
|
115
|
+
* `DATA_SOURCING` to `DESCRIPTION` while re-appending the
|
|
116
|
+
* original at index 1), so positional / append-only diff would
|
|
117
|
+
* miss re-types and double-count re-orders.
|
|
118
|
+
*/
|
|
119
|
+
export declare const detectThinking: EventDetector<"thinking">;
|
|
120
|
+
/** Text-attachment `content` appeared or changed. */
|
|
121
|
+
export declare const detectText: EventDetector<"text">;
|
|
122
|
+
/** SQL transitioned undefined -> string, or changed. */
|
|
123
|
+
export declare const detectQuery: EventDetector<"query">;
|
|
124
|
+
/** Warehouse-statement id assigned. */
|
|
125
|
+
export declare const detectStatement: EventDetector<"statement">;
|
|
126
|
+
/**
|
|
127
|
+
* `row_count` changed - fires on every transition including the
|
|
128
|
+
* initial `undefined -> 0` and the post-execution `0 -> N`.
|
|
129
|
+
* Carries the statement id when available for correlation.
|
|
130
|
+
*/
|
|
131
|
+
export declare const detectRows: EventDetector<"rows">;
|
|
132
|
+
/**
|
|
133
|
+
* Follow-up suggested-questions array appeared or changed.
|
|
134
|
+
* Compares JSON-stringified arrays so a length-preserving content
|
|
135
|
+
* rewrite still fires.
|
|
136
|
+
*/
|
|
137
|
+
export declare const detectSuggestedQuestions: EventDetector<"suggested_questions">;
|
|
138
|
+
/**
|
|
139
|
+
* Walk the diff between `current` and `previous` and yield every
|
|
140
|
+
* derived event the snapshot produced. Detector order mirrors
|
|
141
|
+
* Genie's wire ordering (status first, then per-attachment field
|
|
142
|
+
* deltas) so a subscriber that simply logs events as they arrive
|
|
143
|
+
* sees them in a sensible sequence.
|
|
144
|
+
*
|
|
145
|
+
* Caller responsibilities (not handled here):
|
|
146
|
+
*
|
|
147
|
+
* - Yield `{ type: "message", message: current }` BEFORE
|
|
148
|
+
* calling this, once per poll yield.
|
|
149
|
+
* - Yield `{ type: "result", ... }` AFTER calling this when
|
|
150
|
+
* `isTerminalStatus(current.status)` - per-turn lifecycle,
|
|
151
|
+
* not a per-snapshot field diff.
|
|
152
|
+
* - Decide what counts as a "fresh turn" and pass `undefined`
|
|
153
|
+
* for `previous` on turn boundaries, so anonymous-attachment
|
|
154
|
+
* state from a prior turn doesn't bleed in.
|
|
155
|
+
*
|
|
156
|
+
* Sync generator: the diff is pure CPU work, no awaits. Use
|
|
157
|
+
* `yield*` from an async generator to splice the events into a
|
|
158
|
+
* stream.
|
|
159
|
+
*/
|
|
160
|
+
export declare function eventsFromMessage(current: GenieMessage, previous: GenieMessage | undefined, space_id: string): Generator<GenieChatEvent, void, void>;
|
|
161
|
+
export {};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure event-detection for `genieEventChat`. Given two
|
|
3
|
+
* `GenieMessage` snapshots (current + prior) and the surrounding
|
|
4
|
+
* `space_id`, derive the semantic deltas (status transitions, new
|
|
5
|
+
* attachments, new thoughts, SQL emission, warehouse submission,
|
|
6
|
+
* row-count progress, follow-up suggestions, text deltas) and
|
|
7
|
+
* yield them as typed {@link GenieChatEvent}s.
|
|
8
|
+
*
|
|
9
|
+
* Architecture:
|
|
10
|
+
*
|
|
11
|
+
* - Each detector is built with {@link eventDetector}, which
|
|
12
|
+
* takes the event name as a literal string and a `detect`
|
|
13
|
+
* callback. TS infers `T extends GenieChatEventType` from the
|
|
14
|
+
* literal, looks up its scope in {@link DetectorScope}, and
|
|
15
|
+
* resolves `detect`'s parameter list (message vs
|
|
16
|
+
* per-attachment) and return type
|
|
17
|
+
* ({@link DetectorResult}<T>) accordingly. Pass an unknown
|
|
18
|
+
* name (`"status2"`) and the call fails to compile; pass a
|
|
19
|
+
* payload shape that doesn't match the named event and the
|
|
20
|
+
* return fails to compile.
|
|
21
|
+
* - {@link eventsFromMessage}: sync generator. Walks the
|
|
22
|
+
* snapshot diff and yields flat `{type, ...fields}` events
|
|
23
|
+
* for every detector that fires, in a stable order (status
|
|
24
|
+
* first, then per-attachment field deltas) so a subscriber
|
|
25
|
+
* that simply logs events as they arrive sees them in a
|
|
26
|
+
* sensible sequence.
|
|
27
|
+
* - Private helpers (`matchPrevAttachment`, `thoughtKey`):
|
|
28
|
+
* diff plumbing shared across detectors.
|
|
29
|
+
*
|
|
30
|
+
* The module is intentionally pure: no `EventEmitter`, no `this`,
|
|
31
|
+
* no I/O, no module-level mutable state. The `message` and
|
|
32
|
+
* `result` events are NOT derived here - they belong to the chat
|
|
33
|
+
* lifecycle layer (`chat.ts`, `genieEventChat`) because they track
|
|
34
|
+
* per-yield / per-turn-completion semantics rather than a
|
|
35
|
+
* field-level snapshot diff.
|
|
36
|
+
*/
|
|
37
|
+
import { detectAttachmentType, } from "./protocol.js";
|
|
38
|
+
/* ----------------------- detector factory ----------------------- */
|
|
39
|
+
/**
|
|
40
|
+
* Build an {@link EventDetector}. Pass the event name as the
|
|
41
|
+
* literal first arg and the matching `detect` callback as the
|
|
42
|
+
* second. TS infers `T` from the literal, narrows `detect`'s
|
|
43
|
+
* signature accordingly, and types the return as
|
|
44
|
+
* `EventDetector<T>`.
|
|
45
|
+
*
|
|
46
|
+
* Build-time guarantees:
|
|
47
|
+
*
|
|
48
|
+
* - `eventDetector("status2", ...)` fails - the name isn't in
|
|
49
|
+
* {@link GenieChatEvent}.
|
|
50
|
+
* - `eventDetector("status", attachmentArgsCallback)` fails -
|
|
51
|
+
* `"status"` is message-scoped, so `detect` must take
|
|
52
|
+
* `(GenieMessage, GenieMessage | undefined, string)`.
|
|
53
|
+
* - Returning a `ThinkingEvent`-shaped fields object from a
|
|
54
|
+
* `"status"` detector fails - the return type is constrained
|
|
55
|
+
* to `DetectorResult<"status">`.
|
|
56
|
+
*
|
|
57
|
+
* Lifecycle event names (`"message"`, `"result"`) resolve `detect`
|
|
58
|
+
* to `never` and won't compile, which is intentional: those have
|
|
59
|
+
* no diff signature and are emitted directly by `chat.ts`.
|
|
60
|
+
*/
|
|
61
|
+
export function eventDetector(type, detect) {
|
|
62
|
+
return { type, detect };
|
|
63
|
+
}
|
|
64
|
+
/* ---------------------------- detectors ---------------------------- */
|
|
65
|
+
/** Top-level `message.status` transitioned. */
|
|
66
|
+
export const detectStatus = eventDetector("status", (current, previous, space_id) => {
|
|
67
|
+
if (!current.status || current.status === previous?.status)
|
|
68
|
+
return;
|
|
69
|
+
return {
|
|
70
|
+
status: current.status,
|
|
71
|
+
previous_status: previous?.status,
|
|
72
|
+
space_id,
|
|
73
|
+
conversation_id: current.conversation_id,
|
|
74
|
+
message_id: current.message_id,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
/** First time we see an attachment slot. */
|
|
78
|
+
export const detectAttachmentAdded = eventDetector("attachment", (current, previous, location, index) => {
|
|
79
|
+
if (previous)
|
|
80
|
+
return;
|
|
81
|
+
return {
|
|
82
|
+
...location,
|
|
83
|
+
index,
|
|
84
|
+
attachment_type: detectAttachmentType(current),
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
/**
|
|
88
|
+
* One emit per new `(thought_type, content)` tuple on a query
|
|
89
|
+
* attachment. Value-based set diff: Genie can mutate existing
|
|
90
|
+
* thought slots in place (e.g. re-typing index 0 from
|
|
91
|
+
* `DATA_SOURCING` to `DESCRIPTION` while re-appending the
|
|
92
|
+
* original at index 1), so positional / append-only diff would
|
|
93
|
+
* miss re-types and double-count re-orders.
|
|
94
|
+
*/
|
|
95
|
+
export const detectThinking = eventDetector("thinking", (current, previous, location) => {
|
|
96
|
+
const currThoughts = current.query?.thoughts ?? [];
|
|
97
|
+
if (currThoughts.length === 0)
|
|
98
|
+
return;
|
|
99
|
+
const seen = new Set((previous?.query?.thoughts ?? []).map(thoughtKey));
|
|
100
|
+
const out = [];
|
|
101
|
+
for (const t of currThoughts) {
|
|
102
|
+
const key = thoughtKey(t);
|
|
103
|
+
if (seen.has(key))
|
|
104
|
+
continue;
|
|
105
|
+
// Defensive: dedupe within a single snapshot in case Genie
|
|
106
|
+
// ever ships the same thought twice in one `thoughts[]`.
|
|
107
|
+
seen.add(key);
|
|
108
|
+
out.push({ ...location, text: t.content, thought_type: t.thought_type });
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
});
|
|
112
|
+
/** Text-attachment `content` appeared or changed. */
|
|
113
|
+
export const detectText = eventDetector("text", (current, previous, location) => {
|
|
114
|
+
const curr = current.text?.content;
|
|
115
|
+
const prev = previous?.text?.content;
|
|
116
|
+
if (curr === undefined || curr === prev)
|
|
117
|
+
return;
|
|
118
|
+
return { ...location, text: curr };
|
|
119
|
+
});
|
|
120
|
+
/** SQL transitioned undefined -> string, or changed. */
|
|
121
|
+
export const detectQuery = eventDetector("query", (current, previous, location) => {
|
|
122
|
+
const curr = current.query?.query;
|
|
123
|
+
const prev = previous?.query?.query;
|
|
124
|
+
if (!curr || curr === prev)
|
|
125
|
+
return;
|
|
126
|
+
return {
|
|
127
|
+
...location,
|
|
128
|
+
sql: curr,
|
|
129
|
+
title: current.query?.title,
|
|
130
|
+
description: current.query?.description,
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
/** Warehouse-statement id assigned. */
|
|
134
|
+
export const detectStatement = eventDetector("statement", (current, previous, location) => {
|
|
135
|
+
const curr = current.query?.statement_id;
|
|
136
|
+
const prev = previous?.query?.statement_id;
|
|
137
|
+
if (!curr || curr === prev)
|
|
138
|
+
return;
|
|
139
|
+
return { ...location, statement_id: curr };
|
|
140
|
+
});
|
|
141
|
+
/**
|
|
142
|
+
* `row_count` changed - fires on every transition including the
|
|
143
|
+
* initial `undefined -> 0` and the post-execution `0 -> N`.
|
|
144
|
+
* Carries the statement id when available for correlation.
|
|
145
|
+
*/
|
|
146
|
+
export const detectRows = eventDetector("rows", (current, previous, location) => {
|
|
147
|
+
const curr = current.query?.query_result_metadata?.row_count;
|
|
148
|
+
const prev = previous?.query?.query_result_metadata?.row_count;
|
|
149
|
+
if (curr === undefined || curr === prev)
|
|
150
|
+
return;
|
|
151
|
+
return {
|
|
152
|
+
...location,
|
|
153
|
+
row_count: curr,
|
|
154
|
+
previous_row_count: prev,
|
|
155
|
+
statement_id: current.query?.statement_id ?? previous?.query?.statement_id,
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
/**
|
|
159
|
+
* Follow-up suggested-questions array appeared or changed.
|
|
160
|
+
* Compares JSON-stringified arrays so a length-preserving content
|
|
161
|
+
* rewrite still fires.
|
|
162
|
+
*/
|
|
163
|
+
export const detectSuggestedQuestions = eventDetector("suggested_questions", (current, previous, location) => {
|
|
164
|
+
const curr = current.suggested_questions?.questions;
|
|
165
|
+
const prev = previous?.suggested_questions?.questions;
|
|
166
|
+
if (!curr || curr.length === 0)
|
|
167
|
+
return;
|
|
168
|
+
if (JSON.stringify(curr) === JSON.stringify(prev))
|
|
169
|
+
return;
|
|
170
|
+
return { ...location, questions: curr };
|
|
171
|
+
});
|
|
172
|
+
/* --------------------------- orchestrator --------------------------- */
|
|
173
|
+
/**
|
|
174
|
+
* Walk the diff between `current` and `previous` and yield every
|
|
175
|
+
* derived event the snapshot produced. Detector order mirrors
|
|
176
|
+
* Genie's wire ordering (status first, then per-attachment field
|
|
177
|
+
* deltas) so a subscriber that simply logs events as they arrive
|
|
178
|
+
* sees them in a sensible sequence.
|
|
179
|
+
*
|
|
180
|
+
* Caller responsibilities (not handled here):
|
|
181
|
+
*
|
|
182
|
+
* - Yield `{ type: "message", message: current }` BEFORE
|
|
183
|
+
* calling this, once per poll yield.
|
|
184
|
+
* - Yield `{ type: "result", ... }` AFTER calling this when
|
|
185
|
+
* `isTerminalStatus(current.status)` - per-turn lifecycle,
|
|
186
|
+
* not a per-snapshot field diff.
|
|
187
|
+
* - Decide what counts as a "fresh turn" and pass `undefined`
|
|
188
|
+
* for `previous` on turn boundaries, so anonymous-attachment
|
|
189
|
+
* state from a prior turn doesn't bleed in.
|
|
190
|
+
*
|
|
191
|
+
* Sync generator: the diff is pure CPU work, no awaits. Use
|
|
192
|
+
* `yield*` from an async generator to splice the events into a
|
|
193
|
+
* stream.
|
|
194
|
+
*/
|
|
195
|
+
export function* eventsFromMessage(current, previous, space_id) {
|
|
196
|
+
// Stamp `type` onto each detector result and yield it. Returning
|
|
197
|
+
// a typed generator keeps the `yield*` callsite tidy. The double
|
|
198
|
+
// cast (`unknown` -> `GenieChatEvent`) is needed because the
|
|
199
|
+
// generic merge `{type: T} & GenieChatEventFields<T>` doesn't
|
|
200
|
+
// structurally narrow back to a discriminated-union member -
|
|
201
|
+
// each detector's runtime output is shaped correctly by
|
|
202
|
+
// construction, so the cast is sound.
|
|
203
|
+
function* emit(detector, result) {
|
|
204
|
+
if (result === undefined)
|
|
205
|
+
return;
|
|
206
|
+
if (Array.isArray(result)) {
|
|
207
|
+
for (const fields of result) {
|
|
208
|
+
yield { type: detector.type, ...fields };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
yield { type: detector.type, ...result };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Message-scoped detectors run once per snapshot.
|
|
216
|
+
yield* emit(detectStatus, detectStatus.detect(current, previous, space_id));
|
|
217
|
+
// Per-attachment detectors run once per attachment slot.
|
|
218
|
+
const currAtts = current.attachments ?? [];
|
|
219
|
+
const prevAtts = previous?.attachments ?? [];
|
|
220
|
+
for (let i = 0; i < currAtts.length; i++) {
|
|
221
|
+
const curr = currAtts[i];
|
|
222
|
+
const prev = matchPrevAttachment(curr, prevAtts, i);
|
|
223
|
+
const location = {
|
|
224
|
+
space_id,
|
|
225
|
+
conversation_id: current.conversation_id,
|
|
226
|
+
message_id: current.message_id,
|
|
227
|
+
attachment_id: curr.attachment_id,
|
|
228
|
+
};
|
|
229
|
+
yield* emit(detectAttachmentAdded, detectAttachmentAdded.detect(curr, prev, location, i));
|
|
230
|
+
yield* emit(detectThinking, detectThinking.detect(curr, prev, location, i));
|
|
231
|
+
yield* emit(detectText, detectText.detect(curr, prev, location, i));
|
|
232
|
+
yield* emit(detectQuery, detectQuery.detect(curr, prev, location, i));
|
|
233
|
+
yield* emit(detectStatement, detectStatement.detect(curr, prev, location, i));
|
|
234
|
+
yield* emit(detectRows, detectRows.detect(curr, prev, location, i));
|
|
235
|
+
yield* emit(detectSuggestedQuestions, detectSuggestedQuestions.detect(curr, prev, location, i));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/* ----------------------------- helpers ----------------------------- */
|
|
239
|
+
/**
|
|
240
|
+
* Find the prior version of `curr` in `prevAtts`. Attachments
|
|
241
|
+
* with ids match by id (Genie keeps ids stable across polls);
|
|
242
|
+
* anonymous attachments (Genie's main-answer text doesn't get
|
|
243
|
+
* one) match positionally against an anonymous prev at the same
|
|
244
|
+
* index, so they don't accidentally bind to an id'd predecessor
|
|
245
|
+
* that happened to share the slot.
|
|
246
|
+
*/
|
|
247
|
+
function matchPrevAttachment(curr, prevAtts, i) {
|
|
248
|
+
if (curr.attachment_id) {
|
|
249
|
+
return prevAtts.find((a) => a.attachment_id === curr.attachment_id);
|
|
250
|
+
}
|
|
251
|
+
const p = prevAtts[i];
|
|
252
|
+
return p && !p.attachment_id ? p : undefined;
|
|
253
|
+
}
|
|
254
|
+
/** Stable key for {@link detectThinking}'s value-based set diff. */
|
|
255
|
+
function thoughtKey(t) {
|
|
256
|
+
return `${t.thought_type}|${t.content}`;
|
|
257
|
+
}
|