@threadplane/langgraph 0.0.50 → 0.0.52

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.
@@ -5,7 +5,7 @@ import { takeUntil, Subject, BehaviorSubject, of, throttleTime, asyncScheduler }
5
5
  import { takeUntil as takeUntil$1 } from 'rxjs/operators';
6
6
  import { Client } from '@langchain/langgraph-sdk';
7
7
  import { getToolCallsWithResults } from '@langchain/langgraph-sdk/utils';
8
- import { mockAgent } from '@threadplane/chat';
8
+ import { toAgentError, isAbortError, AgentError, AGENT_ERROR_MESSAGES, mockAgent } from '@threadplane/chat';
9
9
 
10
10
  // SPDX-License-Identifier: MIT
11
11
  /**
@@ -537,6 +537,8 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
537
537
  let abortController = null;
538
538
  let historyAbortController = null;
539
539
  let hasSeenThreadId = false;
540
+ /** True when the current abort was user-initiated (via stop()). Reset at the start of every new runStream(). */
541
+ let userAbortRequested = false;
540
542
  const toolProgressMap = new Map();
541
543
  const queuedRuns = [];
542
544
  let drainingQueue = false;
@@ -570,7 +572,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
570
572
  subjects.toolCalls$.next([]);
571
573
  subjects.messageMetadata$.next(new Map());
572
574
  subjects.subagents$.next(new Map());
573
- void cancelQueueEntries(takeQueuedRuns()).catch(err => subjects.error$.next(err));
575
+ void cancelQueueEntries(takeQueuedRuns()).catch(err => subjects.error$.next(toAgentError(err)));
574
576
  publishQueue();
575
577
  subjects.custom$.next([]);
576
578
  subjects.isThreadLoading$.next(false);
@@ -647,7 +649,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
647
649
  }
648
650
  catch (err) {
649
651
  if (!controller.signal.aborted && err?.name !== 'AbortError') {
650
- subjects.error$.next(err);
652
+ subjects.error$.next(toAgentError(err));
651
653
  }
652
654
  }
653
655
  finally {
@@ -759,7 +761,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
759
761
  }
760
762
  }
761
763
  catch (err) {
762
- subjects.error$.next(err);
764
+ subjects.error$.next(toAgentError(err));
763
765
  subjects.status$.next(ResourceStatus.Error);
764
766
  captureAgentRuntimeTelemetry(options.telemetry, 'ngaf:stream_errored', {
765
767
  ...telemetryProperties,
@@ -771,6 +773,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
771
773
  async function runStream(payload, opts, requestType = 'submit') {
772
774
  abortController?.abort();
773
775
  abortController = new AbortController();
776
+ userAbortRequested = false;
774
777
  const startedAt = Date.now();
775
778
  captureRuntimeRequestTelemetry(requestType);
776
779
  captureAgentRuntimeTelemetry(options.telemetry, 'ngaf:stream_started', telemetryProperties);
@@ -781,6 +784,10 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
781
784
  toolProgressMap.clear();
782
785
  lastPayload = payload;
783
786
  lastOptions = opts;
787
+ // Tracks whether at least one stream event has been processed this run.
788
+ // Used to distinguish a mid-stream network interruption (kind:'interrupted')
789
+ // from a fresh connect failure (falls through to toAgentError classification).
790
+ let streamingStarted = false;
784
791
  // Optimistically inject human messages so they appear immediately
785
792
  // without waiting for the server to echo them back. Assign a stable id
786
793
  // when missing — track-by-id in the chat-message-list relies on stable
@@ -804,6 +811,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
804
811
  for await (const event of iter) {
805
812
  if (abortController.signal.aborted)
806
813
  break;
814
+ streamingStarted = true;
807
815
  processEvent(event);
808
816
  }
809
817
  if (!abortController.signal.aborted) {
@@ -819,11 +827,26 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
819
827
  }
820
828
  }
821
829
  catch (err) {
822
- if (err?.name === 'AbortError') {
823
- subjects.status$.next(ResourceStatus.Resolved);
830
+ if (isAbortError(err) && userAbortRequested) {
831
+ // User explicitly called stop() — treat as graceful idle, not an error.
832
+ subjects.status$.next(ResourceStatus.Idle);
833
+ }
834
+ else if (isAbortError(err)) {
835
+ // A non-user-requested abort: interrupted if a stream had started, else a
836
+ // connect-phase failure. Never "aborted" (that's reserved for user stop).
837
+ const e = streamingStarted
838
+ ? new AgentError({ kind: 'interrupted', message: AGENT_ERROR_MESSAGES.interrupted, retryable: true, cause: err })
839
+ : new AgentError({ kind: 'connection', message: AGENT_ERROR_MESSAGES.connection, retryable: true, cause: err });
840
+ subjects.error$.next(e);
841
+ subjects.status$.next(ResourceStatus.Error);
842
+ captureAgentRuntimeTelemetry(options.telemetry, 'ngaf:stream_errored', {
843
+ ...telemetryProperties,
844
+ durationMs: Date.now() - startedAt,
845
+ errorClass: agentRuntimeTelemetryErrorClass(err),
846
+ });
824
847
  }
825
848
  else {
826
- subjects.error$.next(err);
849
+ subjects.error$.next(toAgentError(err));
827
850
  subjects.status$.next(ResourceStatus.Error);
828
851
  captureAgentRuntimeTelemetry(options.telemetry, 'ngaf:stream_errored', {
829
852
  ...telemetryProperties,
@@ -958,7 +981,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
958
981
  break;
959
982
  }
960
983
  case 'error':
961
- subjects.error$.next(event['error']);
984
+ subjects.error$.next(toAgentError(event['error']));
962
985
  subjects.status$.next(ResourceStatus.Error);
963
986
  break;
964
987
  case 'interrupt':
@@ -1102,9 +1125,16 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
1102
1125
  await runStream(payload, opts);
1103
1126
  },
1104
1127
  stop: async () => {
1128
+ userAbortRequested = true;
1105
1129
  abortController?.abort();
1106
1130
  await clearQueue();
1107
- subjects.status$.next(ResourceStatus.Resolved);
1131
+ // Note: status is set to Idle by the runStream() catch when it sees
1132
+ // isAbortError && userAbortRequested. The explicit set here handles
1133
+ // the case where stop() is called when no stream is active (so the
1134
+ // catch never fires) or when clearQueue() raised an error.
1135
+ if (subjects.status$.value !== ResourceStatus.Idle) {
1136
+ subjects.status$.next(ResourceStatus.Idle);
1137
+ }
1108
1138
  },
1109
1139
  switchThread: (id) => {
1110
1140
  setThreadId(id, true);
@@ -1136,7 +1166,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
1136
1166
  });
1137
1167
  }
1138
1168
  catch (err) {
1139
- subjects.error$.next(err);
1169
+ subjects.error$.next(toAgentError(err));
1140
1170
  subjects.status$.next(ResourceStatus.Error);
1141
1171
  captureAgentRuntimeTelemetry(options.telemetry, 'ngaf:stream_errored', {
1142
1172
  ...telemetryProperties,
@@ -1830,6 +1860,19 @@ function findLatestDescendantId(parentId, childrenMap) {
1830
1860
  return latestId;
1831
1861
  }
1832
1862
 
1863
+ /**
1864
+ * Normalize {@link Citation}s out of a message's `additional_kwargs` (reading
1865
+ * `citations` or `sources`). Exposed for advanced consumers building custom
1866
+ * adapters or bridging non-LangGraph message shapes into the neutral
1867
+ * `Citation[]` form.
1868
+ *
1869
+ * @param msg Any object with an `additional_kwargs` bag (e.g. a LangChain message).
1870
+ * @returns The normalized citations, or `undefined` when none are present.
1871
+ * @example
1872
+ * ```ts
1873
+ * const citations = extractCitations(lcMessage);
1874
+ * ```
1875
+ */
1833
1876
  function extractCitations(msg) {
1834
1877
  const raw = msg.additional_kwargs?.['citations'] ?? msg.additional_kwargs?.['sources'];
1835
1878
  if (!Array.isArray(raw) || raw.length === 0)
@@ -2218,6 +2261,9 @@ function agent(options) {
2218
2261
  // CD anyway.
2219
2262
  const rawMessages = toSignal(messages$, { initialValue: [] });
2220
2263
  const statusSig = toSignal(status$, { initialValue: ResourceStatus.Idle });
2264
+ // Cast justified: error$ accepts only AgentError | undefined (bridge catch normalizes all errors via
2265
+ // toAgentError before calling next(); resetDerivedThreadState passes undefined). The BehaviorSubject
2266
+ // is typed unknown to satisfy StreamSubjects<unknown> invariance at the subjects-bag assignment.
2221
2267
  const errorSig = toSignal(error$, { initialValue: undefined });
2222
2268
  const hasValueSig = toSignal(hasValue$, { initialValue: false });
2223
2269
  const interruptSig = toSignal(interrupt$, { initialValue: undefined });
@@ -2314,6 +2360,12 @@ function agent(options) {
2314
2360
  return manager.submit(payload, request.options);
2315
2361
  },
2316
2362
  stop: () => manager.stop(),
2363
+ retry: async () => {
2364
+ if (isLoading())
2365
+ return; // no-op while a run is in flight
2366
+ error$.next(undefined); // clear the error before re-running
2367
+ await manager.resubmitLast();
2368
+ },
2317
2369
  clientTools: clientToolsCap,
2318
2370
  regenerate: async (assistantMessageIndex) => {
2319
2371
  if (isLoading()) {
@@ -2655,81 +2707,53 @@ const AGENT_CONFIG = new InjectionToken('AGENT_CONFIG');
2655
2707
  * @internal — exported for spec access only. Consumers must use `injectAgent()`.
2656
2708
  */
2657
2709
  const AGENT = new InjectionToken('AGENT');
2658
- /**
2659
- * Wire the LangGraph adapter into Angular's dependency injection.
2660
- *
2661
- * Registers a singleton `LangGraphAgent` constructed from `config`. Retrieve it
2662
- * in any component with `injectAgent()`. Provide this at the application root
2663
- * (`app.config.ts`) for an app-wide agent.
2664
- *
2665
- * To use a different agent in a component subtree, re-provide
2666
- * `provideAgent({...})` in that component's `providers: []` array —
2667
- * Angular's hierarchical DI scopes the singleton accordingly.
2668
- *
2669
- * **Static vs factory config.** Pass a plain `AgentConfig` object when the
2670
- * config is known up front. Pass a `() => AgentConfig` factory when the config
2671
- * depends on runtime/DI state the factory runs inside an Angular injection
2672
- * context, so it may call `inject()` to read services, route params, or
2673
- * component-scoped signals:
2674
- *
2675
- * ```ts
2676
- * providers: [
2677
- * provideAgent(() => {
2678
- * const route = inject(ActivatedRoute);
2679
- * return { assistantId: 'chat', threadId: toSignal(route.paramMap) };
2680
- * }),
2681
- * ];
2682
- * ```
2683
- */
2684
- function provideAgent(configOrFactory) {
2710
+ /** @internal — shared factory that reads AGENT_CONFIG and constructs the singleton. */
2711
+ function agentFactory() {
2712
+ // useFactory runs in an injection context, so the legacy `agent()`
2713
+ // factory's `inject(DestroyRef)` calls work.
2714
+ const config = inject(AGENT_CONFIG);
2715
+ if (config.assistantId === undefined) {
2716
+ throw new Error('provideAgent: `assistantId` is required to construct the AGENT singleton.');
2717
+ }
2718
+ return agent({
2719
+ assistantId: config.assistantId,
2720
+ ...(config.apiUrl !== undefined ? { apiUrl: config.apiUrl } : {}),
2721
+ ...(config.threadId !== undefined ? { threadId: config.threadId } : {}),
2722
+ ...(config.onThreadId !== undefined ? { onThreadId: config.onThreadId } : {}),
2723
+ ...(config.initialValues !== undefined ? { initialValues: config.initialValues } : {}),
2724
+ ...(config.throttle !== undefined ? { throttle: config.throttle } : {}),
2725
+ ...(config.toMessage !== undefined ? { toMessage: config.toMessage } : {}),
2726
+ ...(config.transport !== undefined ? { transport: config.transport } : {}),
2727
+ ...(config.clientOptions !== undefined ? { clientOptions: config.clientOptions } : {}),
2728
+ ...(config.telemetry !== undefined ? { telemetry: config.telemetry } : {}),
2729
+ ...(config.filterSubagentMessages !== undefined ? { filterSubagentMessages: config.filterSubagentMessages } : {}),
2730
+ ...(config.subagentToolNames !== undefined ? { subagentToolNames: config.subagentToolNames } : {}),
2731
+ });
2732
+ }
2733
+ function isAgentRef(x) {
2734
+ return typeof x === 'object' && x !== null && 'token' in x;
2735
+ }
2736
+ function provideAgent(refOrConfig, maybeConfig) {
2737
+ const ref = isAgentRef(refOrConfig) ? refOrConfig : undefined;
2738
+ const configOrFactory = (ref ? maybeConfig : refOrConfig);
2685
2739
  // Resolve the factory (if any) lazily, inside the injection context of the
2686
2740
  // AGENT_CONFIG useFactory below — never at decoration time.
2687
2741
  const resolveConfig = () => typeof configOrFactory === 'function' ? configOrFactory() : configOrFactory;
2688
- return [
2742
+ const providers = [
2689
2743
  // AGENT_CONFIG resolves the config once (running the factory in an
2690
2744
  // injection context if a factory was passed). AGENT reads the resolved
2691
2745
  // config from here, so the factory is invoked exactly once.
2692
2746
  { provide: AGENT_CONFIG, useFactory: resolveConfig },
2693
- {
2694
- provide: AGENT,
2695
- useFactory: () => {
2696
- // useFactory runs in an injection context, so the legacy `agent()`
2697
- // factory's `inject(DestroyRef)` calls work.
2698
- const config = inject(AGENT_CONFIG);
2699
- if (config.assistantId === undefined) {
2700
- throw new Error('provideAgent: `assistantId` is required to construct the AGENT singleton.');
2701
- }
2702
- return agent({
2703
- assistantId: config.assistantId,
2704
- ...(config.apiUrl !== undefined ? { apiUrl: config.apiUrl } : {}),
2705
- ...(config.threadId !== undefined ? { threadId: config.threadId } : {}),
2706
- ...(config.onThreadId !== undefined ? { onThreadId: config.onThreadId } : {}),
2707
- ...(config.initialValues !== undefined ? { initialValues: config.initialValues } : {}),
2708
- ...(config.throttle !== undefined ? { throttle: config.throttle } : {}),
2709
- ...(config.toMessage !== undefined ? { toMessage: config.toMessage } : {}),
2710
- ...(config.transport !== undefined ? { transport: config.transport } : {}),
2711
- ...(config.clientOptions !== undefined ? { clientOptions: config.clientOptions } : {}),
2712
- ...(config.telemetry !== undefined ? { telemetry: config.telemetry } : {}),
2713
- ...(config.filterSubagentMessages !== undefined ? { filterSubagentMessages: config.filterSubagentMessages } : {}),
2714
- ...(config.subagentToolNames !== undefined ? { subagentToolNames: config.subagentToolNames } : {}),
2715
- });
2716
- },
2717
- },
2747
+ { provide: AGENT, useFactory: (agentFactory) },
2718
2748
  ];
2749
+ if (ref)
2750
+ providers.push({ provide: ref.token, useExisting: AGENT });
2751
+ return providers;
2719
2752
  }
2720
2753
 
2721
2754
  // SPDX-License-Identifier: MIT
2722
- /**
2723
- * Retrieve the LangGraph-backed Agent from the current Angular injection context.
2724
- *
2725
- * Mirrors `@threadplane/ag-ui`'s `injectAgent()` so consumer code is identical
2726
- * regardless of which adapter is wired in `app.config.ts`. The agent is a
2727
- * singleton scoped to the injector that called `provideAgent()` — re-provide
2728
- * in a child component's `providers: []` to scope a different agent to that
2729
- * subtree (Angular's hierarchical DI handles the rest).
2730
- */
2731
- function injectAgent() {
2732
- return inject(AGENT);
2755
+ function injectAgent(ref) {
2756
+ return inject(ref ? ref.token : AGENT);
2733
2757
  }
2734
2758
 
2735
2759
  // SPDX-License-Identifier: MIT