@threadplane/langgraph 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.
- package/README.md +163 -42
- package/fesm2022/threadplane-langgraph.mjs +412 -54
- package/fesm2022/threadplane-langgraph.mjs.map +1 -1
- package/package.json +10 -1
- package/types/threadplane-langgraph.d.ts +165 -56
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import {
|
|
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
|
-
|
|
9
|
-
// SPDX-License-Identifier: MIT
|
|
10
|
-
const AGENT_CONFIG = new InjectionToken('AGENT_CONFIG');
|
|
11
|
-
/**
|
|
12
|
-
* Angular provider factory that registers global defaults for all
|
|
13
|
-
* agent instances in the application.
|
|
14
|
-
*/
|
|
15
|
-
function provideAgent(config) {
|
|
16
|
-
return { provide: AGENT_CONFIG, useValue: config };
|
|
17
|
-
}
|
|
8
|
+
import { mockAgent } from '@threadplane/chat';
|
|
18
9
|
|
|
19
10
|
// SPDX-License-Identifier: MIT
|
|
20
11
|
/**
|
|
@@ -65,14 +56,26 @@ const ResourceStatus = {
|
|
|
65
56
|
* transport (`fetch-stream.transport.ts`) and the threads adapter
|
|
66
57
|
* (`LangGraphThreadsAdapter`) both go through here.
|
|
67
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
|
+
*
|
|
68
66
|
* @example
|
|
69
67
|
* ```ts
|
|
70
68
|
* const client = createLangGraphClient(environment.langGraphApiUrl);
|
|
71
69
|
* const threads = await client.threads.search({ limit: 50 });
|
|
72
70
|
* ```
|
|
73
71
|
*/
|
|
74
|
-
function createLangGraphClient(apiUrl) {
|
|
75
|
-
return new Client({
|
|
72
|
+
function createLangGraphClient(apiUrl, clientOptions) {
|
|
73
|
+
return new Client({
|
|
74
|
+
apiUrl: toAbsoluteApiUrl(apiUrl),
|
|
75
|
+
...(clientOptions?.maxRetries !== undefined
|
|
76
|
+
? { callerOptions: { maxRetries: clientOptions.maxRetries } }
|
|
77
|
+
: {}),
|
|
78
|
+
});
|
|
76
79
|
}
|
|
77
80
|
/** Exported separately so non-Client callers (e.g. raw fetch) can
|
|
78
81
|
* share the same normalization logic. */
|
|
@@ -102,12 +105,13 @@ class FetchStreamTransport {
|
|
|
102
105
|
/**
|
|
103
106
|
* @param apiUrl - Base URL of the LangGraph Platform API
|
|
104
107
|
* @param onThreadId - Optional callback invoked when a new thread is created
|
|
108
|
+
* @param clientOptions - Optional SDK client tuning (e.g. `maxRetries`)
|
|
105
109
|
*/
|
|
106
|
-
constructor(apiUrl, onThreadId) {
|
|
110
|
+
constructor(apiUrl, onThreadId, clientOptions) {
|
|
107
111
|
// createLangGraphClient handles the absolute-URL normalization
|
|
108
112
|
// required by the SDK when `apiUrl` is a relative `/api`-style
|
|
109
113
|
// path proxied by middleware in production.
|
|
110
|
-
this.client = createLangGraphClient(apiUrl);
|
|
114
|
+
this.client = createLangGraphClient(apiUrl, clientOptions);
|
|
111
115
|
this.onThreadId = onThreadId;
|
|
112
116
|
}
|
|
113
117
|
/** Open a streaming connection, creating a thread if needed. */
|
|
@@ -526,7 +530,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
526
530
|
currentThreadId = id;
|
|
527
531
|
userOnThreadId?.(id);
|
|
528
532
|
};
|
|
529
|
-
const transport = options.transport ?? new FetchStreamTransport(options.apiUrl, wrappedOnThreadId);
|
|
533
|
+
const transport = options.transport ?? new FetchStreamTransport(options.apiUrl, wrappedOnThreadId, options.clientOptions);
|
|
530
534
|
let currentThreadId = null;
|
|
531
535
|
let lastPayload = null;
|
|
532
536
|
let lastOptions;
|
|
@@ -1705,6 +1709,29 @@ const _internalsForTesting = {
|
|
|
1705
1709
|
isFinalCanonicalReasoningContent,
|
|
1706
1710
|
};
|
|
1707
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
|
+
|
|
1708
1735
|
const ROOT_ID = '$';
|
|
1709
1736
|
/**
|
|
1710
1737
|
* Builds a branch-aware checkpoint tree from LangGraph thread history.
|
|
@@ -1835,6 +1862,124 @@ function normalizeCitation(entry, fallbackIndex) {
|
|
|
1835
1862
|
};
|
|
1836
1863
|
}
|
|
1837
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
|
+
|
|
1838
1983
|
// SPDX-License-Identifier: MIT
|
|
1839
1984
|
/**
|
|
1840
1985
|
* Walk LangGraph history (newest-first) and pair each AIMessage id with
|
|
@@ -1868,10 +2013,14 @@ function computeMessageCheckpoints(history) {
|
|
|
1868
2013
|
return out;
|
|
1869
2014
|
}
|
|
1870
2015
|
/**
|
|
1871
|
-
*
|
|
2016
|
+
* Internal factory that constructs a LangGraph-backed Angular agent.
|
|
1872
2017
|
*
|
|
1873
|
-
*
|
|
1874
|
-
*
|
|
2018
|
+
* @internal Consumers do not call this directly. Configure the adapter with
|
|
2019
|
+
* `provideAgent({...})` in `app.config.ts` (or a component's `providers`), then
|
|
2020
|
+
* retrieve the agent with `injectAgent()`. This factory is the construction
|
|
2021
|
+
* logic invoked by `provideAgent`'s DI factory.
|
|
2022
|
+
*
|
|
2023
|
+
* Must run within an Angular injection context. Returns a unified
|
|
1875
2024
|
* {@link LangGraphAgent} whose properties are Angular Signals that update
|
|
1876
2025
|
* in real time as LangGraph streams messages, values, tool calls, interrupts,
|
|
1877
2026
|
* subagent state, and checkpoint history.
|
|
@@ -1883,13 +2032,18 @@ function computeMessageCheckpoints(history) {
|
|
|
1883
2032
|
*
|
|
1884
2033
|
* @example
|
|
1885
2034
|
* ```typescript
|
|
1886
|
-
* //
|
|
1887
|
-
*
|
|
1888
|
-
*
|
|
1889
|
-
*
|
|
1890
|
-
*
|
|
1891
|
-
*
|
|
1892
|
-
*
|
|
2035
|
+
* // app.config.ts — configure once
|
|
2036
|
+
* providers: [
|
|
2037
|
+
* provideAgent({
|
|
2038
|
+
* assistantId: 'chat',
|
|
2039
|
+
* apiUrl: 'http://localhost:2024',
|
|
2040
|
+
* threadId: signal(savedThreadId),
|
|
2041
|
+
* onThreadId: (id) => localStorage.setItem('threadId', id),
|
|
2042
|
+
* }),
|
|
2043
|
+
* ];
|
|
2044
|
+
*
|
|
2045
|
+
* // component — retrieve from DI
|
|
2046
|
+
* const chat = injectAgent();
|
|
1893
2047
|
*
|
|
1894
2048
|
* // Access signals in template
|
|
1895
2049
|
* // chat.messages(), chat.status(), chat.error()
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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');
|
|
@@ -2457,6 +2644,94 @@ function isRecord(v) {
|
|
|
2457
2644
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
2458
2645
|
}
|
|
2459
2646
|
|
|
2647
|
+
// SPDX-License-Identifier: MIT
|
|
2648
|
+
/**
|
|
2649
|
+
* @internal — exported only so the legacy in-tree `agent({...})` factory (and
|
|
2650
|
+
* its tests) can read provider-supplied defaults. Not part of the public API;
|
|
2651
|
+
* consumers should construct config inline at `provideAgent({...})`.
|
|
2652
|
+
*/
|
|
2653
|
+
const AGENT_CONFIG = new InjectionToken('AGENT_CONFIG');
|
|
2654
|
+
/**
|
|
2655
|
+
* @internal — exported for spec access only. Consumers must use `injectAgent()`.
|
|
2656
|
+
*/
|
|
2657
|
+
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) {
|
|
2685
|
+
// Resolve the factory (if any) lazily, inside the injection context of the
|
|
2686
|
+
// AGENT_CONFIG useFactory below — never at decoration time.
|
|
2687
|
+
const resolveConfig = () => typeof configOrFactory === 'function' ? configOrFactory() : configOrFactory;
|
|
2688
|
+
return [
|
|
2689
|
+
// AGENT_CONFIG resolves the config once (running the factory in an
|
|
2690
|
+
// injection context if a factory was passed). AGENT reads the resolved
|
|
2691
|
+
// config from here, so the factory is invoked exactly once.
|
|
2692
|
+
{ 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
|
+
},
|
|
2718
|
+
];
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
// 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);
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2460
2735
|
// SPDX-License-Identifier: MIT
|
|
2461
2736
|
const AGENT_LIFECYCLE = new InjectionToken('AGENT_LIFECYCLE');
|
|
2462
2737
|
|
|
@@ -2592,18 +2867,22 @@ class MockAgentTransport {
|
|
|
2592
2867
|
/**
|
|
2593
2868
|
* Creates a mock LangGraphAgent with writable signals for testing.
|
|
2594
2869
|
* Control state by writing to the returned writable signals directly.
|
|
2870
|
+
*
|
|
2871
|
+
* Neutral `Agent`-contract signals come from {@link mockAgent}; LangGraph-specific
|
|
2872
|
+
* signals are declared here and layered on top.
|
|
2595
2873
|
*/
|
|
2596
2874
|
function mockLangGraphAgent(initial = {}) {
|
|
2597
|
-
const
|
|
2875
|
+
const base = mockAgent({
|
|
2876
|
+
...initial,
|
|
2877
|
+
withInterrupt: true,
|
|
2878
|
+
withSubagents: true,
|
|
2879
|
+
history: initial.history ?? [],
|
|
2880
|
+
});
|
|
2881
|
+
// ── LangGraph-specific writable signals (defaults copied verbatim) ────────
|
|
2598
2882
|
const langGraphMessages$ = signal(initial.langGraphMessages ?? [], ...(ngDevMode ? [{ debugName: "langGraphMessages$" }] : []));
|
|
2599
|
-
const status$ = signal(initial.status ?? 'idle', ...(ngDevMode ? [{ debugName: "status$" }] : []));
|
|
2600
|
-
const isLoading$ = signal(initial.isLoading ?? false, ...(ngDevMode ? [{ debugName: "isLoading$" }] : []));
|
|
2601
|
-
const error$ = signal(initial.error ?? null, ...(ngDevMode ? [{ debugName: "error$" }] : []));
|
|
2602
2883
|
const hasValue$ = signal(initial.hasValue ?? false, ...(ngDevMode ? [{ debugName: "hasValue$" }] : []));
|
|
2603
2884
|
const value$ = signal(null, ...(ngDevMode ? [{ debugName: "value$" }] : []));
|
|
2604
|
-
const interrupt$ = signal(undefined, ...(ngDevMode ? [{ debugName: "interrupt$" }] : []));
|
|
2605
2885
|
const langGraphInterrupts$ = signal([], ...(ngDevMode ? [{ debugName: "langGraphInterrupts$" }] : []));
|
|
2606
|
-
const toolCalls$ = signal([], ...(ngDevMode ? [{ debugName: "toolCalls$" }] : []));
|
|
2607
2886
|
const langGraphToolCalls$ = signal([], ...(ngDevMode ? [{ debugName: "langGraphToolCalls$" }] : []));
|
|
2608
2887
|
const toolProgress$ = signal([], ...(ngDevMode ? [{ debugName: "toolProgress$" }] : []));
|
|
2609
2888
|
const queue$ = signal({
|
|
@@ -2613,33 +2892,20 @@ function mockLangGraphAgent(initial = {}) {
|
|
|
2613
2892
|
clear: async () => undefined,
|
|
2614
2893
|
}, ...(ngDevMode ? [{ debugName: "queue$" }] : []));
|
|
2615
2894
|
const branch$ = signal('', ...(ngDevMode ? [{ debugName: "branch$" }] : []));
|
|
2616
|
-
const history$ = signal([], ...(ngDevMode ? [{ debugName: "history$" }] : []));
|
|
2617
2895
|
const langGraphHistory$ = signal([], ...(ngDevMode ? [{ debugName: "langGraphHistory$" }] : []));
|
|
2618
2896
|
const experimentalBranchTree$ = signal({ type: 'sequence', items: [] }, ...(ngDevMode ? [{ debugName: "experimentalBranchTree$" }] : []));
|
|
2619
2897
|
const isThreadLoading$ = signal(initial.isThreadLoading ?? false, ...(ngDevMode ? [{ debugName: "isThreadLoading$" }] : []));
|
|
2620
|
-
const subagents$ = signal(new Map(), ...(ngDevMode ? [{ debugName: "subagents$" }] : []));
|
|
2621
2898
|
const activeSubagents$ = signal([], ...(ngDevMode ? [{ debugName: "activeSubagents$" }] : []));
|
|
2622
2899
|
const customEvents$ = signal([], ...(ngDevMode ? [{ debugName: "customEvents$" }] : []));
|
|
2900
|
+
// `state` derives from the raw LangGraph value (preserves current behavior).
|
|
2623
2901
|
const state$ = computed(() => {
|
|
2624
2902
|
const v = value$();
|
|
2625
2903
|
return v && typeof v === 'object' ? v : {};
|
|
2626
2904
|
}, ...(ngDevMode ? [{ debugName: "state$" }] : []));
|
|
2627
|
-
const eventsSubject = new Subject();
|
|
2628
2905
|
const mock = {
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
status: status$,
|
|
2632
|
-
isLoading: isLoading$,
|
|
2633
|
-
error: error$,
|
|
2634
|
-
toolCalls: toolCalls$,
|
|
2906
|
+
...base,
|
|
2907
|
+
// ── Neutral surface: override `state` to derive from the LangGraph value ─
|
|
2635
2908
|
state: state$,
|
|
2636
|
-
interrupt: interrupt$,
|
|
2637
|
-
subagents: subagents$,
|
|
2638
|
-
events$: eventsSubject.asObservable(),
|
|
2639
|
-
history: history$,
|
|
2640
|
-
submit: (_input, _opts) => Promise.resolve(),
|
|
2641
|
-
stop: () => Promise.resolve(),
|
|
2642
|
-
regenerate: (_assistantMessageIndex) => Promise.resolve(),
|
|
2643
2909
|
// ── Raw LangGraph signals ─────────────────────────────────────────────
|
|
2644
2910
|
langGraphMessages: langGraphMessages$,
|
|
2645
2911
|
langGraphInterrupts: langGraphInterrupts$,
|
|
@@ -2694,6 +2960,97 @@ function mockLangGraphAgent(initial = {}) {
|
|
|
2694
2960
|
return mock;
|
|
2695
2961
|
}
|
|
2696
2962
|
|
|
2963
|
+
const DEFAULT_TOKENS = ['Hello', ' from', ' the', ' fake', ' LangGraph', ' agent.'];
|
|
2964
|
+
/**
|
|
2965
|
+
* In-process AgentTransport that auto-streams a canned assistant reply.
|
|
2966
|
+
*
|
|
2967
|
+
* Backs `provideFakeAgent()`. Unlike `MockAgentTransport` (passive, driven
|
|
2968
|
+
* manually from specs), this transport emits its tokens automatically on
|
|
2969
|
+
* `stream()`, then completes — suitable for offline demos and integration tests.
|
|
2970
|
+
*
|
|
2971
|
+
* NOT for production use.
|
|
2972
|
+
*/
|
|
2973
|
+
class FakeStreamTransport {
|
|
2974
|
+
tokens;
|
|
2975
|
+
reasoningTokens;
|
|
2976
|
+
delayMs;
|
|
2977
|
+
constructor(config = {}) {
|
|
2978
|
+
this.tokens = config.tokens ?? DEFAULT_TOKENS;
|
|
2979
|
+
this.reasoningTokens = config.reasoningTokens ?? [];
|
|
2980
|
+
// Default 60ms matches @threadplane/ag-ui's FakeAgent so both adapters'
|
|
2981
|
+
// provideFakeAgent() stream at the same cadence for the same config.
|
|
2982
|
+
this.delayMs = config.delayMs ?? 60;
|
|
2983
|
+
}
|
|
2984
|
+
async *stream(_assistantId, _threadId, _payload, signal, _options) {
|
|
2985
|
+
const id = 'fake-ai-1';
|
|
2986
|
+
let reasoning = '';
|
|
2987
|
+
for (const chunk of this.reasoningTokens) {
|
|
2988
|
+
if (signal.aborted)
|
|
2989
|
+
return;
|
|
2990
|
+
reasoning += chunk;
|
|
2991
|
+
yield {
|
|
2992
|
+
type: 'messages',
|
|
2993
|
+
messages: [
|
|
2994
|
+
{ id, type: 'ai', content: '', additional_kwargs: { reasoning_content: reasoning } },
|
|
2995
|
+
],
|
|
2996
|
+
};
|
|
2997
|
+
if (this.delayMs > 0)
|
|
2998
|
+
await delay(this.delayMs);
|
|
2999
|
+
}
|
|
3000
|
+
let content = '';
|
|
3001
|
+
for (const tok of this.tokens) {
|
|
3002
|
+
if (signal.aborted)
|
|
3003
|
+
return;
|
|
3004
|
+
content += tok;
|
|
3005
|
+
yield {
|
|
3006
|
+
type: 'messages',
|
|
3007
|
+
messages: [{ id, type: 'ai', content }],
|
|
3008
|
+
};
|
|
3009
|
+
if (this.delayMs > 0)
|
|
3010
|
+
await delay(this.delayMs);
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
async createQueuedRun(_assistantId, threadId, payload, _signal, options) {
|
|
3014
|
+
return {
|
|
3015
|
+
id: 'fake-queued-run',
|
|
3016
|
+
threadId,
|
|
3017
|
+
values: payload,
|
|
3018
|
+
options: { ...options, multitaskStrategy: 'enqueue' },
|
|
3019
|
+
createdAt: new Date(),
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
async cancelRun(_threadId, _runId, _signal) {
|
|
3023
|
+
// No-op: the fake has no real runs to cancel.
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
async getHistory(_threadId, _signal) {
|
|
3027
|
+
return [];
|
|
3028
|
+
}
|
|
3029
|
+
async *joinStream() {
|
|
3030
|
+
// No queued-run replay in the fake; yields nothing.
|
|
3031
|
+
yield* [];
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
function delay(ms) {
|
|
3035
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
/**
|
|
3039
|
+
* Wire an in-process fake LangGraph agent into Angular DI.
|
|
3040
|
+
*
|
|
3041
|
+
* Streams a canned assistant reply (see FakeAgentConfig) with no backend —
|
|
3042
|
+
* the symmetric counterpart to @threadplane/ag-ui's provideFakeAgent(). For
|
|
3043
|
+
* advanced manual scripting (tool calls, interrupts, multi-batch), provide
|
|
3044
|
+
* the agent yourself with
|
|
3045
|
+
* `provideAgent({ assistantId, transport: new MockAgentTransport(...) })`.
|
|
3046
|
+
*/
|
|
3047
|
+
function provideFakeAgent(config = {}) {
|
|
3048
|
+
return provideAgent({
|
|
3049
|
+
assistantId: 'fake',
|
|
3050
|
+
transport: new FakeStreamTransport(config),
|
|
3051
|
+
});
|
|
3052
|
+
}
|
|
3053
|
+
|
|
2697
3054
|
// SPDX-License-Identifier: MIT
|
|
2698
3055
|
const LANGGRAPH_THREADS_CONFIG = new InjectionToken('LANGGRAPH_THREADS_CONFIG');
|
|
2699
3056
|
/** Optional adapter clients can pass an explicit Client (e.g. for
|
|
@@ -2720,8 +3077,9 @@ const LANGGRAPH_CLIENT = new InjectionToken('LANGGRAPH_CLIENT');
|
|
|
2720
3077
|
*/
|
|
2721
3078
|
class LangGraphThreadsAdapter {
|
|
2722
3079
|
config = inject(LANGGRAPH_THREADS_CONFIG);
|
|
3080
|
+
sharedClientOptions = inject(LANGGRAPH_CLIENT_OPTIONS, { optional: true }) ?? undefined;
|
|
2723
3081
|
client = inject(LANGGRAPH_CLIENT, { optional: true })
|
|
2724
|
-
?? createLangGraphClient(this.config.apiUrl);
|
|
3082
|
+
?? createLangGraphClient(this.config.apiUrl, this.sharedClientOptions);
|
|
2725
3083
|
fallback = this.config.titleFallback ?? 'Untitled';
|
|
2726
3084
|
_threads = signal([], ...(ngDevMode ? [{ debugName: "_threads" }] : []));
|
|
2727
3085
|
_archived = signal([], ...(ngDevMode ? [{ debugName: "_archived" }] : []));
|
|
@@ -2918,11 +3276,11 @@ function refreshOnTransition(watch, isActive, fn) {
|
|
|
2918
3276
|
}
|
|
2919
3277
|
|
|
2920
3278
|
// SPDX-License-Identifier: MIT
|
|
2921
|
-
//
|
|
3279
|
+
// Provider
|
|
2922
3280
|
|
|
2923
3281
|
/**
|
|
2924
3282
|
* Generated bundle index. Do not edit.
|
|
2925
3283
|
*/
|
|
2926
3284
|
|
|
2927
|
-
export {
|
|
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 };
|
|
2928
3286
|
//# sourceMappingURL=threadplane-langgraph.mjs.map
|