@threadplane/langgraph 0.0.59 → 0.0.60
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.
|
@@ -243,6 +243,18 @@ class SubagentTracker {
|
|
|
243
243
|
onSubagentChange;
|
|
244
244
|
subagents = new Map();
|
|
245
245
|
namespaceToToolCallId = new Map();
|
|
246
|
+
/**
|
|
247
|
+
* Child messages received under a namespace that is not yet attributed to a
|
|
248
|
+
* registered subagent. LangGraph's `tools:<id>` namespace carries an internal
|
|
249
|
+
* run UUID, not the parent's tool-call id, and the two are only reconciled
|
|
250
|
+
* once a `values` event arrives carrying the child's first human message. Any
|
|
251
|
+
* chunk streamed before that point would otherwise be dropped — which is what
|
|
252
|
+
* made subagent cards render "0 message(s)" despite a full child transcript.
|
|
253
|
+
*
|
|
254
|
+
* Merged by id like a real transcript, so this stays bounded by the child's
|
|
255
|
+
* distinct message count rather than by chunk volume.
|
|
256
|
+
*/
|
|
257
|
+
unattributedMessages = new Map();
|
|
246
258
|
pendingMatches = new Map();
|
|
247
259
|
constructor(options = {}) {
|
|
248
260
|
this.subagentToolNames = new Set(options.subagentToolNames ?? DEFAULT_SUBAGENT_TOOL_NAMES);
|
|
@@ -252,6 +264,7 @@ class SubagentTracker {
|
|
|
252
264
|
this.subagents.clear();
|
|
253
265
|
this.namespaceToToolCallId.clear();
|
|
254
266
|
this.pendingMatches.clear();
|
|
267
|
+
this.unattributedMessages.clear();
|
|
255
268
|
this.onSubagentChange?.();
|
|
256
269
|
}
|
|
257
270
|
getSubagents() {
|
|
@@ -320,10 +333,13 @@ class SubagentTracker {
|
|
|
320
333
|
this.namespaceToToolCallId.set(namespaceId, toolCallId);
|
|
321
334
|
const subagent = this.subagents.get(toolCallId);
|
|
322
335
|
if (subagent) {
|
|
336
|
+
const buffered = this.unattributedMessages.get(namespaceId);
|
|
323
337
|
this.subagents.set(toolCallId, {
|
|
324
338
|
...subagent,
|
|
325
339
|
status: subagent.status === 'complete' || subagent.status === 'error' ? subagent.status : 'running',
|
|
340
|
+
messages: buffered ? mergeMessages$1(subagent.messages, buffered) : subagent.messages,
|
|
326
341
|
});
|
|
342
|
+
this.unattributedMessages.delete(namespaceId);
|
|
327
343
|
}
|
|
328
344
|
this.onSubagentChange?.();
|
|
329
345
|
return toolCallId;
|
|
@@ -377,6 +393,27 @@ class SubagentTracker {
|
|
|
377
393
|
});
|
|
378
394
|
this.onSubagentChange?.();
|
|
379
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Attribute a `tools:` child stream to its parent tool call as soon as the
|
|
398
|
+
* child is seen, without requiring a description to match on.
|
|
399
|
+
*
|
|
400
|
+
* The description ladder needs two things this repo's own graphs don't
|
|
401
|
+
* reliably provide: a delegation tool that names its argument `description`,
|
|
402
|
+
* and a child whose first message is the human task. `cockpit/chat/subagents`
|
|
403
|
+
* has neither — it uses `task_description`, and its child's messages begin
|
|
404
|
+
* with the AI reply. Attribution therefore never ran, so the child's
|
|
405
|
+
* transcript was never claimed and every card rendered "0 message(s)".
|
|
406
|
+
*
|
|
407
|
+
* Calling the ladder with no description skips both description rungs and
|
|
408
|
+
* lands on the positional fallback (first unmapped pending/running tool
|
|
409
|
+
* child), which is correct for sequential dispatch and is the same heuristic
|
|
410
|
+
* the ladder already relied on in practice.
|
|
411
|
+
*/
|
|
412
|
+
ensureToolStreamAttribution(namespaceId) {
|
|
413
|
+
if (this.namespaceToToolCallId.has(namespaceId))
|
|
414
|
+
return;
|
|
415
|
+
this.matchSubgraphToSubagent(namespaceId, '');
|
|
416
|
+
}
|
|
380
417
|
/**
|
|
381
418
|
* Register a plain-subgraph child stream on its first namespaced event.
|
|
382
419
|
*
|
|
@@ -432,8 +469,12 @@ class SubagentTracker {
|
|
|
432
469
|
addMessageToSubagent(namespaceId, message) {
|
|
433
470
|
const toolCallId = this.resolveToolCallId(namespaceId);
|
|
434
471
|
const subagent = this.subagents.get(toolCallId);
|
|
435
|
-
if (!subagent)
|
|
472
|
+
if (!subagent) {
|
|
473
|
+
// Not attributed yet — hold it rather than drop it. `establish()` will
|
|
474
|
+
// replay the buffer the moment this namespace is matched to a tool call.
|
|
475
|
+
this.unattributedMessages.set(namespaceId, mergeMessages$1(this.unattributedMessages.get(namespaceId) ?? [], [message]));
|
|
436
476
|
return;
|
|
477
|
+
}
|
|
437
478
|
this.subagents.set(toolCallId, {
|
|
438
479
|
...subagent,
|
|
439
480
|
status: subagent.status === 'complete' || subagent.status === 'error' ? subagent.status : 'running',
|
|
@@ -558,7 +599,7 @@ function mergeMessages$1(existing, incoming) {
|
|
|
558
599
|
const id = getMessageId(msg);
|
|
559
600
|
const idx = id ? merged.findIndex(m => getMessageId(m) === id) : -1;
|
|
560
601
|
if (idx >= 0) {
|
|
561
|
-
merged[idx] = msg;
|
|
602
|
+
merged[idx] = accumulateChunk(merged[idx], msg);
|
|
562
603
|
}
|
|
563
604
|
else {
|
|
564
605
|
merged.push(msg);
|
|
@@ -566,6 +607,57 @@ function mergeMessages$1(existing, incoming) {
|
|
|
566
607
|
}
|
|
567
608
|
return merged;
|
|
568
609
|
}
|
|
610
|
+
/**
|
|
611
|
+
* Fold a streamed chunk into the message it belongs to.
|
|
612
|
+
*
|
|
613
|
+
* A child graph streams `AIMessageChunk`s that are *deltas* — a handful of
|
|
614
|
+
* characters each, hundreds per message. Replacing by id (the previous
|
|
615
|
+
* behavior) therefore kept only the final delta, so a fully attributed
|
|
616
|
+
* subagent still rendered a near-empty message. Snapshots, which carry the
|
|
617
|
+
* message-so-far, still replace.
|
|
618
|
+
*
|
|
619
|
+
* This mirrors the parent transcript's delta handling: append unconditionally
|
|
620
|
+
* rather than comparing text, because a prefix-style "dedupe" silently eats
|
|
621
|
+
* legitimate tokens that happen to repeat the accumulated prefix.
|
|
622
|
+
*/
|
|
623
|
+
function accumulateChunk(existing, incoming) {
|
|
624
|
+
if (!isChunkMessage(incoming))
|
|
625
|
+
return incoming;
|
|
626
|
+
const previousText = extractText$1(existing['content']);
|
|
627
|
+
const incomingText = extractText$1(incoming['content']);
|
|
628
|
+
if (!incomingText)
|
|
629
|
+
return existing;
|
|
630
|
+
if (!previousText)
|
|
631
|
+
return incoming;
|
|
632
|
+
return { ...incoming, content: previousText + incomingText };
|
|
633
|
+
}
|
|
634
|
+
function isChunkMessage(message) {
|
|
635
|
+
const type = message['type'];
|
|
636
|
+
return typeof type === 'string' && type.endsWith('Chunk');
|
|
637
|
+
}
|
|
638
|
+
function extractText$1(content) {
|
|
639
|
+
if (typeof content === 'string')
|
|
640
|
+
return content;
|
|
641
|
+
if (!Array.isArray(content))
|
|
642
|
+
return '';
|
|
643
|
+
let out = '';
|
|
644
|
+
for (const block of content) {
|
|
645
|
+
if (typeof block === 'string') {
|
|
646
|
+
out += block;
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (block == null || typeof block !== 'object')
|
|
650
|
+
continue;
|
|
651
|
+
const record = block;
|
|
652
|
+
const blockType = record['type'];
|
|
653
|
+
if (blockType === 'text' || blockType === 'output_text' || blockType === undefined) {
|
|
654
|
+
const text = record['text'];
|
|
655
|
+
if (typeof text === 'string')
|
|
656
|
+
out += text;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return out;
|
|
660
|
+
}
|
|
569
661
|
function getMessageId(message) {
|
|
570
662
|
return message.id;
|
|
571
663
|
}
|
|
@@ -1208,6 +1300,10 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
1208
1300
|
if (child.kind === 'subgraph') {
|
|
1209
1301
|
subagentManager.ensureSubgraphStream(child.key, child.name);
|
|
1210
1302
|
}
|
|
1303
|
+
else {
|
|
1304
|
+
// Claim the stream for its tool call before its tokens arrive.
|
|
1305
|
+
subagentManager.ensureToolStreamAttribution(child.key);
|
|
1306
|
+
}
|
|
1211
1307
|
for (const msg of normalized) {
|
|
1212
1308
|
subagentManager.addMessageToSubagent(child.key, msg);
|
|
1213
1309
|
}
|
|
@@ -1410,14 +1506,16 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
|
|
|
1410
1506
|
if (!child)
|
|
1411
1507
|
return;
|
|
1412
1508
|
if (child.kind === 'tool') {
|
|
1413
|
-
//
|
|
1414
|
-
//
|
|
1509
|
+
// Prefer the precise description match when the child's first message is
|
|
1510
|
+
// the human task; otherwise claim the stream positionally so a graph that
|
|
1511
|
+
// doesn't fit that shape still gets attributed.
|
|
1415
1512
|
const messages = values['messages'];
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1513
|
+
const first = Array.isArray(messages) && messages.length > 0 ? messages[0] : undefined;
|
|
1514
|
+
if (isRecord$1(first) && (first['type'] === 'human' || first['type'] === 'user') && typeof first['content'] === 'string') {
|
|
1515
|
+
subagentManager.matchSubgraphToSubagent(child.key, first['content']);
|
|
1516
|
+
}
|
|
1517
|
+
else {
|
|
1518
|
+
subagentManager.ensureToolStreamAttribution(child.key);
|
|
1421
1519
|
}
|
|
1422
1520
|
}
|
|
1423
1521
|
else {
|