@threadplane/langgraph 0.0.50 → 0.0.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -5,7 +5,7 @@ import { takeUntil, Subject, BehaviorSubject, of, throttleTime, asyncScheduler }
|
|
|
5
5
|
import { takeUntil as takeUntil$1 } from 'rxjs/operators';
|
|
6
6
|
import { Client } from '@langchain/langgraph-sdk';
|
|
7
7
|
import { getToolCallsWithResults } from '@langchain/langgraph-sdk/utils';
|
|
8
|
-
import { mockAgent } from '@threadplane/chat';
|
|
8
|
+
import { toAgentError, isAbortError, AgentError, AGENT_ERROR_MESSAGES, mockAgent } from '@threadplane/chat';
|
|
9
9
|
|
|
10
10
|
// SPDX-License-Identifier: MIT
|
|
11
11
|
/**
|
|
@@ -537,6 +537,8 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
537
537
|
let abortController = null;
|
|
538
538
|
let historyAbortController = null;
|
|
539
539
|
let hasSeenThreadId = false;
|
|
540
|
+
/** True when the current abort was user-initiated (via stop()). Reset at the start of every new runStream(). */
|
|
541
|
+
let userAbortRequested = false;
|
|
540
542
|
const toolProgressMap = new Map();
|
|
541
543
|
const queuedRuns = [];
|
|
542
544
|
let drainingQueue = false;
|
|
@@ -570,7 +572,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
570
572
|
subjects.toolCalls$.next([]);
|
|
571
573
|
subjects.messageMetadata$.next(new Map());
|
|
572
574
|
subjects.subagents$.next(new Map());
|
|
573
|
-
void cancelQueueEntries(takeQueuedRuns()).catch(err => subjects.error$.next(err));
|
|
575
|
+
void cancelQueueEntries(takeQueuedRuns()).catch(err => subjects.error$.next(toAgentError(err)));
|
|
574
576
|
publishQueue();
|
|
575
577
|
subjects.custom$.next([]);
|
|
576
578
|
subjects.isThreadLoading$.next(false);
|
|
@@ -647,7 +649,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
647
649
|
}
|
|
648
650
|
catch (err) {
|
|
649
651
|
if (!controller.signal.aborted && err?.name !== 'AbortError') {
|
|
650
|
-
subjects.error$.next(err);
|
|
652
|
+
subjects.error$.next(toAgentError(err));
|
|
651
653
|
}
|
|
652
654
|
}
|
|
653
655
|
finally {
|
|
@@ -759,7 +761,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
759
761
|
}
|
|
760
762
|
}
|
|
761
763
|
catch (err) {
|
|
762
|
-
subjects.error$.next(err);
|
|
764
|
+
subjects.error$.next(toAgentError(err));
|
|
763
765
|
subjects.status$.next(ResourceStatus.Error);
|
|
764
766
|
captureAgentRuntimeTelemetry(options.telemetry, 'ngaf:stream_errored', {
|
|
765
767
|
...telemetryProperties,
|
|
@@ -771,6 +773,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
771
773
|
async function runStream(payload, opts, requestType = 'submit') {
|
|
772
774
|
abortController?.abort();
|
|
773
775
|
abortController = new AbortController();
|
|
776
|
+
userAbortRequested = false;
|
|
774
777
|
const startedAt = Date.now();
|
|
775
778
|
captureRuntimeRequestTelemetry(requestType);
|
|
776
779
|
captureAgentRuntimeTelemetry(options.telemetry, 'ngaf:stream_started', telemetryProperties);
|
|
@@ -781,6 +784,10 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
781
784
|
toolProgressMap.clear();
|
|
782
785
|
lastPayload = payload;
|
|
783
786
|
lastOptions = opts;
|
|
787
|
+
// Tracks whether at least one stream event has been processed this run.
|
|
788
|
+
// Used to distinguish a mid-stream network interruption (kind:'interrupted')
|
|
789
|
+
// from a fresh connect failure (falls through to toAgentError classification).
|
|
790
|
+
let streamingStarted = false;
|
|
784
791
|
// Optimistically inject human messages so they appear immediately
|
|
785
792
|
// without waiting for the server to echo them back. Assign a stable id
|
|
786
793
|
// when missing — track-by-id in the chat-message-list relies on stable
|
|
@@ -804,6 +811,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
804
811
|
for await (const event of iter) {
|
|
805
812
|
if (abortController.signal.aborted)
|
|
806
813
|
break;
|
|
814
|
+
streamingStarted = true;
|
|
807
815
|
processEvent(event);
|
|
808
816
|
}
|
|
809
817
|
if (!abortController.signal.aborted) {
|
|
@@ -819,11 +827,26 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
819
827
|
}
|
|
820
828
|
}
|
|
821
829
|
catch (err) {
|
|
822
|
-
if (err
|
|
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,
|
|
@@ -2218,6 +2248,9 @@ function agent(options) {
|
|
|
2218
2248
|
// CD anyway.
|
|
2219
2249
|
const rawMessages = toSignal(messages$, { initialValue: [] });
|
|
2220
2250
|
const statusSig = toSignal(status$, { initialValue: ResourceStatus.Idle });
|
|
2251
|
+
// Cast justified: error$ accepts only AgentError | undefined (bridge catch normalizes all errors via
|
|
2252
|
+
// toAgentError before calling next(); resetDerivedThreadState passes undefined). The BehaviorSubject
|
|
2253
|
+
// is typed unknown to satisfy StreamSubjects<unknown> invariance at the subjects-bag assignment.
|
|
2221
2254
|
const errorSig = toSignal(error$, { initialValue: undefined });
|
|
2222
2255
|
const hasValueSig = toSignal(hasValue$, { initialValue: false });
|
|
2223
2256
|
const interruptSig = toSignal(interrupt$, { initialValue: undefined });
|
|
@@ -2314,6 +2347,12 @@ function agent(options) {
|
|
|
2314
2347
|
return manager.submit(payload, request.options);
|
|
2315
2348
|
},
|
|
2316
2349
|
stop: () => manager.stop(),
|
|
2350
|
+
retry: async () => {
|
|
2351
|
+
if (isLoading())
|
|
2352
|
+
return; // no-op while a run is in flight
|
|
2353
|
+
error$.next(undefined); // clear the error before re-running
|
|
2354
|
+
await manager.resubmitLast();
|
|
2355
|
+
},
|
|
2317
2356
|
clientTools: clientToolsCap,
|
|
2318
2357
|
regenerate: async (assistantMessageIndex) => {
|
|
2319
2358
|
if (isLoading()) {
|
|
@@ -2655,81 +2694,53 @@ const AGENT_CONFIG = new InjectionToken('AGENT_CONFIG');
|
|
|
2655
2694
|
* @internal — exported for spec access only. Consumers must use `injectAgent()`.
|
|
2656
2695
|
*/
|
|
2657
2696
|
const AGENT = new InjectionToken('AGENT');
|
|
2658
|
-
/**
|
|
2659
|
-
|
|
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(
|
|
2697
|
+
/** @internal — shared factory that reads AGENT_CONFIG and constructs the singleton. */
|
|
2698
|
+
function agentFactory() {
|
|
2699
|
+
// useFactory runs in an injection context, so the legacy `agent()`
|
|
2700
|
+
// factory's `inject(DestroyRef)` calls work.
|
|
2701
|
+
const config = inject(AGENT_CONFIG);
|
|
2702
|
+
if (config.assistantId === undefined) {
|
|
2703
|
+
throw new Error('provideAgent: `assistantId` is required to construct the AGENT singleton.');
|
|
2704
|
+
}
|
|
2705
|
+
return agent({
|
|
2706
|
+
assistantId: config.assistantId,
|
|
2707
|
+
...(config.apiUrl !== undefined ? { apiUrl: config.apiUrl } : {}),
|
|
2708
|
+
...(config.threadId !== undefined ? { threadId: config.threadId } : {}),
|
|
2709
|
+
...(config.onThreadId !== undefined ? { onThreadId: config.onThreadId } : {}),
|
|
2710
|
+
...(config.initialValues !== undefined ? { initialValues: config.initialValues } : {}),
|
|
2711
|
+
...(config.throttle !== undefined ? { throttle: config.throttle } : {}),
|
|
2712
|
+
...(config.toMessage !== undefined ? { toMessage: config.toMessage } : {}),
|
|
2713
|
+
...(config.transport !== undefined ? { transport: config.transport } : {}),
|
|
2714
|
+
...(config.clientOptions !== undefined ? { clientOptions: config.clientOptions } : {}),
|
|
2715
|
+
...(config.telemetry !== undefined ? { telemetry: config.telemetry } : {}),
|
|
2716
|
+
...(config.filterSubagentMessages !== undefined ? { filterSubagentMessages: config.filterSubagentMessages } : {}),
|
|
2717
|
+
...(config.subagentToolNames !== undefined ? { subagentToolNames: config.subagentToolNames } : {}),
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
function isAgentRef(x) {
|
|
2721
|
+
return typeof x === 'object' && x !== null && 'token' in x;
|
|
2722
|
+
}
|
|
2723
|
+
function provideAgent(refOrConfig, maybeConfig) {
|
|
2724
|
+
const ref = isAgentRef(refOrConfig) ? refOrConfig : undefined;
|
|
2725
|
+
const configOrFactory = (ref ? maybeConfig : refOrConfig);
|
|
2685
2726
|
// Resolve the factory (if any) lazily, inside the injection context of the
|
|
2686
2727
|
// AGENT_CONFIG useFactory below — never at decoration time.
|
|
2687
2728
|
const resolveConfig = () => typeof configOrFactory === 'function' ? configOrFactory() : configOrFactory;
|
|
2688
|
-
|
|
2729
|
+
const providers = [
|
|
2689
2730
|
// AGENT_CONFIG resolves the config once (running the factory in an
|
|
2690
2731
|
// injection context if a factory was passed). AGENT reads the resolved
|
|
2691
2732
|
// config from here, so the factory is invoked exactly once.
|
|
2692
2733
|
{ provide: AGENT_CONFIG, useFactory: resolveConfig },
|
|
2693
|
-
{
|
|
2694
|
-
provide: AGENT,
|
|
2695
|
-
useFactory: () => {
|
|
2696
|
-
// useFactory runs in an injection context, so the legacy `agent()`
|
|
2697
|
-
// factory's `inject(DestroyRef)` calls work.
|
|
2698
|
-
const config = inject(AGENT_CONFIG);
|
|
2699
|
-
if (config.assistantId === undefined) {
|
|
2700
|
-
throw new Error('provideAgent: `assistantId` is required to construct the AGENT singleton.');
|
|
2701
|
-
}
|
|
2702
|
-
return agent({
|
|
2703
|
-
assistantId: config.assistantId,
|
|
2704
|
-
...(config.apiUrl !== undefined ? { apiUrl: config.apiUrl } : {}),
|
|
2705
|
-
...(config.threadId !== undefined ? { threadId: config.threadId } : {}),
|
|
2706
|
-
...(config.onThreadId !== undefined ? { onThreadId: config.onThreadId } : {}),
|
|
2707
|
-
...(config.initialValues !== undefined ? { initialValues: config.initialValues } : {}),
|
|
2708
|
-
...(config.throttle !== undefined ? { throttle: config.throttle } : {}),
|
|
2709
|
-
...(config.toMessage !== undefined ? { toMessage: config.toMessage } : {}),
|
|
2710
|
-
...(config.transport !== undefined ? { transport: config.transport } : {}),
|
|
2711
|
-
...(config.clientOptions !== undefined ? { clientOptions: config.clientOptions } : {}),
|
|
2712
|
-
...(config.telemetry !== undefined ? { telemetry: config.telemetry } : {}),
|
|
2713
|
-
...(config.filterSubagentMessages !== undefined ? { filterSubagentMessages: config.filterSubagentMessages } : {}),
|
|
2714
|
-
...(config.subagentToolNames !== undefined ? { subagentToolNames: config.subagentToolNames } : {}),
|
|
2715
|
-
});
|
|
2716
|
-
},
|
|
2717
|
-
},
|
|
2734
|
+
{ provide: AGENT, useFactory: (agentFactory) },
|
|
2718
2735
|
];
|
|
2736
|
+
if (ref)
|
|
2737
|
+
providers.push({ provide: ref.token, useExisting: AGENT });
|
|
2738
|
+
return providers;
|
|
2719
2739
|
}
|
|
2720
2740
|
|
|
2721
2741
|
// SPDX-License-Identifier: MIT
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
*
|
|
2725
|
-
* Mirrors `@threadplane/ag-ui`'s `injectAgent()` so consumer code is identical
|
|
2726
|
-
* regardless of which adapter is wired in `app.config.ts`. The agent is a
|
|
2727
|
-
* singleton scoped to the injector that called `provideAgent()` — re-provide
|
|
2728
|
-
* in a child component's `providers: []` to scope a different agent to that
|
|
2729
|
-
* subtree (Angular's hierarchical DI handles the rest).
|
|
2730
|
-
*/
|
|
2731
|
-
function injectAgent() {
|
|
2732
|
-
return inject(AGENT);
|
|
2742
|
+
function injectAgent(ref) {
|
|
2743
|
+
return inject(ref ? ref.token : AGENT);
|
|
2733
2744
|
}
|
|
2734
2745
|
|
|
2735
2746
|
// SPDX-License-Identifier: MIT
|