@threadplane/langgraph 0.0.49 → 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.
@@ -1,11 +1,11 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, Injectable, inject, DestroyRef, isSignal, computed, effect, InjectionToken } from '@angular/core';
2
+ import { signal, Injectable, InjectionToken, computed, inject, DestroyRef, isSignal, effect } from '@angular/core';
3
3
  import { toObservable, toSignal } from '@angular/core/rxjs-interop';
4
4
  import { takeUntil, Subject, BehaviorSubject, of, throttleTime, asyncScheduler } from 'rxjs';
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
  /**
@@ -56,14 +56,26 @@ const ResourceStatus = {
56
56
  * transport (`fetch-stream.transport.ts`) and the threads adapter
57
57
  * (`LangGraphThreadsAdapter`) both go through here.
58
58
  *
59
+ * `clientOptions.maxRetries` maps to the SDK's `callerOptions.maxRetries`,
60
+ * which governs how many times a failed request (including the initial
61
+ * stream connect) is retried with exponential backoff before the error
62
+ * surfaces. Omitted → the SDK default (currently 4). Apps under test set
63
+ * `0` so a forced connection failure surfaces immediately instead of after
64
+ * the full backoff window.
65
+ *
59
66
  * @example
60
67
  * ```ts
61
68
  * const client = createLangGraphClient(environment.langGraphApiUrl);
62
69
  * const threads = await client.threads.search({ limit: 50 });
63
70
  * ```
64
71
  */
65
- function createLangGraphClient(apiUrl) {
66
- return new Client({ apiUrl: toAbsoluteApiUrl(apiUrl) });
72
+ function createLangGraphClient(apiUrl, clientOptions) {
73
+ return new Client({
74
+ apiUrl: toAbsoluteApiUrl(apiUrl),
75
+ ...(clientOptions?.maxRetries !== undefined
76
+ ? { callerOptions: { maxRetries: clientOptions.maxRetries } }
77
+ : {}),
78
+ });
67
79
  }
68
80
  /** Exported separately so non-Client callers (e.g. raw fetch) can
69
81
  * share the same normalization logic. */
@@ -93,12 +105,13 @@ class FetchStreamTransport {
93
105
  /**
94
106
  * @param apiUrl - Base URL of the LangGraph Platform API
95
107
  * @param onThreadId - Optional callback invoked when a new thread is created
108
+ * @param clientOptions - Optional SDK client tuning (e.g. `maxRetries`)
96
109
  */
97
- constructor(apiUrl, onThreadId) {
110
+ constructor(apiUrl, onThreadId, clientOptions) {
98
111
  // createLangGraphClient handles the absolute-URL normalization
99
112
  // required by the SDK when `apiUrl` is a relative `/api`-style
100
113
  // path proxied by middleware in production.
101
- this.client = createLangGraphClient(apiUrl);
114
+ this.client = createLangGraphClient(apiUrl, clientOptions);
102
115
  this.onThreadId = onThreadId;
103
116
  }
104
117
  /** Open a streaming connection, creating a thread if needed. */
@@ -517,13 +530,15 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
517
530
  currentThreadId = id;
518
531
  userOnThreadId?.(id);
519
532
  };
520
- const transport = options.transport ?? new FetchStreamTransport(options.apiUrl, wrappedOnThreadId);
533
+ const transport = options.transport ?? new FetchStreamTransport(options.apiUrl, wrappedOnThreadId, options.clientOptions);
521
534
  let currentThreadId = null;
522
535
  let lastPayload = null;
523
536
  let lastOptions;
524
537
  let abortController = null;
525
538
  let historyAbortController = null;
526
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;
527
542
  const toolProgressMap = new Map();
528
543
  const queuedRuns = [];
529
544
  let drainingQueue = false;
@@ -557,7 +572,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
557
572
  subjects.toolCalls$.next([]);
558
573
  subjects.messageMetadata$.next(new Map());
559
574
  subjects.subagents$.next(new Map());
560
- void cancelQueueEntries(takeQueuedRuns()).catch(err => subjects.error$.next(err));
575
+ void cancelQueueEntries(takeQueuedRuns()).catch(err => subjects.error$.next(toAgentError(err)));
561
576
  publishQueue();
562
577
  subjects.custom$.next([]);
563
578
  subjects.isThreadLoading$.next(false);
@@ -634,7 +649,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
634
649
  }
635
650
  catch (err) {
636
651
  if (!controller.signal.aborted && err?.name !== 'AbortError') {
637
- subjects.error$.next(err);
652
+ subjects.error$.next(toAgentError(err));
638
653
  }
639
654
  }
640
655
  finally {
@@ -746,7 +761,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
746
761
  }
747
762
  }
748
763
  catch (err) {
749
- subjects.error$.next(err);
764
+ subjects.error$.next(toAgentError(err));
750
765
  subjects.status$.next(ResourceStatus.Error);
751
766
  captureAgentRuntimeTelemetry(options.telemetry, 'ngaf:stream_errored', {
752
767
  ...telemetryProperties,
@@ -758,6 +773,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
758
773
  async function runStream(payload, opts, requestType = 'submit') {
759
774
  abortController?.abort();
760
775
  abortController = new AbortController();
776
+ userAbortRequested = false;
761
777
  const startedAt = Date.now();
762
778
  captureRuntimeRequestTelemetry(requestType);
763
779
  captureAgentRuntimeTelemetry(options.telemetry, 'ngaf:stream_started', telemetryProperties);
@@ -768,6 +784,10 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
768
784
  toolProgressMap.clear();
769
785
  lastPayload = payload;
770
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;
771
791
  // Optimistically inject human messages so they appear immediately
772
792
  // without waiting for the server to echo them back. Assign a stable id
773
793
  // when missing — track-by-id in the chat-message-list relies on stable
@@ -791,6 +811,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
791
811
  for await (const event of iter) {
792
812
  if (abortController.signal.aborted)
793
813
  break;
814
+ streamingStarted = true;
794
815
  processEvent(event);
795
816
  }
796
817
  if (!abortController.signal.aborted) {
@@ -806,11 +827,26 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
806
827
  }
807
828
  }
808
829
  catch (err) {
809
- if (err?.name === 'AbortError') {
810
- 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
+ });
811
847
  }
812
848
  else {
813
- subjects.error$.next(err);
849
+ subjects.error$.next(toAgentError(err));
814
850
  subjects.status$.next(ResourceStatus.Error);
815
851
  captureAgentRuntimeTelemetry(options.telemetry, 'ngaf:stream_errored', {
816
852
  ...telemetryProperties,
@@ -945,7 +981,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
945
981
  break;
946
982
  }
947
983
  case 'error':
948
- subjects.error$.next(event['error']);
984
+ subjects.error$.next(toAgentError(event['error']));
949
985
  subjects.status$.next(ResourceStatus.Error);
950
986
  break;
951
987
  case 'interrupt':
@@ -1089,9 +1125,16 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
1089
1125
  await runStream(payload, opts);
1090
1126
  },
1091
1127
  stop: async () => {
1128
+ userAbortRequested = true;
1092
1129
  abortController?.abort();
1093
1130
  await clearQueue();
1094
- 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
+ }
1095
1138
  },
1096
1139
  switchThread: (id) => {
1097
1140
  setThreadId(id, true);
@@ -1123,7 +1166,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
1123
1166
  });
1124
1167
  }
1125
1168
  catch (err) {
1126
- subjects.error$.next(err);
1169
+ subjects.error$.next(toAgentError(err));
1127
1170
  subjects.status$.next(ResourceStatus.Error);
1128
1171
  captureAgentRuntimeTelemetry(options.telemetry, 'ngaf:stream_errored', {
1129
1172
  ...telemetryProperties,
@@ -1696,6 +1739,29 @@ const _internalsForTesting = {
1696
1739
  isFinalCanonicalReasoningContent,
1697
1740
  };
1698
1741
 
1742
+ // SPDX-License-Identifier: MIT
1743
+ /**
1744
+ * App-wide LangGraph SDK client tuning (e.g. `maxRetries`). Provide once at the
1745
+ * app root; both the agent's default {@link FetchStreamTransport} and the
1746
+ * {@link LangGraphThreadsAdapter} read it so the retry budget is configured in
1747
+ * one place. A call-site `agent({ clientOptions })` or per-agent
1748
+ * `provideAgent({ clientOptions })` overrides it for that agent.
1749
+ * Absent → the SDK default.
1750
+ */
1751
+ const LANGGRAPH_CLIENT_OPTIONS = new InjectionToken('LANGGRAPH_CLIENT_OPTIONS');
1752
+ /**
1753
+ * First-defined-wins resolution across precedence layers (highest first).
1754
+ * Whole-object semantics — no per-field merge — so the winning layer is the
1755
+ * single source for every option. Returns undefined when all layers are absent.
1756
+ */
1757
+ function resolveClientOptions(...layers) {
1758
+ for (const layer of layers) {
1759
+ if (layer)
1760
+ return layer;
1761
+ }
1762
+ return undefined;
1763
+ }
1764
+
1699
1765
  const ROOT_ID = '$';
1700
1766
  /**
1701
1767
  * Builds a branch-aware checkpoint tree from LangGraph thread history.
@@ -1826,6 +1892,124 @@ function normalizeCitation(entry, fallbackIndex) {
1826
1892
  };
1827
1893
  }
1828
1894
 
1895
+ // SPDX-License-Identifier: MIT
1896
+ /** Serialize a tool result value to a string for the ToolMessage content. */
1897
+ function safeStringify(v) {
1898
+ return typeof v === 'string' ? v : JSON.stringify(v);
1899
+ }
1900
+ /**
1901
+ * Merge client_tools into a run payload.
1902
+ *
1903
+ * If payload is null we keep it null — a null payload signals a no-input
1904
+ * resume (used by regenerate and command resumes) and the server must
1905
+ * receive null, not an object. The catalog can only be injected when the
1906
+ * payload is a plain object that the graph's add_messages reducer can
1907
+ * process; it cannot be injected into a command-resume (null payload) or
1908
+ * into an already-typed non-record payload.
1909
+ *
1910
+ * Returns a new object; never mutates the original.
1911
+ */
1912
+ function mergeClientTools(payload, catalog) {
1913
+ if (catalog.length === 0)
1914
+ return payload;
1915
+ if (payload === null || payload === undefined)
1916
+ return payload;
1917
+ if (typeof payload !== 'object' || Array.isArray(payload))
1918
+ return payload;
1919
+ return { ...payload, client_tools: catalog };
1920
+ }
1921
+ /**
1922
+ * Creates a ClientToolsCapability backed by a LangGraph submit function and
1923
+ * a store of tool-call signals. Extracted into a factory so it can be
1924
+ * unit-tested in isolation without standing up a full Angular DI environment.
1925
+ *
1926
+ * The capability:
1927
+ * - Maintains a catalog of client tool specs (setCatalog). The caller
1928
+ * is responsible for threading the catalog into every run payload via
1929
+ * mergeClientTools() before calling manager.submit — see agent.fn.ts.
1930
+ * - Exposes a `pending` computed signal: tool calls whose name is in the
1931
+ * catalog, have no backend result, and haven't been resolved client-side
1932
+ * yet — but ONLY when the run is not in progress (isLoading===false).
1933
+ * The backend ends the run without emitting a ToolMessage result for
1934
+ * client tools, so `result` stays undefined on those entries.
1935
+ * - resolve(id, result): marks the call as resolved, then issues a NEW
1936
+ * run on the SAME thread by calling submitFn with:
1937
+ * input: {
1938
+ * messages: [{ type: 'tool', role: 'tool', tool_call_id: id, content }],
1939
+ * client_tools: catalog(),
1940
+ * }
1941
+ * The `add_messages` reducer on the Python side appends the ToolMessage
1942
+ * to thread state. Including `client_tools` ensures the model sees the
1943
+ * full tool catalog on the continuation run.
1944
+ *
1945
+ * Catalog shipping: the catalog is NOT injected by this factory's
1946
+ * submitFn call in resolve() — the resolved-tool run builds the payload
1947
+ * directly. For normal submit/regenerate runs, the agent.fn.ts wrapper
1948
+ * uses mergeClientTools() to inject `client_tools` into the payload
1949
+ * before forwarding to manager.submit. This keeps injection concerns
1950
+ * co-located with the run-issuing call sites.
1951
+ */
1952
+ function createClientToolsCapability(submitFn, store) {
1953
+ const catalog = signal([], ...(ngDevMode ? [{ debugName: "catalog" }] : []));
1954
+ const resolvedIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "resolvedIds" }] : []));
1955
+ const pending = computed(() => {
1956
+ // Client tools are only actionable after the run ends (the backend
1957
+ // signals this by ending the run WITHOUT emitting a ToolMessage result
1958
+ // for client tools).
1959
+ if (store.isLoading())
1960
+ return [];
1961
+ const names = new Set(catalog().map((s) => s.name));
1962
+ const done = resolvedIds();
1963
+ return store.toolCalls().filter((tc) => names.has(tc.name) && tc.result === undefined && !done.has(tc.id));
1964
+ }, ...(ngDevMode ? [{ debugName: "pending" }] : []));
1965
+ const capability = {
1966
+ catalog,
1967
+ setCatalog(specs) {
1968
+ catalog.set([...specs]);
1969
+ },
1970
+ pending,
1971
+ resolve(id, result) {
1972
+ // Mark as resolved first so pending() drops it immediately.
1973
+ resolvedIds.update((s) => new Set(s).add(id));
1974
+ // Cast rather than rely on discriminant narrowing: consumer apps that
1975
+ // compile this source with `strictNullChecks: false` don't narrow the
1976
+ // ClientToolResult union in a ternary.
1977
+ const ok = result.ok;
1978
+ const value = result.value;
1979
+ const error = result.error;
1980
+ // Write the outcome onto the LOCAL ToolCall (via the adapter's override
1981
+ // layer). The client tool DID produce a result client-side, so this is
1982
+ // semantically correct — and it freezes the transcript card: the mounted
1983
+ // ask component re-renders with its own emitted value as props and can
1984
+ // branch to a resolved/frozen state. Without this, the LOCAL tool call
1985
+ // never gets a result (only the backend ToolMessage does) so the card
1986
+ // stays interactive forever.
1987
+ store.applyClientResult(id, {
1988
+ result: ok ? value : { error },
1989
+ ...(ok ? {} : { error, status: 'error' }),
1990
+ });
1991
+ const content = ok
1992
+ ? safeStringify(value)
1993
+ : `Error: ${error}`;
1994
+ // Issue a new run on the same thread. LangGraph's add_messages reducer
1995
+ // appends the ToolMessage to the thread state. `client_tools` is
1996
+ // included so the model sees the full tool catalog on the continuation.
1997
+ //
1998
+ // Message shape: both `type` and `role` are set for compatibility —
1999
+ // the LangGraph server's add_messages coercion reads `role` (Python
2000
+ // side), while the bridge's local optimistic-message path reads `type`
2001
+ // (via toMessage's normalizeMessageType). This mirrors the human-message
2002
+ // shape used in buildSubmitUpdate (agent.fn.ts line 732).
2003
+ const toolPayload = {
2004
+ messages: [{ type: 'tool', role: 'tool', tool_call_id: id, content }],
2005
+ client_tools: catalog(),
2006
+ };
2007
+ void submitFn(toolPayload);
2008
+ },
2009
+ };
2010
+ return capability;
2011
+ }
2012
+
1829
2013
  // SPDX-License-Identifier: MIT
1830
2014
  /**
1831
2015
  * Walk LangGraph history (newest-first) and pair each AIMessage id with
@@ -1899,11 +2083,15 @@ function agent(options) {
1899
2083
  // Injection context required
1900
2084
  const destroyRef = inject(DestroyRef);
1901
2085
  const globalConfig = inject(AGENT_CONFIG, { optional: true });
2086
+ const sharedClientOptions = inject(LANGGRAPH_CLIENT_OPTIONS, { optional: true });
1902
2087
  const destroy$ = new Subject();
1903
2088
  destroyRef.onDestroy(() => { destroy$.next(); destroy$.complete(); });
1904
2089
  // Merge: call-site options take precedence over global provider config
1905
2090
  const apiUrl = options.apiUrl ?? globalConfig?.apiUrl ?? '';
1906
2091
  const transport = options.transport ?? globalConfig?.transport;
2092
+ // clientOptions precedence: agent({...}) call-site → provideAgent config →
2093
+ // app-wide LANGGRAPH_CLIENT_OPTIONS token → SDK default.
2094
+ const clientOptions = resolveClientOptions(options.clientOptions, globalConfig?.clientOptions, sharedClientOptions);
1907
2095
  const init = (options.initialValues ?? {});
1908
2096
  // All subjects created before the bridge
1909
2097
  const status$ = new BehaviorSubject(ResourceStatus.Idle);
@@ -2039,7 +2227,7 @@ function agent(options) {
2039
2227
  lcThreadPersistedAt.set(Date.now());
2040
2228
  });
2041
2229
  const manager = createStreamManagerBridge({
2042
- options: { ...options, apiUrl, transport },
2230
+ options: { ...options, apiUrl, transport, clientOptions },
2043
2231
  subjects,
2044
2232
  threadId$,
2045
2233
  destroy$: destroy$.asObservable(),
@@ -2060,6 +2248,9 @@ function agent(options) {
2060
2248
  // CD anyway.
2061
2249
  const rawMessages = toSignal(messages$, { initialValue: [] });
2062
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.
2063
2254
  const errorSig = toSignal(error$, { initialValue: undefined });
2064
2255
  const hasValueSig = toSignal(hasValue$, { initialValue: false });
2065
2256
  const interruptSig = toSignal(interrupt$, { initialValue: undefined });
@@ -2083,7 +2274,21 @@ function agent(options) {
2083
2274
  // updates per token. DOM stability is provided by `track message.id`
2084
2275
  // in chat-message-list, not by Message identity.
2085
2276
  const messagesNeutral = computed(() => rawMessages().map((m) => toMessage(m, manager.getReasoningDurationMs)), ...(ngDevMode ? [{ debugName: "messagesNeutral" }] : []));
2086
- const toolCallsNeutral = computed(() => rawToolCalls().map(toToolCall), ...(ngDevMode ? [{ debugName: "toolCallsNeutral" }] : []));
2277
+ // Client-tool resolutions written client-side. The raw `toolCalls$` stream
2278
+ // (and thus `rawToolCalls`) only ever carries backend results — a resolved
2279
+ // client tool (`ask`/`view`) never receives a backend ToolMessage on its
2280
+ // LOCAL call. These overrides layer the client-side outcome over the raw
2281
+ // projection so the transcript card can freeze (see chat-tool-views
2282
+ // toToolViewSpec, which spreads `result` into the mounted component's props).
2283
+ const clientResultOverrides = signal(new Map(), ...(ngDevMode ? [{ debugName: "clientResultOverrides" }] : []));
2284
+ const toolCallsNeutral = computed(() => {
2285
+ const overrides = clientResultOverrides();
2286
+ return rawToolCalls().map((tc) => {
2287
+ const neutral = toToolCall(tc);
2288
+ const patch = overrides.get(neutral.id);
2289
+ return patch ? { ...neutral, ...patch } : neutral;
2290
+ });
2291
+ }, ...(ngDevMode ? [{ debugName: "toolCallsNeutral" }] : []));
2087
2292
  const statusNeutral = computed(() => mapStatus(statusSig()), ...(ngDevMode ? [{ debugName: "statusNeutral" }] : []));
2088
2293
  const stateNeutral = computed(() => {
2089
2294
  const v = value();
@@ -2102,6 +2307,16 @@ function agent(options) {
2102
2307
  const messageCheckpointsSig = computed(() => computeMessageCheckpoints(historySig()), ...(ngDevMode ? [{ debugName: "messageCheckpointsSig" }] : []));
2103
2308
  const experimentalBranchTree = computed(() => buildBranchTree(historySig()), ...(ngDevMode ? [{ debugName: "experimentalBranchTree" }] : []));
2104
2309
  const events$ = buildEvents$(customSig);
2310
+ // ── Client tools capability ──────────────────────────────────────────────
2311
+ // The capability takes a direct reference to manager.submit so it can issue
2312
+ // follow-up runs (resolve) without going through the full submit() wrapper.
2313
+ // The catalog is injected into every outbound payload via mergeClientTools()
2314
+ // in the submit wrapper below and in the resolve path inside the capability.
2315
+ const clientToolsCap = createClientToolsCapability((payload, opts) => manager.submit(payload, opts), {
2316
+ toolCalls: toolCallsNeutral,
2317
+ isLoading,
2318
+ applyClientResult: (id, patch) => clientResultOverrides.update((m) => new Map(m).set(id, patch)),
2319
+ });
2105
2320
  return {
2106
2321
  // ── Runtime-neutral surface (AgentWithHistory) ────────────────────────
2107
2322
  messages: messagesNeutral,
@@ -2125,9 +2340,20 @@ function agent(options) {
2125
2340
  lcInterruptResolvedAt.set(Date.now());
2126
2341
  }
2127
2342
  const request = buildSubmitRequest(input, opts);
2128
- return manager.submit(request.payload, request.options);
2343
+ // Thread the client-tools catalog into every outbound payload so the
2344
+ // backend middleware can merge them into the model's tool list. Null
2345
+ // payloads (regenerate re-runs, command resumes) are left unchanged.
2346
+ const payload = mergeClientTools(request.payload, clientToolsCap.catalog());
2347
+ return manager.submit(payload, request.options);
2129
2348
  },
2130
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
+ },
2356
+ clientTools: clientToolsCap,
2131
2357
  regenerate: async (assistantMessageIndex) => {
2132
2358
  if (isLoading()) {
2133
2359
  throw new Error('Cannot regenerate while agent is loading another response');
@@ -2468,80 +2694,53 @@ const AGENT_CONFIG = new InjectionToken('AGENT_CONFIG');
2468
2694
  * @internal — exported for spec access only. Consumers must use `injectAgent()`.
2469
2695
  */
2470
2696
  const AGENT = new InjectionToken('AGENT');
2471
- /**
2472
- * Wire the LangGraph adapter into Angular's dependency injection.
2473
- *
2474
- * Registers a singleton `LangGraphAgent` constructed from `config`. Retrieve it
2475
- * in any component with `injectAgent()`. Provide this at the application root
2476
- * (`app.config.ts`) for an app-wide agent.
2477
- *
2478
- * To use a different agent in a component subtree, re-provide
2479
- * `provideAgent({...})` in that component's `providers: []` array —
2480
- * Angular's hierarchical DI scopes the singleton accordingly.
2481
- *
2482
- * **Static vs factory config.** Pass a plain `AgentConfig` object when the
2483
- * config is known up front. Pass a `() => AgentConfig` factory when the config
2484
- * depends on runtime/DI state the factory runs inside an Angular injection
2485
- * context, so it may call `inject()` to read services, route params, or
2486
- * component-scoped signals:
2487
- *
2488
- * ```ts
2489
- * providers: [
2490
- * provideAgent(() => {
2491
- * const route = inject(ActivatedRoute);
2492
- * return { assistantId: 'chat', threadId: toSignal(route.paramMap) };
2493
- * }),
2494
- * ];
2495
- * ```
2496
- */
2497
- 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);
2498
2726
  // Resolve the factory (if any) lazily, inside the injection context of the
2499
2727
  // AGENT_CONFIG useFactory below — never at decoration time.
2500
2728
  const resolveConfig = () => typeof configOrFactory === 'function' ? configOrFactory() : configOrFactory;
2501
- return [
2729
+ const providers = [
2502
2730
  // AGENT_CONFIG resolves the config once (running the factory in an
2503
2731
  // injection context if a factory was passed). AGENT reads the resolved
2504
2732
  // config from here, so the factory is invoked exactly once.
2505
2733
  { provide: AGENT_CONFIG, useFactory: resolveConfig },
2506
- {
2507
- provide: AGENT,
2508
- useFactory: () => {
2509
- // useFactory runs in an injection context, so the legacy `agent()`
2510
- // factory's `inject(DestroyRef)` calls work.
2511
- const config = inject(AGENT_CONFIG);
2512
- if (config.assistantId === undefined) {
2513
- throw new Error('provideAgent: `assistantId` is required to construct the AGENT singleton.');
2514
- }
2515
- return agent({
2516
- assistantId: config.assistantId,
2517
- ...(config.apiUrl !== undefined ? { apiUrl: config.apiUrl } : {}),
2518
- ...(config.threadId !== undefined ? { threadId: config.threadId } : {}),
2519
- ...(config.onThreadId !== undefined ? { onThreadId: config.onThreadId } : {}),
2520
- ...(config.initialValues !== undefined ? { initialValues: config.initialValues } : {}),
2521
- ...(config.throttle !== undefined ? { throttle: config.throttle } : {}),
2522
- ...(config.toMessage !== undefined ? { toMessage: config.toMessage } : {}),
2523
- ...(config.transport !== undefined ? { transport: config.transport } : {}),
2524
- ...(config.telemetry !== undefined ? { telemetry: config.telemetry } : {}),
2525
- ...(config.filterSubagentMessages !== undefined ? { filterSubagentMessages: config.filterSubagentMessages } : {}),
2526
- ...(config.subagentToolNames !== undefined ? { subagentToolNames: config.subagentToolNames } : {}),
2527
- });
2528
- },
2529
- },
2734
+ { provide: AGENT, useFactory: (agentFactory) },
2530
2735
  ];
2736
+ if (ref)
2737
+ providers.push({ provide: ref.token, useExisting: AGENT });
2738
+ return providers;
2531
2739
  }
2532
2740
 
2533
2741
  // SPDX-License-Identifier: MIT
2534
- /**
2535
- * Retrieve the LangGraph-backed Agent from the current Angular injection context.
2536
- *
2537
- * Mirrors `@threadplane/ag-ui`'s `injectAgent()` so consumer code is identical
2538
- * regardless of which adapter is wired in `app.config.ts`. The agent is a
2539
- * singleton scoped to the injector that called `provideAgent()` — re-provide
2540
- * in a child component's `providers: []` to scope a different agent to that
2541
- * subtree (Angular's hierarchical DI handles the rest).
2542
- */
2543
- function injectAgent() {
2544
- return inject(AGENT);
2742
+ function injectAgent(ref) {
2743
+ return inject(ref ? ref.token : AGENT);
2545
2744
  }
2546
2745
 
2547
2746
  // SPDX-License-Identifier: MIT
@@ -2889,8 +3088,9 @@ const LANGGRAPH_CLIENT = new InjectionToken('LANGGRAPH_CLIENT');
2889
3088
  */
2890
3089
  class LangGraphThreadsAdapter {
2891
3090
  config = inject(LANGGRAPH_THREADS_CONFIG);
3091
+ sharedClientOptions = inject(LANGGRAPH_CLIENT_OPTIONS, { optional: true }) ?? undefined;
2892
3092
  client = inject(LANGGRAPH_CLIENT, { optional: true })
2893
- ?? createLangGraphClient(this.config.apiUrl);
3093
+ ?? createLangGraphClient(this.config.apiUrl, this.sharedClientOptions);
2894
3094
  fallback = this.config.titleFallback ?? 'Untitled';
2895
3095
  _threads = signal([], ...(ngDevMode ? [{ debugName: "_threads" }] : []));
2896
3096
  _archived = signal([], ...(ngDevMode ? [{ debugName: "_archived" }] : []));
@@ -3093,5 +3293,5 @@ function refreshOnTransition(watch, isActive, fn) {
3093
3293
  * Generated bundle index. Do not edit.
3094
3294
  */
3095
3295
 
3096
- export { AGENT_LIFECYCLE, AgentLifecycleRegistry, FakeStreamTransport, FetchStreamTransport, LANGGRAPH_CLIENT, LANGGRAPH_THREADS_CONFIG, LangGraphThreadsAdapter, MockAgentTransport, ResourceStatus, createLangGraphClient, extractCitations, injectAgent, mockLangGraphAgent, provideAgent, provideFakeAgent, refreshOnRunEnd, refreshOnTransition, toAbsoluteApiUrl };
3296
+ export { AGENT_LIFECYCLE, AgentLifecycleRegistry, FakeStreamTransport, FetchStreamTransport, LANGGRAPH_CLIENT, LANGGRAPH_CLIENT_OPTIONS, LANGGRAPH_THREADS_CONFIG, LangGraphThreadsAdapter, MockAgentTransport, ResourceStatus, createLangGraphClient, extractCitations, injectAgent, mockLangGraphAgent, provideAgent, provideFakeAgent, refreshOnRunEnd, refreshOnTransition, toAbsoluteApiUrl };
3097
3297
  //# sourceMappingURL=threadplane-langgraph.mjs.map