@threadplane/ag-ui 0.0.47 → 0.0.50
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 +128 -14
- package/fesm2022/threadplane-ag-ui.mjs +426 -33
- package/fesm2022/threadplane-ag-ui.mjs.map +1 -1
- package/package.json +10 -1
- package/types/threadplane-ag-ui.d.ts +50 -22
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { signal, InjectionToken, inject } from '@angular/core';
|
|
1
|
+
import { signal, computed, InjectionToken, inject } from '@angular/core';
|
|
2
2
|
import { Subject, Observable } from 'rxjs';
|
|
3
3
|
import { HttpAgent, AbstractAgent, EventType } from '@ag-ui/client';
|
|
4
4
|
|
|
@@ -259,6 +259,11 @@ function normalizeCitation(entry, fallbackIndex) {
|
|
|
259
259
|
};
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
// SPDX-License-Identifier: MIT
|
|
263
|
+
// @ag-ui/client@0.0.52 — EventType is a string enum with uppercase values.
|
|
264
|
+
// Discriminator strings (e.g. 'RUN_STARTED') match EventType enum members
|
|
265
|
+
// verbatim; the switch cases below use the string literals directly so this
|
|
266
|
+
// file has no runtime dependency on the EventType enum import.
|
|
262
267
|
/**
|
|
263
268
|
* Per-message reasoning timing. Populated by REASONING_MESSAGE_START /
|
|
264
269
|
* REASONING_MESSAGE_END handlers. The map lives on the module — same
|
|
@@ -288,6 +293,9 @@ function reduceEvent(event, store) {
|
|
|
288
293
|
store.status.set('running');
|
|
289
294
|
store.isLoading.set(true);
|
|
290
295
|
store.error.set(null);
|
|
296
|
+
store.interrupt.set(undefined);
|
|
297
|
+
store.customEvents.set([]);
|
|
298
|
+
store.activities.set(new Map());
|
|
291
299
|
return;
|
|
292
300
|
}
|
|
293
301
|
case 'RUN_FINISHED': {
|
|
@@ -358,22 +366,60 @@ function reduceEvent(event, store) {
|
|
|
358
366
|
...prev,
|
|
359
367
|
{ id: e.toolCallId, name: e.toolCallName, args: {}, status: 'running' },
|
|
360
368
|
]);
|
|
369
|
+
// Link the tool call to its parent assistant message so the chat lib's
|
|
370
|
+
// per-message tool-call resolution (chat-tool-calls / chat-tool-views)
|
|
371
|
+
// can scope it. ag-ui-langgraph emits parentMessageId for every tool
|
|
372
|
+
// call. If the parent assistant message hasn't been created yet (a
|
|
373
|
+
// tool-call-only turn emits no TEXT_MESSAGE_START), create a slot.
|
|
374
|
+
const parentId = e.parentMessageId;
|
|
375
|
+
if (parentId) {
|
|
376
|
+
store.messages.update((prev) => {
|
|
377
|
+
const existing = prev.find((m) => m.id === parentId);
|
|
378
|
+
if (existing) {
|
|
379
|
+
return prev.map((m) => m.id === parentId
|
|
380
|
+
? { ...m, toolCallIds: [...(m.toolCallIds ?? []), e.toolCallId] }
|
|
381
|
+
: m);
|
|
382
|
+
}
|
|
383
|
+
return [...prev, { id: parentId, role: 'assistant', content: '', toolCallIds: [e.toolCallId] }];
|
|
384
|
+
});
|
|
385
|
+
}
|
|
361
386
|
return;
|
|
362
387
|
}
|
|
363
388
|
case 'TOOL_CALL_ARGS': {
|
|
364
389
|
const e = event;
|
|
365
|
-
|
|
366
|
-
|
|
390
|
+
// Deltas are FRAGMENTS of a JSON document, not standalone JSON: a live
|
|
391
|
+
// model streams args token-by-token (`{"loca`, `tion":"Pa`, …), so we
|
|
392
|
+
// accumulate the raw text and parse the accumulated buffer. Until the
|
|
393
|
+
// buffer parses, keep the last-good args (initially {}).
|
|
394
|
+
const buffers = (store.argsBuffers ??= new Map());
|
|
395
|
+
const buffer = (buffers.get(e.toolCallId) ?? '') + e.delta;
|
|
396
|
+
buffers.set(e.toolCallId, buffer);
|
|
397
|
+
const args = tryParseArgs(buffer);
|
|
398
|
+
if (args !== undefined) {
|
|
399
|
+
store.toolCalls.update((prev) => prev.map((t) => t.id === e.toolCallId ? { ...t, args } : t));
|
|
400
|
+
}
|
|
367
401
|
return;
|
|
368
402
|
}
|
|
369
403
|
case 'TOOL_CALL_END': {
|
|
370
404
|
const e = event;
|
|
371
|
-
|
|
405
|
+
// Belt and braces: apply the final accumulated args (in case the last
|
|
406
|
+
// ARGS delta arrived but an intermediate state was left unparsed), then
|
|
407
|
+
// drop the buffer.
|
|
408
|
+
const finalBuffer = store.argsBuffers?.get(e.toolCallId);
|
|
409
|
+
store.argsBuffers?.delete(e.toolCallId);
|
|
410
|
+
const finalArgs = finalBuffer !== undefined ? tryParseArgs(finalBuffer) : undefined;
|
|
411
|
+
store.toolCalls.update((prev) => prev.map((t) => t.id === e.toolCallId
|
|
412
|
+
? { ...t, status: 'complete', ...(finalArgs !== undefined ? { args: finalArgs } : {}) }
|
|
413
|
+
: t));
|
|
372
414
|
return;
|
|
373
415
|
}
|
|
374
416
|
case 'TOOL_CALL_RESULT': {
|
|
375
417
|
const e = event;
|
|
376
|
-
|
|
418
|
+
// ag_ui_langgraph serialises tool results via normalize_tool_content()
|
|
419
|
+
// which always returns a string. Parse it so downstream consumers
|
|
420
|
+
// (chat-tool-views / toToolViewSpec) can spread the object into props.
|
|
421
|
+
const result = typeof e.content === 'string' ? safeParseJson(e.content) : e.content;
|
|
422
|
+
store.toolCalls.update((prev) => prev.map((t) => t.id === e.toolCallId ? { ...t, result } : t));
|
|
377
423
|
return;
|
|
378
424
|
}
|
|
379
425
|
case 'STATE_SNAPSHOT': {
|
|
@@ -392,17 +438,87 @@ function reduceEvent(event, store) {
|
|
|
392
438
|
}
|
|
393
439
|
case 'MESSAGES_SNAPSHOT': {
|
|
394
440
|
const e = event;
|
|
395
|
-
|
|
441
|
+
const raw = e.messages ?? [];
|
|
442
|
+
// AG-UI AssistantMessage carries `toolCalls` (ToolCall objects) on the
|
|
443
|
+
// snapshot wire. Bridge them to `toolCallIds` so that the chat lib's
|
|
444
|
+
// per-message tool-call resolution (resolveMessageToolCalls) can scope
|
|
445
|
+
// correctly. Also merge any snapshot-only tool calls into store.toolCalls
|
|
446
|
+
// so the data is visible to <chat-tool-views>.
|
|
447
|
+
const snapshotToolCalls = [];
|
|
448
|
+
const messages = raw.map((m) => {
|
|
449
|
+
if (m.role !== 'assistant' || !m.toolCalls || m.toolCalls.length === 0) {
|
|
450
|
+
return m;
|
|
451
|
+
}
|
|
452
|
+
const ids = [];
|
|
453
|
+
for (const tc of m.toolCalls) {
|
|
454
|
+
ids.push(tc.id);
|
|
455
|
+
snapshotToolCalls.push({
|
|
456
|
+
id: tc.id,
|
|
457
|
+
name: tc.function.name,
|
|
458
|
+
args: safeParseArgs(tc.function.arguments),
|
|
459
|
+
status: 'complete',
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
const { toolCalls: _dropped, ...rest } = m;
|
|
463
|
+
return { ...rest, toolCallIds: ids };
|
|
464
|
+
});
|
|
465
|
+
store.messages.set(messages);
|
|
466
|
+
if (snapshotToolCalls.length > 0) {
|
|
467
|
+
store.toolCalls.update((prev) => {
|
|
468
|
+
// Merge: keep existing entries (they may carry richer state from
|
|
469
|
+
// streaming) and only insert entries not already present by id.
|
|
470
|
+
const existingIds = new Set(prev.map((tc) => tc.id));
|
|
471
|
+
const toAdd = snapshotToolCalls.filter((tc) => !existingIds.has(tc.id));
|
|
472
|
+
return toAdd.length > 0 ? [...prev, ...toAdd] : prev;
|
|
473
|
+
});
|
|
474
|
+
}
|
|
396
475
|
return;
|
|
397
476
|
}
|
|
398
477
|
case 'CUSTOM': {
|
|
399
478
|
const e = event;
|
|
400
|
-
|
|
401
|
-
|
|
479
|
+
// ag_ui_langgraph serializes interrupt payloads as JSON strings.
|
|
480
|
+
// Parse the value if it arrives as a string so downstream consumers
|
|
481
|
+
// (e.g. ChatApprovalCardComponent) receive a plain object, not a string.
|
|
482
|
+
const parsedValue = typeof e.value === 'string' ? safeParseJson(e.value) : e.value;
|
|
483
|
+
if (e.name === 'on_interrupt') {
|
|
484
|
+
store.interrupt.set({ id: randomId$1(), value: parsedValue, resumable: true });
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
// Surface every other custom event on the customEvents signal so the
|
|
488
|
+
// chat a2ui partial-args bridge (which reads agent.customEvents()) lights
|
|
489
|
+
// up live/progressive a2ui rendering — parity with the LangGraph adapter.
|
|
490
|
+
store.customEvents.update((prev) => [...prev, { name: e.name, data: parsedValue }]);
|
|
491
|
+
if (e.name === 'state_update' && isRecord(parsedValue)) {
|
|
492
|
+
store.events$.next({ type: 'state_update', data: parsedValue });
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
store.events$.next({ type: 'custom', name: e.name, data: parsedValue });
|
|
496
|
+
}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
case 'ACTIVITY_SNAPSHOT': {
|
|
500
|
+
const e = event;
|
|
501
|
+
const map = new Map(store.activities());
|
|
502
|
+
const existing = map.get(e.messageId);
|
|
503
|
+
if (existing && existing.activityType === e.activityType && !e.replace) {
|
|
504
|
+
existing.content.update((c) => ({ ...c, ...e.content }));
|
|
402
505
|
}
|
|
403
506
|
else {
|
|
404
|
-
|
|
507
|
+
map.set(e.messageId, {
|
|
508
|
+
messageId: e.messageId,
|
|
509
|
+
activityType: e.activityType,
|
|
510
|
+
content: signal(e.content ?? {}),
|
|
511
|
+
});
|
|
405
512
|
}
|
|
513
|
+
store.activities.set(map); // new ref → projection picks up membership change
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
case 'ACTIVITY_DELTA': {
|
|
517
|
+
const e = event;
|
|
518
|
+
const entry = store.activities().get(e.messageId);
|
|
519
|
+
if (!entry)
|
|
520
|
+
return; // unknown activity — ignore
|
|
521
|
+
entry.content.update((c) => applyPatch(c, e.patch)); // inner signal → live, no map churn
|
|
406
522
|
return;
|
|
407
523
|
}
|
|
408
524
|
default: {
|
|
@@ -413,6 +529,9 @@ function reduceEvent(event, store) {
|
|
|
413
529
|
}
|
|
414
530
|
}
|
|
415
531
|
}
|
|
532
|
+
function randomId$1() {
|
|
533
|
+
return Math.random().toString(36).slice(2);
|
|
534
|
+
}
|
|
416
535
|
function messageIdFrom(event) {
|
|
417
536
|
return event.messageId ?? 'unknown';
|
|
418
537
|
}
|
|
@@ -425,6 +544,26 @@ function safeParseArgs(delta) {
|
|
|
425
544
|
return {};
|
|
426
545
|
}
|
|
427
546
|
}
|
|
547
|
+
/** Parse an (accumulated) args buffer; `undefined` when it isn't valid JSON
|
|
548
|
+
* yet — callers keep the previous args rather than clobbering them with {}. */
|
|
549
|
+
function tryParseArgs(buffer) {
|
|
550
|
+
try {
|
|
551
|
+
const parsed = JSON.parse(buffer);
|
|
552
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
return undefined;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/** Parse a JSON string to its value; return the original string on failure. */
|
|
559
|
+
function safeParseJson(s) {
|
|
560
|
+
try {
|
|
561
|
+
return JSON.parse(s);
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
return s;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
428
567
|
function isRecord(v) {
|
|
429
568
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
430
569
|
}
|
|
@@ -432,6 +571,93 @@ function deepClone(v) {
|
|
|
432
571
|
return JSON.parse(JSON.stringify(v));
|
|
433
572
|
}
|
|
434
573
|
|
|
574
|
+
// SPDX-License-Identifier: MIT
|
|
575
|
+
/** Convert a ClientToolSpec to the AG-UI Tool wire shape. */
|
|
576
|
+
function toAgUiTool(spec) {
|
|
577
|
+
return { name: spec.name, description: spec.description, parameters: spec.parameters };
|
|
578
|
+
}
|
|
579
|
+
/** Serialize a tool result value to a string for the ToolMessage content. */
|
|
580
|
+
function safeStringify(v) {
|
|
581
|
+
return typeof v === 'string' ? v : JSON.stringify(v);
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Creates a ClientToolsCapability backed by an AG-UI source agent and a
|
|
585
|
+
* ReducerStore. Extracted into a factory so it can be unit-tested in isolation
|
|
586
|
+
* without standing up a full Angular DI environment.
|
|
587
|
+
*
|
|
588
|
+
* The capability:
|
|
589
|
+
* - Maintains a catalog of client tool specs (setCatalog).
|
|
590
|
+
* - Exposes a `pending` computed signal: tool calls whose name is in the catalog,
|
|
591
|
+
* have no backend result, and haven't been resolved client-side yet — but ONLY
|
|
592
|
+
* when the run is not in progress (isLoading===false). The backend ends the run
|
|
593
|
+
* without emitting TOOL_CALL_RESULT for client tools, so result stays undefined.
|
|
594
|
+
* - resolve(id, result): marks the call as resolved, writes the outcome onto
|
|
595
|
+
* the local ToolCall in the store (so the transcript freezes: the mounted
|
|
596
|
+
* ask component re-renders with its emitted value as props and can branch to
|
|
597
|
+
* a frozen state), adds a ToolMessage via source.addMessage, then re-runs
|
|
598
|
+
* the agent with the catalog tools attached.
|
|
599
|
+
*
|
|
600
|
+
* Call catalogAsAgUiTools() to get the current catalog as AG-UI Tool[] for
|
|
601
|
+
* threading into runAgent().
|
|
602
|
+
*/
|
|
603
|
+
function createClientToolsCapability(source, store) {
|
|
604
|
+
const catalog = signal([], ...(ngDevMode ? [{ debugName: "catalog" }] : []));
|
|
605
|
+
const resolvedIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "resolvedIds" }] : []));
|
|
606
|
+
function catalogAsAgUiTools() {
|
|
607
|
+
return catalog().map(toAgUiTool);
|
|
608
|
+
}
|
|
609
|
+
const clientTools = {
|
|
610
|
+
setCatalog(specs) {
|
|
611
|
+
catalog.set([...specs]);
|
|
612
|
+
},
|
|
613
|
+
pending: computed(() => {
|
|
614
|
+
// Client tools are only actionable after the run ends (backend signals it
|
|
615
|
+
// by ending the run WITHOUT emitting TOOL_CALL_RESULT for client tools).
|
|
616
|
+
if (store.isLoading())
|
|
617
|
+
return [];
|
|
618
|
+
const names = new Set(catalog().map((s) => s.name));
|
|
619
|
+
const done = resolvedIds();
|
|
620
|
+
return store.toolCalls().filter((tc) => names.has(tc.name) && tc.result === undefined && !done.has(tc.id));
|
|
621
|
+
}),
|
|
622
|
+
resolve(id, result) {
|
|
623
|
+
// Mark as resolved first so pending() drops it immediately.
|
|
624
|
+
resolvedIds.update((s) => new Set(s).add(id));
|
|
625
|
+
// Write the outcome onto the LOCAL ToolCall in the store. The client tool
|
|
626
|
+
// DID produce a result client-side, so this is semantically correct — and
|
|
627
|
+
// it freezes the transcript card: toToolViewSpec spreads `{...args,
|
|
628
|
+
// ...result, status}` into the mounted ask component, so the component
|
|
629
|
+
// re-renders with its own emitted value as props and can branch to a
|
|
630
|
+
// resolved/frozen state. The backend ToolMessage never reaches this local
|
|
631
|
+
// ToolCall, so without this write the card stays interactive forever.
|
|
632
|
+
const ok = result.ok;
|
|
633
|
+
const value = result.value;
|
|
634
|
+
const error = result.error;
|
|
635
|
+
store.toolCalls.update((calls) => calls.map((tc) => tc.id === id
|
|
636
|
+
? {
|
|
637
|
+
...tc,
|
|
638
|
+
result: ok ? value : { error },
|
|
639
|
+
...(ok ? {} : { error, status: 'error' }),
|
|
640
|
+
}
|
|
641
|
+
: tc));
|
|
642
|
+
// Cast rather than rely on discriminant narrowing: consumer apps that
|
|
643
|
+
// compile this source with `strictNullChecks: false` don't narrow the
|
|
644
|
+
// ClientToolResult union in a ternary.
|
|
645
|
+
const content = ok
|
|
646
|
+
? safeStringify(value)
|
|
647
|
+
: `Error: ${error}`;
|
|
648
|
+
source.addMessage({
|
|
649
|
+
id: `tool-${id}`,
|
|
650
|
+
role: 'tool',
|
|
651
|
+
toolCallId: id,
|
|
652
|
+
content,
|
|
653
|
+
});
|
|
654
|
+
void source.runAgent({ tools: catalogAsAgUiTools() });
|
|
655
|
+
},
|
|
656
|
+
catalogAsAgUiTools,
|
|
657
|
+
};
|
|
658
|
+
return clientTools;
|
|
659
|
+
}
|
|
660
|
+
|
|
435
661
|
// SPDX-License-Identifier: MIT
|
|
436
662
|
function captureAgentRuntimeTelemetry(sink, event, properties) {
|
|
437
663
|
if (!sink)
|
|
@@ -477,10 +703,71 @@ function toAgent(source, options = {}) {
|
|
|
477
703
|
error: signal(null),
|
|
478
704
|
toolCalls: signal([]),
|
|
479
705
|
state: signal({}),
|
|
706
|
+
interrupt: signal(undefined),
|
|
480
707
|
events$: new Subject(),
|
|
708
|
+
customEvents: signal([]),
|
|
709
|
+
activities: signal(new Map()),
|
|
481
710
|
};
|
|
482
711
|
const telemetryProperties = { transport: 'ag-ui', surface: 'to_agent' };
|
|
483
712
|
let activeRun = null;
|
|
713
|
+
// Set by stop(); lets run-failure handlers distinguish a user-initiated
|
|
714
|
+
// abort (graceful cancel) from a genuine stream failure.
|
|
715
|
+
let abortRequested = false;
|
|
716
|
+
// Set to true the first time settleIfAborted() handles an abort error for the
|
|
717
|
+
// current run. The AG-UI client can surface the same abort via both the event
|
|
718
|
+
// stream (RUN_ERROR event) AND onRunFailed — abortSettled lets the second
|
|
719
|
+
// delivery see through as a no-op rather than re-writing store state or
|
|
720
|
+
// triggering a real error path. Both flags are reset together at the top of
|
|
721
|
+
// submit() so the next run starts clean.
|
|
722
|
+
let abortSettled = false;
|
|
723
|
+
function isAbortError(error) {
|
|
724
|
+
return error instanceof Error
|
|
725
|
+
&& (error.name === 'AbortError' || /abort/i.test(error.message));
|
|
726
|
+
}
|
|
727
|
+
/** Settles the store as idle for stop()-induced failures; returns true if handled. */
|
|
728
|
+
function settleIfAborted(error) {
|
|
729
|
+
// If we already settled this abort (duplicate delivery — e.g. RUN_ERROR
|
|
730
|
+
// event THEN onRunFailed), defensively re-apply the idle settle so any
|
|
731
|
+
// state written between the two deliveries (e.g. RUN_STARTED from a new
|
|
732
|
+
// run that started before flags were reset) is corrected. Telemetry is
|
|
733
|
+
// not re-emitted — the guard returns true to suppress further processing.
|
|
734
|
+
if (abortSettled && isAbortError(error)) {
|
|
735
|
+
store.status.set('idle');
|
|
736
|
+
store.isLoading.set(false);
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
if (!abortRequested || !isAbortError(error))
|
|
740
|
+
return false;
|
|
741
|
+
abortRequested = false;
|
|
742
|
+
abortSettled = true;
|
|
743
|
+
store.status.set('idle');
|
|
744
|
+
store.isLoading.set(false);
|
|
745
|
+
// Not a failure: leave store.error null and close out telemetry as a
|
|
746
|
+
// normal finish so the aborted run doesn't count as errored.
|
|
747
|
+
const run = activeRun;
|
|
748
|
+
if (run) {
|
|
749
|
+
finishRunTelemetry(run);
|
|
750
|
+
// Mark errored so any subsequent finishRunTelemetry/failRunTelemetry
|
|
751
|
+
// call on the same run object (e.g. from submit's try block resolving
|
|
752
|
+
// after the abort) is a no-op — telemetry fires at most once per run.
|
|
753
|
+
run.errored = true;
|
|
754
|
+
}
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
// Build the client-tools capability. catalogAsAgUiTools() is used below to
|
|
758
|
+
// thread the catalog into every runAgent() call.
|
|
759
|
+
const clientToolsCap = createClientToolsCapability(source, store);
|
|
760
|
+
/** Forward a neutral-contract state patch onto the AG-UI run input.
|
|
761
|
+
* Mirrors the canonical demo's `input.state` mechanism: the patch is
|
|
762
|
+
* merged into the source agent's client state (carried on
|
|
763
|
+
* RunAgentInput.state) and reflected optimistically in the local
|
|
764
|
+
* state signal — the server's next STATE_SNAPSHOT stays authoritative. */
|
|
765
|
+
const applyStatePatch = (patch) => {
|
|
766
|
+
if (!patch || Object.keys(patch).length === 0)
|
|
767
|
+
return;
|
|
768
|
+
source.state = { ...(source.state ?? {}), ...patch };
|
|
769
|
+
store.state.update((prev) => ({ ...prev, ...patch }));
|
|
770
|
+
};
|
|
484
771
|
captureAgentRuntimeTelemetry(options.telemetry, 'ngaf:runtime_instance_created', telemetryProperties);
|
|
485
772
|
function startRunTelemetry(requestType) {
|
|
486
773
|
const run = { startedAt: Date.now(), errored: false };
|
|
@@ -518,15 +805,44 @@ function toAgent(source, options = {}) {
|
|
|
518
805
|
// This subscription lives for the lifetime of `source`.
|
|
519
806
|
source.subscribe({
|
|
520
807
|
onEvent({ event }) {
|
|
808
|
+
// The AG-UI client surfaces a user-initiated abort both as a
|
|
809
|
+
// RUN_ERROR event (here) and via onRunFailed; guard the event path too
|
|
810
|
+
// so the reducer never marks a deliberate stop as an error.
|
|
811
|
+
if (event.type === 'RUN_ERROR') {
|
|
812
|
+
const message = event.message ?? '';
|
|
813
|
+
if (settleIfAborted(new Error(message)))
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
521
816
|
reduceEvent(event, store);
|
|
522
817
|
},
|
|
523
818
|
onRunFailed({ error }) {
|
|
819
|
+
if (settleIfAborted(error))
|
|
820
|
+
return;
|
|
524
821
|
store.status.set('error');
|
|
525
822
|
store.isLoading.set(false);
|
|
526
823
|
store.error.set(error);
|
|
527
824
|
failRunTelemetry(error);
|
|
528
825
|
},
|
|
529
826
|
});
|
|
827
|
+
// Stable Subagent wrappers per messageId so chat-subagents (tracks by
|
|
828
|
+
// toolCallId) doesn't churn as activity content streams.
|
|
829
|
+
const subagentWrappers = new Map();
|
|
830
|
+
function subagentFor(id, entry) {
|
|
831
|
+
let w = subagentWrappers.get(id);
|
|
832
|
+
if (!w) {
|
|
833
|
+
w = {
|
|
834
|
+
toolCallId: entry.content()['toolCallId'] ?? id,
|
|
835
|
+
name: entry.content()['name'],
|
|
836
|
+
status: computed(() => entry.content()['status'] ?? 'running'),
|
|
837
|
+
messages: computed(() => [
|
|
838
|
+
{ id, role: 'assistant', content: String(entry.content()['text'] ?? '') },
|
|
839
|
+
]),
|
|
840
|
+
state: computed(() => entry.content()['state'] ?? {}),
|
|
841
|
+
};
|
|
842
|
+
subagentWrappers.set(id, w);
|
|
843
|
+
}
|
|
844
|
+
return w;
|
|
845
|
+
}
|
|
530
846
|
return {
|
|
531
847
|
messages: store.messages,
|
|
532
848
|
status: store.status,
|
|
@@ -534,8 +850,56 @@ function toAgent(source, options = {}) {
|
|
|
534
850
|
error: store.error,
|
|
535
851
|
toolCalls: store.toolCalls,
|
|
536
852
|
state: store.state,
|
|
853
|
+
interrupt: store.interrupt,
|
|
537
854
|
events$: store.events$.asObservable(),
|
|
855
|
+
customEvents: store.customEvents,
|
|
856
|
+
subagents: computed(() => {
|
|
857
|
+
const out = new Map();
|
|
858
|
+
for (const [id, entry] of store.activities()) {
|
|
859
|
+
if (entry.activityType !== 'subagent')
|
|
860
|
+
continue;
|
|
861
|
+
out.set(id, subagentFor(id, entry));
|
|
862
|
+
}
|
|
863
|
+
// Prune stale wrappers: keeps the cache bounded and prevents a reused
|
|
864
|
+
// tool-call-id from binding to an orphaned (pre-RUN_STARTED) content signal.
|
|
865
|
+
for (const id of subagentWrappers.keys()) {
|
|
866
|
+
if (!out.has(id))
|
|
867
|
+
subagentWrappers.delete(id);
|
|
868
|
+
}
|
|
869
|
+
return out;
|
|
870
|
+
}),
|
|
871
|
+
clientTools: clientToolsCap,
|
|
538
872
|
submit: async (input, _opts) => {
|
|
873
|
+
// Reset both abort flags so a new submit starts clean and genuine
|
|
874
|
+
// failures after a previous stop are never swallowed.
|
|
875
|
+
abortRequested = false;
|
|
876
|
+
abortSettled = false;
|
|
877
|
+
if (input.resume !== undefined) {
|
|
878
|
+
// Resume path: clear the pending interrupt and replay the run with the
|
|
879
|
+
// resume payload forwarded to the LangGraph backend via AG-UI's
|
|
880
|
+
// forwardedProps.command.resume mechanism.
|
|
881
|
+
applyStatePatch(input.state);
|
|
882
|
+
store.interrupt.set(undefined);
|
|
883
|
+
const run = startRunTelemetry('resume');
|
|
884
|
+
const tools = clientToolsCap.catalogAsAgUiTools();
|
|
885
|
+
try {
|
|
886
|
+
await source.runAgent({
|
|
887
|
+
forwardedProps: { command: { resume: input.resume } },
|
|
888
|
+
...(tools.length > 0 ? { tools } : {}),
|
|
889
|
+
});
|
|
890
|
+
finishRunTelemetry(run);
|
|
891
|
+
}
|
|
892
|
+
catch (err) {
|
|
893
|
+
if (!settleIfAborted(err)) {
|
|
894
|
+
store.status.set('error');
|
|
895
|
+
store.isLoading.set(false);
|
|
896
|
+
store.error.set(err);
|
|
897
|
+
failRunTelemetry(err, run);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
applyStatePatch(input.state);
|
|
539
903
|
// Optimistic append of user message to our signals and to the source
|
|
540
904
|
// agent's own message list so runAgent() sees the new message.
|
|
541
905
|
const userMsg = buildUserMessage(input);
|
|
@@ -545,26 +909,34 @@ function toAgent(source, options = {}) {
|
|
|
545
909
|
source.addMessage(userMsg);
|
|
546
910
|
}
|
|
547
911
|
const run = startRunTelemetry('submit');
|
|
912
|
+
const tools = clientToolsCap.catalogAsAgUiTools();
|
|
548
913
|
try {
|
|
549
|
-
await source.runAgent();
|
|
914
|
+
await source.runAgent(tools.length > 0 ? { tools } : undefined);
|
|
550
915
|
finishRunTelemetry(run);
|
|
551
916
|
}
|
|
552
917
|
catch (err) {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
918
|
+
if (!settleIfAborted(err)) {
|
|
919
|
+
store.status.set('error');
|
|
920
|
+
store.isLoading.set(false);
|
|
921
|
+
store.error.set(err);
|
|
922
|
+
failRunTelemetry(err, run);
|
|
923
|
+
}
|
|
559
924
|
}
|
|
560
925
|
},
|
|
561
926
|
stop: async () => {
|
|
927
|
+
abortRequested = true;
|
|
562
928
|
source.abortRun();
|
|
563
929
|
},
|
|
564
930
|
regenerate: async (assistantMessageIndex) => {
|
|
565
931
|
if (store.isLoading()) {
|
|
566
932
|
throw new Error('Cannot regenerate while agent is loading another response');
|
|
567
933
|
}
|
|
934
|
+
// Reset abort flags so a regenerate starts clean, exactly like submit().
|
|
935
|
+
// Without this, flags left over from a prior stop() would cause the
|
|
936
|
+
// duplicate-delivery guard in settleIfAborted() to silently swallow the
|
|
937
|
+
// abort error without settling, wedging the store in streaming/running.
|
|
938
|
+
abortRequested = false;
|
|
939
|
+
abortSettled = false;
|
|
568
940
|
const msgs = store.messages();
|
|
569
941
|
const target = msgs[assistantMessageIndex];
|
|
570
942
|
if (!target || target.role !== 'assistant') {
|
|
@@ -591,15 +963,18 @@ function toAgent(source, options = {}) {
|
|
|
591
963
|
// message in `trimmed` becomes the active prompt for the next run.
|
|
592
964
|
source.setMessages(trimmed);
|
|
593
965
|
const run = startRunTelemetry('regenerate');
|
|
966
|
+
const regenTools = clientToolsCap.catalogAsAgUiTools();
|
|
594
967
|
try {
|
|
595
|
-
await source.runAgent();
|
|
968
|
+
await source.runAgent(regenTools.length > 0 ? { tools: regenTools } : undefined);
|
|
596
969
|
finishRunTelemetry(run);
|
|
597
970
|
}
|
|
598
971
|
catch (err) {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
972
|
+
if (!settleIfAborted(err)) {
|
|
973
|
+
store.status.set('error');
|
|
974
|
+
store.isLoading.set(false);
|
|
975
|
+
store.error.set(err);
|
|
976
|
+
failRunTelemetry(err, run);
|
|
977
|
+
}
|
|
603
978
|
}
|
|
604
979
|
},
|
|
605
980
|
};
|
|
@@ -617,18 +992,31 @@ function randomId() {
|
|
|
617
992
|
}
|
|
618
993
|
|
|
619
994
|
// SPDX-License-Identifier: MIT
|
|
620
|
-
|
|
995
|
+
/**
|
|
996
|
+
* @internal — exported for spec access only. Consumers must use injectAgent().
|
|
997
|
+
* Both `provideAgent` and `provideFakeAgent` register the result of `toAgent()`,
|
|
998
|
+
* which is always an `AgUiAgent`, so the token is typed accordingly.
|
|
999
|
+
*/
|
|
1000
|
+
const AGENT = new InjectionToken('AGENT');
|
|
621
1001
|
/**
|
|
622
1002
|
* Provides an Agent instance wired through HttpAgent and toAgent.
|
|
623
1003
|
* Constructs an HttpAgent from config and wraps it in the runtime-neutral
|
|
624
1004
|
* Agent contract via toAgent(). Returns a provider array suitable for
|
|
625
1005
|
* bootstrapApplication or TestBed.configureTestingModule().
|
|
1006
|
+
*
|
|
1007
|
+
* **Static vs factory config.** Pass a plain `AgentConfig` object when the
|
|
1008
|
+
* config is known up front. Pass a `() => AgentConfig` factory when the config
|
|
1009
|
+
* depends on runtime/DI state — the factory runs inside an Angular injection
|
|
1010
|
+
* context, so it may call `inject()` to read services or route params.
|
|
626
1011
|
*/
|
|
627
|
-
function
|
|
1012
|
+
function provideAgent(configOrFactory) {
|
|
628
1013
|
return [
|
|
629
1014
|
{
|
|
630
|
-
provide:
|
|
1015
|
+
provide: AGENT,
|
|
631
1016
|
useFactory: () => {
|
|
1017
|
+
// useFactory runs in an injection context, so a config factory may
|
|
1018
|
+
// call inject() to read runtime/DI state.
|
|
1019
|
+
const config = typeof configOrFactory === 'function' ? configOrFactory() : configOrFactory;
|
|
632
1020
|
const source = new HttpAgent({
|
|
633
1021
|
url: config.url,
|
|
634
1022
|
...(config.agentId !== undefined ? { agentId: config.agentId } : {}),
|
|
@@ -641,11 +1029,16 @@ function provideAgUiAgent(config) {
|
|
|
641
1029
|
];
|
|
642
1030
|
}
|
|
643
1031
|
/**
|
|
644
|
-
* Injects the
|
|
645
|
-
* Use this in components or services
|
|
1032
|
+
* Injects the AG-UI agent from Angular's dependency injection container.
|
|
1033
|
+
* Use this in components or services provided via `provideAgent()` (or
|
|
1034
|
+
* `provideFakeAgent()`).
|
|
1035
|
+
*
|
|
1036
|
+
* Returns an `AgUiAgent` — the runtime-neutral `Agent` contract plus the
|
|
1037
|
+
* AG-UI-specific `customEvents` signal — so `customEvents` is reachable
|
|
1038
|
+
* directly, without casting.
|
|
646
1039
|
*/
|
|
647
|
-
function
|
|
648
|
-
return inject(
|
|
1040
|
+
function injectAgent() {
|
|
1041
|
+
return inject(AGENT);
|
|
649
1042
|
}
|
|
650
1043
|
|
|
651
1044
|
// libs/ag-ui/src/lib/testing/fake-agent.ts
|
|
@@ -725,15 +1118,15 @@ class FakeAgent extends AbstractAgent {
|
|
|
725
1118
|
}
|
|
726
1119
|
|
|
727
1120
|
/**
|
|
728
|
-
* Registers an in-process FakeAgent under
|
|
1121
|
+
* Registers an in-process FakeAgent under AGENT.
|
|
729
1122
|
*
|
|
730
1123
|
* Use for offline demos and development. Drop-in replacement for
|
|
731
|
-
*
|
|
1124
|
+
* provideAgent({ url }) when no real backend is available.
|
|
732
1125
|
*/
|
|
733
|
-
function
|
|
1126
|
+
function provideFakeAgent(config = {}) {
|
|
734
1127
|
return [
|
|
735
1128
|
{
|
|
736
|
-
provide:
|
|
1129
|
+
provide: AGENT,
|
|
737
1130
|
useFactory: () => toAgent(new FakeAgent(config)),
|
|
738
1131
|
},
|
|
739
1132
|
];
|
|
@@ -745,5 +1138,5 @@ function provideFakeAgUiAgent(config = {}) {
|
|
|
745
1138
|
* Generated bundle index. Do not edit.
|
|
746
1139
|
*/
|
|
747
1140
|
|
|
748
|
-
export {
|
|
1141
|
+
export { FakeAgent, bridgeCitationsState, injectAgent, provideAgent, provideFakeAgent, toAgent };
|
|
749
1142
|
//# sourceMappingURL=threadplane-ag-ui.mjs.map
|