@botbotgo/agent-harness 0.0.157 → 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.
package/README.md CHANGED
@@ -450,10 +450,18 @@ const result = await run(runtime, {
450
450
  onEvent(event) {
451
451
  console.log(event.eventType, event.payload);
452
452
  },
453
+ onUpstreamEvent(event) {
454
+ console.log("raw upstream event", event);
455
+ },
456
+ onUpstreamItem(item) {
457
+ console.log("upstream agent", item.agentId, item.event);
458
+ },
453
459
  },
454
460
  });
455
461
  ```
456
462
 
463
+ `onUpstreamEvent(...)` preserves the raw upstream event payload. `onUpstreamItem(...)` adds runtime correlation metadata such as `threadId`, `runId`, and the current `agentId`, which is useful for flow-graph capture and delegated sub-agent inspection.
464
+
457
465
  `subscribe(...)` is the read-only observer surface over stored lifecycle events.
458
466
 
459
467
  The runtime event stream includes:
@@ -563,8 +571,14 @@ Discovery rules:
563
571
  - local tools are auto-discovered from `resources/tools/**/*.js|*.mjs|*.cjs` when they export `tool({...})`
564
572
  - skills are auto-discovered from `resources/skills/**/SKILL.md`
565
573
 
574
+ Example workspaces:
575
+
576
+ - `examples/hello-skill-app/` keeps the smallest local tool + skill workspace
577
+ - `examples/runtime-flow-demo/` runs one real hosted-model request and exports a Mermaid flowchart from runtime plus upstream events
578
+
566
579
  Workspace-local tool modules in `resources/tools/` should be exported with `tool({...})`.
567
580
  Any other local module shape is not supported, and unsupported shapes are rejected at load time.
581
+ When local tools use Zod-authored schemas, keep the workspace or isolated `resources` package on `zod@^4` so raw-shape validators and runtime parsing stay on one supported major version.
568
582
 
569
583
  Default wiring guidance:
570
584
 
@@ -872,3 +886,16 @@ Primary exports:
872
886
  - `createToolMcpServer`
873
887
  - `serveToolsOverStdio`
874
888
  - `stop`
889
+ - `createUpstreamTimelineReducer`
890
+ - `buildFlowGraph`
891
+ - `exportFlowGraphToMermaid`
892
+ - `exportFlowGraphToSequenceMermaid`
893
+
894
+ Inspection helpers:
895
+
896
+ - `createUpstreamTimelineReducer()` reduces raw upstream model/tool/chain events into ordered step-like projections for inspection and visualization.
897
+ - `buildFlowGraph(...)` combines persisted runtime timeline items with optional upstream projections into a detailed flow graph utility shape, including best-effort delegated subagent transitions when raw upstream events are available.
898
+ - `exportFlowGraphToMermaid(...)` renders that inspection graph as Mermaid flowchart text. By default it emits the product view: agent and sub-agent delegation plus user-defined model, tool, and skill calls. Use `view: "debug"` to include runtime inspection detail.
899
+ - `exportFlowGraphToSequenceMermaid(...)` renders the same inspection graph as a Mermaid sequence diagram. By default it emits only user-defined participants and calls, while `view: "debug"` includes runtime participants and lifecycle messages.
900
+
901
+ These helpers are visualization and inspection utilities. They do not introduce a canonical harness-owned execution protocol.
package/README.zh.md CHANGED
@@ -535,8 +535,14 @@ await stop(runtime);
535
535
  - 本地工具会从 `resources/tools/**/*.js|*.mjs|*.cjs` 中自动发现,前提是模块导出 `tool({...})`
536
536
  - skills 会从 `resources/skills/**/SKILL.md` 自动发现
537
537
 
538
+ 示例工作区:
539
+
540
+ - `examples/hello-skill-app/` 保留最小的本地 tool + skill 工作区
541
+ - `examples/runtime-flow-demo/` 会跑一次真实 hosted model 请求,并把 runtime 与 upstream events 导出为 Mermaid flowchart
542
+
538
543
  `resources/tools/` 下的工作区本地工具模块应统一用 `tool({...})` 导出。
539
544
  不支持历史/兼容写法,任何不带该导出形式的工具模块都会在工作区加载时被拒绝。
545
+ 若本地工具使用 Zod schema,请让工作区或隔离的 `resources` 包统一使用 `zod@^4`,避免 raw shape validator 与 runtime 解析落在不同 major 版本上。
540
546
 
541
547
  主要有三层配置:
542
548
 
@@ -839,3 +845,16 @@ spec:
839
845
  - `createToolMcpServer`
840
846
  - `serveToolsOverStdio`
841
847
  - `stop`
848
+ - `createUpstreamTimelineReducer`
849
+ - `buildFlowGraph`
850
+ - `exportFlowGraphToMermaid`
851
+ - `exportFlowGraphToSequenceMermaid`
852
+
853
+ Inspection 辅助工具:
854
+
855
+ - `createUpstreamTimelineReducer()` 可把上游 model/tool/chain 原始事件归约成有序的 step-like 投影,便于检查和可视化。
856
+ - `buildFlowGraph(...)` 可把持久化 runtime timeline 与可选的 upstream projections 组合成详细 flow graph utility 数据结构;当提供原始 upstream events 时,也会尽力补出 subagent delegation 转移。
857
+ - `exportFlowGraphToMermaid(...)` 可把该 inspection graph 导出为 Mermaid flowchart 文本。默认输出 product 视图,只保留 agent / sub-agent delegation 以及用户定义的 model、tool、skill 调用;传 `view: "debug"` 才会带上 runtime 检查细节。
858
+ - `exportFlowGraphToSequenceMermaid(...)` 可把同一份 inspection graph 导出为 Mermaid sequence diagram。默认只输出用户定义的参与者与调用;传 `view: "debug"` 才会包含 runtime participant 与生命周期消息。
859
+
860
+ 这些 helper 只用于可视化与检查,不代表新的 harness 官方执行协议。
package/dist/api.d.ts CHANGED
@@ -6,6 +6,7 @@ import type { ToolMcpServerOptions } from "./mcp.js";
6
6
  export { AgentHarnessAcpServer, createAcpServer } from "./acp.js";
7
7
  export type { AcpApproval, AcpArtifact, AcpEventNotification, AcpJsonRpcError, AcpJsonRpcRequest, AcpJsonRpcResponse, AcpJsonRpcSuccess, AcpRequestRecord, AcpRunRequestParams, AcpServerCapabilities, AcpSessionRecord, } from "./acp.js";
8
8
  export { AgentHarnessRuntime } from "./runtime/harness.js";
9
+ export { buildFlowGraph, exportFlowGraphToMermaid, exportFlowGraphToSequenceMermaid } from "./flow/index.js";
9
10
  export { createUpstreamTimelineReducer } from "./upstream-events.js";
10
11
  export type { ListMemoriesInput, ListMemoriesResult, MemoryDecision, MemoryKind, MemoryRecord, MemoryScope, MemorizeInput, MemorizeResult, RecallInput, RecallResult, RemoveMemoryInput, RuntimeEvaluationExport, RuntimeEvaluationExportInput, UpdateMemoryInput, } from "./contracts/types.js";
11
12
  type PublicApprovalRecord = {
package/dist/api.js CHANGED
@@ -3,6 +3,7 @@ import { normalizeMessageContent } from "./utils/message-content.js";
3
3
  import { loadWorkspace } from "./workspace/compile.js";
4
4
  export { AgentHarnessAcpServer, createAcpServer } from "./acp.js";
5
5
  export { AgentHarnessRuntime } from "./runtime/harness.js";
6
+ export { buildFlowGraph, exportFlowGraphToMermaid, exportFlowGraphToSequenceMermaid } from "./flow/index.js";
6
7
  export { createUpstreamTimelineReducer } from "./upstream-events.js";
7
8
  function toSessionSummary(summary) {
8
9
  return {
@@ -335,9 +335,16 @@ export type RunResult = {
335
335
  metadata?: Record<string, unknown>;
336
336
  };
337
337
  export type UpstreamRuntimeEvent = unknown;
338
+ export type UpstreamRuntimeEventItem = {
339
+ threadId: string;
340
+ runId: string;
341
+ agentId: string;
342
+ event: UpstreamRuntimeEvent;
343
+ };
338
344
  export type RuntimeListeners = {
339
345
  onEvent?: (event: HarnessEvent) => void | Promise<void>;
340
346
  onUpstreamEvent?: (event: UpstreamRuntimeEvent) => void | Promise<void>;
347
+ onUpstreamItem?: (item: UpstreamRuntimeEventItem) => void | Promise<void>;
341
348
  };
342
349
  export type RunListeners = RuntimeListeners;
343
350
  export type MessageContentPart = {
@@ -0,0 +1,2 @@
1
+ import type { BuildFlowGraphInput, FlowGraph } from "./types.js";
2
+ export declare function buildFlowGraph(input: BuildFlowGraphInput): FlowGraph;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import type { FlowGraph, FlowGraphMermaidOptions } from "./types.js";
2
+ export declare function exportFlowGraphToMermaid(graph: FlowGraph, options?: FlowGraphMermaidOptions): string;
@@ -0,0 +1,96 @@
1
+ const PRODUCT_VIEW_KINDS = new Set(["agent", "llm", "tool", "skill"]);
2
+ function sanitizeMermaidId(value) {
3
+ return value.replace(/[^A-Za-z0-9_]/g, "_");
4
+ }
5
+ function escapeMermaidLabel(value) {
6
+ return value.replace(/"/g, '\\"');
7
+ }
8
+ function renderNode(node, mermaidId) {
9
+ const label = escapeMermaidLabel(`${node.label} [${node.status}]`);
10
+ switch (node.kind) {
11
+ case "approval":
12
+ return ` ${mermaidId}{"${label}"}`;
13
+ case "queue":
14
+ return ` ${mermaidId}[/"${label}"/]`;
15
+ case "recovery":
16
+ return ` ${mermaidId}{{"${label}"}}`;
17
+ case "artifact":
18
+ return ` ${mermaidId}[("${label}")]`;
19
+ case "skill":
20
+ return ` ${mermaidId}[["${label}"]]`;
21
+ case "memory":
22
+ return ` ${mermaidId}[("${label}")]`;
23
+ default:
24
+ return ` ${mermaidId}["${label}"]`;
25
+ }
26
+ }
27
+ function renderEdge(edge, fromId, toId) {
28
+ const connector = edge.kind === "retry" || edge.kind === "fallback" ? "-.->" : edge.kind === "resume" ? "==>" : "-->";
29
+ const label = edge.label ?? edge.condition ?? (edge.kind !== "sequence" && edge.kind !== "contains" ? edge.kind : "");
30
+ return label
31
+ ? ` ${fromId} ${connector}|"${escapeMermaidLabel(label)}"| ${toId}`
32
+ : ` ${fromId} ${connector} ${toId}`;
33
+ }
34
+ export function exportFlowGraphToMermaid(graph, options = {}) {
35
+ const direction = options.direction ?? "TD";
36
+ const view = options.view ?? "product";
37
+ const includedKinds = options.includeKinds
38
+ ? new Set(options.includeKinds)
39
+ : view === "debug"
40
+ ? null
41
+ : PRODUCT_VIEW_KINDS;
42
+ const includedEdgeKinds = options.includeEdgeKinds ? new Set(options.includeEdgeKinds) : null;
43
+ const includeGroups = options.includeGroups ?? true;
44
+ const includeDetails = options.includeDetails ?? false;
45
+ const nodes = graph.nodes.filter((node) => {
46
+ if (!includeDetails && node.layer === "detail") {
47
+ return false;
48
+ }
49
+ return includedKinds ? includedKinds.has(node.kind) : true;
50
+ });
51
+ const nodeIdSet = new Set(nodes.map((node) => node.id));
52
+ const edges = graph.edges.filter((edge) => {
53
+ if (!nodeIdSet.has(edge.from) || !nodeIdSet.has(edge.to)) {
54
+ return false;
55
+ }
56
+ return includedEdgeKinds ? includedEdgeKinds.has(edge.kind) : true;
57
+ });
58
+ const lines = [`flowchart ${direction}`];
59
+ const mermaidIdByNodeId = new Map();
60
+ for (const node of nodes) {
61
+ mermaidIdByNodeId.set(node.id, sanitizeMermaidId(node.id));
62
+ }
63
+ if (includeGroups) {
64
+ const groupedNodeIds = new Set();
65
+ for (const group of graph.groups) {
66
+ const groupNodes = group.nodeIds.filter((nodeId) => nodeIdSet.has(nodeId));
67
+ if (groupNodes.length === 0) {
68
+ continue;
69
+ }
70
+ lines.push(` subgraph ${sanitizeMermaidId(group.id)}["${escapeMermaidLabel(group.label)}"]`);
71
+ for (const nodeId of groupNodes) {
72
+ groupedNodeIds.add(nodeId);
73
+ const node = nodes.find((candidate) => candidate.id === nodeId);
74
+ if (!node) {
75
+ continue;
76
+ }
77
+ lines.push(renderNode(node, mermaidIdByNodeId.get(node.id)));
78
+ }
79
+ lines.push(" end");
80
+ }
81
+ for (const node of nodes) {
82
+ if (!groupedNodeIds.has(node.id)) {
83
+ lines.push(renderNode(node, mermaidIdByNodeId.get(node.id)));
84
+ }
85
+ }
86
+ }
87
+ else {
88
+ for (const node of nodes) {
89
+ lines.push(renderNode(node, mermaidIdByNodeId.get(node.id)));
90
+ }
91
+ }
92
+ for (const edge of edges) {
93
+ lines.push(renderEdge(edge, mermaidIdByNodeId.get(edge.from), mermaidIdByNodeId.get(edge.to)));
94
+ }
95
+ return lines.join("\n");
96
+ }
@@ -0,0 +1,3 @@
1
+ import type { FlowGraph } from "./types.js";
2
+ import type { FlowGraphSequenceMermaidOptions } from "./types.js";
3
+ export declare function exportFlowGraphToSequenceMermaid(graph: FlowGraph, options?: FlowGraphSequenceMermaidOptions): string;
@@ -0,0 +1,169 @@
1
+ const PRODUCT_VIEW_KINDS = new Set(["agent", "llm", "tool", "skill"]);
2
+ function sanitizeAlias(value) {
3
+ return value.replace(/[^A-Za-z0-9_]/g, "_");
4
+ }
5
+ function escapeLabel(value) {
6
+ return value.replace(/"/g, '\\"');
7
+ }
8
+ function selectNodes(graph, options) {
9
+ const includedKinds = options.includeKinds
10
+ ? new Set(options.includeKinds)
11
+ : options.view === "debug"
12
+ ? null
13
+ : PRODUCT_VIEW_KINDS;
14
+ return graph.nodes.filter((node) => {
15
+ if (node.layer === "detail") {
16
+ return false;
17
+ }
18
+ if (includedKinds) {
19
+ return includedKinds.has(node.kind);
20
+ }
21
+ return node.kind === "run"
22
+ || node.kind === "agent"
23
+ || node.kind === "approval"
24
+ || node.kind === "recovery"
25
+ || node.kind === "llm"
26
+ || node.kind === "tool"
27
+ || node.kind === "skill"
28
+ || node.kind === "artifact";
29
+ });
30
+ }
31
+ function getParticipantsForNode(node) {
32
+ const runtimeParticipant = { id: "runtime", alias: "Runtime", label: "Runtime" };
33
+ const agentParticipant = {
34
+ id: `agent:${node.agentId ?? "agent"}`,
35
+ alias: sanitizeAlias(`Agent_${node.agentId ?? "agent"}`),
36
+ label: `Agent${node.agentId ? `:${node.agentId}` : ""}`,
37
+ };
38
+ if (node.kind === "run") {
39
+ return [
40
+ { id: "user", alias: "User", label: "User" },
41
+ runtimeParticipant,
42
+ agentParticipant,
43
+ ];
44
+ }
45
+ if (node.kind === "agent") {
46
+ const fromAgentId = typeof node.detail.fromAgentId === "string" ? node.detail.fromAgentId : "agent";
47
+ return [
48
+ {
49
+ id: `agent:${fromAgentId}`,
50
+ alias: sanitizeAlias(`Agent_${fromAgentId}`),
51
+ label: `Agent:${fromAgentId}`,
52
+ },
53
+ agentParticipant,
54
+ ];
55
+ }
56
+ if (node.kind === "approval") {
57
+ return [
58
+ runtimeParticipant,
59
+ { id: "operator", alias: "Operator", label: "Operator" },
60
+ ];
61
+ }
62
+ if (node.kind === "llm") {
63
+ return [
64
+ agentParticipant,
65
+ { id: "model", alias: "Model", label: "Model" },
66
+ ];
67
+ }
68
+ if (node.kind === "tool") {
69
+ return [
70
+ agentParticipant,
71
+ {
72
+ id: `tool:${node.label}`,
73
+ alias: sanitizeAlias(`Tool_${node.label}`),
74
+ label: `Tool:${node.label.replace(/\s+\[[^\]]+\]$/, "")}`,
75
+ },
76
+ ];
77
+ }
78
+ if (node.kind === "skill") {
79
+ return [
80
+ agentParticipant,
81
+ { id: "skill", alias: "Skill", label: "Skill" },
82
+ ];
83
+ }
84
+ if (node.kind === "recovery") {
85
+ return [
86
+ runtimeParticipant,
87
+ agentParticipant,
88
+ ];
89
+ }
90
+ if (node.kind === "artifact") {
91
+ return [
92
+ runtimeParticipant,
93
+ { id: "artifact", alias: "ArtifactStore", label: "ArtifactStore" },
94
+ ];
95
+ }
96
+ return [runtimeParticipant, agentParticipant];
97
+ }
98
+ function renderArrowForNode(node) {
99
+ if (node.kind === "run" && node.status === "started") {
100
+ return { from: "User", to: "Runtime", text: node.label };
101
+ }
102
+ if (node.kind === "run" && (node.status === "completed" || node.status === "failed")) {
103
+ return { from: "Runtime", to: "User", text: node.label };
104
+ }
105
+ if (node.kind === "agent") {
106
+ const fromAgentId = typeof node.detail.fromAgentId === "string" ? node.detail.fromAgentId : "agent";
107
+ return {
108
+ from: sanitizeAlias(`Agent_${fromAgentId}`),
109
+ to: sanitizeAlias(`Agent_${node.agentId ?? "agent"}`),
110
+ text: node.label,
111
+ };
112
+ }
113
+ if (node.kind === "approval" && node.status === "waiting") {
114
+ return { from: "Runtime", to: "Operator", text: node.label };
115
+ }
116
+ if (node.kind === "approval" && node.status === "resolved") {
117
+ return { from: "Operator", to: "Runtime", text: node.label };
118
+ }
119
+ if (node.kind === "recovery") {
120
+ return { from: "Runtime", to: sanitizeAlias(`Agent_${node.agentId ?? "agent"}`), text: node.label };
121
+ }
122
+ if (node.kind === "llm") {
123
+ return {
124
+ from: sanitizeAlias(`Agent_${node.agentId ?? "agent"}`),
125
+ to: "Model",
126
+ text: node.label,
127
+ dashed: node.status === "completed" || node.status === "failed",
128
+ };
129
+ }
130
+ if (node.kind === "tool") {
131
+ return {
132
+ from: sanitizeAlias(`Agent_${node.agentId ?? "agent"}`),
133
+ to: sanitizeAlias(`Tool_${node.label}`),
134
+ text: node.label,
135
+ dashed: node.status === "completed" || node.status === "failed",
136
+ };
137
+ }
138
+ if (node.kind === "skill") {
139
+ return {
140
+ from: sanitizeAlias(`Agent_${node.agentId ?? "agent"}`),
141
+ to: "Skill",
142
+ text: node.label,
143
+ dashed: node.status === "completed" || node.status === "failed",
144
+ };
145
+ }
146
+ if (node.kind === "artifact") {
147
+ return { from: "Runtime", to: "ArtifactStore", text: node.label };
148
+ }
149
+ return { from: "Runtime", to: sanitizeAlias(`Agent_${node.agentId ?? "agent"}`), text: node.label };
150
+ }
151
+ export function exportFlowGraphToSequenceMermaid(graph, options = {}) {
152
+ const nodes = selectNodes(graph, options);
153
+ const lines = ["sequenceDiagram"];
154
+ const participants = new Map();
155
+ for (const node of nodes) {
156
+ for (const participant of getParticipantsForNode(node)) {
157
+ participants.set(participant.id, participant);
158
+ }
159
+ }
160
+ for (const participant of participants.values()) {
161
+ lines.push(` participant ${participant.alias} as ${escapeLabel(participant.label)}`);
162
+ }
163
+ for (const node of nodes) {
164
+ const arrow = renderArrowForNode(node);
165
+ const connector = arrow.dashed ? "-->>" : "->>";
166
+ lines.push(` ${arrow.from}${connector}${arrow.to}: ${escapeLabel(node.label)}`);
167
+ }
168
+ return lines.join("\n");
169
+ }
@@ -0,0 +1,4 @@
1
+ export { buildFlowGraph } from "./build-flow-graph.js";
2
+ export { exportFlowGraphToMermaid } from "./export-mermaid.js";
3
+ export { exportFlowGraphToSequenceMermaid } from "./export-sequence-mermaid.js";
4
+ export type { BuildFlowGraphInput, FlowEdge, FlowEdgeKind, FlowGraph, FlowGraphMermaidOptions, FlowGraphSequenceMermaidOptions, FlowGroup, FlowGroupKind, FlowNode, FlowNodeKind, FlowNodeLayer, FlowNodeStatus, } from "./types.js";
@@ -0,0 +1,3 @@
1
+ export { buildFlowGraph } from "./build-flow-graph.js";
2
+ export { exportFlowGraphToMermaid } from "./export-mermaid.js";
3
+ export { exportFlowGraphToSequenceMermaid } from "./export-sequence-mermaid.js";
@@ -0,0 +1,75 @@
1
+ import type { HarnessEvent, RuntimeTimelineItem } from "../contracts/types.js";
2
+ import type { UpstreamTimelineProjection } from "../upstream-events.js";
3
+ export type FlowNodeLayer = "runtime" | "execution" | "attempt" | "detail";
4
+ export type FlowNodeKind = "run" | "agent" | "queue" | "approval" | "recovery" | "artifact" | "llm" | "tool" | "skill" | "memory" | "chain" | "result" | "thinking" | "other";
5
+ export type FlowNodeStatus = "pending" | "started" | "completed" | "failed" | "waiting" | "resolved" | "skipped";
6
+ export type FlowEdgeKind = "sequence" | "contains" | "approval" | "resume" | "retry" | "fallback" | "result" | "spawn";
7
+ export type FlowGroupKind = "segment" | "approval-window" | "attempt-set" | "agent-scope";
8
+ export type FlowNode = {
9
+ id: string;
10
+ layer: FlowNodeLayer;
11
+ kind: FlowNodeKind;
12
+ label: string;
13
+ status: FlowNodeStatus;
14
+ threadId: string;
15
+ runId: string;
16
+ agentId?: string;
17
+ startedAt?: string;
18
+ endedAt?: string;
19
+ sequenceStart?: number;
20
+ sequenceEnd?: number;
21
+ parentNodeId?: string;
22
+ groupId?: string;
23
+ sourceEventIds: string[];
24
+ detail: Record<string, unknown>;
25
+ };
26
+ export type FlowEdge = {
27
+ id: string;
28
+ from: string;
29
+ to: string;
30
+ kind: FlowEdgeKind;
31
+ label?: string;
32
+ condition?: string;
33
+ sourceEventIds: string[];
34
+ confidence?: number;
35
+ };
36
+ export type FlowGroup = {
37
+ id: string;
38
+ kind: FlowGroupKind;
39
+ label: string;
40
+ nodeIds: string[];
41
+ startedAt?: string;
42
+ endedAt?: string;
43
+ };
44
+ export type FlowGraph = {
45
+ graphId: string;
46
+ scope: "run" | "thread";
47
+ threadId: string;
48
+ runId: string;
49
+ nodes: FlowNode[];
50
+ edges: FlowEdge[];
51
+ groups: FlowGroup[];
52
+ metadata: Record<string, unknown>;
53
+ };
54
+ export type BuildFlowGraphInput = {
55
+ threadId?: string;
56
+ runId?: string;
57
+ scope?: FlowGraph["scope"];
58
+ runtimeEvents?: readonly HarnessEvent[];
59
+ runtimeTimeline?: readonly RuntimeTimelineItem[];
60
+ upstreamEvents?: readonly unknown[];
61
+ upstreamProjections?: readonly UpstreamTimelineProjection[];
62
+ metadata?: Record<string, unknown>;
63
+ };
64
+ export type FlowGraphMermaidOptions = {
65
+ view?: "product" | "debug";
66
+ direction?: "TD" | "LR";
67
+ includeGroups?: boolean;
68
+ includeDetails?: boolean;
69
+ includeKinds?: readonly FlowNodeKind[];
70
+ includeEdgeKinds?: readonly FlowEdgeKind[];
71
+ };
72
+ export type FlowGraphSequenceMermaidOptions = {
73
+ view?: "product" | "debug";
74
+ includeKinds?: readonly FlowNodeKind[];
75
+ };
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- export { AgentHarnessAcpServer, AgentHarnessRuntime, cancelRun, createAgentHarness, createAcpServer, createUpstreamTimelineReducer, createToolMcpServer, deleteSession, describeInventory, exportEvaluationBundle, getArtifact, getAgent, getApproval, getRequest, getHealth, listMemories, getSession, listAgentSkills, listArtifacts, listApprovals, listRequests, listSessions, memorize, normalizeUserChatInput, recall, removeMemory, resolveApproval, run, serveToolsOverStdio, subscribe, stop, updateMemory, } from "./api.js";
1
+ export { AgentHarnessAcpServer, AgentHarnessRuntime, buildFlowGraph, cancelRun, createAgentHarness, createAcpServer, createUpstreamTimelineReducer, createToolMcpServer, deleteSession, describeInventory, exportEvaluationBundle, getArtifact, getAgent, getApproval, getRequest, getHealth, listMemories, getSession, listAgentSkills, listArtifacts, listApprovals, listRequests, listSessions, memorize, normalizeUserChatInput, recall, removeMemory, resolveApproval, run, serveToolsOverStdio, subscribe, stop, updateMemory, exportFlowGraphToMermaid, exportFlowGraphToSequenceMermaid, } from "./api.js";
2
2
  export type { AcpApproval, AcpArtifact, AcpEventNotification, AcpJsonRpcError, AcpJsonRpcRequest, AcpJsonRpcResponse, AcpJsonRpcSuccess, AcpRequestRecord, AcpRunRequestParams, AcpServerCapabilities, AcpSessionRecord, } from "./acp.js";
3
3
  export type { ListMemoriesInput, ListMemoriesResult, MemoryDecision, MemoryKind, MemoryRecord, MemoryScope, MemorizeInput, MemorizeResult, NormalizeUserChatInputOptions, RecallInput, RecallResult, RemoveMemoryInput, RuntimeEvaluationExport, RuntimeEvaluationExportInput, UpdateMemoryInput, UserChatInput, UserChatMessage, } from "./api.js";
4
+ export type { BuildFlowGraphInput, FlowEdge, FlowEdgeKind, FlowGraph, FlowGraphMermaidOptions, FlowGraphSequenceMermaidOptions, FlowGroup, FlowGroupKind, FlowNode, FlowNodeKind, FlowNodeLayer, FlowNodeStatus, } from "./flow/index.js";
4
5
  export type { ToolMcpServerOptions } from "./mcp.js";
5
6
  export { tool } from "./tools.js";
6
7
  export type { UpstreamTimelineProjection, UpstreamTimelineReducer } from "./upstream-events.js";
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { AgentHarnessAcpServer, AgentHarnessRuntime, cancelRun, createAgentHarness, createAcpServer, createUpstreamTimelineReducer, createToolMcpServer, deleteSession, describeInventory, exportEvaluationBundle, getArtifact, getAgent, getApproval, getRequest, getHealth, listMemories, getSession, listAgentSkills, listArtifacts, listApprovals, listRequests, listSessions, memorize, normalizeUserChatInput, recall, removeMemory, resolveApproval, run, serveToolsOverStdio, subscribe, stop, updateMemory, } from "./api.js";
1
+ export { AgentHarnessAcpServer, AgentHarnessRuntime, buildFlowGraph, cancelRun, createAgentHarness, createAcpServer, createUpstreamTimelineReducer, createToolMcpServer, deleteSession, describeInventory, exportEvaluationBundle, getArtifact, getAgent, getApproval, getRequest, getHealth, listMemories, getSession, listAgentSkills, listArtifacts, listApprovals, listRequests, listSessions, memorize, normalizeUserChatInput, recall, removeMemory, resolveApproval, run, serveToolsOverStdio, subscribe, stop, updateMemory, exportFlowGraphToMermaid, exportFlowGraphToSequenceMermaid, } from "./api.js";
2
2
  export { tool } from "./tools.js";
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.156";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.157";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.156";
1
+ export const AGENT_HARNESS_VERSION = "0.0.157";
@@ -14,5 +14,5 @@ export declare function wrapResolvedToolWithModelFacingName<T>(resolvedTool: T,
14
14
  export declare function hasCallableToolHandler(value: unknown): value is ResolvedToolLike;
15
15
  export declare function normalizeResolvedToolSchema(resolvedTool: ResolvedToolLike): Record<string, unknown> | {
16
16
  parse?: (input: unknown) => unknown;
17
- } | z.ZodObject<{}, "passthrough", z.ZodTypeAny, z.objectOutputType<{}, z.ZodTypeAny, "passthrough">, z.objectInputType<{}, z.ZodTypeAny, "passthrough">> | undefined;
17
+ } | z.ZodObject<{}, z.core.$loose> | undefined;
18
18
  export declare function asStructuredExecutableTool(resolvedTool: unknown, modelFacingName: string, description: string): unknown;
@@ -42,6 +42,12 @@ export async function dispatchRunListeners(stream, listeners, options) {
42
42
  }
43
43
  if (item.type === "upstream-event") {
44
44
  await options.notifyListener(listeners.onUpstreamEvent, item.event);
45
+ await options.notifyListener(listeners.onUpstreamItem, {
46
+ threadId: item.threadId,
47
+ runId: item.runId,
48
+ agentId: item.agentId,
49
+ event: item.event,
50
+ });
45
51
  continue;
46
52
  }
47
53
  if (item.type === "result") {
@@ -85,7 +85,7 @@ export async function* streamHarnessRun(options) {
85
85
  type: "upstream-event",
86
86
  threadId: options.threadId,
87
87
  runId: options.runId,
88
- agentId: options.selectedAgentId,
88
+ agentId: currentAgentId,
89
89
  event: normalizedChunk.event,
90
90
  };
91
91
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.157",
3
+ "version": "0.0.158",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",
@@ -59,7 +59,7 @@
59
59
  "mem0ai": "^2.4.4",
60
60
  "mustache": "^4.2.0",
61
61
  "yaml": "^2.8.1",
62
- "zod": "^3.25.67"
62
+ "zod": "^4.3.6"
63
63
  },
64
64
  "scripts": {
65
65
  "build": "rm -rf dist tsconfig.tsbuildinfo && tsc -p tsconfig.json && cp -R config dist/",
@@ -75,5 +75,9 @@
75
75
  "@types/node": "^24.6.0",
76
76
  "typescript": "^5.9.3",
77
77
  "vitest": "^3.2.4"
78
+ },
79
+ "overrides": {
80
+ "@browserbasehq/stagehand": "3.2.0",
81
+ "openai": "6.33.0"
78
82
  }
79
83
  }