@threadplane/langgraph 0.0.54 → 0.0.56
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
|
@@ -27,7 +27,7 @@ Adapter that wraps a LangGraph agent into the runtime-neutral `Agent` contract f
|
|
|
27
27
|
## Install
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
npm install @threadplane/langgraph @threadplane/chat
|
|
30
|
+
npm install @threadplane/langgraph @threadplane/chat @langchain/core @langchain/langgraph-sdk
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
**Peer dependencies:**
|
|
@@ -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
|
],
|
|
@@ -159,7 +159,7 @@ Pair it with the lifecycle helpers to keep your thread list fresh:
|
|
|
159
159
|
```ts
|
|
160
160
|
import { refreshOnRunEnd, refreshOnTransition } from '@threadplane/langgraph';
|
|
161
161
|
|
|
162
|
-
refreshOnRunEnd(chat, () => threadsAdapter.
|
|
162
|
+
refreshOnRunEnd(chat, () => threadsAdapter.refresh());
|
|
163
163
|
```
|
|
164
164
|
|
|
165
165
|
### Citations
|
|
@@ -13,8 +13,8 @@ import { toAgentError, isAbortError, AgentError, AGENT_ERROR_MESSAGES, mockAgent
|
|
|
13
13
|
* an Angular injection context. External instrumentation packages
|
|
14
14
|
* (e.g. cockpit-telemetry) provide this token and read from it.
|
|
15
15
|
*
|
|
16
|
-
* `@threadplane/langgraph` does NOT provide this itself —
|
|
17
|
-
* the registry only when an external consumer has provided it.
|
|
16
|
+
* `@threadplane/langgraph` does NOT provide this itself — the configured agent
|
|
17
|
+
* instance writes to the registry only when an external consumer has provided it.
|
|
18
18
|
*/
|
|
19
19
|
class AgentLifecycleRegistry {
|
|
20
20
|
_lifecycles = signal([], ...(ngDevMode ? [{ debugName: "_lifecycles" }] : []));
|
|
@@ -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({
|
|
@@ -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) {
|
|
@@ -782,6 +787,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
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.
|
|
@@ -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,
|
|
@@ -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) {
|
|
@@ -3077,7 +3105,7 @@ function delay(ms) {
|
|
|
3077
3105
|
* @example
|
|
3078
3106
|
* ```ts
|
|
3079
3107
|
* TestBed.configureTestingModule({
|
|
3080
|
-
* providers: [provideFakeAgent({
|
|
3108
|
+
* providers: [provideFakeAgent({ tokens: ['Hi from the fake LangGraph agent'] })],
|
|
3081
3109
|
* });
|
|
3082
3110
|
* ```
|
|
3083
3111
|
*/
|