@threadplane/langgraph 0.0.53 → 0.0.55
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
CHANGED
|
@@ -147,7 +147,7 @@ import { provideAgent, LangGraphThreadsAdapter, LANGGRAPH_THREADS_CONFIG } from
|
|
|
147
147
|
|
|
148
148
|
export const appConfig: ApplicationConfig = {
|
|
149
149
|
providers: [
|
|
150
|
-
provideAgent({ apiUrl: 'https://your-langgraph-platform.com' }),
|
|
150
|
+
provideAgent({ apiUrl: 'https://your-langgraph-platform.com', assistantId: 'my-agent' }),
|
|
151
151
|
{ provide: LANGGRAPH_THREADS_CONFIG, useValue: { apiUrl: 'https://your-langgraph-platform.com' } },
|
|
152
152
|
LangGraphThreadsAdapter,
|
|
153
153
|
],
|
|
@@ -496,7 +496,7 @@ function isLgTraceEnabled() {
|
|
|
496
496
|
function lgTrace(...args) {
|
|
497
497
|
if (isLgTraceEnabled()) {
|
|
498
498
|
// eslint-disable-next-line no-console
|
|
499
|
-
console.debug('[
|
|
499
|
+
console.debug('[tplane-chat-stream]', ...args);
|
|
500
500
|
}
|
|
501
501
|
}
|
|
502
502
|
function captureAgentRuntimeTelemetry(sink, event, properties) {
|
|
@@ -540,6 +540,10 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
540
540
|
/** True when the current abort was user-initiated (via stop()). Reset at the start of every new runStream(). */
|
|
541
541
|
let userAbortRequested = false;
|
|
542
542
|
const toolProgressMap = new Map();
|
|
543
|
+
// Message ids whose content is known-final (installed by a canonical
|
|
544
|
+
// replacement). Late streamed deltas for these ids are stale stragglers and
|
|
545
|
+
// are ignored — decided by identity, never by comparing text to text.
|
|
546
|
+
const canonicalMessageIds = new Set();
|
|
543
547
|
const queuedRuns = [];
|
|
544
548
|
let drainingQueue = false;
|
|
545
549
|
const subagentManager = new SubagentTracker({
|
|
@@ -547,9 +551,9 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
547
551
|
onSubagentChange: publishSubagents,
|
|
548
552
|
});
|
|
549
553
|
const telemetryProperties = { transport: 'langgraph', surface: 'agent' };
|
|
550
|
-
captureAgentRuntimeTelemetry(options.telemetry, '
|
|
554
|
+
captureAgentRuntimeTelemetry(options.telemetry, 'tplane:runtime_instance_created', telemetryProperties);
|
|
551
555
|
function captureRuntimeRequestTelemetry(requestType) {
|
|
552
|
-
captureAgentRuntimeTelemetry(options.telemetry, '
|
|
556
|
+
captureAgentRuntimeTelemetry(options.telemetry, 'tplane:runtime_request_created', {
|
|
553
557
|
...telemetryProperties,
|
|
554
558
|
requestType,
|
|
555
559
|
});
|
|
@@ -579,6 +583,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
579
583
|
toolProgressMap.clear();
|
|
580
584
|
subagentManager.clear();
|
|
581
585
|
reasoningTimingMap.clear();
|
|
586
|
+
canonicalMessageIds.clear();
|
|
582
587
|
}
|
|
583
588
|
function setThreadId(id, resetState) {
|
|
584
589
|
if (resetState) {
|
|
@@ -734,7 +739,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
734
739
|
abortController = new AbortController();
|
|
735
740
|
const startedAt = Date.now();
|
|
736
741
|
captureRuntimeRequestTelemetry('join_queued');
|
|
737
|
-
captureAgentRuntimeTelemetry(options.telemetry, '
|
|
742
|
+
captureAgentRuntimeTelemetry(options.telemetry, 'tplane:stream_started', telemetryProperties);
|
|
738
743
|
subjects.custom$.next([]);
|
|
739
744
|
subjects.toolProgress$.next([]);
|
|
740
745
|
toolProgressMap.clear();
|
|
@@ -754,7 +759,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
754
759
|
// post-process node mutations (RemoveMessage, id-match content
|
|
755
760
|
// replacement) reflected on the server are picked up client-side.
|
|
756
761
|
await refreshHistory(true);
|
|
757
|
-
captureAgentRuntimeTelemetry(options.telemetry, '
|
|
762
|
+
captureAgentRuntimeTelemetry(options.telemetry, 'tplane:stream_ended', {
|
|
758
763
|
...telemetryProperties,
|
|
759
764
|
durationMs: Date.now() - startedAt,
|
|
760
765
|
});
|
|
@@ -763,7 +768,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
763
768
|
catch (err) {
|
|
764
769
|
subjects.error$.next(toAgentError(err));
|
|
765
770
|
subjects.status$.next(ResourceStatus.Error);
|
|
766
|
-
captureAgentRuntimeTelemetry(options.telemetry, '
|
|
771
|
+
captureAgentRuntimeTelemetry(options.telemetry, 'tplane:stream_errored', {
|
|
767
772
|
...telemetryProperties,
|
|
768
773
|
durationMs: Date.now() - startedAt,
|
|
769
774
|
errorClass: agentRuntimeTelemetryErrorClass(err),
|
|
@@ -776,12 +781,13 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
776
781
|
userAbortRequested = false;
|
|
777
782
|
const startedAt = Date.now();
|
|
778
783
|
captureRuntimeRequestTelemetry(requestType);
|
|
779
|
-
captureAgentRuntimeTelemetry(options.telemetry, '
|
|
784
|
+
captureAgentRuntimeTelemetry(options.telemetry, 'tplane:stream_started', telemetryProperties);
|
|
780
785
|
subjects.status$.next(ResourceStatus.Loading);
|
|
781
786
|
subjects.error$.next(undefined);
|
|
782
787
|
subjects.custom$.next([]);
|
|
783
788
|
subjects.toolProgress$.next([]);
|
|
784
789
|
toolProgressMap.clear();
|
|
790
|
+
canonicalMessageIds.clear();
|
|
785
791
|
lastPayload = payload;
|
|
786
792
|
lastOptions = opts;
|
|
787
793
|
// Tracks whether at least one stream event has been processed this run.
|
|
@@ -820,7 +826,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
820
826
|
// authoritative after run completion.
|
|
821
827
|
await refreshHistory(true);
|
|
822
828
|
await drainQueue();
|
|
823
|
-
captureAgentRuntimeTelemetry(options.telemetry, '
|
|
829
|
+
captureAgentRuntimeTelemetry(options.telemetry, 'tplane:stream_ended', {
|
|
824
830
|
...telemetryProperties,
|
|
825
831
|
durationMs: Date.now() - startedAt,
|
|
826
832
|
});
|
|
@@ -839,7 +845,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
839
845
|
: new AgentError({ kind: 'connection', message: AGENT_ERROR_MESSAGES.connection, retryable: true, cause: err });
|
|
840
846
|
subjects.error$.next(e);
|
|
841
847
|
subjects.status$.next(ResourceStatus.Error);
|
|
842
|
-
captureAgentRuntimeTelemetry(options.telemetry, '
|
|
848
|
+
captureAgentRuntimeTelemetry(options.telemetry, 'tplane:stream_errored', {
|
|
843
849
|
...telemetryProperties,
|
|
844
850
|
durationMs: Date.now() - startedAt,
|
|
845
851
|
errorClass: agentRuntimeTelemetryErrorClass(err),
|
|
@@ -848,7 +854,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
848
854
|
else {
|
|
849
855
|
subjects.error$.next(toAgentError(err));
|
|
850
856
|
subjects.status$.next(ResourceStatus.Error);
|
|
851
|
-
captureAgentRuntimeTelemetry(options.telemetry, '
|
|
857
|
+
captureAgentRuntimeTelemetry(options.telemetry, 'tplane:stream_errored', {
|
|
852
858
|
...telemetryProperties,
|
|
853
859
|
durationMs: Date.now() - startedAt,
|
|
854
860
|
errorClass: agentRuntimeTelemetryErrorClass(err),
|
|
@@ -881,7 +887,8 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
881
887
|
// Partial and message-tuple events are incremental. Merge them by id
|
|
882
888
|
// so optimistic human messages and earlier tool messages are preserved.
|
|
883
889
|
if (event.type === 'messages/partial' || event.messageMetadata) {
|
|
884
|
-
|
|
890
|
+
const mode = event.messageMetadata ? 'delta' : 'snapshot';
|
|
891
|
+
subjects.messages$.next(mergeMessages(subjects.messages$.value, normalized, reasoningTimingMap, mode, canonicalMessageIds));
|
|
885
892
|
if (isLgTraceEnabled()) {
|
|
886
893
|
const msgs = subjects.messages$.value;
|
|
887
894
|
const last = msgs[msgs.length - 1];
|
|
@@ -950,7 +957,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
950
957
|
// drop the partial AI (or even the optimistic human) and
|
|
951
958
|
// tear down their DOM mid-stream. Merge by id keeps both,
|
|
952
959
|
// updates content where ids match, preserves the rest.
|
|
953
|
-
subjects.messages$.next(mergeMessages(subjects.messages$.value, remapped, reasoningTimingMap));
|
|
960
|
+
subjects.messages$.next(mergeMessages(subjects.messages$.value, remapped, reasoningTimingMap, 'snapshot', canonicalMessageIds));
|
|
954
961
|
if (isLgTraceEnabled()) {
|
|
955
962
|
lgTrace('bridge.values-sync', {
|
|
956
963
|
incomingLength: stateMessages.length,
|
|
@@ -1146,7 +1153,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
1146
1153
|
abortController = new AbortController();
|
|
1147
1154
|
const startedAt = Date.now();
|
|
1148
1155
|
captureRuntimeRequestTelemetry('join');
|
|
1149
|
-
captureAgentRuntimeTelemetry(options.telemetry, '
|
|
1156
|
+
captureAgentRuntimeTelemetry(options.telemetry, 'tplane:stream_started', telemetryProperties);
|
|
1150
1157
|
subjects.custom$.next([]);
|
|
1151
1158
|
subjects.toolProgress$.next([]);
|
|
1152
1159
|
toolProgressMap.clear();
|
|
@@ -1160,7 +1167,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
1160
1167
|
}
|
|
1161
1168
|
subjects.status$.next(ResourceStatus.Resolved);
|
|
1162
1169
|
await refreshHistory();
|
|
1163
|
-
captureAgentRuntimeTelemetry(options.telemetry, '
|
|
1170
|
+
captureAgentRuntimeTelemetry(options.telemetry, 'tplane:stream_ended', {
|
|
1164
1171
|
...telemetryProperties,
|
|
1165
1172
|
durationMs: Date.now() - startedAt,
|
|
1166
1173
|
});
|
|
@@ -1168,7 +1175,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
1168
1175
|
catch (err) {
|
|
1169
1176
|
subjects.error$.next(toAgentError(err));
|
|
1170
1177
|
subjects.status$.next(ResourceStatus.Error);
|
|
1171
|
-
captureAgentRuntimeTelemetry(options.telemetry, '
|
|
1178
|
+
captureAgentRuntimeTelemetry(options.telemetry, 'tplane:stream_errored', {
|
|
1172
1179
|
...telemetryProperties,
|
|
1173
1180
|
durationMs: Date.now() - startedAt,
|
|
1174
1181
|
errorClass: agentRuntimeTelemetryErrorClass(err),
|
|
@@ -1360,7 +1367,7 @@ function collapseAdjacentAi(messages) {
|
|
|
1360
1367
|
}
|
|
1361
1368
|
return out;
|
|
1362
1369
|
}
|
|
1363
|
-
function mergeMessages(existing, incoming, reasoningTimingMap) {
|
|
1370
|
+
function mergeMessages(existing, incoming, reasoningTimingMap, mode = 'snapshot', canonicalMessageIds) {
|
|
1364
1371
|
const merged = [...existing];
|
|
1365
1372
|
for (const msg of incoming) {
|
|
1366
1373
|
const rawIn = msg;
|
|
@@ -1400,6 +1407,13 @@ function mergeMessages(existing, incoming, reasoningTimingMap) {
|
|
|
1400
1407
|
const existing = merged[idx];
|
|
1401
1408
|
const existingId = existing['id'];
|
|
1402
1409
|
const incomingRaw = msg;
|
|
1410
|
+
const targetId = (existingId ?? incomingRaw['id']);
|
|
1411
|
+
// Identity backstop: once a message's content is known-final, late
|
|
1412
|
+
// streamed deltas for it are stale stragglers — ignore them outright.
|
|
1413
|
+
if (mode === 'delta' && targetId && canonicalMessageIds?.has(targetId)
|
|
1414
|
+
&& !isFinalCanonicalReasoningContent(incomingRaw['content'])) {
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1403
1417
|
// Keep the *existing* id so downstream track-by-id sees stable identity.
|
|
1404
1418
|
// For complex-content streaming (OpenAI gpt-5/o-series, Anthropic) the
|
|
1405
1419
|
// SDK emits per-chunk *delta* arrays — not accumulated arrays — so a
|
|
@@ -1407,7 +1421,10 @@ function mergeMessages(existing, incoming, reasoningTimingMap) {
|
|
|
1407
1421
|
// latest token. Accumulate text-bearing content across chunks here
|
|
1408
1422
|
// and hand a string to consumers; downstream code already handles
|
|
1409
1423
|
// string content uniformly.
|
|
1410
|
-
const accumulatedContent = accumulateContent(existing.content, incomingRaw['content']);
|
|
1424
|
+
const accumulatedContent = accumulateContent(existing.content, incomingRaw['content'], mode);
|
|
1425
|
+
if (targetId && isFinalCanonicalReasoningContent(incomingRaw['content'])) {
|
|
1426
|
+
canonicalMessageIds?.add(targetId);
|
|
1427
|
+
}
|
|
1411
1428
|
// Only accumulate reasoning when the incoming message explicitly carries
|
|
1412
1429
|
// a `reasoning` field or complex-content array blocks with
|
|
1413
1430
|
// type='reasoning'/'thinking'. Never use a plain string content value
|
|
@@ -1456,12 +1473,22 @@ function mergeMessages(existing, incoming, reasoningTimingMap) {
|
|
|
1456
1473
|
}
|
|
1457
1474
|
/**
|
|
1458
1475
|
* Merge an incoming chunk's content into prior accumulated content for the
|
|
1459
|
-
* same message id.
|
|
1476
|
+
* same message id. Behavior is governed by `mode`, which reflects the
|
|
1477
|
+
* DECLARED kind of the source event rather than a guess from comparing text:
|
|
1478
|
+
*
|
|
1479
|
+
* - mode 'delta' (messages-tuple / `event.messageMetadata` truthy): the
|
|
1480
|
+
* payload is a genuine per-chunk delta. Append unconditionally — a
|
|
1481
|
+
* prefix-comparison "dedupe" here would silently drop legitimate tokens
|
|
1482
|
+
* that coincide with the message-so-far (e.g. every bare "|" while
|
|
1483
|
+
* streaming a markdown table). Staleness after the message goes canonical
|
|
1484
|
+
* is instead handled by identity in `mergeMessages` (canonicalMessageIds).
|
|
1485
|
+
* - mode 'snapshot' (messages/partial, values-sync): the payload carries the
|
|
1486
|
+
* message-so-far, not a delta, so mutual prefix comparison picks the
|
|
1487
|
+
* longer state and ignores stale shorter snapshots.
|
|
1460
1488
|
*
|
|
1461
|
-
*
|
|
1462
|
-
*
|
|
1463
|
-
*
|
|
1464
|
-
* - empty existing → use incoming as-is
|
|
1489
|
+
* In both modes, a "final canonical" reasoning+text array (see
|
|
1490
|
+
* `isFinalCanonicalReasoningContent`) always replaces whatever was
|
|
1491
|
+
* accumulated — it's the authoritative final message, not another chunk.
|
|
1465
1492
|
*
|
|
1466
1493
|
* We deliberately collapse complex content arrays to a string at this layer.
|
|
1467
1494
|
* The langgraph-sdk client does not accumulate complex-content arrays the
|
|
@@ -1493,7 +1520,7 @@ function isFinalCanonicalReasoningContent(content) {
|
|
|
1493
1520
|
}
|
|
1494
1521
|
return hasReasoning && hasText;
|
|
1495
1522
|
}
|
|
1496
|
-
function accumulateContent(existing, incoming) {
|
|
1523
|
+
function accumulateContent(existing, incoming, mode = 'snapshot') {
|
|
1497
1524
|
const existingText = extractText(existing);
|
|
1498
1525
|
const incomingText = extractText(incoming);
|
|
1499
1526
|
// Always return a string. We never want array content escaping the bridge:
|
|
@@ -1504,23 +1531,24 @@ function accumulateContent(existing, incoming) {
|
|
|
1504
1531
|
return incomingText;
|
|
1505
1532
|
if (incomingText.length === 0)
|
|
1506
1533
|
return existingText;
|
|
1507
|
-
//
|
|
1534
|
+
// Final-canonical detection applies in both modes: the authoritative
|
|
1535
|
+
// "reasoning + text" array replaces whatever was accumulated.
|
|
1536
|
+
if (isFinalCanonicalReasoningContent(incoming))
|
|
1537
|
+
return incomingText;
|
|
1538
|
+
if (mode === 'delta') {
|
|
1539
|
+
// Tuple chunks are declared deltas. Append unconditionally — any
|
|
1540
|
+
// text-comparison "dedupe" here can silently drop legitimate tokens
|
|
1541
|
+
// that coincide with the message prefix (e.g. every bare "|" in a
|
|
1542
|
+
// markdown table). Staleness is handled by identity in mergeMessages.
|
|
1543
|
+
return existingText + incomingText;
|
|
1544
|
+
}
|
|
1545
|
+
// Snapshot mode (messages/partial, values-sync): payloads carry the
|
|
1546
|
+
// message-so-far, so mutual prefix comparison picks the longer state and
|
|
1547
|
+
// ignores stale shorter snapshots.
|
|
1508
1548
|
if (incomingText.startsWith(existingText))
|
|
1509
1549
|
return incomingText;
|
|
1510
|
-
// Existing already a strict-superset — chunk arrived after the canonical
|
|
1511
|
-
// message merged in via values-sync. Keep what we have.
|
|
1512
1550
|
if (existingText.startsWith(incomingText))
|
|
1513
1551
|
return existingText;
|
|
1514
|
-
// Final-canonical detection: when incoming is the "reasoning + text"
|
|
1515
|
-
// array shape that ships the authoritative final message after a
|
|
1516
|
-
// streaming run, replace the partial streamed accumulator with the
|
|
1517
|
-
// canonical text instead of appending. Without this branch a small
|
|
1518
|
-
// formatting difference between the streamed accumulator and the
|
|
1519
|
-
// canonical text breaks the prefix checks above and visible content
|
|
1520
|
-
// is duplicated (`existingText + incomingText`).
|
|
1521
|
-
if (isFinalCanonicalReasoningContent(incoming))
|
|
1522
|
-
return incomingText;
|
|
1523
|
-
// Otherwise treat incoming as a delta and append.
|
|
1524
1552
|
return existingText + incomingText;
|
|
1525
1553
|
}
|
|
1526
1554
|
function extractText(content) {
|