@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.
@@ -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
- const args = safeParseArgs(e.delta);
366
- store.toolCalls.update((prev) => prev.map((t) => t.id === e.toolCallId ? { ...t, args } : t));
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
- store.toolCalls.update((prev) => prev.map((t) => t.id === e.toolCallId ? { ...t, status: 'complete' } : t));
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
- store.toolCalls.update((prev) => prev.map((t) => t.id === e.toolCallId ? { ...t, result: e.content } : t));
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
- store.messages.set(e.messages ?? []);
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
- if (e.name === 'state_update' && isRecord(e.value)) {
401
- store.events$.next({ type: 'state_update', data: e.value });
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
- store.events$.next({ type: 'custom', name: e.name, data: e.value });
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
- // If the run was aborted via stop(), abortRun() resolves the promise
554
- // rather than rejecting — but catch any unexpected errors here.
555
- store.status.set('error');
556
- store.isLoading.set(false);
557
- store.error.set(err);
558
- failRunTelemetry(err, run);
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
- store.status.set('error');
600
- store.isLoading.set(false);
601
- store.error.set(err);
602
- failRunTelemetry(err, run);
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
- const AG_UI_AGENT = new InjectionToken('AG_UI_AGENT');
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 provideAgUiAgent(config) {
1012
+ function provideAgent(configOrFactory) {
628
1013
  return [
629
1014
  {
630
- provide: AG_UI_AGENT,
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 AG_UI_AGENT from Angular's dependency injection container.
645
- * Use this in components or services that have been provided via provideAgUiAgent().
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 injectAgUiAgent() {
648
- return inject(AG_UI_AGENT);
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 AG_UI_AGENT.
1121
+ * Registers an in-process FakeAgent under AGENT.
729
1122
  *
730
1123
  * Use for offline demos and development. Drop-in replacement for
731
- * provideAgUiAgent({ url }) when no real backend is available.
1124
+ * provideAgent({ url }) when no real backend is available.
732
1125
  */
733
- function provideFakeAgUiAgent(config = {}) {
1126
+ function provideFakeAgent(config = {}) {
734
1127
  return [
735
1128
  {
736
- provide: AG_UI_AGENT,
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 { AG_UI_AGENT, FakeAgent, bridgeCitationsState, injectAgUiAgent, provideAgUiAgent, provideFakeAgUiAgent, toAgent };
1141
+ export { FakeAgent, bridgeCitationsState, injectAgent, provideAgent, provideFakeAgent, toAgent };
749
1142
  //# sourceMappingURL=threadplane-ag-ui.mjs.map