@copilotkit/shared 1.73.0 → 1.73.2-canary.1790068297

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.
Files changed (74) hide show
  1. package/README.md +22 -0
  2. package/dist/attachments/content.cjs +17 -0
  3. package/dist/attachments/content.cjs.map +1 -0
  4. package/dist/attachments/content.d.cts +9 -0
  5. package/dist/attachments/content.d.cts.map +1 -0
  6. package/dist/attachments/content.d.mts +9 -0
  7. package/dist/attachments/content.d.mts.map +1 -0
  8. package/dist/attachments/content.mjs +16 -0
  9. package/dist/attachments/content.mjs.map +1 -0
  10. package/dist/event-transforms/index.cjs +15 -0
  11. package/dist/event-transforms/index.d.cts +4 -0
  12. package/dist/event-transforms/index.d.mts +4 -0
  13. package/dist/event-transforms/index.mjs +5 -0
  14. package/dist/event-transforms/open-generative-ui-middleware.cjs +286 -0
  15. package/dist/event-transforms/open-generative-ui-middleware.cjs.map +1 -0
  16. package/dist/event-transforms/open-generative-ui-middleware.d.cts +60 -0
  17. package/dist/event-transforms/open-generative-ui-middleware.d.cts.map +1 -0
  18. package/dist/event-transforms/open-generative-ui-middleware.d.mts +60 -0
  19. package/dist/event-transforms/open-generative-ui-middleware.d.mts.map +1 -0
  20. package/dist/event-transforms/open-generative-ui-middleware.mjs +283 -0
  21. package/dist/event-transforms/open-generative-ui-middleware.mjs.map +1 -0
  22. package/dist/event-transforms/recorded-events.cjs +59 -0
  23. package/dist/event-transforms/recorded-events.cjs.map +1 -0
  24. package/dist/event-transforms/recorded-events.d.cts +23 -0
  25. package/dist/event-transforms/recorded-events.d.cts.map +1 -0
  26. package/dist/event-transforms/recorded-events.d.mts +23 -0
  27. package/dist/event-transforms/recorded-events.d.mts.map +1 -0
  28. package/dist/event-transforms/recorded-events.mjs +58 -0
  29. package/dist/event-transforms/recorded-events.mjs.map +1 -0
  30. package/dist/index.cjs +8 -0
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +3 -1
  33. package/dist/index.d.cts.map +1 -1
  34. package/dist/index.d.mts +3 -1
  35. package/dist/index.d.mts.map +1 -1
  36. package/dist/index.mjs +3 -1
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/index.umd.js +224 -1
  39. package/dist/index.umd.js.map +1 -1
  40. package/dist/package.cjs +1 -1
  41. package/dist/package.mjs +1 -1
  42. package/dist/types/message.d.cts +3 -3
  43. package/dist/types/message.d.cts.map +1 -1
  44. package/dist/types/message.d.mts +3 -3
  45. package/dist/types/message.d.mts.map +1 -1
  46. package/dist/utils/index.cjs +1 -0
  47. package/dist/utils/index.cjs.map +1 -1
  48. package/dist/utils/index.d.cts +1 -0
  49. package/dist/utils/index.d.cts.map +1 -1
  50. package/dist/utils/index.d.mts +1 -0
  51. package/dist/utils/index.d.mts.map +1 -1
  52. package/dist/utils/index.mjs +1 -0
  53. package/dist/utils/index.mjs.map +1 -1
  54. package/dist/utils/row-render-keys.cjs +206 -0
  55. package/dist/utils/row-render-keys.cjs.map +1 -0
  56. package/dist/utils/row-render-keys.d.cts +65 -0
  57. package/dist/utils/row-render-keys.d.cts.map +1 -0
  58. package/dist/utils/row-render-keys.d.mts +65 -0
  59. package/dist/utils/row-render-keys.d.mts.map +1 -0
  60. package/dist/utils/row-render-keys.mjs +201 -0
  61. package/dist/utils/row-render-keys.mjs.map +1 -0
  62. package/package.json +12 -1
  63. package/src/__tests__/root-entry-browser-safety.test.ts +7 -1
  64. package/src/attachments/content.test.ts +48 -0
  65. package/src/attachments/content.ts +16 -0
  66. package/src/attachments/index.ts +2 -0
  67. package/src/event-transforms/index.ts +5 -0
  68. package/src/event-transforms/open-generative-ui-middleware.ts +392 -0
  69. package/src/event-transforms/recorded-events.test.ts +166 -0
  70. package/src/event-transforms/recorded-events.ts +74 -0
  71. package/src/utils/__tests__/row-render-keys.test.ts +388 -0
  72. package/src/utils/index.ts +1 -0
  73. package/src/utils/row-render-keys.ts +263 -0
  74. package/tsdown.config.ts +7 -4
@@ -0,0 +1,392 @@
1
+ import type {
2
+ RunAgentInput,
3
+ AbstractAgent,
4
+ BaseEvent,
5
+ ToolCallStartEvent,
6
+ ToolCallArgsEvent,
7
+ ActivitySnapshotEventProps,
8
+ ActivityDeltaEvent,
9
+ } from "@ag-ui/client";
10
+ import { Middleware, EventType } from "@ag-ui/client";
11
+ import { Observable } from "rxjs";
12
+ import clarinet from "clarinet";
13
+
14
+ const TOOL_NAME = "generateSandboxedUi";
15
+ const ACTIVITY_TYPE = "open-generative-ui";
16
+
17
+ /**
18
+ * Parsed parameters from the generateSandboxedUi tool call.
19
+ */
20
+ export interface GenerateSandboxedUIParams {
21
+ initialHeight?: number;
22
+ placeholderMessages?: string[];
23
+ css?: string;
24
+ html?: string;
25
+ jsFunctions?: string;
26
+ jsExpressions?: string[];
27
+ }
28
+
29
+ /**
30
+ * Callback invoked by ArgsParser whenever a parameter (or array item) finishes parsing.
31
+ */
32
+ export type OnParamEvent = (event: BaseEvent) => void;
33
+
34
+ /**
35
+ * Tracks incremental JSON parsing state for a single tool call's arguments.
36
+ * Emits activity events via the onEvent callback as parameters complete.
37
+ */
38
+ export class ArgsParser {
39
+ private parser: ReturnType<typeof clarinet.parser> & {
40
+ error?: Error | null;
41
+ textNode?: string;
42
+ };
43
+ private currentKey: string | null = null;
44
+ private depth = 0;
45
+ private currentArrayKey: string | null = null;
46
+ private snapshotEmitted = false;
47
+
48
+ // Streaming html state — reads parser.textNode to emit incremental chunks
49
+ private streamingHtmlKey = false;
50
+ private htmlEmittedLength = 0;
51
+ private htmlArrayEmitted = false;
52
+
53
+ public readonly params: GenerateSandboxedUIParams = {};
54
+ public readonly messageId: string;
55
+ private readonly onEvent: OnParamEvent;
56
+
57
+ constructor(toolCallId: string, onEvent: OnParamEvent) {
58
+ this.messageId = `${toolCallId}-activity`;
59
+ this.onEvent = onEvent;
60
+ this.parser = clarinet.parser();
61
+
62
+ this.parser.onopenobject = (key: string | undefined) => {
63
+ this.depth++;
64
+ if (key !== undefined && this.depth === 1) {
65
+ this.currentKey = key;
66
+ this.initHtmlStreaming(key);
67
+ }
68
+ };
69
+
70
+ this.parser.onkey = (key: string) => {
71
+ if (this.depth === 1) {
72
+ this.currentKey = key;
73
+ this.initHtmlStreaming(key);
74
+ }
75
+ };
76
+
77
+ this.parser.onvalue = (value: string | boolean | number | null) => {
78
+ if (this.depth === 1 && this.currentKey) {
79
+ if (this.currentArrayKey) {
80
+ const strValue = String(value);
81
+ if (this.currentArrayKey === "jsExpressions") {
82
+ if (!this.params.jsExpressions) this.params.jsExpressions = [];
83
+ this.params.jsExpressions.push(strValue);
84
+ } else if (this.currentArrayKey === "placeholderMessages") {
85
+ if (!this.params.placeholderMessages)
86
+ this.params.placeholderMessages = [];
87
+ this.params.placeholderMessages.push(strValue);
88
+ }
89
+ this.emitArrayItemDelta(this.currentArrayKey, strValue);
90
+ } else if (this.streamingHtmlKey) {
91
+ // HTML string completed — flush any remaining content immediately + htmlComplete
92
+ const fullHtml = value != null ? String(value) : "";
93
+ this.params.html = fullHtml || undefined;
94
+ this.emitPendingHtml(fullHtml);
95
+ this.emitParamDelta("htmlComplete", true);
96
+ this.streamingHtmlKey = false;
97
+ } else {
98
+ this.setParam(this.currentKey, value);
99
+ }
100
+ }
101
+ };
102
+
103
+ this.parser.onopenarray = () => {
104
+ if (this.depth === 1 && this.currentKey) {
105
+ const key = this.currentKey;
106
+ if (key === "jsExpressions" || key === "placeholderMessages") {
107
+ this.currentArrayKey = key;
108
+ if (key === "jsExpressions") this.params.jsExpressions = [];
109
+ else this.params.placeholderMessages = [];
110
+ // Emit a delta to create the array in the activity content.
111
+ // Subsequent "add" ops with path "/<key>/-" append to this array.
112
+ this.emitParamDelta(key, []);
113
+ }
114
+ }
115
+ };
116
+
117
+ this.parser.onclosearray = () => {
118
+ if (this.depth === 1) {
119
+ if (this.currentArrayKey === "jsExpressions") {
120
+ this.emitParamDelta("jsExpressionsComplete", true);
121
+ }
122
+ this.currentArrayKey = null;
123
+ }
124
+ };
125
+
126
+ this.parser.oncloseobject = () => {
127
+ this.depth--;
128
+ };
129
+
130
+ this.parser.onerror = (err: Error) => {
131
+ console.warn(
132
+ "[OpenGenerativeUI] JSON parse error in streaming args, resuming:",
133
+ err?.message ?? err,
134
+ );
135
+ // Reset error state so parsing can continue with the next chunk
136
+ this.parser.error = null;
137
+ this.parser.resume();
138
+ };
139
+ }
140
+
141
+ write(chunk: string): void {
142
+ this.parser.write(chunk);
143
+ this.flushHtmlChunks();
144
+ }
145
+
146
+ private initHtmlStreaming(key: string): void {
147
+ if (key === "html") {
148
+ this.streamingHtmlKey = true;
149
+ this.htmlEmittedLength = 0;
150
+ this.htmlArrayEmitted = false;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Read clarinet's internal textNode buffer to emit html chunks incrementally.
156
+ * Called after every write() so partial string content is emitted as it streams in.
157
+ */
158
+ private flushHtmlChunks(): void {
159
+ if (!this.streamingHtmlKey) return;
160
+ const textNode = this.parser.textNode;
161
+ if (typeof textNode !== "string") return;
162
+ if (textNode.length === this.htmlEmittedLength) return;
163
+
164
+ this.emitPendingHtml(textNode);
165
+ }
166
+
167
+ /**
168
+ * Emit accumulated html content since the last emission.
169
+ * Called by flushHtmlChunks and directly when html completes.
170
+ */
171
+ private emitPendingHtml(textNode: string): void {
172
+ const newContent = textNode.slice(this.htmlEmittedLength);
173
+ if (newContent.length === 0) return;
174
+
175
+ if (!this.htmlArrayEmitted) {
176
+ this.htmlArrayEmitted = true;
177
+ this.emitParamDelta("html", []);
178
+ }
179
+ this.emitArrayItemDelta("html", newContent);
180
+ this.htmlEmittedLength = textNode.length;
181
+ }
182
+
183
+ private setParam(key: string, value: string | boolean | number | null): void {
184
+ switch (key) {
185
+ case "initialHeight":
186
+ this.params.initialHeight =
187
+ typeof value === "number" ? value : undefined;
188
+ if (this.snapshotEmitted) {
189
+ // Snapshot already went out (another param parsed first) — deliver
190
+ // the height as a delta instead.
191
+ this.emitParamDelta("initialHeight", this.params.initialHeight);
192
+ } else {
193
+ this.emitSnapshot();
194
+ }
195
+ break;
196
+ case "css":
197
+ this.params.css = value != null ? String(value) : undefined;
198
+ this.emitParamDelta("css", this.params.css);
199
+ this.emitParamDelta("cssComplete", true);
200
+ break;
201
+ case "jsFunctions":
202
+ this.params.jsFunctions = value != null ? String(value) : undefined;
203
+ this.emitParamDelta("jsFunctions", this.params.jsFunctions);
204
+ this.emitParamDelta("jsFunctionsComplete", true);
205
+ break;
206
+ }
207
+ }
208
+
209
+ private emitSnapshot(): void {
210
+ if (this.snapshotEmitted) return;
211
+ this.snapshotEmitted = true;
212
+
213
+ const event = {
214
+ type: EventType.ACTIVITY_SNAPSHOT,
215
+ messageId: this.messageId,
216
+ activityType: ACTIVITY_TYPE,
217
+ content: { initialHeight: this.params.initialHeight, generating: true },
218
+ } satisfies ActivitySnapshotEventProps;
219
+ this.onEvent(event);
220
+ }
221
+
222
+ private emitParamDelta(key: string, value: unknown): void {
223
+ // A JSON Patch "add" requires a value; emitting one without it (e.g. when
224
+ // the LLM sends a null/empty jsFunctions or css) makes fast-json-patch
225
+ // reject the whole patch client-side with OPERATION_VALUE_REQUIRED and
226
+ // drops it. Skip the delta when there's nothing to add.
227
+ if (value === undefined) return;
228
+ // The activity message must exist before any delta can be applied —
229
+ // the client silently drops ACTIVITY_DELTA events whose messageId has
230
+ // no prior ACTIVITY_SNAPSHOT. The LLM controls the key order of the
231
+ // streamed args, so the snapshot cannot wait for initialHeight.
232
+ this.emitSnapshot();
233
+ const event: ActivityDeltaEvent = {
234
+ type: EventType.ACTIVITY_DELTA,
235
+ messageId: this.messageId,
236
+ activityType: ACTIVITY_TYPE,
237
+ patch: [{ op: "add", path: `/${key}`, value }],
238
+ };
239
+ this.onEvent(event);
240
+ }
241
+
242
+ private emitArrayItemDelta(arrayKey: string, value: string): void {
243
+ this.emitSnapshot();
244
+ const event: ActivityDeltaEvent = {
245
+ type: EventType.ACTIVITY_DELTA,
246
+ messageId: this.messageId,
247
+ activityType: ACTIVITY_TYPE,
248
+ patch: [{ op: "add", path: `/${arrayKey}/-`, value }],
249
+ };
250
+ this.onEvent(event);
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Extract EventWithState type from Middleware.runNextWithState return type
256
+ */
257
+ type ExtractObservableType<T> = T extends Observable<infer U> ? U : never;
258
+ type RunNextWithStateReturn = ReturnType<Middleware["runNextWithState"]>;
259
+ type EventWithState = ExtractObservableType<RunNextWithStateReturn>;
260
+
261
+ export class OpenGenerativeUIMiddleware extends Middleware {
262
+ run(input: RunAgentInput, next: AbstractAgent): Observable<BaseEvent> {
263
+ return this.processStream(this.runNextWithState(input, next));
264
+ }
265
+
266
+ private processStream(
267
+ source: Observable<EventWithState>,
268
+ ): Observable<BaseEvent> {
269
+ return new Observable<BaseEvent>((subscriber) => {
270
+ let heldRunFinished: EventWithState | null = null;
271
+ // Track active generateSandboxedUi tool call IDs → their streaming parser
272
+ const activeParsers = new Map<string, ArgsParser>();
273
+ // Hold genui tool call events until the first activity event is emitted
274
+ const heldToolCallEvents = new Map<string, BaseEvent[]>();
275
+ const flushedToolCalls = new Set<string>();
276
+
277
+ const flushHeldEvents = (toolCallId: string) => {
278
+ if (flushedToolCalls.has(toolCallId)) return;
279
+ flushedToolCalls.add(toolCallId);
280
+ const held = heldToolCallEvents.get(toolCallId);
281
+ if (held) {
282
+ for (const e of held) {
283
+ subscriber.next(e);
284
+ }
285
+ heldToolCallEvents.delete(toolCallId);
286
+ }
287
+ };
288
+
289
+ const subscription = source.subscribe({
290
+ next: (eventWithState) => {
291
+ const event = eventWithState.event;
292
+
293
+ if (heldRunFinished) {
294
+ subscriber.next(heldRunFinished.event);
295
+ heldRunFinished = null;
296
+ }
297
+
298
+ if (event.type === EventType.RUN_FINISHED) {
299
+ heldRunFinished = eventWithState;
300
+ return;
301
+ }
302
+
303
+ // Hold TOOL_CALL_START for genui until the first activity event
304
+ if (event.type === EventType.TOOL_CALL_START) {
305
+ const startEvent = event as ToolCallStartEvent;
306
+ if (startEvent.toolCallName === TOOL_NAME) {
307
+ heldToolCallEvents.set(startEvent.toolCallId, [event]);
308
+ activeParsers.set(
309
+ startEvent.toolCallId,
310
+ new ArgsParser(startEvent.toolCallId, (activityEvent) => {
311
+ subscriber.next(activityEvent);
312
+ flushHeldEvents(startEvent.toolCallId);
313
+ }),
314
+ );
315
+ return;
316
+ }
317
+ }
318
+
319
+ // Hold or emit TOOL_CALL_ARGS for genui tool calls
320
+ if (event.type === EventType.TOOL_CALL_ARGS) {
321
+ const argsEvent = event as ToolCallArgsEvent;
322
+ const parser = activeParsers.get(argsEvent.toolCallId);
323
+ if (parser) {
324
+ if (!flushedToolCalls.has(argsEvent.toolCallId)) {
325
+ heldToolCallEvents.get(argsEvent.toolCallId)!.push(event);
326
+ } else {
327
+ subscriber.next(event);
328
+ }
329
+ parser.write(argsEvent.delta);
330
+ return;
331
+ }
332
+ }
333
+
334
+ // Hold or emit TOOL_CALL_END for genui tool calls
335
+ if (event.type === EventType.TOOL_CALL_END) {
336
+ const endEvent = event as { toolCallId: string } & BaseEvent;
337
+ const parser = activeParsers.get(endEvent.toolCallId);
338
+ if (parser) {
339
+ // Mark generation complete
340
+ const completeEvent: ActivityDeltaEvent = {
341
+ type: EventType.ACTIVITY_DELTA,
342
+ messageId: parser.messageId,
343
+ activityType: ACTIVITY_TYPE,
344
+ patch: [{ op: "add", path: "/generating", value: false }],
345
+ };
346
+ subscriber.next(completeEvent);
347
+
348
+ if (!flushedToolCalls.has(endEvent.toolCallId)) {
349
+ heldToolCallEvents.get(endEvent.toolCallId)!.push(event);
350
+ } else {
351
+ subscriber.next(event);
352
+ }
353
+ return;
354
+ }
355
+ }
356
+
357
+ subscriber.next(event);
358
+ },
359
+ error: (err) => {
360
+ // Flush any held tool call events so downstream sees them before the error
361
+ for (const [, events] of heldToolCallEvents) {
362
+ for (const event of events) {
363
+ subscriber.next(event);
364
+ }
365
+ }
366
+ heldToolCallEvents.clear();
367
+
368
+ if (heldRunFinished) {
369
+ subscriber.next(heldRunFinished.event);
370
+ heldRunFinished = null;
371
+ }
372
+ subscriber.error(err);
373
+ },
374
+ complete: () => {
375
+ // Flush any remaining held tool call events (e.g. parser never emitted)
376
+ heldToolCallEvents.forEach((_, toolCallId) => {
377
+ flushHeldEvents(toolCallId);
378
+ });
379
+
380
+ if (heldRunFinished) {
381
+ subscriber.next(heldRunFinished.event);
382
+ heldRunFinished = null;
383
+ }
384
+ activeParsers.clear();
385
+ subscriber.complete();
386
+ },
387
+ });
388
+
389
+ return () => subscription.unsubscribe();
390
+ });
391
+ }
392
+ }
@@ -0,0 +1,166 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { AbstractAgent, EventType } from "@ag-ui/client";
3
+ import type { BaseEvent, RunAgentInput } from "@ag-ui/client";
4
+ import { A2UIMiddleware } from "@ag-ui/a2ui-middleware";
5
+ import { from, firstValueFrom, toArray } from "rxjs";
6
+ import { OpenGenerativeUIMiddleware } from "./open-generative-ui-middleware";
7
+ import { transformRecordedEvents } from "./recorded-events";
8
+
9
+ const input: RunAgentInput = {
10
+ threadId: "thread",
11
+ runId: "run",
12
+ messages: [],
13
+ state: {},
14
+ tools: [],
15
+ context: [],
16
+ forwardedProps: {},
17
+ };
18
+
19
+ function toolRun(name: string, args: string): BaseEvent[] {
20
+ return [
21
+ { type: EventType.RUN_STARTED, threadId: "thread", runId: "run" },
22
+ {
23
+ type: EventType.TOOL_CALL_START,
24
+ toolCallId: "call",
25
+ toolCallName: name,
26
+ parentMessageId: "message",
27
+ },
28
+ { type: EventType.TOOL_CALL_ARGS, toolCallId: "call", delta: args },
29
+ { type: EventType.TOOL_CALL_END, toolCallId: "call" },
30
+ {
31
+ type: EventType.TOOL_CALL_RESULT,
32
+ toolCallId: "call",
33
+ messageId: "result",
34
+ content: "done",
35
+ },
36
+ { type: EventType.RUN_FINISHED, threadId: "thread", runId: "run" },
37
+ ];
38
+ }
39
+
40
+ class ScriptedAgent extends AbstractAgent {
41
+ constructor(private readonly events: BaseEvent[]) {
42
+ super();
43
+ }
44
+ run() {
45
+ return from(this.events);
46
+ }
47
+ clone() {
48
+ return new ScriptedAgent(this.events);
49
+ }
50
+ }
51
+
52
+ describe("transformRecordedEvents", () => {
53
+ it("retains pending tool calls from the recorded input history", async () => {
54
+ const resumed: RunAgentInput = {
55
+ ...input,
56
+ messages: [
57
+ {
58
+ id: "prior",
59
+ role: "assistant",
60
+ toolCalls: [
61
+ {
62
+ id: "pending",
63
+ type: "function",
64
+ function: { name: "render_a2ui", arguments: "{}" },
65
+ },
66
+ ],
67
+ },
68
+ ],
69
+ };
70
+ const events: BaseEvent[] = [
71
+ {
72
+ type: EventType.TEXT_MESSAGE_START,
73
+ messageId: "new",
74
+ role: "assistant",
75
+ },
76
+ {
77
+ type: EventType.TEXT_MESSAGE_CONTENT,
78
+ messageId: "new",
79
+ delta: "Hello",
80
+ },
81
+ { type: EventType.TEXT_MESSAGE_END, messageId: "new" },
82
+ { type: EventType.RUN_FINISHED, threadId: "thread", runId: "run" },
83
+ ];
84
+ const output = await transformRecordedEvents(resumed, events, {
85
+ a2ui: {},
86
+ openGenerativeUI: true,
87
+ });
88
+ expect(output).toEqual([
89
+ ...events.slice(0, -1),
90
+ expect.objectContaining({
91
+ type: EventType.TOOL_CALL_RESULT,
92
+ toolCallId: "pending",
93
+ content: '{"status":"rendered"}',
94
+ }),
95
+ events.at(-1),
96
+ ]);
97
+ });
98
+ it("matches the live middleware chain event-for-event without changing its input", async () => {
99
+ const events = toolRun(
100
+ "generateSandboxedUi",
101
+ JSON.stringify({ initialHeight: 200, html: "<p>Hello</p>" }),
102
+ );
103
+ const original = structuredClone(events);
104
+ const live = new ScriptedAgent(events);
105
+ live.use(new A2UIMiddleware({}), new OpenGenerativeUIMiddleware());
106
+ const expected: BaseEvent[] = [];
107
+ await live.runAgent(
108
+ { runId: input.runId },
109
+ {
110
+ onEvent: ({ event }) => {
111
+ expected.push(event);
112
+ },
113
+ },
114
+ );
115
+ const actual = await transformRecordedEvents(input, events, {
116
+ a2ui: {},
117
+ openGenerativeUI: true,
118
+ });
119
+ expect(actual).toEqual(expected);
120
+ expect(
121
+ actual.filter((event) => event.type === EventType.ACTIVITY_SNAPSHOT),
122
+ ).toEqual([
123
+ {
124
+ type: EventType.ACTIVITY_SNAPSHOT,
125
+ messageId: "call-activity",
126
+ activityType: "open-generative-ui",
127
+ content: { initialHeight: 200, generating: true },
128
+ },
129
+ ]);
130
+ expect(events).toEqual(original);
131
+ });
132
+
133
+ it("uses the existing A2UI converter and respects catalog configuration", async () => {
134
+ const events = toolRun(
135
+ "render_a2ui",
136
+ JSON.stringify({
137
+ surfaceId: "surface",
138
+ components: [{ id: "root", component: "Text", text: "Hello" }],
139
+ }),
140
+ );
141
+ const config = { defaultCatalogId: "test://catalog" };
142
+ const expected = await firstValueFrom(
143
+ new A2UIMiddleware(config)
144
+ .run(input, new ScriptedAgent(events))
145
+ .pipe(toArray()),
146
+ );
147
+ const actual = await transformRecordedEvents(input, events, {
148
+ a2ui: config,
149
+ });
150
+ expect(actual).toEqual(expected);
151
+ expect(JSON.stringify(actual)).toContain("test://catalog");
152
+ });
153
+
154
+ it("passes ordinary events and terminal errors through unchanged", async () => {
155
+ const events: BaseEvent[] = [
156
+ { type: EventType.RUN_ERROR, message: "recorded failure" },
157
+ ];
158
+ expect(
159
+ await transformRecordedEvents(input, events, {
160
+ a2ui: {},
161
+ openGenerativeUI: true,
162
+ }),
163
+ ).toEqual(events);
164
+ expect(await transformRecordedEvents(input, [], {})).toEqual([]);
165
+ });
166
+ });
@@ -0,0 +1,74 @@
1
+ import { AbstractAgent } from "@ag-ui/client";
2
+ import type { BaseEvent, RunAgentInput } from "@ag-ui/client";
3
+ import { A2UIMiddleware } from "@ag-ui/a2ui-middleware";
4
+ import type { A2UIMiddlewareConfig } from "@ag-ui/a2ui-middleware";
5
+ import { firstValueFrom, from, toArray } from "rxjs";
6
+ import type { Observable } from "rxjs";
7
+ import { OpenGenerativeUIMiddleware } from "./open-generative-ui-middleware";
8
+
9
+ export interface RecordedEventTransformOptions {
10
+ /** Use the original runtime configuration, including its catalog when known. */
11
+ a2ui?: A2UIMiddlewareConfig;
12
+ openGenerativeUI?: boolean;
13
+ }
14
+
15
+ /** An inert event source: it cannot contact an agent or execute tools. */
16
+ class RecordedEventAgent extends AbstractAgent {
17
+ constructor(
18
+ private readonly input: RunAgentInput,
19
+ private readonly stream: (input: RunAgentInput) => Observable<BaseEvent>,
20
+ ) {
21
+ super({
22
+ threadId: input.threadId,
23
+ initialMessages: input.messages,
24
+ initialState: input.state,
25
+ });
26
+ }
27
+
28
+ /** Emit recorded events through this layer without contacting an agent. */
29
+ run(input: RunAgentInput): Observable<BaseEvent> {
30
+ return this.stream(input);
31
+ }
32
+
33
+ /** Retain the current replay history and state in an independent agent. */
34
+ clone(): AbstractAgent {
35
+ return new RecordedEventAgent(
36
+ { ...this.input, messages: this.messages, state: this.state },
37
+ this.stream,
38
+ );
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Apply CPK's rich-UI middleware to one recorded, pre-middleware run.
44
+ * Supply complete AG-UI run events and the original input/configuration. This
45
+ * does not run an agent, fetch resources, or execute tools. Already transformed
46
+ * histories must not be passed through a second time.
47
+ *
48
+ * Returns events in runtime order, including generated activity events and any
49
+ * synthetic A2UI tool results. Persistence IDs, timestamps, and provenance are
50
+ * the caller's responsibility (A2UI can generate random result message IDs).
51
+ */
52
+ export async function transformRecordedEvents(
53
+ input: RunAgentInput,
54
+ events: readonly BaseEvent[],
55
+ options: RecordedEventTransformOptions,
56
+ ): Promise<BaseEvent[]> {
57
+ let agent: AbstractAgent = new RecordedEventAgent(input, () => from(events));
58
+ // Runtime registers A2UI before Open GenUI; the first middleware is outermost.
59
+ if (options.openGenerativeUI) {
60
+ const next = agent;
61
+ const middleware = new OpenGenerativeUIMiddleware();
62
+ agent = new RecordedEventAgent(input, (runInput) =>
63
+ middleware.run(runInput, next),
64
+ );
65
+ }
66
+ if (options.a2ui) {
67
+ const next = agent;
68
+ const middleware = new A2UIMiddleware(options.a2ui);
69
+ agent = new RecordedEventAgent(input, (runInput) =>
70
+ middleware.run(runInput, next),
71
+ );
72
+ }
73
+ return firstValueFrom(agent.run(input).pipe(toArray()));
74
+ }