@threadplane/ag-ui 0.0.56 → 0.0.57
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { signal, computed, InjectionToken, inject } from '@angular/core';
|
|
2
2
|
import { Subject, Observable } from 'rxjs';
|
|
3
|
-
import { toAgentError, isAbortError } from '@threadplane/chat';
|
|
3
|
+
import { completeDelivery, staticDelivery, streamingDelivery, toAgentError, selectPendingClientToolCalls, isAbortError } from '@threadplane/chat';
|
|
4
4
|
import { HttpAgent, AbstractAgent, EventType } from '@ag-ui/client';
|
|
5
5
|
|
|
6
6
|
// SPDX-License-Identifier: MIT
|
|
@@ -263,12 +263,19 @@ function normalizeCitation(entry, fallbackIndex) {
|
|
|
263
263
|
}
|
|
264
264
|
return undefined;
|
|
265
265
|
};
|
|
266
|
+
const publishedAt = e['publishedAt'];
|
|
267
|
+
const normalizedPublishedAt = typeof publishedAt === 'string' || typeof publishedAt === 'number' || publishedAt instanceof Date
|
|
268
|
+
? publishedAt
|
|
269
|
+
: undefined;
|
|
266
270
|
return {
|
|
267
271
|
id: str('id') ?? str('refId') ?? `c${fallbackIndex}`,
|
|
268
272
|
index: typeof e['index'] === 'number' ? e['index'] : fallbackIndex,
|
|
269
273
|
title: firstStr('title', 'name'),
|
|
270
274
|
url: firstStr('url', 'href', 'source'),
|
|
271
275
|
snippet: firstStr('snippet', 'content', 'excerpt'),
|
|
276
|
+
sourceType: str('sourceType'),
|
|
277
|
+
iconUrl: str('iconUrl'),
|
|
278
|
+
publishedAt: normalizedPublishedAt,
|
|
272
279
|
extra: typeof e['extra'] === 'object' && e['extra'] !== null
|
|
273
280
|
? e['extra']
|
|
274
281
|
: undefined,
|
|
@@ -280,6 +287,16 @@ function normalizeCitation(entry, fallbackIndex) {
|
|
|
280
287
|
// Discriminator strings (e.g. 'RUN_STARTED') match EventType enum members
|
|
281
288
|
// verbatim; the switch cases below use the string literals directly so this
|
|
282
289
|
// file has no runtime dependency on the EventType enum import.
|
|
290
|
+
/** Finalize one run without touching messages owned by another generation. */
|
|
291
|
+
function finalizeDeliveryRun(store, run, outcome) {
|
|
292
|
+
if (run.outcome !== undefined)
|
|
293
|
+
return false;
|
|
294
|
+
run.outcome = outcome;
|
|
295
|
+
store.messages.update(messages => messages.map(message => message.delivery.generation === run.generation
|
|
296
|
+
? { ...message, delivery: completeDelivery(run.generation, outcome) }
|
|
297
|
+
: message));
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
283
300
|
/**
|
|
284
301
|
* Per-message reasoning timing. Populated by REASONING_MESSAGE_START /
|
|
285
302
|
* REASONING_MESSAGE_END handlers. The map lives on the module — same
|
|
@@ -306,6 +323,9 @@ function resolveReasoningDurationMs(messageId) {
|
|
|
306
323
|
function reduceEvent(event, store) {
|
|
307
324
|
switch (event.type) {
|
|
308
325
|
case 'RUN_STARTED': {
|
|
326
|
+
const run = store.deliveryRun;
|
|
327
|
+
if (!run || run.outcome !== undefined || !bindRunId(event, run))
|
|
328
|
+
return;
|
|
309
329
|
store.status.set('running');
|
|
310
330
|
store.isLoading.set(true);
|
|
311
331
|
store.error.set(undefined);
|
|
@@ -315,11 +335,17 @@ function reduceEvent(event, store) {
|
|
|
315
335
|
return;
|
|
316
336
|
}
|
|
317
337
|
case 'RUN_FINISHED': {
|
|
338
|
+
const run = currentRunForEvent(event, store);
|
|
339
|
+
if (!run || !finalizeDeliveryRun(store, run, 'success'))
|
|
340
|
+
return;
|
|
318
341
|
store.status.set('idle');
|
|
319
342
|
store.isLoading.set(false);
|
|
320
343
|
return;
|
|
321
344
|
}
|
|
322
345
|
case 'RUN_ERROR': {
|
|
346
|
+
const run = currentRunForEvent(event, store);
|
|
347
|
+
if (!run || !finalizeDeliveryRun(store, run, 'error'))
|
|
348
|
+
return;
|
|
323
349
|
store.status.set('error');
|
|
324
350
|
store.isLoading.set(false);
|
|
325
351
|
const runErrorMsg = event.message;
|
|
@@ -328,28 +354,37 @@ function reduceEvent(event, store) {
|
|
|
328
354
|
}
|
|
329
355
|
case 'TEXT_MESSAGE_START': {
|
|
330
356
|
const id = messageIdFrom(event);
|
|
357
|
+
const delivery = ownAssistantMessage(store, id);
|
|
358
|
+
if (!delivery)
|
|
359
|
+
return;
|
|
331
360
|
store.messages.update((prev) => prev.some((m) => m.id === id)
|
|
332
|
-
? prev.map((m) => m.id === id ? { ...m, content: m.content ?? '' } : m)
|
|
333
|
-
: [...prev, { id, role: 'assistant', content: '' }]);
|
|
361
|
+
? prev.map((m) => m.id === id ? { ...m, content: m.content ?? '', delivery } : m)
|
|
362
|
+
: [...prev, { id, role: 'assistant', content: '', delivery }]);
|
|
334
363
|
return;
|
|
335
364
|
}
|
|
336
365
|
case 'REASONING_MESSAGE_START': {
|
|
337
366
|
const id = messageIdFrom(event);
|
|
367
|
+
const delivery = ownAssistantMessage(store, id);
|
|
368
|
+
if (!delivery)
|
|
369
|
+
return;
|
|
338
370
|
reasoningTimingMap.set(id, { startedAt: Date.now() });
|
|
339
371
|
// Initialize an assistant slot with empty reasoning if it doesn't already exist.
|
|
340
372
|
store.messages.update((prev) => prev.some((m) => m.id === id)
|
|
341
373
|
? prev.map((m) => m.id === id
|
|
342
|
-
? { ...m, reasoning: m.reasoning ?? '' }
|
|
374
|
+
? { ...m, reasoning: m.reasoning ?? '', delivery }
|
|
343
375
|
: m)
|
|
344
|
-
: [...prev, { id, role: 'assistant', content: '', reasoning: '' }]);
|
|
376
|
+
: [...prev, { id, role: 'assistant', content: '', reasoning: '', delivery }]);
|
|
345
377
|
return;
|
|
346
378
|
}
|
|
347
379
|
case 'REASONING_MESSAGE_CONTENT':
|
|
348
380
|
case 'REASONING_MESSAGE_CHUNK': {
|
|
349
381
|
const id = messageIdFrom(event);
|
|
382
|
+
const delivery = ownAssistantMessage(store, id);
|
|
383
|
+
if (!delivery)
|
|
384
|
+
return;
|
|
350
385
|
const delta = event.delta ?? '';
|
|
351
386
|
store.messages.update((prev) => prev.map((m) => m.id === id
|
|
352
|
-
? { ...m, reasoning: (m.reasoning ?? '') + delta }
|
|
387
|
+
? { ...m, reasoning: (m.reasoning ?? '') + delta, delivery }
|
|
353
388
|
: m));
|
|
354
389
|
return;
|
|
355
390
|
}
|
|
@@ -368,8 +403,11 @@ function reduceEvent(event, store) {
|
|
|
368
403
|
}
|
|
369
404
|
case 'TEXT_MESSAGE_CONTENT': {
|
|
370
405
|
const id = messageIdFrom(event);
|
|
406
|
+
const delivery = ownAssistantMessage(store, id);
|
|
407
|
+
if (!delivery)
|
|
408
|
+
return;
|
|
371
409
|
const delta = event.delta ?? '';
|
|
372
|
-
store.messages.update((prev) => prev.map((m) => m.id === id ? { ...m, content: m.content + delta } : m));
|
|
410
|
+
store.messages.update((prev) => prev.map((m) => m.id === id ? { ...m, content: m.content + delta, delivery } : m));
|
|
373
411
|
return;
|
|
374
412
|
}
|
|
375
413
|
case 'TEXT_MESSAGE_END': {
|
|
@@ -390,14 +428,17 @@ function reduceEvent(event, store) {
|
|
|
390
428
|
// tool-call-only turn emits no TEXT_MESSAGE_START), create a slot.
|
|
391
429
|
const parentId = e.parentMessageId;
|
|
392
430
|
if (parentId) {
|
|
431
|
+
const delivery = ownAssistantMessage(store, parentId);
|
|
432
|
+
if (!delivery)
|
|
433
|
+
return;
|
|
393
434
|
store.messages.update((prev) => {
|
|
394
435
|
const existing = prev.find((m) => m.id === parentId);
|
|
395
436
|
if (existing) {
|
|
396
437
|
return prev.map((m) => m.id === parentId
|
|
397
|
-
? { ...m, toolCallIds: [...(m.toolCallIds ?? []), e.toolCallId] }
|
|
438
|
+
? { ...m, toolCallIds: [...(m.toolCallIds ?? []), e.toolCallId], delivery }
|
|
398
439
|
: m);
|
|
399
440
|
}
|
|
400
|
-
return [...prev, { id: parentId, role: 'assistant', content: '', toolCallIds: [e.toolCallId] }];
|
|
441
|
+
return [...prev, { id: parentId, role: 'assistant', content: '', toolCallIds: [e.toolCallId], delivery }];
|
|
401
442
|
});
|
|
402
443
|
}
|
|
403
444
|
return;
|
|
@@ -456,6 +497,13 @@ function reduceEvent(event, store) {
|
|
|
456
497
|
case 'MESSAGES_SNAPSHOT': {
|
|
457
498
|
const e = event;
|
|
458
499
|
const raw = e.messages ?? [];
|
|
500
|
+
const run = store.deliveryRun?.outcome === undefined ? store.deliveryRun : null;
|
|
501
|
+
const canonicalAssistantId = resolveCanonicalAssistantId(raw, run);
|
|
502
|
+
const activeCanonicalAssistantId = canonicalAssistantId
|
|
503
|
+
&& ownAssistantMessage(store, canonicalAssistantId)
|
|
504
|
+
? canonicalAssistantId
|
|
505
|
+
: undefined;
|
|
506
|
+
const previousById = new Map(store.messages().map(message => [message.id, message]));
|
|
459
507
|
// AG-UI AssistantMessage carries `toolCalls` (ToolCall objects) on the
|
|
460
508
|
// snapshot wire. Bridge them to `toolCallIds` so that the chat lib's
|
|
461
509
|
// per-message tool-call resolution (resolveMessageToolCalls) can scope
|
|
@@ -463,22 +511,64 @@ function reduceEvent(event, store) {
|
|
|
463
511
|
// so the data is visible to <chat-tool-views>.
|
|
464
512
|
const snapshotToolCalls = [];
|
|
465
513
|
const messages = raw.map((m) => {
|
|
514
|
+
const previous = previousById.get(m.id);
|
|
515
|
+
const completedMessage = m.id !== activeCanonicalAssistantId
|
|
516
|
+
&& previous?.delivery.phase === 'complete'
|
|
517
|
+
&& (!run
|
|
518
|
+
|| run.ownedMessageIds.has(m.id)
|
|
519
|
+
|| run.snapshotReplacementIds.has(m.id))
|
|
520
|
+
? previous
|
|
521
|
+
: undefined;
|
|
522
|
+
let delivery = previous?.delivery ?? staticDelivery(m.id);
|
|
523
|
+
let snapshotMessage;
|
|
466
524
|
if (m.role !== 'assistant' || !m.toolCalls || m.toolCalls.length === 0) {
|
|
467
|
-
|
|
525
|
+
snapshotMessage = m;
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
const ids = [];
|
|
529
|
+
for (const tc of m.toolCalls) {
|
|
530
|
+
ids.push(tc.id);
|
|
531
|
+
snapshotToolCalls.push({
|
|
532
|
+
id: tc.id,
|
|
533
|
+
name: tc.function.name,
|
|
534
|
+
args: safeParseArgs(tc.function.arguments),
|
|
535
|
+
status: 'complete',
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
const { toolCalls: _dropped, ...rest } = m;
|
|
539
|
+
snapshotMessage = { ...rest, toolCallIds: ids };
|
|
468
540
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
541
|
+
if (completedMessage) {
|
|
542
|
+
const snapshotChanged = completedMessage.content !== snapshotMessage.content
|
|
543
|
+
|| !sameStringArray(completedMessage.toolCallIds, snapshotMessage.toolCallIds);
|
|
544
|
+
if (!snapshotChanged)
|
|
545
|
+
return completedMessage;
|
|
546
|
+
run?.snapshotReplacementIds.add(m.id);
|
|
547
|
+
return {
|
|
548
|
+
...completedMessage,
|
|
549
|
+
...snapshotMessage,
|
|
550
|
+
delivery: completeDelivery(store.allocateDeliveryGeneration(`snapshot:${m.id}`), 'success'),
|
|
551
|
+
};
|
|
478
552
|
}
|
|
479
|
-
|
|
480
|
-
|
|
553
|
+
if (run && (run.ownedMessageIds.has(m.id) || m.id === canonicalAssistantId)) {
|
|
554
|
+
delivery = delivery.generation === run.generation && delivery.phase === 'complete'
|
|
555
|
+
? delivery
|
|
556
|
+
: streamingDelivery(run.generation);
|
|
557
|
+
run.ownedMessageIds.add(m.id);
|
|
558
|
+
if (m.id === activeCanonicalAssistantId)
|
|
559
|
+
run.currentAssistantMessageId = m.id;
|
|
560
|
+
}
|
|
561
|
+
return { ...snapshotMessage, delivery };
|
|
481
562
|
});
|
|
563
|
+
const currentAssistant = run?.currentAssistantMessageId
|
|
564
|
+
? previousById.get(run.currentAssistantMessageId)
|
|
565
|
+
: undefined;
|
|
566
|
+
if (run
|
|
567
|
+
&& currentAssistant?.delivery.generation === run.generation
|
|
568
|
+
&& currentAssistant.delivery.phase === 'streaming'
|
|
569
|
+
&& !messages.some(message => message.id === currentAssistant.id)) {
|
|
570
|
+
messages.push(currentAssistant);
|
|
571
|
+
}
|
|
482
572
|
// Re-apply per-message citations from the already-received STATE. A
|
|
483
573
|
// MESSAGES_SNAPSHOT replaces the streamed messages wholesale — and the
|
|
484
574
|
// final snapshot message id (str(AIMessage.id), e.g. "resp-…") differs
|
|
@@ -504,7 +594,14 @@ function reduceEvent(event, store) {
|
|
|
504
594
|
// (e.g. ChatApprovalCardComponent) receive a plain object, not a string.
|
|
505
595
|
const parsedValue = typeof e.value === 'string' ? safeParseJson(e.value) : e.value;
|
|
506
596
|
if (e.name === 'on_interrupt') {
|
|
597
|
+
const run = currentRunForEvent(event, store);
|
|
598
|
+
if (store.deliveryRun && !run)
|
|
599
|
+
return;
|
|
507
600
|
store.interrupt.set({ id: randomId$1(), value: parsedValue, resumable: true });
|
|
601
|
+
if (run && finalizeDeliveryRun(store, run, 'paused')) {
|
|
602
|
+
store.status.set('idle');
|
|
603
|
+
store.isLoading.set(false);
|
|
604
|
+
}
|
|
508
605
|
return;
|
|
509
606
|
}
|
|
510
607
|
// Surface every other custom event on the customEvents signal so the
|
|
@@ -523,13 +620,17 @@ function reduceEvent(event, store) {
|
|
|
523
620
|
const e = event;
|
|
524
621
|
const map = new Map(store.activities());
|
|
525
622
|
const existing = map.get(e.messageId);
|
|
526
|
-
if (existing && existing.activityType === e.activityType
|
|
527
|
-
|
|
623
|
+
if (existing && existing.activityType === e.activityType) {
|
|
624
|
+
if (e.replace)
|
|
625
|
+
existing.content.set(e.content ?? {});
|
|
626
|
+
else
|
|
627
|
+
existing.content.update((c) => ({ ...c, ...e.content }));
|
|
528
628
|
}
|
|
529
629
|
else {
|
|
530
630
|
map.set(e.messageId, {
|
|
531
631
|
messageId: e.messageId,
|
|
532
632
|
activityType: e.activityType,
|
|
633
|
+
generation: store.allocateDeliveryGeneration(`activity:${e.messageId}`),
|
|
533
634
|
content: signal(e.content ?? {}),
|
|
534
635
|
});
|
|
535
636
|
}
|
|
@@ -565,6 +666,68 @@ function reduceEvent(event, store) {
|
|
|
565
666
|
function randomId$1() {
|
|
566
667
|
return Math.random().toString(36).slice(2);
|
|
567
668
|
}
|
|
669
|
+
function eventRunId(event) {
|
|
670
|
+
const runId = event.runId;
|
|
671
|
+
return typeof runId === 'string' ? runId : undefined;
|
|
672
|
+
}
|
|
673
|
+
function bindRunId(event, run) {
|
|
674
|
+
const runId = eventRunId(event);
|
|
675
|
+
if (!runId)
|
|
676
|
+
return true;
|
|
677
|
+
if (run.protocolRunId && run.protocolRunId !== runId)
|
|
678
|
+
return false;
|
|
679
|
+
run.protocolRunId = runId;
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
function currentRunForEvent(event, store) {
|
|
683
|
+
const run = store.deliveryRun;
|
|
684
|
+
if (!run || !bindRunId(event, run))
|
|
685
|
+
return null;
|
|
686
|
+
return run;
|
|
687
|
+
}
|
|
688
|
+
function ownAssistantMessage(store, id) {
|
|
689
|
+
const run = store.deliveryRun;
|
|
690
|
+
if (!run || run.outcome !== undefined)
|
|
691
|
+
return undefined;
|
|
692
|
+
const currentId = run.currentAssistantMessageId;
|
|
693
|
+
if (currentId && currentId !== id) {
|
|
694
|
+
if (run.ownedMessageIds.has(id))
|
|
695
|
+
return undefined;
|
|
696
|
+
store.messages.update(messages => messages.map(message => message.id === currentId
|
|
697
|
+
&& message.delivery.generation === run.generation
|
|
698
|
+
&& message.delivery.phase === 'streaming'
|
|
699
|
+
? { ...message, delivery: completeDelivery(run.generation, 'success') }
|
|
700
|
+
: message));
|
|
701
|
+
}
|
|
702
|
+
run.ownedMessageIds.add(id);
|
|
703
|
+
run.currentAssistantMessageId = id;
|
|
704
|
+
return streamingDelivery(run.generation);
|
|
705
|
+
}
|
|
706
|
+
function sameStringArray(left, right) {
|
|
707
|
+
if (left === right)
|
|
708
|
+
return true;
|
|
709
|
+
if (!left || !right || left.length !== right.length)
|
|
710
|
+
return false;
|
|
711
|
+
return left.every((value, index) => value === right[index]);
|
|
712
|
+
}
|
|
713
|
+
function resolveCanonicalAssistantId(messages, run) {
|
|
714
|
+
if (!run)
|
|
715
|
+
return undefined;
|
|
716
|
+
if (run.currentAssistantMessageId
|
|
717
|
+
&& messages.some(message => message.id === run.currentAssistantMessageId)) {
|
|
718
|
+
return run.currentAssistantMessageId;
|
|
719
|
+
}
|
|
720
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
721
|
+
const message = messages[index];
|
|
722
|
+
if (message.role === 'assistant' && !run.baselineMessageIds.has(message.id)) {
|
|
723
|
+
return message.id;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return run.eligibleBaselineAssistantId
|
|
727
|
+
&& messages.some(message => message.role === 'assistant' && message.id === run.eligibleBaselineAssistantId)
|
|
728
|
+
? run.eligibleBaselineAssistantId
|
|
729
|
+
: undefined;
|
|
730
|
+
}
|
|
568
731
|
function messageIdFrom(event) {
|
|
569
732
|
return event.messageId ?? 'unknown';
|
|
570
733
|
}
|
|
@@ -624,21 +787,59 @@ function safeStringify(v) {
|
|
|
624
787
|
* have no backend result, and haven't been resolved client-side yet — but ONLY
|
|
625
788
|
* when the run is not in progress (isLoading===false). The backend ends the run
|
|
626
789
|
* without emitting TOOL_CALL_RESULT for client tools, so result stays undefined.
|
|
627
|
-
* -
|
|
790
|
+
* - settle(id, result): marks the call as resolved, writes the outcome onto
|
|
628
791
|
* the local ToolCall in the store (so the transcript freezes: the mounted
|
|
629
792
|
* ask component re-renders with its emitted value as props and can branch to
|
|
630
|
-
* a frozen state), adds a ToolMessage via source.addMessage
|
|
631
|
-
*
|
|
793
|
+
* a frozen state), and adds a ToolMessage via source.addMessage without
|
|
794
|
+
* starting a run.
|
|
795
|
+
* - flush(): no-op. settle() already made the result durable by adding it to
|
|
796
|
+
* the source's message list, so there is nothing left to write.
|
|
797
|
+
* - resolve(id, result): settles the result, then requests a continuation
|
|
798
|
+
* through the adapter-owned run gateway. Any ToolMessages previously
|
|
799
|
+
* settled into the source are flushed by that single run.
|
|
632
800
|
*
|
|
633
801
|
* Call catalogAsAgUiTools() to get the current catalog as AG-UI Tool[] for
|
|
634
|
-
*
|
|
802
|
+
* attaching to each adapter-owned run.
|
|
635
803
|
*/
|
|
636
|
-
function createClientToolsCapability(source, store) {
|
|
804
|
+
function createClientToolsCapability(source, store, continueRun) {
|
|
637
805
|
const catalog = signal([], ...(ngDevMode ? [{ debugName: "catalog" }] : []));
|
|
638
806
|
const resolvedIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "resolvedIds" }] : []));
|
|
639
807
|
function catalogAsAgUiTools() {
|
|
640
808
|
return catalog().map(toAgUiTool);
|
|
641
809
|
}
|
|
810
|
+
function settleResult(id, result) {
|
|
811
|
+
// Mark as resolved first so pending() drops it immediately.
|
|
812
|
+
resolvedIds.update((s) => new Set(s).add(id));
|
|
813
|
+
// Write the outcome onto the LOCAL ToolCall in the store. The client tool
|
|
814
|
+
// DID produce a result client-side, so this is semantically correct — and
|
|
815
|
+
// it freezes the transcript card: toToolViewSpec spreads `{...args,
|
|
816
|
+
// ...result, status}` into the mounted ask component, so the component
|
|
817
|
+
// re-renders with its own emitted value as props and can branch to a
|
|
818
|
+
// resolved/frozen state. The backend ToolMessage never reaches this local
|
|
819
|
+
// ToolCall, so without this write the card stays interactive forever.
|
|
820
|
+
const ok = result.ok;
|
|
821
|
+
const value = result.value;
|
|
822
|
+
const error = result.error;
|
|
823
|
+
store.toolCalls.update((calls) => calls.map((tc) => tc.id === id
|
|
824
|
+
? {
|
|
825
|
+
...tc,
|
|
826
|
+
result: ok ? value : { error },
|
|
827
|
+
...(ok ? {} : { error, status: 'error' }),
|
|
828
|
+
}
|
|
829
|
+
: tc));
|
|
830
|
+
// Cast rather than rely on discriminant narrowing: consumer apps that
|
|
831
|
+
// compile this source with `strictNullChecks: false` don't narrow the
|
|
832
|
+
// ClientToolResult union in a ternary.
|
|
833
|
+
const content = ok
|
|
834
|
+
? safeStringify(value)
|
|
835
|
+
: `Error: ${error}`;
|
|
836
|
+
source.addMessage({
|
|
837
|
+
id: `tool-${id}`,
|
|
838
|
+
role: 'tool',
|
|
839
|
+
toolCallId: id,
|
|
840
|
+
content,
|
|
841
|
+
});
|
|
842
|
+
}
|
|
642
843
|
const clientTools = {
|
|
643
844
|
setCatalog(specs) {
|
|
644
845
|
catalog.set([...specs]);
|
|
@@ -646,45 +847,25 @@ function createClientToolsCapability(source, store) {
|
|
|
646
847
|
pending: computed(() => {
|
|
647
848
|
// Client tools are only actionable after the run ends (backend signals it
|
|
648
849
|
// by ending the run WITHOUT emitting TOOL_CALL_RESULT for client tools).
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
850
|
+
return selectPendingClientToolCalls({
|
|
851
|
+
isLoading: store.isLoading(),
|
|
852
|
+
toolCalls: store.toolCalls(),
|
|
853
|
+
catalogNames: new Set(catalog().map((s) => s.name)),
|
|
854
|
+
resolvedIds: resolvedIds(),
|
|
855
|
+
});
|
|
654
856
|
}),
|
|
857
|
+
settle(id, result) {
|
|
858
|
+
settleResult(id, result);
|
|
859
|
+
},
|
|
860
|
+
// AG-UI's settle() already calls source.addMessage(), which places the
|
|
861
|
+
// ToolMessage in the outgoing message list. Nothing further is needed to
|
|
862
|
+
// make it durable — the next run carries it.
|
|
863
|
+
flush() {
|
|
864
|
+
/* no-op: settle() is already durable */
|
|
865
|
+
},
|
|
655
866
|
resolve(id, result) {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
// Write the outcome onto the LOCAL ToolCall in the store. The client tool
|
|
659
|
-
// DID produce a result client-side, so this is semantically correct — and
|
|
660
|
-
// it freezes the transcript card: toToolViewSpec spreads `{...args,
|
|
661
|
-
// ...result, status}` into the mounted ask component, so the component
|
|
662
|
-
// re-renders with its own emitted value as props and can branch to a
|
|
663
|
-
// resolved/frozen state. The backend ToolMessage never reaches this local
|
|
664
|
-
// ToolCall, so without this write the card stays interactive forever.
|
|
665
|
-
const ok = result.ok;
|
|
666
|
-
const value = result.value;
|
|
667
|
-
const error = result.error;
|
|
668
|
-
store.toolCalls.update((calls) => calls.map((tc) => tc.id === id
|
|
669
|
-
? {
|
|
670
|
-
...tc,
|
|
671
|
-
result: ok ? value : { error },
|
|
672
|
-
...(ok ? {} : { error, status: 'error' }),
|
|
673
|
-
}
|
|
674
|
-
: tc));
|
|
675
|
-
// Cast rather than rely on discriminant narrowing: consumer apps that
|
|
676
|
-
// compile this source with `strictNullChecks: false` don't narrow the
|
|
677
|
-
// ClientToolResult union in a ternary.
|
|
678
|
-
const content = ok
|
|
679
|
-
? safeStringify(value)
|
|
680
|
-
: `Error: ${error}`;
|
|
681
|
-
source.addMessage({
|
|
682
|
-
id: `tool-${id}`,
|
|
683
|
-
role: 'tool',
|
|
684
|
-
toolCallId: id,
|
|
685
|
-
content,
|
|
686
|
-
});
|
|
687
|
-
void source.runAgent({ tools: catalogAsAgUiTools() });
|
|
867
|
+
settleResult(id, result);
|
|
868
|
+
void continueRun();
|
|
688
869
|
},
|
|
689
870
|
catalogAsAgUiTools,
|
|
690
871
|
};
|
|
@@ -737,6 +918,8 @@ function agentRuntimeTelemetryErrorClass(error) {
|
|
|
737
918
|
* ```
|
|
738
919
|
*/
|
|
739
920
|
function toAgent(source, options = {}) {
|
|
921
|
+
let generationSequence = 0;
|
|
922
|
+
const allocateDeliveryGeneration = (scope) => `${scope}-${++generationSequence}-${Math.random().toString(36).slice(2, 10)}`;
|
|
740
923
|
const store = {
|
|
741
924
|
messages: signal([]),
|
|
742
925
|
status: signal('idle'),
|
|
@@ -748,55 +931,39 @@ function toAgent(source, options = {}) {
|
|
|
748
931
|
events$: new Subject(),
|
|
749
932
|
customEvents: signal([]),
|
|
750
933
|
activities: signal(new Map()),
|
|
934
|
+
deliveryRun: null,
|
|
935
|
+
allocateDeliveryGeneration,
|
|
751
936
|
};
|
|
752
937
|
const telemetryProperties = { transport: 'ag-ui', surface: 'to_agent' };
|
|
753
938
|
let activeRun = null;
|
|
754
|
-
|
|
755
|
-
// abort (graceful cancel) from a genuine stream failure.
|
|
756
|
-
let abortRequested = false;
|
|
757
|
-
// Set to true the first time settleIfAborted() handles an abort error for the
|
|
758
|
-
// current run. The AG-UI client can surface the same abort via both the event
|
|
759
|
-
// stream (RUN_ERROR event) AND onRunFailed — abortSettled lets the second
|
|
760
|
-
// delivery see through as a no-op rather than re-writing store state or
|
|
761
|
-
// triggering a real error path. Both flags are reset together at the top of
|
|
762
|
-
// submit() so the next run starts clean.
|
|
763
|
-
let abortSettled = false;
|
|
939
|
+
const runsByProtocolId = new Map();
|
|
764
940
|
// Tracks the last AgentSubmitInput so retry() can re-run it without
|
|
765
941
|
// duplicating the user message. Set at the top of submit()'s message path.
|
|
766
942
|
let lastInput;
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
if (run) {
|
|
789
|
-
finishRunTelemetry(run);
|
|
790
|
-
// Mark errored so any subsequent finishRunTelemetry/failRunTelemetry
|
|
791
|
-
// call on the same run object (e.g. from submit's try block resolving
|
|
792
|
-
// after the abort) is a no-op — telemetry fires at most once per run.
|
|
793
|
-
run.errored = true;
|
|
943
|
+
function resolveCallbackRun(protocolRunId) {
|
|
944
|
+
if (!protocolRunId)
|
|
945
|
+
return activeRun;
|
|
946
|
+
const known = runsByProtocolId.get(protocolRunId);
|
|
947
|
+
if (known)
|
|
948
|
+
return known;
|
|
949
|
+
if (!activeRun || activeRun.protocolRunId)
|
|
950
|
+
return null;
|
|
951
|
+
activeRun.protocolRunId = protocolRunId;
|
|
952
|
+
runsByProtocolId.set(protocolRunId, activeRun);
|
|
953
|
+
while (runsByProtocolId.size > 16) {
|
|
954
|
+
const oldestId = runsByProtocolId.keys().next().value;
|
|
955
|
+
if (!oldestId)
|
|
956
|
+
break;
|
|
957
|
+
if (runsByProtocolId.get(oldestId) === activeRun) {
|
|
958
|
+
const current = runsByProtocolId.get(oldestId);
|
|
959
|
+
runsByProtocolId.delete(oldestId);
|
|
960
|
+
runsByProtocolId.set(oldestId, current);
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
runsByProtocolId.delete(oldestId);
|
|
794
964
|
}
|
|
795
|
-
return
|
|
965
|
+
return activeRun;
|
|
796
966
|
}
|
|
797
|
-
// Build the client-tools capability. catalogAsAgUiTools() is used below to
|
|
798
|
-
// thread the catalog into every runAgent() call.
|
|
799
|
-
const clientToolsCap = createClientToolsCapability(source, store);
|
|
800
967
|
/** Forward a neutral-contract state patch onto the AG-UI run input.
|
|
801
968
|
* Mirrors the canonical demo's `input.state` mechanism: the patch is
|
|
802
969
|
* merged into the source agent's client state (carried on
|
|
@@ -809,9 +976,27 @@ function toAgent(source, options = {}) {
|
|
|
809
976
|
store.state.update((prev) => ({ ...prev, ...patch }));
|
|
810
977
|
};
|
|
811
978
|
captureAgentRuntimeTelemetry(options.telemetry, 'tplane:runtime_instance_created', telemetryProperties);
|
|
812
|
-
function
|
|
813
|
-
|
|
979
|
+
function beginRun(requestType, allowBaselineTail = false) {
|
|
980
|
+
if (activeRun && activeRun.outcome === undefined) {
|
|
981
|
+
const supersededRun = activeRun;
|
|
982
|
+
finalizeDeliveryRun(store, supersededRun, 'interrupted');
|
|
983
|
+
const interruption = new Error('Run superseded by a newer request');
|
|
984
|
+
interruption.name = 'InterruptedError';
|
|
985
|
+
failRunTelemetry(interruption, supersededRun);
|
|
986
|
+
}
|
|
987
|
+
const run = {
|
|
988
|
+
generation: allocateDeliveryGeneration('run'),
|
|
989
|
+
baselineMessageIds: new Set(store.messages().map(message => message.id)),
|
|
990
|
+
ownedMessageIds: new Set(),
|
|
991
|
+
snapshotReplacementIds: new Set(),
|
|
992
|
+
eligibleBaselineAssistantId: allowBaselineTail
|
|
993
|
+
? getTailAssistantMessageId(store.messages())
|
|
994
|
+
: undefined,
|
|
995
|
+
startedAt: Date.now(),
|
|
996
|
+
telemetrySettled: false,
|
|
997
|
+
};
|
|
814
998
|
activeRun = run;
|
|
999
|
+
store.deliveryRun = run;
|
|
815
1000
|
captureAgentRuntimeTelemetry(options.telemetry, 'tplane:runtime_request_created', {
|
|
816
1001
|
...telemetryProperties,
|
|
817
1002
|
requestType,
|
|
@@ -820,76 +1005,115 @@ function toAgent(source, options = {}) {
|
|
|
820
1005
|
return run;
|
|
821
1006
|
}
|
|
822
1007
|
function finishRunTelemetry(run) {
|
|
823
|
-
if (run.
|
|
1008
|
+
if (run.telemetrySettled)
|
|
824
1009
|
return;
|
|
1010
|
+
run.telemetrySettled = true;
|
|
825
1011
|
captureAgentRuntimeTelemetry(options.telemetry, 'tplane:stream_ended', {
|
|
826
1012
|
...telemetryProperties,
|
|
827
1013
|
durationMs: Date.now() - run.startedAt,
|
|
828
1014
|
});
|
|
829
|
-
if (activeRun === run)
|
|
830
|
-
activeRun = null;
|
|
831
1015
|
}
|
|
832
1016
|
function failRunTelemetry(error, run = activeRun) {
|
|
833
|
-
if (!run || run.
|
|
1017
|
+
if (!run || run.telemetrySettled)
|
|
834
1018
|
return;
|
|
835
|
-
run.
|
|
1019
|
+
run.telemetrySettled = true;
|
|
836
1020
|
captureAgentRuntimeTelemetry(options.telemetry, 'tplane:stream_errored', {
|
|
837
1021
|
...telemetryProperties,
|
|
838
1022
|
durationMs: Date.now() - run.startedAt,
|
|
839
1023
|
errorClass: agentRuntimeTelemetryErrorClass(error),
|
|
840
1024
|
});
|
|
841
|
-
if (activeRun === run)
|
|
842
|
-
activeRun = null;
|
|
843
1025
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1026
|
+
function failRun(run, error) {
|
|
1027
|
+
if (run.outcome !== undefined)
|
|
1028
|
+
return;
|
|
1029
|
+
finalizeDeliveryRun(store, run, 'error');
|
|
1030
|
+
if (activeRun === run) {
|
|
1031
|
+
store.status.set('error');
|
|
1032
|
+
store.isLoading.set(false);
|
|
1033
|
+
store.error.set(toAgentError(error));
|
|
1034
|
+
}
|
|
1035
|
+
failRunTelemetry(error, run);
|
|
1036
|
+
}
|
|
1037
|
+
function settleTransportClose(run) {
|
|
1038
|
+
if (run.outcome === undefined) {
|
|
1039
|
+
finalizeDeliveryRun(store, run, run.ownedMessageIds.size > 0 ? 'interrupted' : 'success');
|
|
1040
|
+
if (activeRun === run) {
|
|
1041
|
+
store.status.set('idle');
|
|
1042
|
+
store.isLoading.set(false);
|
|
1043
|
+
store.error.set(undefined);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
finishRunTelemetry(run);
|
|
1047
|
+
}
|
|
1048
|
+
async function executeRun(requestType, parameters, allowBaselineTail = false) {
|
|
1049
|
+
const run = beginRun(requestType, allowBaselineTail);
|
|
851
1050
|
const tools = clientToolsCap.catalogAsAgUiTools();
|
|
1051
|
+
const runParameters = parameters === undefined && tools.length === 0
|
|
1052
|
+
? undefined
|
|
1053
|
+
: { ...parameters, ...(tools.length > 0 ? { tools } : {}) };
|
|
852
1054
|
try {
|
|
853
|
-
await source.runAgent(
|
|
854
|
-
|
|
1055
|
+
await source.runAgent(runParameters);
|
|
1056
|
+
settleTransportClose(run);
|
|
855
1057
|
}
|
|
856
1058
|
catch (err) {
|
|
857
|
-
if (
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
store.error.set(toAgentError(err));
|
|
861
|
-
failRunTelemetry(err, run);
|
|
862
|
-
}
|
|
1059
|
+
if (run.outcome === 'aborted' && isAbortError(err))
|
|
1060
|
+
return;
|
|
1061
|
+
failRun(run, err);
|
|
863
1062
|
}
|
|
864
1063
|
}
|
|
1064
|
+
const clientToolsCap = createClientToolsCapability(source, store, () => executeRun('client-tool-continuation', undefined, true));
|
|
865
1065
|
// Tap all events from the source agent via the AgentSubscriber API.
|
|
866
1066
|
// This subscription lives for the lifetime of `source`.
|
|
867
1067
|
source.subscribe({
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1068
|
+
onRunInitialized({ input }) {
|
|
1069
|
+
resolveCallbackRun(input.runId);
|
|
1070
|
+
},
|
|
1071
|
+
onEvent({ event, input }) {
|
|
1072
|
+
const callbackRunId = input?.runId ?? event.runId;
|
|
1073
|
+
const run = resolveCallbackRun(callbackRunId);
|
|
1074
|
+
if (!run) {
|
|
1075
|
+
if (!callbackRunId)
|
|
1076
|
+
reduceEvent(event, store);
|
|
1077
|
+
return;
|
|
876
1078
|
}
|
|
1079
|
+
if (run !== activeRun) {
|
|
1080
|
+
if (event.type === 'RUN_FINISHED')
|
|
1081
|
+
finalizeDeliveryRun(store, run, 'success');
|
|
1082
|
+
else if (event.type === 'RUN_ERROR')
|
|
1083
|
+
finalizeDeliveryRun(store, run, 'error');
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
if (event.type === 'RUN_ERROR' && run.outcome === 'aborted')
|
|
1087
|
+
return;
|
|
877
1088
|
reduceEvent(event, store);
|
|
1089
|
+
if (run && event.type === 'RUN_FINISHED' && run.outcome === 'success') {
|
|
1090
|
+
finishRunTelemetry(run);
|
|
1091
|
+
}
|
|
1092
|
+
else if (run && event.type === 'RUN_ERROR' && run.outcome === 'error') {
|
|
1093
|
+
failRunTelemetry(event.message ?? event, run);
|
|
1094
|
+
}
|
|
878
1095
|
},
|
|
879
|
-
onRunFailed({ error }) {
|
|
880
|
-
|
|
1096
|
+
onRunFailed({ error, input }) {
|
|
1097
|
+
const run = resolveCallbackRun(input?.runId);
|
|
1098
|
+
if (run) {
|
|
1099
|
+
if (run.outcome === 'aborted' && isAbortError(error))
|
|
1100
|
+
return;
|
|
1101
|
+
failRun(run, error);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
if (input?.runId)
|
|
881
1105
|
return;
|
|
882
1106
|
store.status.set('error');
|
|
883
1107
|
store.isLoading.set(false);
|
|
884
1108
|
store.error.set(toAgentError(error));
|
|
885
|
-
failRunTelemetry(error);
|
|
886
1109
|
},
|
|
887
1110
|
});
|
|
888
1111
|
// Stable Subagent wrappers per messageId so chat-subagents (tracks by
|
|
889
1112
|
// toolCallId) doesn't churn as activity content streams.
|
|
890
1113
|
const subagentWrappers = new Map();
|
|
891
1114
|
function subagentFor(id, entry) {
|
|
892
|
-
|
|
1115
|
+
const cached = subagentWrappers.get(id);
|
|
1116
|
+
let w = cached?.generation === entry.generation ? cached.wrapper : undefined;
|
|
893
1117
|
if (!w) {
|
|
894
1118
|
w = {
|
|
895
1119
|
toolCallId: entry.content()['toolCallId'] ?? id,
|
|
@@ -897,17 +1121,27 @@ function toAgent(source, options = {}) {
|
|
|
897
1121
|
status: computed(() => entry.content()['status'] ?? 'running'),
|
|
898
1122
|
messages: computed(() => {
|
|
899
1123
|
const c = entry.content();
|
|
1124
|
+
const status = c['status'] ?? 'running';
|
|
1125
|
+
const assistantDelivery = status === 'error'
|
|
1126
|
+
? completeDelivery(entry.generation, 'error')
|
|
1127
|
+
: status === 'complete'
|
|
1128
|
+
? completeDelivery(entry.generation, 'success')
|
|
1129
|
+
: streamingDelivery(entry.generation);
|
|
900
1130
|
const raw = c['messages'];
|
|
901
1131
|
if (Array.isArray(raw)) {
|
|
902
|
-
return raw.map((m, i) =>
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1132
|
+
return raw.map((m, i) => {
|
|
1133
|
+
const messageId = m['id'] ?? `${id}-${i}`;
|
|
1134
|
+
return {
|
|
1135
|
+
id: messageId,
|
|
1136
|
+
role: 'assistant',
|
|
1137
|
+
content: typeof m['content'] === 'string' ? m['content'] : m['content'] ?? '',
|
|
1138
|
+
delivery: m['role'] === 'assistant' ? assistantDelivery : staticDelivery(messageId),
|
|
1139
|
+
...(Array.isArray(m['toolCallIds']) ? { toolCallIds: m['toolCallIds'] } : {}),
|
|
1140
|
+
...(typeof m['reasoning'] === 'string' ? { reasoning: m['reasoning'] } : {}),
|
|
1141
|
+
};
|
|
1142
|
+
});
|
|
909
1143
|
}
|
|
910
|
-
return [{ id, role: 'assistant', content: String(c['text'] ?? '') }];
|
|
1144
|
+
return [{ id, role: 'assistant', content: String(c['text'] ?? ''), delivery: assistantDelivery }];
|
|
911
1145
|
}),
|
|
912
1146
|
toolCalls: computed(() => {
|
|
913
1147
|
const raw = entry.content()['toolCalls'];
|
|
@@ -915,7 +1149,7 @@ function toAgent(source, options = {}) {
|
|
|
915
1149
|
}),
|
|
916
1150
|
state: computed(() => entry.content()['state'] ?? {}),
|
|
917
1151
|
};
|
|
918
|
-
subagentWrappers.set(id, w);
|
|
1152
|
+
subagentWrappers.set(id, { generation: entry.generation, wrapper: w });
|
|
919
1153
|
}
|
|
920
1154
|
return w;
|
|
921
1155
|
}
|
|
@@ -946,33 +1180,13 @@ function toAgent(source, options = {}) {
|
|
|
946
1180
|
}),
|
|
947
1181
|
clientTools: clientToolsCap,
|
|
948
1182
|
submit: async (input, _opts) => {
|
|
949
|
-
// Reset both abort flags so a new submit starts clean and genuine
|
|
950
|
-
// failures after a previous stop are never swallowed.
|
|
951
|
-
abortRequested = false;
|
|
952
|
-
abortSettled = false;
|
|
953
1183
|
if (input.resume !== undefined) {
|
|
954
1184
|
// Resume path: clear the pending interrupt and replay the run with the
|
|
955
1185
|
// resume payload forwarded to the LangGraph backend via AG-UI's
|
|
956
1186
|
// forwardedProps.command.resume mechanism.
|
|
957
1187
|
applyStatePatch(input.state);
|
|
958
1188
|
store.interrupt.set(undefined);
|
|
959
|
-
|
|
960
|
-
const tools = clientToolsCap.catalogAsAgUiTools();
|
|
961
|
-
try {
|
|
962
|
-
await source.runAgent({
|
|
963
|
-
forwardedProps: { command: { resume: input.resume } },
|
|
964
|
-
...(tools.length > 0 ? { tools } : {}),
|
|
965
|
-
});
|
|
966
|
-
finishRunTelemetry(run);
|
|
967
|
-
}
|
|
968
|
-
catch (err) {
|
|
969
|
-
if (!settleIfAborted(err)) {
|
|
970
|
-
store.status.set('error');
|
|
971
|
-
store.isLoading.set(false);
|
|
972
|
-
store.error.set(toAgentError(err));
|
|
973
|
-
failRunTelemetry(err, run);
|
|
974
|
-
}
|
|
975
|
-
}
|
|
1189
|
+
await executeRun('resume', { forwardedProps: { command: { resume: input.resume } } }, true);
|
|
976
1190
|
return;
|
|
977
1191
|
}
|
|
978
1192
|
applyStatePatch(input.state);
|
|
@@ -987,7 +1201,7 @@ function toAgent(source, options = {}) {
|
|
|
987
1201
|
// Record the input so retry() can re-run it without re-appending the
|
|
988
1202
|
// user message (the message is already in the list by this point).
|
|
989
1203
|
lastInput = input;
|
|
990
|
-
await
|
|
1204
|
+
await executeRun('submit');
|
|
991
1205
|
},
|
|
992
1206
|
retry: async () => {
|
|
993
1207
|
if (store.isLoading())
|
|
@@ -998,22 +1212,23 @@ function toAgent(source, options = {}) {
|
|
|
998
1212
|
// Re-run the same message list against the source without appending a
|
|
999
1213
|
// duplicate user message — the message is already in store.messages and
|
|
1000
1214
|
// source's internal list from the original submit().
|
|
1001
|
-
await
|
|
1215
|
+
await executeRun('retry', undefined, true);
|
|
1002
1216
|
},
|
|
1003
1217
|
stop: async () => {
|
|
1004
|
-
|
|
1218
|
+
const run = activeRun;
|
|
1219
|
+
if (run && run.outcome === undefined) {
|
|
1220
|
+
finalizeDeliveryRun(store, run, 'aborted');
|
|
1221
|
+
store.status.set('idle');
|
|
1222
|
+
store.isLoading.set(false);
|
|
1223
|
+
store.error.set(undefined);
|
|
1224
|
+
finishRunTelemetry(run);
|
|
1225
|
+
}
|
|
1005
1226
|
source.abortRun();
|
|
1006
1227
|
},
|
|
1007
1228
|
regenerate: async (assistantMessageIndex) => {
|
|
1008
1229
|
if (store.isLoading()) {
|
|
1009
1230
|
throw new Error('Cannot regenerate while agent is loading another response');
|
|
1010
1231
|
}
|
|
1011
|
-
// Reset abort flags so a regenerate starts clean, exactly like submit().
|
|
1012
|
-
// Without this, flags left over from a prior stop() would cause the
|
|
1013
|
-
// duplicate-delivery guard in settleIfAborted() to silently swallow the
|
|
1014
|
-
// abort error without settling, wedging the store in streaming/running.
|
|
1015
|
-
abortRequested = false;
|
|
1016
|
-
abortSettled = false;
|
|
1017
1232
|
const msgs = store.messages();
|
|
1018
1233
|
const target = msgs[assistantMessageIndex];
|
|
1019
1234
|
if (!target || target.role !== 'assistant') {
|
|
@@ -1039,20 +1254,7 @@ function toAgent(source, options = {}) {
|
|
|
1039
1254
|
// agent's internal message list without appending — the trailing user
|
|
1040
1255
|
// message in `trimmed` becomes the active prompt for the next run.
|
|
1041
1256
|
source.setMessages(trimmed);
|
|
1042
|
-
|
|
1043
|
-
const regenTools = clientToolsCap.catalogAsAgUiTools();
|
|
1044
|
-
try {
|
|
1045
|
-
await source.runAgent(regenTools.length > 0 ? { tools: regenTools } : undefined);
|
|
1046
|
-
finishRunTelemetry(run);
|
|
1047
|
-
}
|
|
1048
|
-
catch (err) {
|
|
1049
|
-
if (!settleIfAborted(err)) {
|
|
1050
|
-
store.status.set('error');
|
|
1051
|
-
store.isLoading.set(false);
|
|
1052
|
-
store.error.set(toAgentError(err));
|
|
1053
|
-
failRunTelemetry(err, run);
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1257
|
+
await executeRun('regenerate');
|
|
1056
1258
|
},
|
|
1057
1259
|
};
|
|
1058
1260
|
}
|
|
@@ -1062,11 +1264,16 @@ function buildUserMessage(input) {
|
|
|
1062
1264
|
const content = typeof input.message === 'string'
|
|
1063
1265
|
? input.message
|
|
1064
1266
|
: input.message.map((b) => b.type === 'text' ? b.text : JSON.stringify(b)).join('');
|
|
1065
|
-
|
|
1267
|
+
const id = randomId();
|
|
1268
|
+
return { id, role: 'user', content, delivery: staticDelivery(id) };
|
|
1066
1269
|
}
|
|
1067
1270
|
function randomId() {
|
|
1068
1271
|
return Math.random().toString(36).slice(2);
|
|
1069
1272
|
}
|
|
1273
|
+
function getTailAssistantMessageId(messages) {
|
|
1274
|
+
const tail = messages[messages.length - 1];
|
|
1275
|
+
return tail?.role === 'assistant' ? tail.id : undefined;
|
|
1276
|
+
}
|
|
1070
1277
|
|
|
1071
1278
|
// SPDX-License-Identifier: MIT
|
|
1072
1279
|
/**
|
|
@@ -1125,6 +1332,8 @@ class FakeAgent extends AbstractAgent {
|
|
|
1125
1332
|
reasoningTokens;
|
|
1126
1333
|
/** Milliseconds between successive token emissions. */
|
|
1127
1334
|
delayMs;
|
|
1335
|
+
/** Optional deterministic event branches for tests that need exact streams. */
|
|
1336
|
+
script;
|
|
1128
1337
|
constructor(opts = {}) {
|
|
1129
1338
|
super();
|
|
1130
1339
|
this.tokens = opts.tokens ?? [
|
|
@@ -1133,11 +1342,14 @@ class FakeAgent extends AbstractAgent {
|
|
|
1133
1342
|
];
|
|
1134
1343
|
this.reasoningTokens = opts.reasoningTokens ?? [];
|
|
1135
1344
|
this.delayMs = opts.delayMs ?? 60;
|
|
1345
|
+
this.script = opts.script ?? [];
|
|
1136
1346
|
}
|
|
1137
1347
|
run(input) {
|
|
1348
|
+
const scripted = this.scriptedSequence(input);
|
|
1349
|
+
if (scripted)
|
|
1350
|
+
return this.emitSequence(scripted, 30);
|
|
1138
1351
|
const tokens = this.tokens;
|
|
1139
1352
|
const reasoningTokens = this.reasoningTokens;
|
|
1140
|
-
const delayMs = this.delayMs;
|
|
1141
1353
|
const messageId = `fake-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1142
1354
|
const sequence = [
|
|
1143
1355
|
{ type: EventType.RUN_STARTED, threadId: input.threadId, runId: input.runId },
|
|
@@ -1155,6 +1367,22 @@ class FakeAgent extends AbstractAgent {
|
|
|
1155
1367
|
}
|
|
1156
1368
|
sequence.push({ type: EventType.TEXT_MESSAGE_END, messageId });
|
|
1157
1369
|
sequence.push({ type: EventType.RUN_FINISHED, threadId: input.threadId, runId: input.runId });
|
|
1370
|
+
return this.emitSequence(sequence, 30);
|
|
1371
|
+
}
|
|
1372
|
+
scriptedSequence(input) {
|
|
1373
|
+
if (this.script.length === 0)
|
|
1374
|
+
return undefined;
|
|
1375
|
+
const branch = this.script.find((candidate) => matchesBranch(candidate.when, input));
|
|
1376
|
+
if (!branch)
|
|
1377
|
+
return undefined;
|
|
1378
|
+
return [
|
|
1379
|
+
{ type: EventType.RUN_STARTED, threadId: input.threadId, runId: input.runId },
|
|
1380
|
+
...branch.events,
|
|
1381
|
+
{ type: EventType.RUN_FINISHED, threadId: input.threadId, runId: input.runId },
|
|
1382
|
+
];
|
|
1383
|
+
}
|
|
1384
|
+
emitSequence(sequence, initialDelayMs) {
|
|
1385
|
+
const delayMs = this.delayMs;
|
|
1158
1386
|
return new Observable((observer) => {
|
|
1159
1387
|
let cancelled = false;
|
|
1160
1388
|
let timer;
|
|
@@ -1171,7 +1399,7 @@ class FakeAgent extends AbstractAgent {
|
|
|
1171
1399
|
// Steady cadence except a tiny initial delay before RUN_STARTED.
|
|
1172
1400
|
timer = setTimeout(emitNext, delayMs);
|
|
1173
1401
|
};
|
|
1174
|
-
timer = setTimeout(emitNext,
|
|
1402
|
+
timer = setTimeout(emitNext, initialDelayMs);
|
|
1175
1403
|
return () => {
|
|
1176
1404
|
cancelled = true;
|
|
1177
1405
|
if (timer !== undefined)
|
|
@@ -1180,6 +1408,26 @@ class FakeAgent extends AbstractAgent {
|
|
|
1180
1408
|
});
|
|
1181
1409
|
}
|
|
1182
1410
|
}
|
|
1411
|
+
function matchesBranch(when, input) {
|
|
1412
|
+
if (when === 'initial')
|
|
1413
|
+
return !hasToolMessages(input);
|
|
1414
|
+
return hasToolMessageFor(input, when.toolMessageFor);
|
|
1415
|
+
}
|
|
1416
|
+
function hasToolMessages(input) {
|
|
1417
|
+
return (input.messages ?? []).some((message) => isToolMessage(message));
|
|
1418
|
+
}
|
|
1419
|
+
function hasToolMessageFor(input, toolCallId) {
|
|
1420
|
+
return (input.messages ?? []).some((message) => {
|
|
1421
|
+
if (!isToolMessage(message))
|
|
1422
|
+
return false;
|
|
1423
|
+
const record = message;
|
|
1424
|
+
return record['toolCallId'] === toolCallId || record['tool_call_id'] === toolCallId;
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
function isToolMessage(message) {
|
|
1428
|
+
const record = message;
|
|
1429
|
+
return record['role'] === 'tool' || record['type'] === 'tool';
|
|
1430
|
+
}
|
|
1183
1431
|
|
|
1184
1432
|
/**
|
|
1185
1433
|
* Registers an in-process FakeAgent under AGENT.
|