@threadplane/langgraph 0.0.50 → 0.0.51

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,
@@ -2218,6 +2248,9 @@ function agent(options) {
2218
2248
  // CD anyway.
2219
2249
  const rawMessages = toSignal(messages$, { initialValue: [] });
2220
2250
  const statusSig = toSignal(status$, { initialValue: ResourceStatus.Idle });
2251
+ // Cast justified: error$ accepts only AgentError | undefined (bridge catch normalizes all errors via
2252
+ // toAgentError before calling next(); resetDerivedThreadState passes undefined). The BehaviorSubject
2253
+ // is typed unknown to satisfy StreamSubjects<unknown> invariance at the subjects-bag assignment.
2221
2254
  const errorSig = toSignal(error$, { initialValue: undefined });
2222
2255
  const hasValueSig = toSignal(hasValue$, { initialValue: false });
2223
2256
  const interruptSig = toSignal(interrupt$, { initialValue: undefined });
@@ -2314,6 +2347,12 @@ function agent(options) {
2314
2347
  return manager.submit(payload, request.options);
2315
2348
  },
2316
2349
  stop: () => manager.stop(),
2350
+ retry: async () => {
2351
+ if (isLoading())
2352
+ return; // no-op while a run is in flight
2353
+ error$.next(undefined); // clear the error before re-running
2354
+ await manager.resubmitLast();
2355
+ },
2317
2356
  clientTools: clientToolsCap,
2318
2357
  regenerate: async (assistantMessageIndex) => {
2319
2358
  if (isLoading()) {
@@ -2655,81 +2694,53 @@ const AGENT_CONFIG = new InjectionToken('AGENT_CONFIG');
2655
2694
  * @internal — exported for spec access only. Consumers must use `injectAgent()`.
2656
2695
  */
2657
2696
  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) {
2697
+ /** @internal — shared factory that reads AGENT_CONFIG and constructs the singleton. */
2698
+ function agentFactory() {
2699
+ // useFactory runs in an injection context, so the legacy `agent()`
2700
+ // factory's `inject(DestroyRef)` calls work.
2701
+ const config = inject(AGENT_CONFIG);
2702
+ if (config.assistantId === undefined) {
2703
+ throw new Error('provideAgent: `assistantId` is required to construct the AGENT singleton.');
2704
+ }
2705
+ return agent({
2706
+ assistantId: config.assistantId,
2707
+ ...(config.apiUrl !== undefined ? { apiUrl: config.apiUrl } : {}),
2708
+ ...(config.threadId !== undefined ? { threadId: config.threadId } : {}),
2709
+ ...(config.onThreadId !== undefined ? { onThreadId: config.onThreadId } : {}),
2710
+ ...(config.initialValues !== undefined ? { initialValues: config.initialValues } : {}),
2711
+ ...(config.throttle !== undefined ? { throttle: config.throttle } : {}),
2712
+ ...(config.toMessage !== undefined ? { toMessage: config.toMessage } : {}),
2713
+ ...(config.transport !== undefined ? { transport: config.transport } : {}),
2714
+ ...(config.clientOptions !== undefined ? { clientOptions: config.clientOptions } : {}),
2715
+ ...(config.telemetry !== undefined ? { telemetry: config.telemetry } : {}),
2716
+ ...(config.filterSubagentMessages !== undefined ? { filterSubagentMessages: config.filterSubagentMessages } : {}),
2717
+ ...(config.subagentToolNames !== undefined ? { subagentToolNames: config.subagentToolNames } : {}),
2718
+ });
2719
+ }
2720
+ function isAgentRef(x) {
2721
+ return typeof x === 'object' && x !== null && 'token' in x;
2722
+ }
2723
+ function provideAgent(refOrConfig, maybeConfig) {
2724
+ const ref = isAgentRef(refOrConfig) ? refOrConfig : undefined;
2725
+ const configOrFactory = (ref ? maybeConfig : refOrConfig);
2685
2726
  // Resolve the factory (if any) lazily, inside the injection context of the
2686
2727
  // AGENT_CONFIG useFactory below — never at decoration time.
2687
2728
  const resolveConfig = () => typeof configOrFactory === 'function' ? configOrFactory() : configOrFactory;
2688
- return [
2729
+ const providers = [
2689
2730
  // AGENT_CONFIG resolves the config once (running the factory in an
2690
2731
  // injection context if a factory was passed). AGENT reads the resolved
2691
2732
  // config from here, so the factory is invoked exactly once.
2692
2733
  { 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
- },
2734
+ { provide: AGENT, useFactory: (agentFactory) },
2718
2735
  ];
2736
+ if (ref)
2737
+ providers.push({ provide: ref.token, useExisting: AGENT });
2738
+ return providers;
2719
2739
  }
2720
2740
 
2721
2741
  // 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);
2742
+ function injectAgent(ref) {
2743
+ return inject(ref ? ref.token : AGENT);
2733
2744
  }
2734
2745
 
2735
2746
  // SPDX-License-Identifier: MIT