@threadplane/ag-ui 0.0.56 → 0.0.58
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 };
|
|
540
|
+
}
|
|
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
|
+
};
|
|
468
552
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
status: 'complete',
|
|
477
|
-
});
|
|
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;
|
|
478
560
|
}
|
|
479
|
-
|
|
480
|
-
return { ...rest, toolCallIds: ids };
|
|
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,16 @@ function agentRuntimeTelemetryErrorClass(error) {
|
|
|
737
918
|
* ```
|
|
738
919
|
*/
|
|
739
920
|
function toAgent(source, options = {}) {
|
|
921
|
+
// Advertise A2UI capabilities via the AG-UI shared state so every
|
|
922
|
+
// RunAgentInput.state carries them (transport metadata, A2UI v0.9).
|
|
923
|
+
if (options.a2uiClientCapabilities) {
|
|
924
|
+
source.state = {
|
|
925
|
+
...(source.state ?? {}),
|
|
926
|
+
a2ui_client_capabilities: options.a2uiClientCapabilities,
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
let generationSequence = 0;
|
|
930
|
+
const allocateDeliveryGeneration = (scope) => `${scope}-${++generationSequence}-${Math.random().toString(36).slice(2, 10)}`;
|
|
740
931
|
const store = {
|
|
741
932
|
messages: signal([]),
|
|
742
933
|
status: signal('idle'),
|
|
@@ -748,55 +939,39 @@ function toAgent(source, options = {}) {
|
|
|
748
939
|
events$: new Subject(),
|
|
749
940
|
customEvents: signal([]),
|
|
750
941
|
activities: signal(new Map()),
|
|
942
|
+
deliveryRun: null,
|
|
943
|
+
allocateDeliveryGeneration,
|
|
751
944
|
};
|
|
752
945
|
const telemetryProperties = { transport: 'ag-ui', surface: 'to_agent' };
|
|
753
946
|
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;
|
|
947
|
+
const runsByProtocolId = new Map();
|
|
764
948
|
// Tracks the last AgentSubmitInput so retry() can re-run it without
|
|
765
949
|
// duplicating the user message. Set at the top of submit()'s message path.
|
|
766
950
|
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;
|
|
951
|
+
function resolveCallbackRun(protocolRunId) {
|
|
952
|
+
if (!protocolRunId)
|
|
953
|
+
return activeRun;
|
|
954
|
+
const known = runsByProtocolId.get(protocolRunId);
|
|
955
|
+
if (known)
|
|
956
|
+
return known;
|
|
957
|
+
if (!activeRun || activeRun.protocolRunId)
|
|
958
|
+
return null;
|
|
959
|
+
activeRun.protocolRunId = protocolRunId;
|
|
960
|
+
runsByProtocolId.set(protocolRunId, activeRun);
|
|
961
|
+
while (runsByProtocolId.size > 16) {
|
|
962
|
+
const oldestId = runsByProtocolId.keys().next().value;
|
|
963
|
+
if (!oldestId)
|
|
964
|
+
break;
|
|
965
|
+
if (runsByProtocolId.get(oldestId) === activeRun) {
|
|
966
|
+
const current = runsByProtocolId.get(oldestId);
|
|
967
|
+
runsByProtocolId.delete(oldestId);
|
|
968
|
+
runsByProtocolId.set(oldestId, current);
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
runsByProtocolId.delete(oldestId);
|
|
794
972
|
}
|
|
795
|
-
return
|
|
973
|
+
return activeRun;
|
|
796
974
|
}
|
|
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
975
|
/** Forward a neutral-contract state patch onto the AG-UI run input.
|
|
801
976
|
* Mirrors the canonical demo's `input.state` mechanism: the patch is
|
|
802
977
|
* merged into the source agent's client state (carried on
|
|
@@ -809,9 +984,27 @@ function toAgent(source, options = {}) {
|
|
|
809
984
|
store.state.update((prev) => ({ ...prev, ...patch }));
|
|
810
985
|
};
|
|
811
986
|
captureAgentRuntimeTelemetry(options.telemetry, 'tplane:runtime_instance_created', telemetryProperties);
|
|
812
|
-
function
|
|
813
|
-
|
|
987
|
+
function beginRun(requestType, allowBaselineTail = false) {
|
|
988
|
+
if (activeRun && activeRun.outcome === undefined) {
|
|
989
|
+
const supersededRun = activeRun;
|
|
990
|
+
finalizeDeliveryRun(store, supersededRun, 'interrupted');
|
|
991
|
+
const interruption = new Error('Run superseded by a newer request');
|
|
992
|
+
interruption.name = 'InterruptedError';
|
|
993
|
+
failRunTelemetry(interruption, supersededRun);
|
|
994
|
+
}
|
|
995
|
+
const run = {
|
|
996
|
+
generation: allocateDeliveryGeneration('run'),
|
|
997
|
+
baselineMessageIds: new Set(store.messages().map(message => message.id)),
|
|
998
|
+
ownedMessageIds: new Set(),
|
|
999
|
+
snapshotReplacementIds: new Set(),
|
|
1000
|
+
eligibleBaselineAssistantId: allowBaselineTail
|
|
1001
|
+
? getTailAssistantMessageId(store.messages())
|
|
1002
|
+
: undefined,
|
|
1003
|
+
startedAt: Date.now(),
|
|
1004
|
+
telemetrySettled: false,
|
|
1005
|
+
};
|
|
814
1006
|
activeRun = run;
|
|
1007
|
+
store.deliveryRun = run;
|
|
815
1008
|
captureAgentRuntimeTelemetry(options.telemetry, 'tplane:runtime_request_created', {
|
|
816
1009
|
...telemetryProperties,
|
|
817
1010
|
requestType,
|
|
@@ -820,76 +1013,115 @@ function toAgent(source, options = {}) {
|
|
|
820
1013
|
return run;
|
|
821
1014
|
}
|
|
822
1015
|
function finishRunTelemetry(run) {
|
|
823
|
-
if (run.
|
|
1016
|
+
if (run.telemetrySettled)
|
|
824
1017
|
return;
|
|
1018
|
+
run.telemetrySettled = true;
|
|
825
1019
|
captureAgentRuntimeTelemetry(options.telemetry, 'tplane:stream_ended', {
|
|
826
1020
|
...telemetryProperties,
|
|
827
1021
|
durationMs: Date.now() - run.startedAt,
|
|
828
1022
|
});
|
|
829
|
-
if (activeRun === run)
|
|
830
|
-
activeRun = null;
|
|
831
1023
|
}
|
|
832
1024
|
function failRunTelemetry(error, run = activeRun) {
|
|
833
|
-
if (!run || run.
|
|
1025
|
+
if (!run || run.telemetrySettled)
|
|
834
1026
|
return;
|
|
835
|
-
run.
|
|
1027
|
+
run.telemetrySettled = true;
|
|
836
1028
|
captureAgentRuntimeTelemetry(options.telemetry, 'tplane:stream_errored', {
|
|
837
1029
|
...telemetryProperties,
|
|
838
1030
|
durationMs: Date.now() - run.startedAt,
|
|
839
1031
|
errorClass: agentRuntimeTelemetryErrorClass(error),
|
|
840
1032
|
});
|
|
841
|
-
if (activeRun === run)
|
|
842
|
-
activeRun = null;
|
|
843
1033
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1034
|
+
function failRun(run, error) {
|
|
1035
|
+
if (run.outcome !== undefined)
|
|
1036
|
+
return;
|
|
1037
|
+
finalizeDeliveryRun(store, run, 'error');
|
|
1038
|
+
if (activeRun === run) {
|
|
1039
|
+
store.status.set('error');
|
|
1040
|
+
store.isLoading.set(false);
|
|
1041
|
+
store.error.set(toAgentError(error));
|
|
1042
|
+
}
|
|
1043
|
+
failRunTelemetry(error, run);
|
|
1044
|
+
}
|
|
1045
|
+
function settleTransportClose(run) {
|
|
1046
|
+
if (run.outcome === undefined) {
|
|
1047
|
+
finalizeDeliveryRun(store, run, run.ownedMessageIds.size > 0 ? 'interrupted' : 'success');
|
|
1048
|
+
if (activeRun === run) {
|
|
1049
|
+
store.status.set('idle');
|
|
1050
|
+
store.isLoading.set(false);
|
|
1051
|
+
store.error.set(undefined);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
finishRunTelemetry(run);
|
|
1055
|
+
}
|
|
1056
|
+
async function executeRun(requestType, parameters, allowBaselineTail = false) {
|
|
1057
|
+
const run = beginRun(requestType, allowBaselineTail);
|
|
851
1058
|
const tools = clientToolsCap.catalogAsAgUiTools();
|
|
1059
|
+
const runParameters = parameters === undefined && tools.length === 0
|
|
1060
|
+
? undefined
|
|
1061
|
+
: { ...parameters, ...(tools.length > 0 ? { tools } : {}) };
|
|
852
1062
|
try {
|
|
853
|
-
await source.runAgent(
|
|
854
|
-
|
|
1063
|
+
await source.runAgent(runParameters);
|
|
1064
|
+
settleTransportClose(run);
|
|
855
1065
|
}
|
|
856
1066
|
catch (err) {
|
|
857
|
-
if (
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
store.error.set(toAgentError(err));
|
|
861
|
-
failRunTelemetry(err, run);
|
|
862
|
-
}
|
|
1067
|
+
if (run.outcome === 'aborted' && isAbortError(err))
|
|
1068
|
+
return;
|
|
1069
|
+
failRun(run, err);
|
|
863
1070
|
}
|
|
864
1071
|
}
|
|
1072
|
+
const clientToolsCap = createClientToolsCapability(source, store, () => executeRun('client-tool-continuation', undefined, true));
|
|
865
1073
|
// Tap all events from the source agent via the AgentSubscriber API.
|
|
866
1074
|
// This subscription lives for the lifetime of `source`.
|
|
867
1075
|
source.subscribe({
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1076
|
+
onRunInitialized({ input }) {
|
|
1077
|
+
resolveCallbackRun(input.runId);
|
|
1078
|
+
},
|
|
1079
|
+
onEvent({ event, input }) {
|
|
1080
|
+
const callbackRunId = input?.runId ?? event.runId;
|
|
1081
|
+
const run = resolveCallbackRun(callbackRunId);
|
|
1082
|
+
if (!run) {
|
|
1083
|
+
if (!callbackRunId)
|
|
1084
|
+
reduceEvent(event, store);
|
|
1085
|
+
return;
|
|
876
1086
|
}
|
|
1087
|
+
if (run !== activeRun) {
|
|
1088
|
+
if (event.type === 'RUN_FINISHED')
|
|
1089
|
+
finalizeDeliveryRun(store, run, 'success');
|
|
1090
|
+
else if (event.type === 'RUN_ERROR')
|
|
1091
|
+
finalizeDeliveryRun(store, run, 'error');
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (event.type === 'RUN_ERROR' && run.outcome === 'aborted')
|
|
1095
|
+
return;
|
|
877
1096
|
reduceEvent(event, store);
|
|
1097
|
+
if (run && event.type === 'RUN_FINISHED' && run.outcome === 'success') {
|
|
1098
|
+
finishRunTelemetry(run);
|
|
1099
|
+
}
|
|
1100
|
+
else if (run && event.type === 'RUN_ERROR' && run.outcome === 'error') {
|
|
1101
|
+
failRunTelemetry(event.message ?? event, run);
|
|
1102
|
+
}
|
|
878
1103
|
},
|
|
879
|
-
onRunFailed({ error }) {
|
|
880
|
-
|
|
1104
|
+
onRunFailed({ error, input }) {
|
|
1105
|
+
const run = resolveCallbackRun(input?.runId);
|
|
1106
|
+
if (run) {
|
|
1107
|
+
if (run.outcome === 'aborted' && isAbortError(error))
|
|
1108
|
+
return;
|
|
1109
|
+
failRun(run, error);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
if (input?.runId)
|
|
881
1113
|
return;
|
|
882
1114
|
store.status.set('error');
|
|
883
1115
|
store.isLoading.set(false);
|
|
884
1116
|
store.error.set(toAgentError(error));
|
|
885
|
-
failRunTelemetry(error);
|
|
886
1117
|
},
|
|
887
1118
|
});
|
|
888
1119
|
// Stable Subagent wrappers per messageId so chat-subagents (tracks by
|
|
889
1120
|
// toolCallId) doesn't churn as activity content streams.
|
|
890
1121
|
const subagentWrappers = new Map();
|
|
891
1122
|
function subagentFor(id, entry) {
|
|
892
|
-
|
|
1123
|
+
const cached = subagentWrappers.get(id);
|
|
1124
|
+
let w = cached?.generation === entry.generation ? cached.wrapper : undefined;
|
|
893
1125
|
if (!w) {
|
|
894
1126
|
w = {
|
|
895
1127
|
toolCallId: entry.content()['toolCallId'] ?? id,
|
|
@@ -897,17 +1129,27 @@ function toAgent(source, options = {}) {
|
|
|
897
1129
|
status: computed(() => entry.content()['status'] ?? 'running'),
|
|
898
1130
|
messages: computed(() => {
|
|
899
1131
|
const c = entry.content();
|
|
1132
|
+
const status = c['status'] ?? 'running';
|
|
1133
|
+
const assistantDelivery = status === 'error'
|
|
1134
|
+
? completeDelivery(entry.generation, 'error')
|
|
1135
|
+
: status === 'complete'
|
|
1136
|
+
? completeDelivery(entry.generation, 'success')
|
|
1137
|
+
: streamingDelivery(entry.generation);
|
|
900
1138
|
const raw = c['messages'];
|
|
901
1139
|
if (Array.isArray(raw)) {
|
|
902
|
-
return raw.map((m, i) =>
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1140
|
+
return raw.map((m, i) => {
|
|
1141
|
+
const messageId = m['id'] ?? `${id}-${i}`;
|
|
1142
|
+
return {
|
|
1143
|
+
id: messageId,
|
|
1144
|
+
role: 'assistant',
|
|
1145
|
+
content: typeof m['content'] === 'string' ? m['content'] : m['content'] ?? '',
|
|
1146
|
+
delivery: m['role'] === 'assistant' ? assistantDelivery : staticDelivery(messageId),
|
|
1147
|
+
...(Array.isArray(m['toolCallIds']) ? { toolCallIds: m['toolCallIds'] } : {}),
|
|
1148
|
+
...(typeof m['reasoning'] === 'string' ? { reasoning: m['reasoning'] } : {}),
|
|
1149
|
+
};
|
|
1150
|
+
});
|
|
909
1151
|
}
|
|
910
|
-
return [{ id, role: 'assistant', content: String(c['text'] ?? '') }];
|
|
1152
|
+
return [{ id, role: 'assistant', content: String(c['text'] ?? ''), delivery: assistantDelivery }];
|
|
911
1153
|
}),
|
|
912
1154
|
toolCalls: computed(() => {
|
|
913
1155
|
const raw = entry.content()['toolCalls'];
|
|
@@ -915,7 +1157,7 @@ function toAgent(source, options = {}) {
|
|
|
915
1157
|
}),
|
|
916
1158
|
state: computed(() => entry.content()['state'] ?? {}),
|
|
917
1159
|
};
|
|
918
|
-
subagentWrappers.set(id, w);
|
|
1160
|
+
subagentWrappers.set(id, { generation: entry.generation, wrapper: w });
|
|
919
1161
|
}
|
|
920
1162
|
return w;
|
|
921
1163
|
}
|
|
@@ -946,33 +1188,13 @@ function toAgent(source, options = {}) {
|
|
|
946
1188
|
}),
|
|
947
1189
|
clientTools: clientToolsCap,
|
|
948
1190
|
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
1191
|
if (input.resume !== undefined) {
|
|
954
1192
|
// Resume path: clear the pending interrupt and replay the run with the
|
|
955
1193
|
// resume payload forwarded to the LangGraph backend via AG-UI's
|
|
956
1194
|
// forwardedProps.command.resume mechanism.
|
|
957
1195
|
applyStatePatch(input.state);
|
|
958
1196
|
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
|
-
}
|
|
1197
|
+
await executeRun('resume', { forwardedProps: { command: { resume: input.resume } } }, true);
|
|
976
1198
|
return;
|
|
977
1199
|
}
|
|
978
1200
|
applyStatePatch(input.state);
|
|
@@ -987,7 +1209,7 @@ function toAgent(source, options = {}) {
|
|
|
987
1209
|
// Record the input so retry() can re-run it without re-appending the
|
|
988
1210
|
// user message (the message is already in the list by this point).
|
|
989
1211
|
lastInput = input;
|
|
990
|
-
await
|
|
1212
|
+
await executeRun('submit');
|
|
991
1213
|
},
|
|
992
1214
|
retry: async () => {
|
|
993
1215
|
if (store.isLoading())
|
|
@@ -998,22 +1220,23 @@ function toAgent(source, options = {}) {
|
|
|
998
1220
|
// Re-run the same message list against the source without appending a
|
|
999
1221
|
// duplicate user message — the message is already in store.messages and
|
|
1000
1222
|
// source's internal list from the original submit().
|
|
1001
|
-
await
|
|
1223
|
+
await executeRun('retry', undefined, true);
|
|
1002
1224
|
},
|
|
1003
1225
|
stop: async () => {
|
|
1004
|
-
|
|
1226
|
+
const run = activeRun;
|
|
1227
|
+
if (run && run.outcome === undefined) {
|
|
1228
|
+
finalizeDeliveryRun(store, run, 'aborted');
|
|
1229
|
+
store.status.set('idle');
|
|
1230
|
+
store.isLoading.set(false);
|
|
1231
|
+
store.error.set(undefined);
|
|
1232
|
+
finishRunTelemetry(run);
|
|
1233
|
+
}
|
|
1005
1234
|
source.abortRun();
|
|
1006
1235
|
},
|
|
1007
1236
|
regenerate: async (assistantMessageIndex) => {
|
|
1008
1237
|
if (store.isLoading()) {
|
|
1009
1238
|
throw new Error('Cannot regenerate while agent is loading another response');
|
|
1010
1239
|
}
|
|
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
1240
|
const msgs = store.messages();
|
|
1018
1241
|
const target = msgs[assistantMessageIndex];
|
|
1019
1242
|
if (!target || target.role !== 'assistant') {
|
|
@@ -1039,20 +1262,7 @@ function toAgent(source, options = {}) {
|
|
|
1039
1262
|
// agent's internal message list without appending — the trailing user
|
|
1040
1263
|
// message in `trimmed` becomes the active prompt for the next run.
|
|
1041
1264
|
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
|
-
}
|
|
1265
|
+
await executeRun('regenerate');
|
|
1056
1266
|
},
|
|
1057
1267
|
};
|
|
1058
1268
|
}
|
|
@@ -1062,11 +1272,16 @@ function buildUserMessage(input) {
|
|
|
1062
1272
|
const content = typeof input.message === 'string'
|
|
1063
1273
|
? input.message
|
|
1064
1274
|
: input.message.map((b) => b.type === 'text' ? b.text : JSON.stringify(b)).join('');
|
|
1065
|
-
|
|
1275
|
+
const id = randomId();
|
|
1276
|
+
return { id, role: 'user', content, delivery: staticDelivery(id) };
|
|
1066
1277
|
}
|
|
1067
1278
|
function randomId() {
|
|
1068
1279
|
return Math.random().toString(36).slice(2);
|
|
1069
1280
|
}
|
|
1281
|
+
function getTailAssistantMessageId(messages) {
|
|
1282
|
+
const tail = messages[messages.length - 1];
|
|
1283
|
+
return tail?.role === 'assistant' ? tail.id : undefined;
|
|
1284
|
+
}
|
|
1070
1285
|
|
|
1071
1286
|
// SPDX-License-Identifier: MIT
|
|
1072
1287
|
/**
|
|
@@ -1125,6 +1340,8 @@ class FakeAgent extends AbstractAgent {
|
|
|
1125
1340
|
reasoningTokens;
|
|
1126
1341
|
/** Milliseconds between successive token emissions. */
|
|
1127
1342
|
delayMs;
|
|
1343
|
+
/** Optional deterministic event branches for tests that need exact streams. */
|
|
1344
|
+
script;
|
|
1128
1345
|
constructor(opts = {}) {
|
|
1129
1346
|
super();
|
|
1130
1347
|
this.tokens = opts.tokens ?? [
|
|
@@ -1133,11 +1350,14 @@ class FakeAgent extends AbstractAgent {
|
|
|
1133
1350
|
];
|
|
1134
1351
|
this.reasoningTokens = opts.reasoningTokens ?? [];
|
|
1135
1352
|
this.delayMs = opts.delayMs ?? 60;
|
|
1353
|
+
this.script = opts.script ?? [];
|
|
1136
1354
|
}
|
|
1137
1355
|
run(input) {
|
|
1356
|
+
const scripted = this.scriptedSequence(input);
|
|
1357
|
+
if (scripted)
|
|
1358
|
+
return this.emitSequence(scripted, 30);
|
|
1138
1359
|
const tokens = this.tokens;
|
|
1139
1360
|
const reasoningTokens = this.reasoningTokens;
|
|
1140
|
-
const delayMs = this.delayMs;
|
|
1141
1361
|
const messageId = `fake-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1142
1362
|
const sequence = [
|
|
1143
1363
|
{ type: EventType.RUN_STARTED, threadId: input.threadId, runId: input.runId },
|
|
@@ -1155,6 +1375,22 @@ class FakeAgent extends AbstractAgent {
|
|
|
1155
1375
|
}
|
|
1156
1376
|
sequence.push({ type: EventType.TEXT_MESSAGE_END, messageId });
|
|
1157
1377
|
sequence.push({ type: EventType.RUN_FINISHED, threadId: input.threadId, runId: input.runId });
|
|
1378
|
+
return this.emitSequence(sequence, 30);
|
|
1379
|
+
}
|
|
1380
|
+
scriptedSequence(input) {
|
|
1381
|
+
if (this.script.length === 0)
|
|
1382
|
+
return undefined;
|
|
1383
|
+
const branch = this.script.find((candidate) => matchesBranch(candidate.when, input));
|
|
1384
|
+
if (!branch)
|
|
1385
|
+
return undefined;
|
|
1386
|
+
return [
|
|
1387
|
+
{ type: EventType.RUN_STARTED, threadId: input.threadId, runId: input.runId },
|
|
1388
|
+
...branch.events,
|
|
1389
|
+
{ type: EventType.RUN_FINISHED, threadId: input.threadId, runId: input.runId },
|
|
1390
|
+
];
|
|
1391
|
+
}
|
|
1392
|
+
emitSequence(sequence, initialDelayMs) {
|
|
1393
|
+
const delayMs = this.delayMs;
|
|
1158
1394
|
return new Observable((observer) => {
|
|
1159
1395
|
let cancelled = false;
|
|
1160
1396
|
let timer;
|
|
@@ -1171,7 +1407,7 @@ class FakeAgent extends AbstractAgent {
|
|
|
1171
1407
|
// Steady cadence except a tiny initial delay before RUN_STARTED.
|
|
1172
1408
|
timer = setTimeout(emitNext, delayMs);
|
|
1173
1409
|
};
|
|
1174
|
-
timer = setTimeout(emitNext,
|
|
1410
|
+
timer = setTimeout(emitNext, initialDelayMs);
|
|
1175
1411
|
return () => {
|
|
1176
1412
|
cancelled = true;
|
|
1177
1413
|
if (timer !== undefined)
|
|
@@ -1180,6 +1416,26 @@ class FakeAgent extends AbstractAgent {
|
|
|
1180
1416
|
});
|
|
1181
1417
|
}
|
|
1182
1418
|
}
|
|
1419
|
+
function matchesBranch(when, input) {
|
|
1420
|
+
if (when === 'initial')
|
|
1421
|
+
return !hasToolMessages(input);
|
|
1422
|
+
return hasToolMessageFor(input, when.toolMessageFor);
|
|
1423
|
+
}
|
|
1424
|
+
function hasToolMessages(input) {
|
|
1425
|
+
return (input.messages ?? []).some((message) => isToolMessage(message));
|
|
1426
|
+
}
|
|
1427
|
+
function hasToolMessageFor(input, toolCallId) {
|
|
1428
|
+
return (input.messages ?? []).some((message) => {
|
|
1429
|
+
if (!isToolMessage(message))
|
|
1430
|
+
return false;
|
|
1431
|
+
const record = message;
|
|
1432
|
+
return record['toolCallId'] === toolCallId || record['tool_call_id'] === toolCallId;
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
function isToolMessage(message) {
|
|
1436
|
+
const record = message;
|
|
1437
|
+
return record['role'] === 'tool' || record['type'] === 'tool';
|
|
1438
|
+
}
|
|
1183
1439
|
|
|
1184
1440
|
/**
|
|
1185
1441
|
* Registers an in-process FakeAgent under AGENT.
|