@assistant-ui/react-ag-ui 0.0.29 → 0.0.31
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/README.md +41 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts +11 -0
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +186 -12
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +18 -5
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +134 -39
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +5 -0
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/subscriber.js +55 -36
- package/dist/runtime/adapter/subscriber.js.map +1 -1
- package/dist/runtime/event-parser.d.ts +5 -1
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +48 -2
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/types.d.ts +22 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/useAgUiRuntime.d.ts +6 -2
- package/dist/useAgUiRuntime.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +7 -1
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +5 -0
- package/src/runtime/AgUiThreadRuntimeCore.ts +251 -11
- package/src/runtime/adapter/run-aggregator.ts +168 -44
- package/src/runtime/adapter/subscriber.ts +53 -43
- package/src/runtime/event-parser.ts +64 -3
- package/src/runtime/types.ts +31 -1
- package/src/useAgUiRuntime.ts +23 -3
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
4
|
ChatModelRunResult,
|
|
5
|
+
MessageTiming,
|
|
5
6
|
ThreadAssistantMessagePart,
|
|
6
7
|
ToolCallMessagePart,
|
|
7
8
|
} from "@assistant-ui/core";
|
|
8
|
-
import type { AgUiEvent } from "../types";
|
|
9
|
+
import type { AgUiEvent, AgUiInterrupt } from "../types";
|
|
9
10
|
import type { Logger } from "../logger";
|
|
10
11
|
|
|
12
|
+
export const AG_UI_METADATA_NAMESPACE = "agui";
|
|
13
|
+
|
|
14
|
+
export type AgUiCustomMetadata = {
|
|
15
|
+
interrupts?: AgUiInterrupt[];
|
|
16
|
+
};
|
|
17
|
+
|
|
11
18
|
type Emit = (update: ChatModelRunResult) => void;
|
|
12
19
|
|
|
13
20
|
type ToolCallState = {
|
|
@@ -18,12 +25,14 @@ type ToolCallState = {
|
|
|
18
25
|
result: unknown;
|
|
19
26
|
isError: boolean | undefined;
|
|
20
27
|
parentMessageId?: string;
|
|
28
|
+
toolMessageId?: string;
|
|
21
29
|
};
|
|
22
30
|
|
|
23
31
|
export type RunAggregatorOptions = {
|
|
24
32
|
showThinking: boolean;
|
|
25
33
|
logger: Logger;
|
|
26
34
|
emit: Emit;
|
|
35
|
+
onServerMessageId?: (messageId: string) => void;
|
|
27
36
|
};
|
|
28
37
|
|
|
29
38
|
/**
|
|
@@ -36,53 +45,75 @@ export class RunAggregator {
|
|
|
36
45
|
private readonly emitUpdate: Emit;
|
|
37
46
|
private readonly showThinking: boolean;
|
|
38
47
|
private readonly logger: Logger;
|
|
48
|
+
private readonly onServerMessageId: ((messageId: string) => void) | undefined;
|
|
39
49
|
|
|
40
50
|
private status: ChatModelRunResult["status"] | undefined;
|
|
51
|
+
private interrupts: AgUiInterrupt[] | undefined;
|
|
41
52
|
private readonly textParts = new Map<
|
|
42
53
|
string,
|
|
43
54
|
{ buffer: string; touched: boolean }
|
|
44
55
|
>();
|
|
45
56
|
private activeTextMessageId: string | undefined;
|
|
46
|
-
private
|
|
47
|
-
private
|
|
57
|
+
private readonly reasoningParts = new Map<string, string>(); // key → buffer
|
|
58
|
+
private activeReasoningKey: string | undefined;
|
|
59
|
+
private reasoningPartCounter = 0;
|
|
48
60
|
private readonly toolCalls = new Map<string, ToolCallState>();
|
|
49
61
|
private readonly partOrder: (
|
|
50
62
|
| { kind: "text"; key: string }
|
|
51
|
-
| { kind: "reasoning" }
|
|
63
|
+
| { kind: "reasoning"; key: string }
|
|
52
64
|
| { kind: "tool-call"; toolCallId: string }
|
|
53
65
|
)[] = [];
|
|
54
|
-
private hasReasoningPart = false;
|
|
55
66
|
private textPartCounter = 0;
|
|
67
|
+
private serverMessageIdReported = false;
|
|
68
|
+
|
|
69
|
+
private streamStartTime: number | undefined;
|
|
70
|
+
private firstTokenTime: number | undefined;
|
|
71
|
+
private totalChunks = 0;
|
|
56
72
|
|
|
57
73
|
constructor(options: RunAggregatorOptions) {
|
|
58
74
|
this.emitUpdate = options.emit;
|
|
59
75
|
this.showThinking = options.showThinking;
|
|
60
76
|
this.logger = options.logger;
|
|
77
|
+
this.onServerMessageId = options.onServerMessageId;
|
|
61
78
|
}
|
|
62
79
|
|
|
63
80
|
handle(event: AgUiEvent): void {
|
|
64
81
|
switch (event.type) {
|
|
65
82
|
case "RUN_STARTED": {
|
|
66
83
|
this.clearTextParts();
|
|
67
|
-
this.
|
|
68
|
-
this.
|
|
84
|
+
this.reasoningParts.clear();
|
|
85
|
+
this.activeReasoningKey = undefined;
|
|
86
|
+
this.reasoningPartCounter = 0;
|
|
69
87
|
this.toolCalls.clear();
|
|
70
88
|
this.partOrder.length = 0;
|
|
71
|
-
this.hasReasoningPart = false;
|
|
72
89
|
this.textPartCounter = 0;
|
|
73
90
|
this.activeTextMessageId = undefined;
|
|
91
|
+
this.interrupts = undefined;
|
|
92
|
+
this.serverMessageIdReported = false;
|
|
93
|
+
this.streamStartTime = Date.now();
|
|
94
|
+
this.firstTokenTime = undefined;
|
|
95
|
+
this.totalChunks = 0;
|
|
74
96
|
this.status = { type: "running" };
|
|
75
97
|
this.emit();
|
|
76
98
|
break;
|
|
77
99
|
}
|
|
78
100
|
case "RUN_FINISHED": {
|
|
101
|
+
if (event.outcome?.type === "interrupt") {
|
|
102
|
+
this.interrupts = event.outcome.interrupts;
|
|
103
|
+
this.status = { type: "requires-action", reason: "interrupt" };
|
|
104
|
+
this.emit();
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.interrupts = undefined;
|
|
79
109
|
const hasUnresolvedToolCalls = Array.from(this.toolCalls.values()).some(
|
|
80
110
|
(tc) => tc.result === undefined,
|
|
81
111
|
);
|
|
82
112
|
|
|
83
|
-
this.status =
|
|
84
|
-
|
|
85
|
-
|
|
113
|
+
this.status =
|
|
114
|
+
event.outcome?.type === "success" || !hasUnresolvedToolCalls
|
|
115
|
+
? { type: "complete", reason: "unknown" }
|
|
116
|
+
: { type: "requires-action", reason: "tool-calls" };
|
|
86
117
|
this.emit();
|
|
87
118
|
break;
|
|
88
119
|
}
|
|
@@ -102,6 +133,7 @@ export class RunAggregator {
|
|
|
102
133
|
}
|
|
103
134
|
|
|
104
135
|
case "TEXT_MESSAGE_START": {
|
|
136
|
+
this.reportServerMessageId(event.messageId);
|
|
105
137
|
const id = this.startTextMessage(event.messageId);
|
|
106
138
|
if (id) {
|
|
107
139
|
this.markTextPartTouched(id);
|
|
@@ -111,15 +143,18 @@ export class RunAggregator {
|
|
|
111
143
|
}
|
|
112
144
|
case "TEXT_MESSAGE_CONTENT":
|
|
113
145
|
case "TEXT_MESSAGE_CHUNK": {
|
|
146
|
+
const incomingId = "messageId" in event ? event.messageId : undefined;
|
|
147
|
+
this.reportServerMessageId(incomingId);
|
|
114
148
|
if (!event.delta) break;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
);
|
|
149
|
+
this.recordFirstToken();
|
|
150
|
+
const id = this.resolveTextMessageId(incomingId);
|
|
118
151
|
this.appendText(id, event.delta);
|
|
152
|
+
this.totalChunks++;
|
|
119
153
|
this.emit();
|
|
120
154
|
break;
|
|
121
155
|
}
|
|
122
156
|
case "TEXT_MESSAGE_END": {
|
|
157
|
+
this.reportServerMessageId(event.messageId);
|
|
123
158
|
if (event.messageId && this.activeTextMessageId === event.messageId) {
|
|
124
159
|
this.activeTextMessageId = undefined;
|
|
125
160
|
}
|
|
@@ -131,11 +166,22 @@ export class RunAggregator {
|
|
|
131
166
|
case "THINKING_TEXT_MESSAGE_START":
|
|
132
167
|
case "REASONING_START":
|
|
133
168
|
case "REASONING_MESSAGE_START":
|
|
134
|
-
this.handleReasoningStart(
|
|
169
|
+
this.handleReasoningStart(
|
|
170
|
+
"messageId" in event ? event.messageId : undefined,
|
|
171
|
+
);
|
|
135
172
|
break;
|
|
136
173
|
case "THINKING_TEXT_MESSAGE_CONTENT":
|
|
137
|
-
case "REASONING_MESSAGE_CONTENT":
|
|
138
174
|
this.handleReasoningContent(event.delta);
|
|
175
|
+
this.totalChunks++;
|
|
176
|
+
this.recordFirstToken();
|
|
177
|
+
break;
|
|
178
|
+
case "REASONING_MESSAGE_CONTENT":
|
|
179
|
+
this.handleReasoningContent(
|
|
180
|
+
event.delta,
|
|
181
|
+
"messageId" in event ? event.messageId : undefined,
|
|
182
|
+
);
|
|
183
|
+
this.totalChunks++;
|
|
184
|
+
this.recordFirstToken();
|
|
139
185
|
break;
|
|
140
186
|
case "THINKING_TEXT_MESSAGE_END":
|
|
141
187
|
case "THINKING_END":
|
|
@@ -145,6 +191,7 @@ export class RunAggregator {
|
|
|
145
191
|
break;
|
|
146
192
|
|
|
147
193
|
case "TOOL_CALL_START": {
|
|
194
|
+
this.reportServerMessageId(event.parentMessageId);
|
|
148
195
|
this.startToolCall(
|
|
149
196
|
event.toolCallId,
|
|
150
197
|
event.toolCallName,
|
|
@@ -155,6 +202,9 @@ export class RunAggregator {
|
|
|
155
202
|
}
|
|
156
203
|
case "TOOL_CALL_ARGS":
|
|
157
204
|
case "TOOL_CALL_CHUNK": {
|
|
205
|
+
if (event.type === "TOOL_CALL_CHUNK") {
|
|
206
|
+
this.reportServerMessageId(event.parentMessageId);
|
|
207
|
+
}
|
|
158
208
|
if (!event.delta) break;
|
|
159
209
|
this.appendToolArgs(event.toolCallId, event.delta);
|
|
160
210
|
this.emit();
|
|
@@ -169,6 +219,7 @@ export class RunAggregator {
|
|
|
169
219
|
event.toolCallId,
|
|
170
220
|
event.content ?? "",
|
|
171
221
|
event.role === "tool" ? false : undefined,
|
|
222
|
+
event.messageId,
|
|
172
223
|
);
|
|
173
224
|
this.emit();
|
|
174
225
|
break;
|
|
@@ -180,6 +231,12 @@ export class RunAggregator {
|
|
|
180
231
|
}
|
|
181
232
|
}
|
|
182
233
|
|
|
234
|
+
private reportServerMessageId(messageId: string | undefined): void {
|
|
235
|
+
if (this.serverMessageIdReported || !messageId) return;
|
|
236
|
+
this.serverMessageIdReported = true;
|
|
237
|
+
this.onServerMessageId?.(messageId);
|
|
238
|
+
}
|
|
239
|
+
|
|
183
240
|
private clearTextParts(): void {
|
|
184
241
|
this.textParts.clear();
|
|
185
242
|
}
|
|
@@ -244,6 +301,9 @@ export class RunAggregator {
|
|
|
244
301
|
parentMessageId?: string,
|
|
245
302
|
) {
|
|
246
303
|
if (!id) return;
|
|
304
|
+
// A new tool call acts as a boundary: any anonymous text that arrives
|
|
305
|
+
// after it should be a new part, not appended to the pre-tool text.
|
|
306
|
+
this.activeTextMessageId = undefined;
|
|
247
307
|
if (
|
|
248
308
|
!this.partOrder.some(
|
|
249
309
|
(part) => part.kind === "tool-call" && part.toolCallId === id,
|
|
@@ -281,7 +341,12 @@ export class RunAggregator {
|
|
|
281
341
|
}
|
|
282
342
|
}
|
|
283
343
|
|
|
284
|
-
private finishToolCall(
|
|
344
|
+
private finishToolCall(
|
|
345
|
+
id: string,
|
|
346
|
+
content: string,
|
|
347
|
+
isError?: boolean,
|
|
348
|
+
toolMessageId?: string,
|
|
349
|
+
) {
|
|
285
350
|
if (!id) return;
|
|
286
351
|
let entry = this.toolCalls.get(id);
|
|
287
352
|
if (!entry) {
|
|
@@ -304,6 +369,9 @@ export class RunAggregator {
|
|
|
304
369
|
}
|
|
305
370
|
entry.result = this.tryParseJSON(content);
|
|
306
371
|
entry.isError = isError;
|
|
372
|
+
if (toolMessageId) {
|
|
373
|
+
entry.toolMessageId = toolMessageId;
|
|
374
|
+
}
|
|
307
375
|
}
|
|
308
376
|
|
|
309
377
|
private tryParseJSON(value: string): unknown {
|
|
@@ -320,14 +388,11 @@ export class RunAggregator {
|
|
|
320
388
|
|
|
321
389
|
for (const part of this.partOrder) {
|
|
322
390
|
if (part.kind === "reasoning") {
|
|
323
|
-
if (
|
|
324
|
-
this.
|
|
325
|
-
(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
type: "reasoning",
|
|
329
|
-
text: this.reasoningBuffer,
|
|
330
|
-
} as const);
|
|
391
|
+
if (this.showThinking) {
|
|
392
|
+
const buffer = this.reasoningParts.get(part.key) ?? "";
|
|
393
|
+
if (buffer.length > 0 || this.activeReasoningKey === part.key) {
|
|
394
|
+
snapshot.push({ type: "reasoning", text: buffer } as const);
|
|
395
|
+
}
|
|
331
396
|
}
|
|
332
397
|
continue;
|
|
333
398
|
}
|
|
@@ -351,45 +416,104 @@ export class RunAggregator {
|
|
|
351
416
|
...(entry.result !== undefined ? { result: entry.result } : {}),
|
|
352
417
|
...(entry.isError !== undefined ? { isError: entry.isError } : {}),
|
|
353
418
|
...(entry.parentMessageId ? { parentId: entry.parentMessageId } : {}),
|
|
354
|
-
|
|
419
|
+
...(entry.toolMessageId
|
|
420
|
+
? { unstable_toolMessageId: entry.toolMessageId }
|
|
421
|
+
: {}),
|
|
422
|
+
} as ToolCallMessagePart & { unstable_toolMessageId?: string };
|
|
355
423
|
snapshot.push(toolPart);
|
|
356
424
|
}
|
|
357
425
|
|
|
426
|
+
const timing = this.getTiming();
|
|
427
|
+
const metadata = {
|
|
428
|
+
...(timing ? { timing } : {}),
|
|
429
|
+
...(this.interrupts
|
|
430
|
+
? {
|
|
431
|
+
custom: {
|
|
432
|
+
[AG_UI_METADATA_NAMESPACE]: {
|
|
433
|
+
interrupts: this.interrupts,
|
|
434
|
+
} satisfies AgUiCustomMetadata,
|
|
435
|
+
},
|
|
436
|
+
}
|
|
437
|
+
: {}),
|
|
438
|
+
};
|
|
358
439
|
const result: ChatModelRunResult = {
|
|
359
440
|
content: snapshot,
|
|
360
441
|
...(this.status ? { status: this.status } : undefined),
|
|
442
|
+
...(Object.keys(metadata).length > 0 ? { metadata } : undefined),
|
|
361
443
|
};
|
|
362
444
|
this.emitUpdate(result);
|
|
363
445
|
}
|
|
364
446
|
|
|
365
|
-
private
|
|
447
|
+
private recordFirstToken(): void {
|
|
448
|
+
if (
|
|
449
|
+
this.firstTokenTime === undefined &&
|
|
450
|
+
this.streamStartTime !== undefined
|
|
451
|
+
) {
|
|
452
|
+
this.firstTokenTime = Date.now() - this.streamStartTime;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private getTiming(): MessageTiming | undefined {
|
|
457
|
+
if (this.streamStartTime === undefined) return undefined;
|
|
458
|
+
|
|
459
|
+
const now = Date.now();
|
|
460
|
+
const totalStreamTime = now - this.streamStartTime;
|
|
461
|
+
const tokenCount =
|
|
462
|
+
this.totalChunks > 0
|
|
463
|
+
? Math.ceil(
|
|
464
|
+
Array.from(this.textParts.values()).reduce(
|
|
465
|
+
(sum, p) => sum + p.buffer.length,
|
|
466
|
+
0,
|
|
467
|
+
) / 4,
|
|
468
|
+
)
|
|
469
|
+
: undefined;
|
|
470
|
+
const tokensPerSecond =
|
|
471
|
+
tokenCount && totalStreamTime > 0
|
|
472
|
+
? (tokenCount / totalStreamTime) * 1000
|
|
473
|
+
: undefined;
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
streamStartTime: this.streamStartTime,
|
|
477
|
+
...(this.firstTokenTime !== undefined
|
|
478
|
+
? { firstTokenTime: this.firstTokenTime }
|
|
479
|
+
: {}),
|
|
480
|
+
totalStreamTime,
|
|
481
|
+
...(tokenCount !== undefined ? { tokenCount } : {}),
|
|
482
|
+
...(tokensPerSecond !== undefined ? { tokensPerSecond } : {}),
|
|
483
|
+
totalChunks: this.totalChunks,
|
|
484
|
+
toolCallCount: this.toolCalls.size,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private handleReasoningStart(messageId?: string): void {
|
|
366
489
|
if (!this.showThinking) return;
|
|
367
|
-
|
|
368
|
-
|
|
490
|
+
// A reasoning block acts as a boundary: anonymous text arriving after it
|
|
491
|
+
// should be a new part, not appended to any pre-reasoning text.
|
|
492
|
+
this.activeTextMessageId = undefined;
|
|
493
|
+
const key = messageId ?? `__auto-reasoning-${++this.reasoningPartCounter}`;
|
|
494
|
+
if (!this.reasoningParts.has(key)) {
|
|
495
|
+
this.reasoningParts.set(key, "");
|
|
496
|
+
this.partOrder.push({ kind: "reasoning", key });
|
|
497
|
+
}
|
|
498
|
+
this.activeReasoningKey = key;
|
|
369
499
|
this.emit();
|
|
370
500
|
}
|
|
371
501
|
|
|
372
|
-
private handleReasoningContent(delta: string): void {
|
|
502
|
+
private handleReasoningContent(delta: string, messageId?: string): void {
|
|
373
503
|
if (!this.showThinking || !delta) return;
|
|
374
|
-
this.
|
|
375
|
-
|
|
504
|
+
if (!this.activeReasoningKey) {
|
|
505
|
+
// Content arrived without a preceding START — create the slot lazily.
|
|
506
|
+
this.handleReasoningStart(messageId);
|
|
507
|
+
}
|
|
508
|
+
const key = this.activeReasoningKey;
|
|
509
|
+
if (!key) return;
|
|
510
|
+
this.reasoningParts.set(key, (this.reasoningParts.get(key) ?? "") + delta);
|
|
376
511
|
this.emit();
|
|
377
512
|
}
|
|
378
513
|
|
|
379
514
|
private handleReasoningEnd(): void {
|
|
380
515
|
if (!this.showThinking) return;
|
|
516
|
+
this.activeReasoningKey = undefined;
|
|
381
517
|
this.emit();
|
|
382
518
|
}
|
|
383
|
-
|
|
384
|
-
private ensureReasoningPart(): void {
|
|
385
|
-
if (this.hasReasoningPart) return;
|
|
386
|
-
// ensure reasoning appears before the first text segment if possible
|
|
387
|
-
const textIndex = this.partOrder.findIndex((part) => part.kind === "text");
|
|
388
|
-
if (textIndex === -1) {
|
|
389
|
-
this.partOrder.push({ kind: "reasoning" });
|
|
390
|
-
} else {
|
|
391
|
-
this.partOrder.splice(textIndex, 0, { kind: "reasoning" });
|
|
392
|
-
}
|
|
393
|
-
this.hasReasoningPart = true;
|
|
394
|
-
}
|
|
395
519
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { AgUiEvent } from "../types";
|
|
4
4
|
import { parseAgUiEvent } from "../event-parser";
|
|
5
|
+
import type { Logger } from "../logger";
|
|
5
6
|
|
|
6
7
|
type Dispatch = (event: AgUiEvent) => void;
|
|
7
8
|
|
|
@@ -31,6 +32,7 @@ type Subscriber = {
|
|
|
31
32
|
onMessagesSnapshotEvent?: (payload: { event: unknown }) => void;
|
|
32
33
|
onCustomEvent?: (payload: { event: unknown }) => void;
|
|
33
34
|
onRawEvent?: (payload: { event: unknown }) => void;
|
|
35
|
+
onRunFinishedEvent?: (payload: { event: unknown }) => void;
|
|
34
36
|
onRunFinalized?: () => void;
|
|
35
37
|
onRunFailed?: (payload: { error: Error }) => void;
|
|
36
38
|
};
|
|
@@ -38,37 +40,40 @@ type Subscriber = {
|
|
|
38
40
|
const ensureEvent = (
|
|
39
41
|
raw: unknown,
|
|
40
42
|
type: AgUiEvent["type"],
|
|
43
|
+
logger?: Logger,
|
|
41
44
|
): AgUiEvent | null => {
|
|
45
|
+
const parseOptions = logger ? { logger } : undefined;
|
|
46
|
+
let parsed: AgUiEvent | null;
|
|
42
47
|
if (raw && typeof raw === "object") {
|
|
43
48
|
const payload = raw as Record<string, unknown>;
|
|
44
49
|
if (typeof payload.type === "string") {
|
|
45
|
-
|
|
50
|
+
parsed = parseAgUiEvent(payload, parseOptions);
|
|
51
|
+
} else {
|
|
52
|
+
parsed = parseAgUiEvent({ type, ...payload }, parseOptions);
|
|
46
53
|
}
|
|
47
|
-
|
|
54
|
+
} else {
|
|
55
|
+
parsed = parseAgUiEvent({ type }, parseOptions);
|
|
48
56
|
}
|
|
49
|
-
return
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const dispatchIfValid = (
|
|
53
|
-
dispatch: Dispatch,
|
|
54
|
-
raw: unknown,
|
|
55
|
-
type: AgUiEvent["type"],
|
|
56
|
-
) => {
|
|
57
|
-
const event = ensureEvent(raw, type);
|
|
58
|
-
if (!event) return;
|
|
59
|
-
dispatch(event);
|
|
57
|
+
return parsed && parsed.type === type ? parsed : null;
|
|
60
58
|
};
|
|
61
59
|
|
|
62
60
|
type SubscriberOptions = {
|
|
63
61
|
dispatch: Dispatch;
|
|
64
62
|
runId: string;
|
|
63
|
+
logger?: Logger;
|
|
65
64
|
onRunFailed?: (error: Error) => void;
|
|
66
65
|
};
|
|
67
66
|
|
|
68
67
|
export const createAgUiSubscriber = (
|
|
69
68
|
options: SubscriberOptions,
|
|
70
69
|
): Subscriber => {
|
|
71
|
-
const { dispatch, runId, onRunFailed } = options;
|
|
70
|
+
const { dispatch, runId, logger, onRunFailed } = options;
|
|
71
|
+
let runFinishedDispatched = false;
|
|
72
|
+
const dispatchIfValid = (raw: unknown, type: AgUiEvent["type"]) => {
|
|
73
|
+
const event = ensureEvent(raw, type, logger);
|
|
74
|
+
if (!event) return;
|
|
75
|
+
dispatch(event);
|
|
76
|
+
};
|
|
72
77
|
return {
|
|
73
78
|
onEvent: ({ event }) => {
|
|
74
79
|
const typeCandidate =
|
|
@@ -83,52 +88,57 @@ export const createAgUiSubscriber = (
|
|
|
83
88
|
if (parsed) dispatch(parsed);
|
|
84
89
|
},
|
|
85
90
|
onTextMessageStartEvent: ({ event }) =>
|
|
86
|
-
dispatchIfValid(
|
|
91
|
+
dispatchIfValid(event, "TEXT_MESSAGE_START"),
|
|
87
92
|
onTextMessageContentEvent: ({ event }) =>
|
|
88
|
-
dispatchIfValid(
|
|
93
|
+
dispatchIfValid(event, "TEXT_MESSAGE_CONTENT"),
|
|
89
94
|
onTextMessageEndEvent: ({ event }) =>
|
|
90
|
-
dispatchIfValid(
|
|
95
|
+
dispatchIfValid(event, "TEXT_MESSAGE_END"),
|
|
91
96
|
onTextMessageChunkEvent: ({ event }) =>
|
|
92
|
-
dispatchIfValid(
|
|
97
|
+
dispatchIfValid(event, "TEXT_MESSAGE_CHUNK"),
|
|
93
98
|
onThinkingStartEvent: ({ event }) =>
|
|
94
|
-
dispatchIfValid(
|
|
95
|
-
onThinkingEndEvent: ({ event }) =>
|
|
96
|
-
dispatchIfValid(dispatch, event, "THINKING_END"),
|
|
99
|
+
dispatchIfValid(event, "THINKING_START"),
|
|
100
|
+
onThinkingEndEvent: ({ event }) => dispatchIfValid(event, "THINKING_END"),
|
|
97
101
|
onThinkingTextMessageStartEvent: ({ event }) =>
|
|
98
|
-
dispatchIfValid(
|
|
102
|
+
dispatchIfValid(event, "THINKING_TEXT_MESSAGE_START"),
|
|
99
103
|
onThinkingTextMessageContentEvent: ({ event }) =>
|
|
100
|
-
dispatchIfValid(
|
|
104
|
+
dispatchIfValid(event, "THINKING_TEXT_MESSAGE_CONTENT"),
|
|
101
105
|
onThinkingTextMessageEndEvent: ({ event }) =>
|
|
102
|
-
dispatchIfValid(
|
|
106
|
+
dispatchIfValid(event, "THINKING_TEXT_MESSAGE_END"),
|
|
103
107
|
onReasoningStartEvent: ({ event }) =>
|
|
104
|
-
dispatchIfValid(
|
|
105
|
-
onReasoningEndEvent: ({ event }) =>
|
|
106
|
-
dispatchIfValid(dispatch, event, "REASONING_END"),
|
|
108
|
+
dispatchIfValid(event, "REASONING_START"),
|
|
109
|
+
onReasoningEndEvent: ({ event }) => dispatchIfValid(event, "REASONING_END"),
|
|
107
110
|
onReasoningMessageStartEvent: ({ event }) =>
|
|
108
|
-
dispatchIfValid(
|
|
111
|
+
dispatchIfValid(event, "REASONING_MESSAGE_START"),
|
|
109
112
|
onReasoningMessageContentEvent: ({ event }) =>
|
|
110
|
-
dispatchIfValid(
|
|
113
|
+
dispatchIfValid(event, "REASONING_MESSAGE_CONTENT"),
|
|
111
114
|
onReasoningMessageEndEvent: ({ event }) =>
|
|
112
|
-
dispatchIfValid(
|
|
115
|
+
dispatchIfValid(event, "REASONING_MESSAGE_END"),
|
|
113
116
|
onToolCallStartEvent: ({ event }) =>
|
|
114
|
-
dispatchIfValid(
|
|
117
|
+
dispatchIfValid(event, "TOOL_CALL_START"),
|
|
115
118
|
onToolCallArgsEvent: ({ event }) =>
|
|
116
|
-
dispatchIfValid(
|
|
117
|
-
onToolCallEndEvent: ({ event }) =>
|
|
118
|
-
dispatchIfValid(dispatch, event, "TOOL_CALL_END"),
|
|
119
|
+
dispatchIfValid(event, "TOOL_CALL_ARGS"),
|
|
120
|
+
onToolCallEndEvent: ({ event }) => dispatchIfValid(event, "TOOL_CALL_END"),
|
|
119
121
|
onToolCallChunkEvent: ({ event }) =>
|
|
120
|
-
dispatchIfValid(
|
|
122
|
+
dispatchIfValid(event, "TOOL_CALL_CHUNK"),
|
|
121
123
|
onToolCallResultEvent: ({ event }) =>
|
|
122
|
-
dispatchIfValid(
|
|
124
|
+
dispatchIfValid(event, "TOOL_CALL_RESULT"),
|
|
123
125
|
onStateSnapshotEvent: ({ event }) =>
|
|
124
|
-
dispatchIfValid(
|
|
125
|
-
onStateDeltaEvent: ({ event }) =>
|
|
126
|
-
dispatchIfValid(dispatch, event, "STATE_DELTA"),
|
|
126
|
+
dispatchIfValid(event, "STATE_SNAPSHOT"),
|
|
127
|
+
onStateDeltaEvent: ({ event }) => dispatchIfValid(event, "STATE_DELTA"),
|
|
127
128
|
onMessagesSnapshotEvent: ({ event }) =>
|
|
128
|
-
dispatchIfValid(
|
|
129
|
-
onCustomEvent: ({ event }) => dispatchIfValid(
|
|
130
|
-
onRawEvent: ({ event }) => dispatchIfValid(
|
|
131
|
-
|
|
129
|
+
dispatchIfValid(event, "MESSAGES_SNAPSHOT"),
|
|
130
|
+
onCustomEvent: ({ event }) => dispatchIfValid(event, "CUSTOM"),
|
|
131
|
+
onRawEvent: ({ event }) => dispatchIfValid(event, "RAW"),
|
|
132
|
+
onRunFinishedEvent: ({ event }) => {
|
|
133
|
+
const parsed = ensureEvent(event, "RUN_FINISHED", logger);
|
|
134
|
+
if (!parsed) return;
|
|
135
|
+
runFinishedDispatched = true;
|
|
136
|
+
dispatch(parsed);
|
|
137
|
+
},
|
|
138
|
+
onRunFinalized: () => {
|
|
139
|
+
if (runFinishedDispatched) return;
|
|
140
|
+
dispatch({ type: "RUN_FINISHED", runId });
|
|
141
|
+
},
|
|
132
142
|
onRunFailed: ({ error }) => {
|
|
133
143
|
onRunFailed?.(error);
|
|
134
144
|
const message =
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type { AgUiEvent } from "./types";
|
|
1
|
+
import type { AgUiEvent, AgUiInterrupt, AgUiRunFinishedOutcome } from "./types";
|
|
2
|
+
import type { Logger } from "./logger";
|
|
3
|
+
|
|
4
|
+
export type ParseAgUiEventOptions = {
|
|
5
|
+
logger?: Logger;
|
|
6
|
+
};
|
|
2
7
|
|
|
3
8
|
const isString = (value: unknown): value is string => typeof value === "string";
|
|
4
9
|
const isNonEmptyString = (value: unknown): value is string =>
|
|
@@ -16,7 +21,57 @@ const withOptional = <T extends object>(
|
|
|
16
21
|
: ({ ...base, ...Object.fromEntries(definedEntries) } as T);
|
|
17
22
|
};
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
|
25
|
+
!!value && typeof value === "object" && !Array.isArray(value);
|
|
26
|
+
|
|
27
|
+
const parseInterrupt = (raw: unknown): AgUiInterrupt | null => {
|
|
28
|
+
if (!isPlainObject(raw)) return null;
|
|
29
|
+
const id = raw.id;
|
|
30
|
+
const reason = raw.reason;
|
|
31
|
+
if (typeof id !== "string" || typeof reason !== "string") return null;
|
|
32
|
+
const interrupt: AgUiInterrupt = { id, reason };
|
|
33
|
+
if (typeof raw.message === "string") interrupt.message = raw.message;
|
|
34
|
+
if (typeof raw.toolCallId === "string") interrupt.toolCallId = raw.toolCallId;
|
|
35
|
+
if (typeof raw.expiresAt === "string") interrupt.expiresAt = raw.expiresAt;
|
|
36
|
+
if (isPlainObject(raw.responseSchema))
|
|
37
|
+
interrupt.responseSchema = raw.responseSchema;
|
|
38
|
+
if (isPlainObject(raw.metadata)) interrupt.metadata = raw.metadata;
|
|
39
|
+
return interrupt;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const parseRunFinishedOutcome = (
|
|
43
|
+
raw: unknown,
|
|
44
|
+
logger: Logger | undefined,
|
|
45
|
+
): AgUiRunFinishedOutcome | undefined => {
|
|
46
|
+
if (!isPlainObject(raw)) return undefined;
|
|
47
|
+
if (raw.type === "success") return { type: "success" };
|
|
48
|
+
if (raw.type === "interrupt") {
|
|
49
|
+
if (!Array.isArray(raw.interrupts)) {
|
|
50
|
+
logger?.debug?.(
|
|
51
|
+
"[agui] RUN_FINISHED interrupt outcome missing interrupts array",
|
|
52
|
+
raw,
|
|
53
|
+
);
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const parsed = raw.interrupts
|
|
57
|
+
.map((entry) => parseInterrupt(entry))
|
|
58
|
+
.filter((entry): entry is AgUiInterrupt => entry !== null);
|
|
59
|
+
if (parsed.length === 0) {
|
|
60
|
+
logger?.debug?.(
|
|
61
|
+
"[agui] RUN_FINISHED interrupt outcome has no valid interrupts",
|
|
62
|
+
raw.interrupts,
|
|
63
|
+
);
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return { type: "interrupt", interrupts: parsed };
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const parseAgUiEvent = (
|
|
72
|
+
event: unknown,
|
|
73
|
+
options?: ParseAgUiEventOptions,
|
|
74
|
+
): AgUiEvent | null => {
|
|
20
75
|
if (!event || typeof event !== "object") return null;
|
|
21
76
|
const payload = event as Record<string, unknown>;
|
|
22
77
|
const typeValue = payload.type;
|
|
@@ -32,7 +87,13 @@ export const parseAgUiEvent = (event: unknown): AgUiEvent | null => {
|
|
|
32
87
|
}
|
|
33
88
|
case "RUN_FINISHED": {
|
|
34
89
|
const runId = getString("runId");
|
|
35
|
-
|
|
90
|
+
if (!runId) return null;
|
|
91
|
+
return withOptional(
|
|
92
|
+
{ type: "RUN_FINISHED" as const, runId },
|
|
93
|
+
{
|
|
94
|
+
outcome: parseRunFinishedOutcome(payload.outcome, options?.logger),
|
|
95
|
+
},
|
|
96
|
+
);
|
|
36
97
|
}
|
|
37
98
|
case "RUN_CANCELLED": {
|
|
38
99
|
const runId = getString("runId");
|
package/src/runtime/types.ts
CHANGED
|
@@ -47,9 +47,39 @@ export type UseAgUiRuntimeOptions = {
|
|
|
47
47
|
adapters?: UseAgUiRuntimeAdapters;
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
export type AgUiInterruptReason =
|
|
51
|
+
| "tool_call"
|
|
52
|
+
| "input_required"
|
|
53
|
+
| "confirmation"
|
|
54
|
+
| (string & {});
|
|
55
|
+
|
|
56
|
+
export type AgUiInterrupt = {
|
|
57
|
+
id: string;
|
|
58
|
+
reason: AgUiInterruptReason;
|
|
59
|
+
message?: string;
|
|
60
|
+
toolCallId?: string;
|
|
61
|
+
responseSchema?: Record<string, unknown>;
|
|
62
|
+
expiresAt?: string;
|
|
63
|
+
metadata?: Record<string, unknown>;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type AgUiResumeEntry = {
|
|
67
|
+
interruptId: string;
|
|
68
|
+
status: "resolved" | "cancelled";
|
|
69
|
+
payload?: unknown;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type AgUiRunFinishedOutcome =
|
|
73
|
+
| { type: "success" }
|
|
74
|
+
| { type: "interrupt"; interrupts: AgUiInterrupt[] };
|
|
75
|
+
|
|
50
76
|
export type AgUiEvent =
|
|
51
77
|
| { type: "RUN_STARTED"; runId: string }
|
|
52
|
-
| {
|
|
78
|
+
| {
|
|
79
|
+
type: "RUN_FINISHED";
|
|
80
|
+
runId: string;
|
|
81
|
+
outcome?: AgUiRunFinishedOutcome;
|
|
82
|
+
}
|
|
53
83
|
| { type: "RUN_CANCELLED"; runId?: string }
|
|
54
84
|
| { type: "RUN_ERROR"; message?: string; code?: string }
|
|
55
85
|
| { type: "TEXT_MESSAGE_START"; messageId?: string }
|