@threadplane/langgraph 0.0.49 → 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,5 +1,5 @@
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';
@@ -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,7 +530,7 @@ 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;
@@ -1696,6 +1709,29 @@ const _internalsForTesting = {
1696
1709
  isFinalCanonicalReasoningContent,
1697
1710
  };
1698
1711
 
1712
+ // SPDX-License-Identifier: MIT
1713
+ /**
1714
+ * App-wide LangGraph SDK client tuning (e.g. `maxRetries`). Provide once at the
1715
+ * app root; both the agent's default {@link FetchStreamTransport} and the
1716
+ * {@link LangGraphThreadsAdapter} read it so the retry budget is configured in
1717
+ * one place. A call-site `agent({ clientOptions })` or per-agent
1718
+ * `provideAgent({ clientOptions })` overrides it for that agent.
1719
+ * Absent → the SDK default.
1720
+ */
1721
+ const LANGGRAPH_CLIENT_OPTIONS = new InjectionToken('LANGGRAPH_CLIENT_OPTIONS');
1722
+ /**
1723
+ * First-defined-wins resolution across precedence layers (highest first).
1724
+ * Whole-object semantics — no per-field merge — so the winning layer is the
1725
+ * single source for every option. Returns undefined when all layers are absent.
1726
+ */
1727
+ function resolveClientOptions(...layers) {
1728
+ for (const layer of layers) {
1729
+ if (layer)
1730
+ return layer;
1731
+ }
1732
+ return undefined;
1733
+ }
1734
+
1699
1735
  const ROOT_ID = '$';
1700
1736
  /**
1701
1737
  * Builds a branch-aware checkpoint tree from LangGraph thread history.
@@ -1826,6 +1862,124 @@ function normalizeCitation(entry, fallbackIndex) {
1826
1862
  };
1827
1863
  }
1828
1864
 
1865
+ // SPDX-License-Identifier: MIT
1866
+ /** Serialize a tool result value to a string for the ToolMessage content. */
1867
+ function safeStringify(v) {
1868
+ return typeof v === 'string' ? v : JSON.stringify(v);
1869
+ }
1870
+ /**
1871
+ * Merge client_tools into a run payload.
1872
+ *
1873
+ * If payload is null we keep it null — a null payload signals a no-input
1874
+ * resume (used by regenerate and command resumes) and the server must
1875
+ * receive null, not an object. The catalog can only be injected when the
1876
+ * payload is a plain object that the graph's add_messages reducer can
1877
+ * process; it cannot be injected into a command-resume (null payload) or
1878
+ * into an already-typed non-record payload.
1879
+ *
1880
+ * Returns a new object; never mutates the original.
1881
+ */
1882
+ function mergeClientTools(payload, catalog) {
1883
+ if (catalog.length === 0)
1884
+ return payload;
1885
+ if (payload === null || payload === undefined)
1886
+ return payload;
1887
+ if (typeof payload !== 'object' || Array.isArray(payload))
1888
+ return payload;
1889
+ return { ...payload, client_tools: catalog };
1890
+ }
1891
+ /**
1892
+ * Creates a ClientToolsCapability backed by a LangGraph submit function and
1893
+ * a store of tool-call signals. Extracted into a factory so it can be
1894
+ * unit-tested in isolation without standing up a full Angular DI environment.
1895
+ *
1896
+ * The capability:
1897
+ * - Maintains a catalog of client tool specs (setCatalog). The caller
1898
+ * is responsible for threading the catalog into every run payload via
1899
+ * mergeClientTools() before calling manager.submit — see agent.fn.ts.
1900
+ * - Exposes a `pending` computed signal: tool calls whose name is in the
1901
+ * catalog, have no backend result, and haven't been resolved client-side
1902
+ * yet — but ONLY when the run is not in progress (isLoading===false).
1903
+ * The backend ends the run without emitting a ToolMessage result for
1904
+ * client tools, so `result` stays undefined on those entries.
1905
+ * - resolve(id, result): marks the call as resolved, then issues a NEW
1906
+ * run on the SAME thread by calling submitFn with:
1907
+ * input: {
1908
+ * messages: [{ type: 'tool', role: 'tool', tool_call_id: id, content }],
1909
+ * client_tools: catalog(),
1910
+ * }
1911
+ * The `add_messages` reducer on the Python side appends the ToolMessage
1912
+ * to thread state. Including `client_tools` ensures the model sees the
1913
+ * full tool catalog on the continuation run.
1914
+ *
1915
+ * Catalog shipping: the catalog is NOT injected by this factory's
1916
+ * submitFn call in resolve() — the resolved-tool run builds the payload
1917
+ * directly. For normal submit/regenerate runs, the agent.fn.ts wrapper
1918
+ * uses mergeClientTools() to inject `client_tools` into the payload
1919
+ * before forwarding to manager.submit. This keeps injection concerns
1920
+ * co-located with the run-issuing call sites.
1921
+ */
1922
+ function createClientToolsCapability(submitFn, store) {
1923
+ const catalog = signal([], ...(ngDevMode ? [{ debugName: "catalog" }] : []));
1924
+ const resolvedIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "resolvedIds" }] : []));
1925
+ const pending = computed(() => {
1926
+ // Client tools are only actionable after the run ends (the backend
1927
+ // signals this by ending the run WITHOUT emitting a ToolMessage result
1928
+ // for client tools).
1929
+ if (store.isLoading())
1930
+ return [];
1931
+ const names = new Set(catalog().map((s) => s.name));
1932
+ const done = resolvedIds();
1933
+ return store.toolCalls().filter((tc) => names.has(tc.name) && tc.result === undefined && !done.has(tc.id));
1934
+ }, ...(ngDevMode ? [{ debugName: "pending" }] : []));
1935
+ const capability = {
1936
+ catalog,
1937
+ setCatalog(specs) {
1938
+ catalog.set([...specs]);
1939
+ },
1940
+ pending,
1941
+ resolve(id, result) {
1942
+ // Mark as resolved first so pending() drops it immediately.
1943
+ resolvedIds.update((s) => new Set(s).add(id));
1944
+ // Cast rather than rely on discriminant narrowing: consumer apps that
1945
+ // compile this source with `strictNullChecks: false` don't narrow the
1946
+ // ClientToolResult union in a ternary.
1947
+ const ok = result.ok;
1948
+ const value = result.value;
1949
+ const error = result.error;
1950
+ // Write the outcome onto the LOCAL ToolCall (via the adapter's override
1951
+ // layer). The client tool DID produce a result client-side, so this is
1952
+ // semantically correct — and it freezes the transcript card: the mounted
1953
+ // ask component re-renders with its own emitted value as props and can
1954
+ // branch to a resolved/frozen state. Without this, the LOCAL tool call
1955
+ // never gets a result (only the backend ToolMessage does) so the card
1956
+ // stays interactive forever.
1957
+ store.applyClientResult(id, {
1958
+ result: ok ? value : { error },
1959
+ ...(ok ? {} : { error, status: 'error' }),
1960
+ });
1961
+ const content = ok
1962
+ ? safeStringify(value)
1963
+ : `Error: ${error}`;
1964
+ // Issue a new run on the same thread. LangGraph's add_messages reducer
1965
+ // appends the ToolMessage to the thread state. `client_tools` is
1966
+ // included so the model sees the full tool catalog on the continuation.
1967
+ //
1968
+ // Message shape: both `type` and `role` are set for compatibility —
1969
+ // the LangGraph server's add_messages coercion reads `role` (Python
1970
+ // side), while the bridge's local optimistic-message path reads `type`
1971
+ // (via toMessage's normalizeMessageType). This mirrors the human-message
1972
+ // shape used in buildSubmitUpdate (agent.fn.ts line 732).
1973
+ const toolPayload = {
1974
+ messages: [{ type: 'tool', role: 'tool', tool_call_id: id, content }],
1975
+ client_tools: catalog(),
1976
+ };
1977
+ void submitFn(toolPayload);
1978
+ },
1979
+ };
1980
+ return capability;
1981
+ }
1982
+
1829
1983
  // SPDX-License-Identifier: MIT
1830
1984
  /**
1831
1985
  * Walk LangGraph history (newest-first) and pair each AIMessage id with
@@ -1899,11 +2053,15 @@ function agent(options) {
1899
2053
  // Injection context required
1900
2054
  const destroyRef = inject(DestroyRef);
1901
2055
  const globalConfig = inject(AGENT_CONFIG, { optional: true });
2056
+ const sharedClientOptions = inject(LANGGRAPH_CLIENT_OPTIONS, { optional: true });
1902
2057
  const destroy$ = new Subject();
1903
2058
  destroyRef.onDestroy(() => { destroy$.next(); destroy$.complete(); });
1904
2059
  // Merge: call-site options take precedence over global provider config
1905
2060
  const apiUrl = options.apiUrl ?? globalConfig?.apiUrl ?? '';
1906
2061
  const transport = options.transport ?? globalConfig?.transport;
2062
+ // clientOptions precedence: agent({...}) call-site → provideAgent config →
2063
+ // app-wide LANGGRAPH_CLIENT_OPTIONS token → SDK default.
2064
+ const clientOptions = resolveClientOptions(options.clientOptions, globalConfig?.clientOptions, sharedClientOptions);
1907
2065
  const init = (options.initialValues ?? {});
1908
2066
  // All subjects created before the bridge
1909
2067
  const status$ = new BehaviorSubject(ResourceStatus.Idle);
@@ -2039,7 +2197,7 @@ function agent(options) {
2039
2197
  lcThreadPersistedAt.set(Date.now());
2040
2198
  });
2041
2199
  const manager = createStreamManagerBridge({
2042
- options: { ...options, apiUrl, transport },
2200
+ options: { ...options, apiUrl, transport, clientOptions },
2043
2201
  subjects,
2044
2202
  threadId$,
2045
2203
  destroy$: destroy$.asObservable(),
@@ -2083,7 +2241,21 @@ function agent(options) {
2083
2241
  // updates per token. DOM stability is provided by `track message.id`
2084
2242
  // in chat-message-list, not by Message identity.
2085
2243
  const messagesNeutral = computed(() => rawMessages().map((m) => toMessage(m, manager.getReasoningDurationMs)), ...(ngDevMode ? [{ debugName: "messagesNeutral" }] : []));
2086
- const toolCallsNeutral = computed(() => rawToolCalls().map(toToolCall), ...(ngDevMode ? [{ debugName: "toolCallsNeutral" }] : []));
2244
+ // Client-tool resolutions written client-side. The raw `toolCalls$` stream
2245
+ // (and thus `rawToolCalls`) only ever carries backend results — a resolved
2246
+ // client tool (`ask`/`view`) never receives a backend ToolMessage on its
2247
+ // LOCAL call. These overrides layer the client-side outcome over the raw
2248
+ // projection so the transcript card can freeze (see chat-tool-views
2249
+ // toToolViewSpec, which spreads `result` into the mounted component's props).
2250
+ const clientResultOverrides = signal(new Map(), ...(ngDevMode ? [{ debugName: "clientResultOverrides" }] : []));
2251
+ const toolCallsNeutral = computed(() => {
2252
+ const overrides = clientResultOverrides();
2253
+ return rawToolCalls().map((tc) => {
2254
+ const neutral = toToolCall(tc);
2255
+ const patch = overrides.get(neutral.id);
2256
+ return patch ? { ...neutral, ...patch } : neutral;
2257
+ });
2258
+ }, ...(ngDevMode ? [{ debugName: "toolCallsNeutral" }] : []));
2087
2259
  const statusNeutral = computed(() => mapStatus(statusSig()), ...(ngDevMode ? [{ debugName: "statusNeutral" }] : []));
2088
2260
  const stateNeutral = computed(() => {
2089
2261
  const v = value();
@@ -2102,6 +2274,16 @@ function agent(options) {
2102
2274
  const messageCheckpointsSig = computed(() => computeMessageCheckpoints(historySig()), ...(ngDevMode ? [{ debugName: "messageCheckpointsSig" }] : []));
2103
2275
  const experimentalBranchTree = computed(() => buildBranchTree(historySig()), ...(ngDevMode ? [{ debugName: "experimentalBranchTree" }] : []));
2104
2276
  const events$ = buildEvents$(customSig);
2277
+ // ── Client tools capability ──────────────────────────────────────────────
2278
+ // The capability takes a direct reference to manager.submit so it can issue
2279
+ // follow-up runs (resolve) without going through the full submit() wrapper.
2280
+ // The catalog is injected into every outbound payload via mergeClientTools()
2281
+ // in the submit wrapper below and in the resolve path inside the capability.
2282
+ const clientToolsCap = createClientToolsCapability((payload, opts) => manager.submit(payload, opts), {
2283
+ toolCalls: toolCallsNeutral,
2284
+ isLoading,
2285
+ applyClientResult: (id, patch) => clientResultOverrides.update((m) => new Map(m).set(id, patch)),
2286
+ });
2105
2287
  return {
2106
2288
  // ── Runtime-neutral surface (AgentWithHistory) ────────────────────────
2107
2289
  messages: messagesNeutral,
@@ -2125,9 +2307,14 @@ function agent(options) {
2125
2307
  lcInterruptResolvedAt.set(Date.now());
2126
2308
  }
2127
2309
  const request = buildSubmitRequest(input, opts);
2128
- return manager.submit(request.payload, request.options);
2310
+ // Thread the client-tools catalog into every outbound payload so the
2311
+ // backend middleware can merge them into the model's tool list. Null
2312
+ // payloads (regenerate re-runs, command resumes) are left unchanged.
2313
+ const payload = mergeClientTools(request.payload, clientToolsCap.catalog());
2314
+ return manager.submit(payload, request.options);
2129
2315
  },
2130
2316
  stop: () => manager.stop(),
2317
+ clientTools: clientToolsCap,
2131
2318
  regenerate: async (assistantMessageIndex) => {
2132
2319
  if (isLoading()) {
2133
2320
  throw new Error('Cannot regenerate while agent is loading another response');
@@ -2521,6 +2708,7 @@ function provideAgent(configOrFactory) {
2521
2708
  ...(config.throttle !== undefined ? { throttle: config.throttle } : {}),
2522
2709
  ...(config.toMessage !== undefined ? { toMessage: config.toMessage } : {}),
2523
2710
  ...(config.transport !== undefined ? { transport: config.transport } : {}),
2711
+ ...(config.clientOptions !== undefined ? { clientOptions: config.clientOptions } : {}),
2524
2712
  ...(config.telemetry !== undefined ? { telemetry: config.telemetry } : {}),
2525
2713
  ...(config.filterSubagentMessages !== undefined ? { filterSubagentMessages: config.filterSubagentMessages } : {}),
2526
2714
  ...(config.subagentToolNames !== undefined ? { subagentToolNames: config.subagentToolNames } : {}),
@@ -2889,8 +3077,9 @@ const LANGGRAPH_CLIENT = new InjectionToken('LANGGRAPH_CLIENT');
2889
3077
  */
2890
3078
  class LangGraphThreadsAdapter {
2891
3079
  config = inject(LANGGRAPH_THREADS_CONFIG);
3080
+ sharedClientOptions = inject(LANGGRAPH_CLIENT_OPTIONS, { optional: true }) ?? undefined;
2892
3081
  client = inject(LANGGRAPH_CLIENT, { optional: true })
2893
- ?? createLangGraphClient(this.config.apiUrl);
3082
+ ?? createLangGraphClient(this.config.apiUrl, this.sharedClientOptions);
2894
3083
  fallback = this.config.titleFallback ?? 'Untitled';
2895
3084
  _threads = signal([], ...(ngDevMode ? [{ debugName: "_threads" }] : []));
2896
3085
  _archived = signal([], ...(ngDevMode ? [{ debugName: "_archived" }] : []));
@@ -3093,5 +3282,5 @@ function refreshOnTransition(watch, isActive, fn) {
3093
3282
  * Generated bundle index. Do not edit.
3094
3283
  */
3095
3284
 
3096
- export { AGENT_LIFECYCLE, AgentLifecycleRegistry, FakeStreamTransport, FetchStreamTransport, LANGGRAPH_CLIENT, LANGGRAPH_THREADS_CONFIG, LangGraphThreadsAdapter, MockAgentTransport, ResourceStatus, createLangGraphClient, extractCitations, injectAgent, mockLangGraphAgent, provideAgent, provideFakeAgent, refreshOnRunEnd, refreshOnTransition, toAbsoluteApiUrl };
3285
+ 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
3286
  //# sourceMappingURL=threadplane-langgraph.mjs.map