@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
|
-
|
|
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.
|
|
445
|
-
return namespace.
|
|
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
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
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(
|
|
1212
|
+
subagentManager.addMessageToSubagent(child.key, msg);
|
|
1126
1213
|
}
|
|
1127
1214
|
publishSubagents();
|
|
1128
1215
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
1189
|
-
|
|
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 (
|
|
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
|
|
1319
|
-
if (!
|
|
1409
|
+
const child = namespace ? childStreamRefFromNamespace(namespace) : undefined;
|
|
1410
|
+
if (!child)
|
|
1320
1411
|
return;
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
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
|
-
|
|
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
|
|
1333
|
-
if (!
|
|
1430
|
+
const child = namespace ? childStreamRefFromNamespace(namespace) : undefined;
|
|
1431
|
+
if (!child)
|
|
1334
1432
|
return;
|
|
1335
|
-
|
|
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
|
-
:
|
|
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
|
});
|