@threadplane/langgraph 0.0.58 → 0.0.59

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.
@@ -278,6 +278,7 @@ class SubagentTracker {
278
278
  this.subagents.set(id, {
279
279
  id,
280
280
  generation: existing?.generation ?? createSubagentGeneration(),
281
+ kind: 'tool',
281
282
  status: existing?.status ?? 'pending',
282
283
  toolCall: {
283
284
  id,
@@ -328,14 +329,14 @@ class SubagentTracker {
328
329
  return toolCallId;
329
330
  };
330
331
  for (const [toolCallId, subagent] of this.subagents) {
331
- if (mapped.has(toolCallId))
332
+ if (subagent.kind !== 'tool' || mapped.has(toolCallId))
332
333
  continue;
333
334
  if (subagent.toolCall.args['description'] === description) {
334
335
  return establish(toolCallId);
335
336
  }
336
337
  }
337
338
  for (const [toolCallId, subagent] of this.subagents) {
338
- if (mapped.has(toolCallId))
339
+ if (subagent.kind !== 'tool' || mapped.has(toolCallId))
339
340
  continue;
340
341
  const subagentDescription = subagent.toolCall.args['description'];
341
342
  if (typeof subagentDescription !== 'string' || !subagentDescription)
@@ -344,7 +345,11 @@ class SubagentTracker {
344
345
  return establish(toolCallId);
345
346
  }
346
347
  }
348
+ // Last-resort fallback — tool children only. A subgraph child is keyed by
349
+ // its own namespace and must never absorb an unrelated child's events.
347
350
  for (const [toolCallId, subagent] of this.subagents) {
351
+ if (subagent.kind !== 'tool')
352
+ continue;
348
353
  if (!mapped.has(toolCallId) && (subagent.status === 'pending' || subagent.status === 'running')) {
349
354
  return establish(toolCallId);
350
355
  }
@@ -372,6 +377,46 @@ class SubagentTracker {
372
377
  });
373
378
  this.onSubagentChange?.();
374
379
  }
380
+ /**
381
+ * Register a plain-subgraph child stream on its first namespaced event.
382
+ *
383
+ * Unlike tool children — announced ahead of time by the parent's tool call —
384
+ * a compiled child added as a plain node has no announcement: its existence
385
+ * is learned from the first event carrying its namespace. It starts
386
+ * 'running' because by the time we see an event, it is.
387
+ */
388
+ ensureSubgraphStream(key, name) {
389
+ if (this.subagents.has(key))
390
+ return;
391
+ this.subagents.set(key, {
392
+ id: key,
393
+ generation: createSubagentGeneration(),
394
+ kind: 'subgraph',
395
+ status: 'running',
396
+ toolCall: { id: key, name, args: {} },
397
+ values: {},
398
+ messages: [],
399
+ });
400
+ this.onSubagentChange?.();
401
+ }
402
+ /**
403
+ * Settle still-running subgraph children when the run reaches a terminal
404
+ * outcome. Tool children settle through their tool result
405
+ * (`processToolMessage`); subgraph children have no result message, so the
406
+ * run's own settle is their completion signal. Paused/interrupted runs must
407
+ * NOT call this — a child can resume with the thread.
408
+ */
409
+ settleRunningSubgraphs(outcome) {
410
+ let changed = false;
411
+ for (const [key, subagent] of this.subagents) {
412
+ if (subagent.kind !== 'subgraph' || subagent.status !== 'running')
413
+ continue;
414
+ this.subagents.set(key, { ...subagent, status: outcome });
415
+ changed = true;
416
+ }
417
+ if (changed)
418
+ this.onSubagentChange?.();
419
+ }
375
420
  updateSubagentValues(namespaceId, values) {
376
421
  const toolCallId = this.resolveToolCallId(namespaceId);
377
422
  const subagent = this.subagents.get(toolCallId);
@@ -437,12 +482,40 @@ class SubagentTracker {
437
482
  return this.namespaceToToolCallId.get(namespaceId) ?? namespaceId;
438
483
  }
439
484
  }
440
- function isSubagentNamespace(namespace) {
485
+ /**
486
+ * True when a stream event belongs to a child graph rather than the parent —
487
+ * i.e. it carries any namespace at all. This is the single classification
488
+ * question; which child owns the event is a separate (attribution) question.
489
+ *
490
+ * Kept consistent with the terminal-evidence guard, which has always refused
491
+ * ANY namespaced event as proof the parent run finished.
492
+ */
493
+ function isChildNamespace(namespace) {
441
494
  if (!namespace)
442
495
  return false;
443
496
  if (typeof namespace === 'string')
444
- return namespace.includes('tools:');
445
- return namespace.some(segment => segment.startsWith('tools:'));
497
+ return namespace.length > 0;
498
+ return namespace.length > 0;
499
+ }
500
+ /**
501
+ * Derive a child stream's identity from an event namespace.
502
+ *
503
+ * `tools:<id>` segments identify a tool-dispatched child by its tool-call id.
504
+ * Any other segment (e.g. `research:<uuid>` from a compiled graph added with
505
+ * `add_node`) identifies a plain subgraph child: the full segment is the key
506
+ * (unique per invocation) and the part before the first ':' is the node name.
507
+ */
508
+ function childStreamRefFromNamespace(namespace) {
509
+ for (const segment of namespace) {
510
+ if (segment.startsWith('tools:')) {
511
+ return { key: segment.slice(6), name: '', kind: 'tool' };
512
+ }
513
+ }
514
+ const first = namespace[0];
515
+ if (!first)
516
+ return undefined;
517
+ const colon = first.indexOf(':');
518
+ return { key: first, name: colon > 0 ? first.slice(0, colon) : first, kind: 'subgraph' };
446
519
  }
447
520
  function extractToolCallIdFromNamespace(namespace) {
448
521
  if (!namespace)
@@ -649,6 +722,13 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
649
722
  continue;
650
723
  finalizeMessage(attempt, id, outcome);
651
724
  }
725
+ // Subgraph children have no tool result to settle them; the run's own
726
+ // terminal outcome is their completion signal. Paused/interrupted runs
727
+ // are excluded — a child can resume with the thread.
728
+ if (outcome === 'success' || outcome === 'error' || outcome === 'aborted') {
729
+ subagentManager.settleRunningSubgraphs(outcome === 'success' ? 'complete' : 'error');
730
+ publishSubagents();
731
+ }
652
732
  }
653
733
  function finishOutcome(attempt) {
654
734
  return attempt.terminalOutcome
@@ -1118,17 +1198,23 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
1118
1198
  const normalized = options.toMessage
1119
1199
  ? msgs.map(options.toMessage)
1120
1200
  : msgs;
1121
- if (isSubagentNamespace(namespace)) {
1122
- const namespaceId = namespace ? extractToolCallIdFromNamespace(namespace) : undefined;
1123
- if (namespaceId) {
1201
+ // Any namespaced message event is child content. It feeds the child's
1202
+ // stream and never merges into the parent transcript — the parent
1203
+ // transcript is what the parent graph says. Shared-state children still
1204
+ // surface at settle through the authoritative top-level `values` sync.
1205
+ if (isChildNamespace(namespace)) {
1206
+ const child = namespace ? childStreamRefFromNamespace(namespace) : undefined;
1207
+ if (child) {
1208
+ if (child.kind === 'subgraph') {
1209
+ subagentManager.ensureSubgraphStream(child.key, child.name);
1210
+ }
1124
1211
  for (const msg of normalized) {
1125
- subagentManager.addMessageToSubagent(namespaceId, msg);
1212
+ subagentManager.addMessageToSubagent(child.key, msg);
1126
1213
  }
1127
1214
  publishSubagents();
1128
1215
  }
1129
- if (options.filterSubagentMessages) {
1130
- return;
1131
- }
1216
+ storeMessageMetadata(normalized, event);
1217
+ return;
1132
1218
  }
1133
1219
  // Partial and message-tuple events are incremental. Merge them by id
1134
1220
  // so optimistic human messages and earlier tool messages are preserved.
@@ -1143,7 +1229,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
1143
1229
  const merged = mergeMessages(subjects.messages$.value, normalized, reasoningTimingMap, mode, canonicalMessageIds, affectedMessageIds, activeAttempt?.currentAssistantMessageId !== undefined
1144
1230
  && activeAttempt.currentStepHasTerminalEvidence !== true);
1145
1231
  subjects.messages$.next(merged);
1146
- if (!isSubagentNamespace(namespace)) {
1232
+ {
1147
1233
  trackAssistantMessages(merged.filter(message => {
1148
1234
  const id = message['id'];
1149
1235
  return typeof id === 'string' && affectedMessageIds.has(id);
@@ -1166,7 +1252,7 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
1166
1252
  const affectedMessageIds = new Set();
1167
1253
  const preserved = preserveIds(subjects.messages$.value, normalized, affectedMessageIds);
1168
1254
  subjects.messages$.next(preserved);
1169
- if (!isSubagentNamespace(namespace)) {
1255
+ {
1170
1256
  trackAssistantMessages(preserved.filter(message => {
1171
1257
  const id = message['id'];
1172
1258
  return typeof id === 'string' && affectedMessageIds.has(id);
@@ -1185,8 +1271,11 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
1185
1271
  switch (baseType) {
1186
1272
  case 'values': {
1187
1273
  const vals = extractEventData(event);
1188
- if (isSubagentNamespace(namespace) && isRecord$1(vals)) {
1189
- updateSubagentValues(namespace, vals);
1274
+ if (isChildNamespace(namespace)) {
1275
+ // A child's state must not clobber the parent's `values$` — route it
1276
+ // to the child stream and stop.
1277
+ if (isRecord$1(vals))
1278
+ updateSubagentValues(namespace, vals);
1190
1279
  break;
1191
1280
  }
1192
1281
  if ((namespace?.length ?? 0) === 0) {
@@ -1252,7 +1341,9 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
1252
1341
  }
1253
1342
  case 'updates': {
1254
1343
  const upd = extractEventData(event);
1255
- if (isSubagentNamespace(namespace)) {
1344
+ if (isChildNamespace(namespace)) {
1345
+ // A child's updates must not spread-merge into the parent's
1346
+ // `values$` — they only mark the child stream running.
1256
1347
  markSubagentRunning(namespace);
1257
1348
  break;
1258
1349
  }
@@ -1315,24 +1406,34 @@ function createStreamManagerBridge({ options, subjects, threadId$, destroy$ }) {
1315
1406
  publishSubagents();
1316
1407
  }
1317
1408
  function updateSubagentValues(namespace, values) {
1318
- const namespaceId = namespace ? extractToolCallIdFromNamespace(namespace) : undefined;
1319
- if (!namespaceId)
1409
+ const child = namespace ? childStreamRefFromNamespace(namespace) : undefined;
1410
+ if (!child)
1320
1411
  return;
1321
- const messages = values['messages'];
1322
- if (Array.isArray(messages) && messages.length > 0) {
1323
- const first = messages[0];
1324
- if (isRecord$1(first) && (first['type'] === 'human' || first['type'] === 'user') && typeof first['content'] === 'string') {
1325
- subagentManager.matchSubgraphToSubagent(namespaceId, first['content']);
1412
+ if (child.kind === 'tool') {
1413
+ // Attribution ladder applies to tool children only: their namespace id
1414
+ // may need mapping onto a registered tool call.
1415
+ const messages = values['messages'];
1416
+ if (Array.isArray(messages) && messages.length > 0) {
1417
+ const first = messages[0];
1418
+ if (isRecord$1(first) && (first['type'] === 'human' || first['type'] === 'user') && typeof first['content'] === 'string') {
1419
+ subagentManager.matchSubgraphToSubagent(child.key, first['content']);
1420
+ }
1326
1421
  }
1327
1422
  }
1328
- subagentManager.updateSubagentValues(namespaceId, values);
1423
+ else {
1424
+ subagentManager.ensureSubgraphStream(child.key, child.name);
1425
+ }
1426
+ subagentManager.updateSubagentValues(child.key, values);
1329
1427
  publishSubagents();
1330
1428
  }
1331
1429
  function markSubagentRunning(namespace) {
1332
- const namespaceId = namespace ? extractToolCallIdFromNamespace(namespace) : undefined;
1333
- if (!namespaceId)
1430
+ const child = namespace ? childStreamRefFromNamespace(namespace) : undefined;
1431
+ if (!child)
1334
1432
  return;
1335
- subagentManager.markRunningFromNamespace(namespaceId, namespace);
1433
+ if (child.kind === 'subgraph') {
1434
+ subagentManager.ensureSubgraphStream(child.key, child.name);
1435
+ }
1436
+ subagentManager.markRunningFromNamespace(child.key, namespace);
1336
1437
  publishSubagents();
1337
1438
  }
1338
1439
  function publishSubagents() {
@@ -2130,9 +2231,13 @@ function toSubagentRefs(subagents) {
2130
2231
  subagents.forEach((subagent, key) => {
2131
2232
  refs.set(key, {
2132
2233
  toolCallId: subagent.id,
2234
+ // Tool children are named by their `subagent_type` arg; subgraph
2235
+ // children by their node name (stored as the synthetic toolCall name).
2133
2236
  name: typeof subagent.toolCall.args['subagent_type'] === 'string'
2134
2237
  ? subagent.toolCall.args['subagent_type']
2135
- : undefined,
2238
+ : subagent.kind === 'subgraph'
2239
+ ? subagent.toolCall.name
2240
+ : undefined,
2136
2241
  status: signal(subagent.status),
2137
2242
  values: signal(subagent.values),
2138
2243
  messages: signal(subagent.messages),
@@ -3424,7 +3529,6 @@ function agentFactory() {
3424
3529
  ...(config.transport !== undefined ? { transport: config.transport } : {}),
3425
3530
  ...(config.clientOptions !== undefined ? { clientOptions: config.clientOptions } : {}),
3426
3531
  ...(config.telemetry !== undefined ? { telemetry: config.telemetry } : {}),
3427
- ...(config.filterSubagentMessages !== undefined ? { filterSubagentMessages: config.filterSubagentMessages } : {}),
3428
3532
  ...(config.subagentToolNames !== undefined ? { subagentToolNames: config.subagentToolNames } : {}),
3429
3533
  ...(config.transcriptNodeNames !== undefined ? { transcriptNodeNames: config.transcriptNodeNames } : {}),
3430
3534
  });