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