@botbotgo/agent-harness 0.0.156 → 0.0.158

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.
Files changed (38) hide show
  1. package/README.md +36 -0
  2. package/README.zh.md +28 -0
  3. package/dist/acp.d.ts +86 -0
  4. package/dist/acp.js +208 -0
  5. package/dist/api.d.ts +15 -2
  6. package/dist/api.js +11 -0
  7. package/dist/contracts/runtime.d.ts +55 -0
  8. package/dist/flow/build-flow-graph.d.ts +2 -0
  9. package/dist/flow/build-flow-graph.js +737 -0
  10. package/dist/flow/export-mermaid.d.ts +2 -0
  11. package/dist/flow/export-mermaid.js +96 -0
  12. package/dist/flow/export-sequence-mermaid.d.ts +3 -0
  13. package/dist/flow/export-sequence-mermaid.js +169 -0
  14. package/dist/flow/index.d.ts +4 -0
  15. package/dist/flow/index.js +3 -0
  16. package/dist/flow/types.d.ts +75 -0
  17. package/dist/flow/types.js +1 -0
  18. package/dist/index.d.ts +4 -2
  19. package/dist/index.js +1 -1
  20. package/dist/package-version.d.ts +1 -1
  21. package/dist/package-version.js +1 -1
  22. package/dist/persistence/file-store.d.ts +1 -0
  23. package/dist/persistence/file-store.js +10 -1
  24. package/dist/persistence/types.d.ts +2 -0
  25. package/dist/runtime/adapter/tool/resolved-tool.d.ts +1 -1
  26. package/dist/runtime/agent-runtime-adapter.d.ts +5 -1
  27. package/dist/runtime/agent-runtime-adapter.js +61 -24
  28. package/dist/runtime/harness/events/streaming.js +6 -0
  29. package/dist/runtime/harness/run/governance.d.ts +2 -0
  30. package/dist/runtime/harness/run/governance.js +76 -0
  31. package/dist/runtime/harness/run/inspection.js +4 -0
  32. package/dist/runtime/harness/run/stream-run.js +1 -1
  33. package/dist/runtime/harness/system/policy-engine.d.ts +2 -1
  34. package/dist/runtime/harness/system/policy-engine.js +5 -1
  35. package/dist/runtime/harness.d.ts +5 -1
  36. package/dist/runtime/harness.js +82 -0
  37. package/dist/workspace/agent-binding-compiler.js +7 -1
  38. package/package.json +11 -2
@@ -0,0 +1,737 @@
1
+ import { projectRuntimeTimeline } from "../runtime/harness/events/timeline.js";
2
+ import { createUpstreamTimelineReducer } from "../upstream-events.js";
3
+ function asObject(value) {
4
+ return typeof value === "object" && value !== null ? value : null;
5
+ }
6
+ function readString(value) {
7
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
8
+ }
9
+ function extractUpstreamEventEnvelope(value) {
10
+ const typed = asObject(value);
11
+ const nestedEvent = typed && Object.prototype.hasOwnProperty.call(typed, "event") ? typed.event : undefined;
12
+ const event = typed
13
+ && nestedEvent
14
+ && typeof nestedEvent === "object"
15
+ && !Array.isArray(nestedEvent)
16
+ && Object.prototype.hasOwnProperty.call(typed, "agentId")
17
+ ? nestedEvent
18
+ : value;
19
+ const agentId = readString(typed?.agentId);
20
+ return { event, agentId: agentId ?? undefined };
21
+ }
22
+ function collectNestedAgentNames(value, names = new Set()) {
23
+ if (Array.isArray(value)) {
24
+ for (const item of value) {
25
+ collectNestedAgentNames(item, names);
26
+ }
27
+ return names;
28
+ }
29
+ const typed = asObject(value);
30
+ if (!typed) {
31
+ return names;
32
+ }
33
+ const idParts = Array.isArray(typed.id) ? typed.id.filter((item) => typeof item === "string") : [];
34
+ const kwargs = asObject(typed.kwargs);
35
+ const kwargsName = idParts.includes("AIMessage") ? readString(kwargs?.name) : null;
36
+ if (kwargsName && kwargsName !== "assistant") {
37
+ names.add(kwargsName);
38
+ }
39
+ for (const nested of Object.values(typed)) {
40
+ collectNestedAgentNames(nested, names);
41
+ }
42
+ return names;
43
+ }
44
+ function slugify(value) {
45
+ return value
46
+ .toLowerCase()
47
+ .replace(/[^a-z0-9]+/g, "-")
48
+ .replace(/^-+|-+$/g, "")
49
+ .slice(0, 80) || "node";
50
+ }
51
+ function titleCase(value) {
52
+ return value
53
+ .replace(/[_-]+/g, " ")
54
+ .replace(/\s+/g, " ")
55
+ .trim()
56
+ .split(" ")
57
+ .filter(Boolean)
58
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
59
+ .join(" ");
60
+ }
61
+ function normalizeLabel(value) {
62
+ return value.replace(/\s+/g, " ").trim();
63
+ }
64
+ function readProjectionLabel(projection) {
65
+ if (projection.type === "step") {
66
+ return projection.step;
67
+ }
68
+ if (projection.type === "tool-result") {
69
+ return `Tool result ${titleCase(projection.toolName)}`;
70
+ }
71
+ return projection.text;
72
+ }
73
+ function deriveNodeKindFromProjection(projection) {
74
+ if (projection.type === "tool-result") {
75
+ return "result";
76
+ }
77
+ if (projection.type === "thinking") {
78
+ return "thinking";
79
+ }
80
+ return projection.category;
81
+ }
82
+ function deriveStatusFromProjection(projection) {
83
+ if (projection.type === "thinking" || projection.type === "tool-result") {
84
+ return "completed";
85
+ }
86
+ return projection.category === "approval" && projection.status === "started" ? "waiting" : projection.status;
87
+ }
88
+ function deriveRuntimeNode(input) {
89
+ if (input.eventType === "run.created") {
90
+ return {
91
+ kind: "run",
92
+ status: "started",
93
+ label: "Run created",
94
+ detail: input.payload,
95
+ };
96
+ }
97
+ if (input.eventType === "run.queued") {
98
+ return {
99
+ kind: "queue",
100
+ status: "waiting",
101
+ label: "Queued",
102
+ detail: input.payload,
103
+ };
104
+ }
105
+ if (input.eventType === "run.dequeued") {
106
+ return {
107
+ kind: "queue",
108
+ status: "completed",
109
+ label: "Dequeued",
110
+ detail: input.payload,
111
+ };
112
+ }
113
+ if (input.eventType === "approval.requested") {
114
+ return {
115
+ kind: "approval",
116
+ status: "waiting",
117
+ label: "Approval requested",
118
+ detail: input.payload,
119
+ };
120
+ }
121
+ if (input.eventType === "approval.resolved") {
122
+ return {
123
+ kind: "approval",
124
+ status: "resolved",
125
+ label: "Approval resolved",
126
+ detail: input.payload,
127
+ };
128
+ }
129
+ if (input.eventType === "run.resumed") {
130
+ return {
131
+ kind: "recovery",
132
+ status: "completed",
133
+ label: "Run resumed",
134
+ detail: input.payload,
135
+ };
136
+ }
137
+ if (input.eventType === "artifact.created") {
138
+ return {
139
+ kind: "artifact",
140
+ status: "completed",
141
+ label: "Artifact created",
142
+ detail: input.payload,
143
+ };
144
+ }
145
+ if (input.eventType === "run.state.changed") {
146
+ const state = typeof input.payload.state === "string" ? input.payload.state : "";
147
+ if (state === "waiting_for_approval") {
148
+ return {
149
+ kind: "approval",
150
+ status: "waiting",
151
+ label: "Waiting for approval",
152
+ detail: input.payload,
153
+ };
154
+ }
155
+ if (state === "failed") {
156
+ return {
157
+ kind: "run",
158
+ status: "failed",
159
+ label: "Run failed",
160
+ detail: input.payload,
161
+ };
162
+ }
163
+ if (state === "completed") {
164
+ return {
165
+ kind: "run",
166
+ status: "completed",
167
+ label: "Run completed",
168
+ detail: input.payload,
169
+ };
170
+ }
171
+ return {
172
+ kind: "run",
173
+ status: "started",
174
+ label: state ? `Run ${state}` : "Run state changed",
175
+ detail: input.payload,
176
+ };
177
+ }
178
+ return {
179
+ kind: input.kind,
180
+ status: "completed",
181
+ label: input.eventType,
182
+ detail: input.payload,
183
+ };
184
+ }
185
+ function buildRuntimeNodes(timeline) {
186
+ const nodes = [];
187
+ const edges = [];
188
+ const groups = [];
189
+ let previousNode = null;
190
+ let segmentOrdinal = 0;
191
+ let approvalOrdinal = 0;
192
+ let activeExecutionGroup = null;
193
+ let activeApprovalGroup = null;
194
+ for (const item of timeline) {
195
+ const mapped = deriveRuntimeNode(item);
196
+ const node = {
197
+ id: `rt:${item.runId}:${item.sequence}:${slugify(item.eventType)}`,
198
+ layer: "runtime",
199
+ kind: mapped.kind,
200
+ label: mapped.label,
201
+ status: mapped.status,
202
+ threadId: item.threadId,
203
+ runId: item.runId,
204
+ startedAt: item.timestamp,
205
+ endedAt: item.timestamp,
206
+ sequenceStart: item.sequence,
207
+ sequenceEnd: item.sequence,
208
+ sourceEventIds: [item.eventId],
209
+ detail: { ...mapped.detail, eventType: item.eventType, source: item.source },
210
+ };
211
+ nodes.push(node);
212
+ if (previousNode) {
213
+ const kind = item.eventType === "approval.resolved"
214
+ ? "approval"
215
+ : item.eventType === "run.resumed"
216
+ ? "resume"
217
+ : "sequence";
218
+ edges.push({
219
+ id: `edge:${previousNode.id}->${node.id}`,
220
+ from: previousNode.id,
221
+ to: node.id,
222
+ kind,
223
+ ...(kind === "approval" ? { label: typeof item.payload.decision === "string" ? item.payload.decision : "resolved" } : {}),
224
+ sourceEventIds: [previousNode.sourceEventIds[0], item.eventId],
225
+ });
226
+ }
227
+ previousNode = node;
228
+ if (item.eventType === "run.created" || item.eventType === "run.dequeued" || item.eventType === "run.resumed") {
229
+ segmentOrdinal += 1;
230
+ activeExecutionGroup = {
231
+ id: `group:${item.runId}:segment:${segmentOrdinal}`,
232
+ kind: "segment",
233
+ label: item.eventType === "run.created"
234
+ ? `Execution segment ${segmentOrdinal}`
235
+ : item.eventType === "run.resumed"
236
+ ? `Resumed segment ${segmentOrdinal}`
237
+ : `Dequeued segment ${segmentOrdinal}`,
238
+ nodeIds: [node.id],
239
+ startedAt: item.timestamp,
240
+ anchorNodeId: node.id,
241
+ };
242
+ groups.push(activeExecutionGroup);
243
+ activeApprovalGroup = null;
244
+ node.groupId = activeExecutionGroup.id;
245
+ continue;
246
+ }
247
+ if (item.eventType === "approval.requested") {
248
+ approvalOrdinal += 1;
249
+ activeApprovalGroup = {
250
+ id: `group:${item.runId}:approval:${approvalOrdinal}`,
251
+ kind: "approval-window",
252
+ label: `Approval window ${approvalOrdinal}`,
253
+ nodeIds: [node.id],
254
+ startedAt: item.timestamp,
255
+ anchorNodeId: node.id,
256
+ };
257
+ groups.push(activeApprovalGroup);
258
+ node.groupId = activeApprovalGroup.id;
259
+ continue;
260
+ }
261
+ if (item.eventType === "approval.resolved") {
262
+ if (!activeApprovalGroup) {
263
+ approvalOrdinal += 1;
264
+ activeApprovalGroup = {
265
+ id: `group:${item.runId}:approval:${approvalOrdinal}`,
266
+ kind: "approval-window",
267
+ label: `Approval window ${approvalOrdinal}`,
268
+ nodeIds: [],
269
+ anchorNodeId: node.id,
270
+ };
271
+ groups.push(activeApprovalGroup);
272
+ }
273
+ activeApprovalGroup.nodeIds.push(node.id);
274
+ activeApprovalGroup.endedAt = item.timestamp;
275
+ node.groupId = activeApprovalGroup.id;
276
+ continue;
277
+ }
278
+ if (activeApprovalGroup && !activeApprovalGroup.endedAt) {
279
+ activeApprovalGroup.nodeIds.push(node.id);
280
+ node.groupId = activeApprovalGroup.id;
281
+ continue;
282
+ }
283
+ if (activeExecutionGroup) {
284
+ activeExecutionGroup.nodeIds.push(node.id);
285
+ activeExecutionGroup.endedAt = item.timestamp;
286
+ node.groupId = activeExecutionGroup.id;
287
+ }
288
+ }
289
+ return { nodes, edges, groups };
290
+ }
291
+ function normalizeAttemptLabel(kind, projection) {
292
+ if (projection.type === "step") {
293
+ const label = normalizeLabel(projection.step);
294
+ if (label) {
295
+ return label;
296
+ }
297
+ }
298
+ if (projection.type === "tool-result") {
299
+ return `Tool ${titleCase(projection.toolName)}`;
300
+ }
301
+ if (projection.type === "thinking") {
302
+ return "Model reasoning";
303
+ }
304
+ return titleCase(kind);
305
+ }
306
+ function semanticNameFromLabel(kind, label) {
307
+ const normalized = normalizeLabel(label)
308
+ .replace(/^Calling LLM\s+/i, "")
309
+ .replace(/^Completed LLM\s+/i, "")
310
+ .replace(/^Calling tool\s+/i, "")
311
+ .replace(/^Completed tool\s+/i, "")
312
+ .replace(/^Tool\s+/i, "")
313
+ .replace(/\s+failed$/i, "")
314
+ .replace(/^Calling skill\s+/i, "")
315
+ .replace(/^Completed skill\s+/i, "")
316
+ .replace(/^Accessing memory\s+/i, "")
317
+ .replace(/^Completed memory\s+/i, "")
318
+ .replace(/^Running\s+/i, "")
319
+ .replace(/^Completed\s+/i, "");
320
+ return `${kind}:${normalized.toLowerCase()}`;
321
+ }
322
+ function normalizeToolIdentity(value) {
323
+ return value
324
+ .toLowerCase()
325
+ .replace(/[_-]+/g, " ")
326
+ .replace(/\s+/g, " ")
327
+ .trim();
328
+ }
329
+ function buildAttemptKey(kind, label, groupId) {
330
+ return `${semanticNameFromLabel(kind, label)}:${groupId ?? "ungrouped"}`;
331
+ }
332
+ function deriveInitialAgentId(input, runtimeTimeline) {
333
+ const candidate = runtimeTimeline.find((item) => {
334
+ const payload = asObject(item.payload);
335
+ return typeof payload?.agentId === "string" && payload.agentId.trim().length > 0;
336
+ });
337
+ const payload = candidate ? asObject(candidate.payload) : null;
338
+ const metadataAgentId = typeof input.metadata?.currentAgentId === "string" ? input.metadata.currentAgentId : null;
339
+ if (metadataAgentId && metadataAgentId.trim().length > 0) {
340
+ return metadataAgentId.trim();
341
+ }
342
+ if (payload?.agentId && typeof payload.agentId === "string" && payload.agentId.trim().length > 0) {
343
+ return payload.agentId.trim();
344
+ }
345
+ return "agent";
346
+ }
347
+ function extractDelegatedAgentId(event) {
348
+ const typed = asObject(event);
349
+ if (!typed) {
350
+ return null;
351
+ }
352
+ const eventName = typeof typed.event === "string" ? typed.event : "";
353
+ const toolName = typeof typed.name === "string" ? typed.name : "";
354
+ if (toolName !== "task" || (eventName !== "on_tool_start" && !(eventName === "on_chain_start" && typed.run_type === "tool"))) {
355
+ return null;
356
+ }
357
+ const data = asObject(typed.data);
358
+ const input = asObject(data?.input);
359
+ const subagentType = typeof input?.subagent_type === "string"
360
+ ? input.subagent_type
361
+ : typeof input?.subagentType === "string"
362
+ ? input.subagentType
363
+ : null;
364
+ return subagentType && subagentType.trim().length > 0 ? subagentType.trim() : null;
365
+ }
366
+ function extractAgentFromNestedMessages(event, currentAgentId) {
367
+ const typed = asObject(event);
368
+ if (!typed) {
369
+ return null;
370
+ }
371
+ const data = asObject(typed.data);
372
+ const input = asObject(data?.input);
373
+ const output = asObject(data?.output);
374
+ const names = new Set();
375
+ collectNestedAgentNames(input?.messages, names);
376
+ collectNestedAgentNames(output?.messages, names);
377
+ return [...names].find((name) => name !== currentAgentId) ?? null;
378
+ }
379
+ function convertUpstreamEventsWithAgents(upstreamEvents, initialAgentId) {
380
+ const reducer = createUpstreamTimelineReducer();
381
+ const projections = [];
382
+ const delegationNodes = [];
383
+ let currentAgentId = initialAgentId;
384
+ let ordinal = 0;
385
+ upstreamEvents.forEach((event, index) => {
386
+ const sourceEventId = `upstream:${index + 1}`;
387
+ const envelope = extractUpstreamEventEnvelope(event);
388
+ if (envelope.agentId && envelope.agentId !== currentAgentId) {
389
+ currentAgentId = envelope.agentId;
390
+ }
391
+ const nestedAgentId = extractAgentFromNestedMessages(envelope.event, currentAgentId);
392
+ if (nestedAgentId && nestedAgentId !== currentAgentId) {
393
+ ordinal += 1;
394
+ delegationNodes.push({
395
+ id: `delegate:${slugify(currentAgentId)}:${slugify(nestedAgentId)}:${ordinal}`,
396
+ layer: "execution",
397
+ kind: "agent",
398
+ label: `Delegate to ${titleCase(nestedAgentId)}`,
399
+ status: "completed",
400
+ threadId: "",
401
+ runId: "",
402
+ agentId: nestedAgentId,
403
+ sourceEventIds: [sourceEventId],
404
+ detail: {
405
+ fromAgentId: currentAgentId,
406
+ toAgentId: nestedAgentId,
407
+ },
408
+ });
409
+ currentAgentId = nestedAgentId;
410
+ }
411
+ const emitted = reducer.consume(envelope.event);
412
+ for (const projection of emitted) {
413
+ projections.push({
414
+ projection,
415
+ agentId: currentAgentId,
416
+ sourceEventId,
417
+ });
418
+ }
419
+ const delegatedAgentId = extractDelegatedAgentId(envelope.event);
420
+ if (!delegatedAgentId || delegatedAgentId === currentAgentId) {
421
+ return;
422
+ }
423
+ ordinal += 1;
424
+ delegationNodes.push({
425
+ id: `delegate:${slugify(initialAgentId)}:${slugify(delegatedAgentId)}:${ordinal}`,
426
+ layer: "execution",
427
+ kind: "agent",
428
+ label: `Delegate to ${titleCase(delegatedAgentId)}`,
429
+ status: "completed",
430
+ threadId: "",
431
+ runId: "",
432
+ agentId: delegatedAgentId,
433
+ sourceEventIds: [sourceEventId],
434
+ detail: {
435
+ fromAgentId: currentAgentId,
436
+ toAgentId: delegatedAgentId,
437
+ },
438
+ });
439
+ currentAgentId = delegatedAgentId;
440
+ });
441
+ return { projections, delegationNodes };
442
+ }
443
+ function selectInitialGroup(groups) {
444
+ return groups.find((group) => group.kind === "segment") ?? groups[0] ?? null;
445
+ }
446
+ function buildAttempts(projectionRecords, delegationNodes, runtimeGroups, threadId, runId) {
447
+ const nodes = [];
448
+ const edges = [];
449
+ const groups = [];
450
+ const attempts = [];
451
+ const activeAttemptsByKey = new Map();
452
+ const toolAttemptsByName = new Map();
453
+ const attemptSetGroups = new Map();
454
+ let currentGroup = selectInitialGroup(runtimeGroups);
455
+ let awaitingResumeAfterApproval = false;
456
+ let previousAttempt = null;
457
+ let ordinal = 0;
458
+ let pendingDelegationNode = null;
459
+ let delegationIndex = 0;
460
+ const segmentGroups = runtimeGroups.filter((group) => group.kind === "segment");
461
+ let nextSegmentIndex = currentGroup && currentGroup.kind === "segment"
462
+ ? Math.max(segmentGroups.findIndex((group) => group.id === currentGroup?.id), 0)
463
+ : 0;
464
+ function promoteToNextSegment() {
465
+ if (nextSegmentIndex + 1 < segmentGroups.length) {
466
+ nextSegmentIndex += 1;
467
+ currentGroup = segmentGroups[nextSegmentIndex] ?? currentGroup;
468
+ }
469
+ }
470
+ function createAttempt(kind, label, projection, agentId) {
471
+ ordinal += 1;
472
+ const attempt = {
473
+ id: `attempt:${runId}:${slugify(kind)}:${slugify(label)}:${ordinal}`,
474
+ kind,
475
+ label,
476
+ status: deriveStatusFromProjection(projection),
477
+ agentId,
478
+ groupId: currentGroup?.id,
479
+ sourceEventIds: [],
480
+ attemptKey: buildAttemptKey(kind, label, currentGroup?.id),
481
+ detail: {},
482
+ projectionKeys: [],
483
+ ...(projection.type === "tool-result" ? { toolName: projection.toolName } : {}),
484
+ };
485
+ attempts.push(attempt);
486
+ if (kind === "tool" || (projection.type === "tool-result" && projection.toolName)) {
487
+ const toolName = projection.type === "tool-result"
488
+ ? normalizeToolIdentity(projection.toolName)
489
+ : normalizeToolIdentity(semanticNameFromLabel("tool", label).replace(/^tool:/, ""));
490
+ const existing = toolAttemptsByName.get(toolName) ?? [];
491
+ if (existing.length > 0) {
492
+ const prior = existing[existing.length - 1];
493
+ const edgeKind = prior.status === "failed" ? "retry" : "fallback";
494
+ edges.push({
495
+ id: `edge:${prior.id}->${attempt.id}`,
496
+ from: prior.id,
497
+ to: attempt.id,
498
+ kind: edgeKind,
499
+ sourceEventIds: [],
500
+ confidence: 0.55,
501
+ });
502
+ }
503
+ existing.push(attempt);
504
+ toolAttemptsByName.set(toolName, existing);
505
+ }
506
+ return attempt;
507
+ }
508
+ for (const record of projectionRecords) {
509
+ const projection = record.projection;
510
+ if (previousAttempt
511
+ && previousAttempt.agentId
512
+ && record.agentId !== previousAttempt.agentId
513
+ && delegationIndex < delegationNodes.length) {
514
+ const sourceNode = delegationNodes[delegationIndex];
515
+ delegationIndex += 1;
516
+ pendingDelegationNode = {
517
+ ...sourceNode,
518
+ threadId,
519
+ runId,
520
+ groupId: currentGroup?.id,
521
+ };
522
+ nodes.push(pendingDelegationNode);
523
+ edges.push({
524
+ id: `edge:${previousAttempt.id}->${pendingDelegationNode.id}`,
525
+ from: previousAttempt.id,
526
+ to: pendingDelegationNode.id,
527
+ kind: "spawn",
528
+ label: "delegate",
529
+ sourceEventIds: [...pendingDelegationNode.sourceEventIds],
530
+ confidence: 0.9,
531
+ });
532
+ }
533
+ if (projection.type === "thinking") {
534
+ ordinal += 1;
535
+ const detailNode = {
536
+ id: `detail:${runId}:thinking:${ordinal}`,
537
+ layer: "detail",
538
+ kind: "thinking",
539
+ label: "Model reasoning",
540
+ status: "completed",
541
+ threadId,
542
+ runId,
543
+ groupId: currentGroup?.id,
544
+ sourceEventIds: [],
545
+ detail: { text: projection.text },
546
+ };
547
+ nodes.push(detailNode);
548
+ if (previousAttempt) {
549
+ edges.push({
550
+ id: `edge:${previousAttempt.id}->${detailNode.id}`,
551
+ from: previousAttempt.id,
552
+ to: detailNode.id,
553
+ kind: "result",
554
+ sourceEventIds: [],
555
+ confidence: 0.6,
556
+ });
557
+ }
558
+ continue;
559
+ }
560
+ const kind = deriveNodeKindFromProjection(projection);
561
+ const label = normalizeAttemptLabel(kind, projection);
562
+ if (kind === "approval") {
563
+ const activeApproval = runtimeGroups.find((group) => group.kind === "approval-window");
564
+ if (activeApproval) {
565
+ currentGroup = activeApproval;
566
+ }
567
+ awaitingResumeAfterApproval = true;
568
+ }
569
+ else if (awaitingResumeAfterApproval && currentGroup?.kind === "approval-window") {
570
+ promoteToNextSegment();
571
+ awaitingResumeAfterApproval = false;
572
+ }
573
+ const attemptKey = buildAttemptKey(kind, label, currentGroup?.id);
574
+ let attempt;
575
+ if (projection.type === "step" && projection.status === "started") {
576
+ attempt = createAttempt(kind, label, projection, record.agentId);
577
+ activeAttemptsByKey.set(attemptKey, attempt);
578
+ }
579
+ else if (projection.type === "tool-result") {
580
+ const toolName = normalizeToolIdentity(projection.toolName);
581
+ const existing = toolAttemptsByName.get(toolName) ?? [];
582
+ attempt = [...existing].reverse().find((candidate) => candidate.groupId === currentGroup?.id) ?? existing[existing.length - 1];
583
+ if (!attempt) {
584
+ attempt = createAttempt("tool", `Calling tool ${titleCase(toolName)}`, projection, record.agentId);
585
+ }
586
+ }
587
+ else {
588
+ attempt = activeAttemptsByKey.get(attemptKey);
589
+ if (!attempt) {
590
+ attempt = createAttempt(kind, label, projection, record.agentId);
591
+ }
592
+ }
593
+ attempt.agentId = attempt.agentId ?? record.agentId;
594
+ attempt.projectionKeys.push(projection.key);
595
+ attempt.sourceEventIds.push(record.sourceEventId);
596
+ if (projection.type === "step") {
597
+ attempt.status = deriveStatusFromProjection(projection);
598
+ attempt.detail.category = projection.category;
599
+ attempt.detail.step = projection.step;
600
+ }
601
+ else if (projection.type === "tool-result") {
602
+ attempt.status = projection.isError ? "failed" : "completed";
603
+ attempt.toolName = projection.toolName;
604
+ attempt.detail.toolName = projection.toolName;
605
+ attempt.detail.result = projection.output;
606
+ attempt.detail.resultIsError = projection.isError === true;
607
+ if (projection.isError) {
608
+ activeAttemptsByKey.delete(buildAttemptKey("tool", attempt.label, attempt.groupId));
609
+ }
610
+ }
611
+ if (!attemptSetGroups.has(`${kind}:${currentGroup?.id ?? "ungrouped"}`)) {
612
+ attemptSetGroups.set(`${kind}:${currentGroup?.id ?? "ungrouped"}`, {
613
+ id: `group:${runId}:attempt-set:${slugify(kind)}:${attemptSetGroups.size + 1}`,
614
+ kind: "attempt-set",
615
+ label: `${titleCase(kind)} attempts`,
616
+ nodeIds: [],
617
+ });
618
+ }
619
+ const attemptGroup = attemptSetGroups.get(`${kind}:${currentGroup?.id ?? "ungrouped"}`);
620
+ if (!attemptGroup.nodeIds.includes(attempt.id)) {
621
+ attemptGroup.nodeIds.push(attempt.id);
622
+ }
623
+ if (pendingDelegationNode && pendingDelegationNode.id !== attempt.id) {
624
+ edges.push({
625
+ id: `edge:${pendingDelegationNode.id}->${attempt.id}`,
626
+ from: pendingDelegationNode.id,
627
+ to: attempt.id,
628
+ kind: "spawn",
629
+ sourceEventIds: [...pendingDelegationNode.sourceEventIds, ...attempt.sourceEventIds],
630
+ confidence: 0.9,
631
+ });
632
+ pendingDelegationNode = null;
633
+ }
634
+ else if (previousAttempt && previousAttempt.id !== attempt.id) {
635
+ edges.push({
636
+ id: `edge:${previousAttempt.id}->${attempt.id}`,
637
+ from: previousAttempt.id,
638
+ to: attempt.id,
639
+ kind: kind === "approval" ? "approval" : "sequence",
640
+ sourceEventIds: [],
641
+ confidence: 0.7,
642
+ });
643
+ }
644
+ else if (!previousAttempt && currentGroup?.anchorNodeId) {
645
+ edges.push({
646
+ id: `edge:${currentGroup.anchorNodeId}->${attempt.id}`,
647
+ from: currentGroup.anchorNodeId,
648
+ to: attempt.id,
649
+ kind: "contains",
650
+ sourceEventIds: [],
651
+ confidence: 0.8,
652
+ });
653
+ }
654
+ previousAttempt = attempt;
655
+ if (projection.type === "step" && projection.status !== "started") {
656
+ activeAttemptsByKey.delete(attemptKey);
657
+ }
658
+ }
659
+ for (const attempt of attempts) {
660
+ nodes.push({
661
+ id: attempt.id,
662
+ layer: "attempt",
663
+ kind: attempt.kind,
664
+ label: attempt.label,
665
+ status: attempt.status,
666
+ threadId,
667
+ runId,
668
+ agentId: attempt.agentId,
669
+ groupId: attempt.groupId,
670
+ sequenceStart: attempt.sequenceStart,
671
+ sequenceEnd: attempt.sequenceEnd,
672
+ startedAt: attempt.startedAt,
673
+ endedAt: attempt.endedAt,
674
+ sourceEventIds: [...new Set(attempt.sourceEventIds)],
675
+ detail: { ...attempt.detail, projectionKeys: attempt.projectionKeys },
676
+ });
677
+ }
678
+ groups.push(...attemptSetGroups.values());
679
+ return { nodes, edges, groups };
680
+ }
681
+ function resolveContext(input, runtimeTimeline, projections) {
682
+ const timelineHead = runtimeTimeline[0];
683
+ if (input.threadId && input.runId) {
684
+ return { threadId: input.threadId, runId: input.runId };
685
+ }
686
+ if (timelineHead) {
687
+ return {
688
+ threadId: input.threadId ?? timelineHead.threadId,
689
+ runId: input.runId ?? timelineHead.runId,
690
+ };
691
+ }
692
+ const firstProjection = projections[0];
693
+ if (!input.threadId || !input.runId) {
694
+ throw new Error(`buildFlowGraph requires threadId and runId when runtime timeline data is absent${firstProjection ? "" : " or empty"}.`);
695
+ }
696
+ return { threadId: input.threadId, runId: input.runId };
697
+ }
698
+ export function buildFlowGraph(input) {
699
+ const runtimeTimeline = input.runtimeTimeline
700
+ ?? (input.runtimeEvents ? projectRuntimeTimeline(input.runtimeEvents, {
701
+ ...(input.threadId ? { threadId: input.threadId } : {}),
702
+ ...(input.runId ? { runId: input.runId } : {}),
703
+ }) : []);
704
+ const initialAgentId = deriveInitialAgentId(input, runtimeTimeline);
705
+ const upstreamContext = input.upstreamEvents
706
+ ? convertUpstreamEventsWithAgents(input.upstreamEvents, initialAgentId)
707
+ : { projections: [], delegationNodes: [] };
708
+ const upstreamProjections = input.upstreamProjections ?? upstreamContext.projections.map((record) => record.projection);
709
+ const { threadId, runId } = resolveContext(input, runtimeTimeline, upstreamProjections);
710
+ const { nodes: runtimeNodes, edges: runtimeEdges, groups: runtimeGroups } = buildRuntimeNodes(runtimeTimeline);
711
+ for (const node of runtimeNodes) {
712
+ node.agentId = typeof node.detail.agentId === "string" ? node.detail.agentId : initialAgentId;
713
+ }
714
+ const projectionRecords = input.upstreamProjections
715
+ ? input.upstreamProjections.map((projection, index) => ({
716
+ projection,
717
+ agentId: initialAgentId,
718
+ sourceEventId: "key" in projection ? projection.key : `projection:${index + 1}`,
719
+ }))
720
+ : upstreamContext.projections;
721
+ const { nodes: attemptNodes, edges: attemptEdges, groups: attemptGroups } = buildAttempts(projectionRecords, upstreamContext.delegationNodes, runtimeGroups, threadId, runId);
722
+ return {
723
+ graphId: `${threadId}/${runId}`,
724
+ scope: input.scope ?? "run",
725
+ threadId,
726
+ runId,
727
+ nodes: [...runtimeNodes, ...attemptNodes],
728
+ edges: [...runtimeEdges, ...attemptEdges],
729
+ groups: [...runtimeGroups, ...attemptGroups],
730
+ metadata: {
731
+ runtimeTimelineCount: runtimeTimeline.length,
732
+ upstreamProjectionCount: upstreamProjections.length,
733
+ delegationCount: upstreamContext.delegationNodes.length,
734
+ ...(input.metadata ?? {}),
735
+ },
736
+ };
737
+ }