@threadplane/langgraph 0.0.60 → 0.0.62

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.
@@ -361,14 +361,28 @@ class SubagentTracker {
361
361
  return establish(toolCallId);
362
362
  }
363
363
  }
364
- // Last-resort fallback — tool children only. A subgraph child is keyed by
365
- // its own namespace and must never absorb an unrelated child's events.
366
- for (const [toolCallId, subagent] of this.subagents) {
367
- if (subagent.kind !== 'tool')
368
- continue;
369
- if (!mapped.has(toolCallId) && (subagent.status === 'pending' || subagent.status === 'running')) {
370
- return establish(toolCallId);
371
- }
364
+ // Last-resort fallback — tool children only, and only when there is
365
+ // nothing to guess between.
366
+ //
367
+ // LangGraph's `tools:<uuid>` namespace is a checkpoint id assigned
368
+ // independently of the parent's `call_*` tool-call id; the two are not
369
+ // linked anywhere on the wire (verified against a live run). So when a
370
+ // delegation tool carries no matchable description, position is the only
371
+ // signal left.
372
+ //
373
+ // That is sound with exactly one outstanding child — the shape every graph
374
+ // in this repo produces, since each dispatches one tool call per assistant
375
+ // turn. With several outstanding at once (parallel fan-out) arrival order
376
+ // is NOT dispatch order, and claiming the first unmapped call cross-wires
377
+ // the children: one card renders another's output. Leaving the stream
378
+ // unattributed keeps its messages buffered instead, so an empty card is
379
+ // the worst case rather than a confidently wrong one. It can still resolve
380
+ // later: as siblings complete, the candidate set shrinks back to one.
381
+ const candidates = [...this.subagents].filter(([toolCallId, subagent]) => subagent.kind === 'tool' &&
382
+ !mapped.has(toolCallId) &&
383
+ (subagent.status === 'pending' || subagent.status === 'running'));
384
+ if (candidates.length === 1) {
385
+ return establish(candidates[0][0]);
372
386
  }
373
387
  if (description) {
374
388
  this.pendingMatches.set(namespaceId, description);
@@ -393,6 +407,32 @@ class SubagentTracker {
393
407
  });
394
408
  this.onSubagentChange?.();
395
409
  }
410
+ /**
411
+ * Authoritative namespace→tool-call binding, from the server.
412
+ *
413
+ * `threadplane.middleware.langgraph.announce_subagent` emits a custom event
414
+ * pairing the child's checkpoint namespace with its tool-call id — the two
415
+ * halves that are never linked on the wire otherwise. Unlike the matching
416
+ * ladder this is not a heuristic: it overrides nothing that is already
417
+ * mapped, works with any number of children outstanding, and replays any
418
+ * chunks that streamed before the binding arrived.
419
+ */
420
+ bindChildStream(namespaceId, toolCallId) {
421
+ if (this.namespaceToToolCallId.get(namespaceId) === toolCallId)
422
+ return;
423
+ this.namespaceToToolCallId.set(namespaceId, toolCallId);
424
+ const subagent = this.subagents.get(toolCallId);
425
+ if (subagent) {
426
+ const buffered = this.unattributedMessages.get(namespaceId);
427
+ this.subagents.set(toolCallId, {
428
+ ...subagent,
429
+ status: subagent.status === 'complete' || subagent.status === 'error' ? subagent.status : 'running',
430
+ messages: buffered ? mergeMessages$1(subagent.messages, buffered) : subagent.messages,
431
+ });
432
+ this.unattributedMessages.delete(namespaceId);
433
+ }
434
+ this.onSubagentChange?.();
435
+ }
396
436
  /**
397
437
  * Attribute a `tools:` child stream to its parent tool call as soon as the
398
438
  * child is seen, without requiring a description to match on.
@@ -1470,6 +1510,20 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
1470
1510
  break;
1471
1511
  case 'custom': {
1472
1512
  const eventData = event['data'];
1513
+ // Server-announced subagent identity (threadplane-middleware's
1514
+ // `announce_subagent`). Consumed here rather than forwarded: it is
1515
+ // protocol chatter, not application data.
1516
+ if (isRecord$1(eventData)
1517
+ && eventData['type'] === 'threadplane.subagent_binding'
1518
+ && typeof eventData['namespace'] === 'string'
1519
+ && typeof eventData['tool_call_id'] === 'string') {
1520
+ const bound = childStreamRefFromNamespace([eventData['namespace']]);
1521
+ if (bound?.kind === 'tool') {
1522
+ subagentManager.bindChildStream(bound.key, eventData['tool_call_id']);
1523
+ publishSubagents();
1524
+ }
1525
+ break;
1526
+ }
1473
1527
  const name = (event['name'] ?? eventData?.['name'] ?? '');
1474
1528
  const data = eventData?.['data'] ?? eventData;
1475
1529
  const current = subjects.custom$.value;