@botbotgo/agent-harness 0.0.248 → 0.0.250

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/dist/api.d.ts CHANGED
@@ -32,6 +32,8 @@ export type RequestUpstreamEventItem = {
32
32
  sessionId: string;
33
33
  requestId: string;
34
34
  agentId: string;
35
+ agentName?: string;
36
+ surfaceItems?: import("./contracts/types.js").RuntimeSurfaceItem[];
35
37
  event: unknown;
36
38
  };
37
39
  export type PublicRunListeners = {
package/dist/api.js CHANGED
@@ -65,6 +65,8 @@ function toPublicRunListeners(listeners) {
65
65
  sessionId: item.threadId,
66
66
  requestId: item.runId,
67
67
  agentId: item.agentId,
68
+ agentName: item.agentName,
69
+ surfaceItems: item.surfaceItems,
68
70
  event: item.event,
69
71
  })
70
72
  : undefined,
@@ -142,6 +144,7 @@ function toPublicRequestSummary(run) {
142
144
  requestId: run.runId,
143
145
  sessionId: run.threadId,
144
146
  agentId: run.agentId,
147
+ parentRunId: run.parentRunId,
145
148
  executionMode: run.executionMode,
146
149
  adapterKind: run.adapterKind,
147
150
  createdAt: run.createdAt,
@@ -98,6 +98,21 @@ export type RuntimeHistoryItem = {
98
98
  isError?: boolean;
99
99
  key: string;
100
100
  };
101
+ export type RuntimeSurfaceKind = "agent" | "llm" | "memory" | "skill" | "tool";
102
+ export type RuntimeSurfaceItem = {
103
+ kind: RuntimeSurfaceKind;
104
+ id: string;
105
+ name: string;
106
+ label: string;
107
+ status: "started" | "completed" | "failed";
108
+ ownerAgentId?: string;
109
+ ownerAgentName?: string;
110
+ sourceEventId?: string;
111
+ };
112
+ export type AgentReference = {
113
+ id: string;
114
+ name: string;
115
+ };
101
116
  export type RuntimeQueueDiagnostics = {
102
117
  status: HealthStatus;
103
118
  activeRunSlots: number;
@@ -414,6 +429,8 @@ export type UpstreamRuntimeEventItem = {
414
429
  threadId: string;
415
430
  runId: string;
416
431
  agentId: string;
432
+ agentName?: string;
433
+ surfaceItems?: RuntimeSurfaceItem[];
417
434
  event: UpstreamRuntimeEvent;
418
435
  };
419
436
  export type RuntimeListeners = {
@@ -461,6 +478,8 @@ export type HarnessStreamItem = {
461
478
  threadId: string;
462
479
  runId: string;
463
480
  agentId: string;
481
+ agentName?: string;
482
+ surfaceItems?: RuntimeSurfaceItem[];
464
483
  event: UpstreamRuntimeEvent;
465
484
  } | {
466
485
  type: "result";
@@ -476,6 +495,7 @@ export type ThreadRunRecord = {
476
495
  runId: string;
477
496
  threadId: string;
478
497
  agentId: string;
498
+ parentRunId?: string;
479
499
  executionMode: string;
480
500
  adapterKind?: string;
481
501
  createdAt: string;
@@ -500,6 +520,7 @@ export type RequestSummary = Omit<ThreadRunRecord, "threadId" | "runId"> & {
500
520
  };
501
521
  export type RequestRecord = RequestSummary & {
502
522
  history?: RuntimeHistoryItem[];
523
+ runtimeSurface?: RuntimeSurfaceItem[];
503
524
  upstreamEvents?: unknown[];
504
525
  runtimeTimeline?: RuntimeTimelineItem[];
505
526
  };
@@ -512,6 +533,7 @@ export type RunSummary = Omit<RequestSummary, "sessionId" | "requestId"> & {
512
533
  };
513
534
  export type RunRecord = RunSummary & {
514
535
  history?: RuntimeHistoryItem[];
536
+ runtimeSurface?: RuntimeSurfaceItem[];
515
537
  upstreamEvents?: unknown[];
516
538
  runtimeTimeline?: RuntimeTimelineItem[];
517
539
  };
@@ -1,5 +1,6 @@
1
1
  import { projectRuntimeTimeline } from "../runtime/harness/events/timeline.js";
2
2
  import { createUpstreamTimelineReducer } from "../upstream-events.js";
3
+ import { formatAgentName } from "../utils/agent-display.js";
3
4
  function asObject(value) {
4
5
  return typeof value === "object" && value !== null ? value : null;
5
6
  }
@@ -17,7 +18,8 @@ function extractUpstreamEventEnvelope(value) {
17
18
  ? nestedEvent
18
19
  : value;
19
20
  const agentId = readString(typed?.agentId);
20
- return { event, agentId: agentId ?? undefined };
21
+ const agentName = readString(typed?.agentName);
22
+ return { event, agentId: agentId ?? undefined, agentName: agentName ?? undefined };
21
23
  }
22
24
  function collectNestedAgentNames(value, names = new Set()) {
23
25
  if (Array.isArray(value)) {
@@ -48,6 +50,9 @@ function slugify(value) {
48
50
  .replace(/^-+|-+$/g, "")
49
51
  .slice(0, 80) || "node";
50
52
  }
53
+ function buildSurfaceId(kind, value) {
54
+ return `${kind}:${slugify(value)}`;
55
+ }
51
56
  function titleCase(value) {
52
57
  return value
53
58
  .replace(/[_-]+/g, " ")
@@ -352,6 +357,9 @@ function deriveInitialAgentId(input, runtimeTimeline) {
352
357
  }
353
358
  return "agent";
354
359
  }
360
+ function deriveAgentName(agentId, explicitName) {
361
+ return explicitName && explicitName.trim().length > 0 ? explicitName.trim() : formatAgentName(agentId);
362
+ }
355
363
  function extractDelegatedAgentId(event) {
356
364
  const typed = asObject(event);
357
365
  if (!typed) {
@@ -389,15 +397,21 @@ function convertUpstreamEventsWithAgents(upstreamEvents, initialAgentId) {
389
397
  const projections = [];
390
398
  const delegationNodes = [];
391
399
  let currentAgentId = initialAgentId;
400
+ let currentAgentName = deriveAgentName(initialAgentId);
392
401
  let ordinal = 0;
393
402
  upstreamEvents.forEach((event, index) => {
394
403
  const sourceEventId = `upstream:${index + 1}`;
395
404
  const envelope = extractUpstreamEventEnvelope(event);
396
405
  if (envelope.agentId && envelope.agentId !== currentAgentId) {
397
406
  currentAgentId = envelope.agentId;
407
+ currentAgentName = deriveAgentName(currentAgentId, envelope.agentName);
408
+ }
409
+ else if (envelope.agentName) {
410
+ currentAgentName = deriveAgentName(currentAgentId, envelope.agentName);
398
411
  }
399
412
  const nestedAgentId = extractAgentFromNestedMessages(envelope.event, currentAgentId);
400
413
  if (nestedAgentId && nestedAgentId !== currentAgentId) {
414
+ const nestedAgentName = deriveAgentName(nestedAgentId);
401
415
  ordinal += 1;
402
416
  delegationNodes.push({
403
417
  id: `delegate:${slugify(currentAgentId)}:${slugify(nestedAgentId)}:${ordinal}`,
@@ -410,19 +424,24 @@ function convertUpstreamEventsWithAgents(upstreamEvents, initialAgentId) {
410
424
  threadId: "",
411
425
  runId: "",
412
426
  agentId: nestedAgentId,
427
+ agentName: nestedAgentName,
413
428
  sourceEventIds: [sourceEventId],
414
429
  detail: {
415
430
  fromAgentId: currentAgentId,
431
+ fromAgentName: currentAgentName,
416
432
  toAgentId: nestedAgentId,
433
+ toAgentName: nestedAgentName,
417
434
  },
418
435
  });
419
436
  currentAgentId = nestedAgentId;
437
+ currentAgentName = nestedAgentName;
420
438
  }
421
439
  const emitted = reducer.consume(envelope.event);
422
440
  for (const projection of emitted) {
423
441
  projections.push({
424
442
  projection,
425
443
  agentId: currentAgentId,
444
+ agentName: currentAgentName,
426
445
  sourceEventId,
427
446
  });
428
447
  }
@@ -430,6 +449,7 @@ function convertUpstreamEventsWithAgents(upstreamEvents, initialAgentId) {
430
449
  if (!delegatedAgentId || delegatedAgentId === currentAgentId) {
431
450
  return;
432
451
  }
452
+ const delegatedAgentName = deriveAgentName(delegatedAgentId);
433
453
  ordinal += 1;
434
454
  delegationNodes.push({
435
455
  id: `delegate:${slugify(initialAgentId)}:${slugify(delegatedAgentId)}:${ordinal}`,
@@ -442,13 +462,17 @@ function convertUpstreamEventsWithAgents(upstreamEvents, initialAgentId) {
442
462
  threadId: "",
443
463
  runId: "",
444
464
  agentId: delegatedAgentId,
465
+ agentName: delegatedAgentName,
445
466
  sourceEventIds: [sourceEventId],
446
467
  detail: {
447
468
  fromAgentId: currentAgentId,
469
+ fromAgentName: currentAgentName,
448
470
  toAgentId: delegatedAgentId,
471
+ toAgentName: delegatedAgentName,
449
472
  },
450
473
  });
451
474
  currentAgentId = delegatedAgentId;
475
+ currentAgentName = delegatedAgentName;
452
476
  });
453
477
  return { projections, delegationNodes };
454
478
  }
@@ -479,7 +503,7 @@ function buildAttempts(projectionRecords, delegationNodes, runtimeGroups, thread
479
503
  currentGroup = segmentGroups[nextSegmentIndex] ?? currentGroup;
480
504
  }
481
505
  }
482
- function createAttempt(kind, label, projection, agentId) {
506
+ function createAttempt(kind, label, projection, agentId, agentName) {
483
507
  ordinal += 1;
484
508
  const attempt = {
485
509
  id: `attempt:${runId}:${slugify(kind)}:${slugify(label)}:${ordinal}`,
@@ -487,6 +511,7 @@ function buildAttempts(projectionRecords, delegationNodes, runtimeGroups, thread
487
511
  label,
488
512
  status: deriveStatusFromProjection(projection),
489
513
  agentId,
514
+ agentName,
490
515
  groupId: currentGroup?.id,
491
516
  sourceEventIds: [],
492
517
  attemptKey: buildAttemptKey(kind, label, currentGroup?.id),
@@ -589,7 +614,7 @@ function buildAttempts(projectionRecords, delegationNodes, runtimeGroups, thread
589
614
  const attemptKey = buildAttemptKey(kind, label, currentGroup?.id);
590
615
  let attempt;
591
616
  if (projection.type === "step" && projection.status === "started") {
592
- attempt = createAttempt(kind, label, projection, record.agentId);
617
+ attempt = createAttempt(kind, label, projection, record.agentId, record.agentName);
593
618
  activeAttemptsByKey.set(attemptKey, attempt);
594
619
  }
595
620
  else if (projection.type === "tool-result") {
@@ -597,16 +622,17 @@ function buildAttempts(projectionRecords, delegationNodes, runtimeGroups, thread
597
622
  const existing = toolAttemptsByName.get(toolName) ?? [];
598
623
  attempt = [...existing].reverse().find((candidate) => candidate.groupId === currentGroup?.id) ?? existing[existing.length - 1];
599
624
  if (!attempt) {
600
- attempt = createAttempt("tool", `Calling tool ${titleCase(toolName)}`, projection, record.agentId);
625
+ attempt = createAttempt("tool", `Calling tool ${titleCase(toolName)}`, projection, record.agentId, record.agentName);
601
626
  }
602
627
  }
603
628
  else {
604
629
  attempt = activeAttemptsByKey.get(attemptKey);
605
630
  if (!attempt) {
606
- attempt = createAttempt(kind, label, projection, record.agentId);
631
+ attempt = createAttempt(kind, label, projection, record.agentId, record.agentName);
607
632
  }
608
633
  }
609
634
  attempt.agentId = attempt.agentId ?? record.agentId;
635
+ attempt.agentName = attempt.agentName ?? record.agentName;
610
636
  attempt.projectionKeys.push(projection.key);
611
637
  attempt.sourceEventIds.push(record.sourceEventId);
612
638
  if (projection.type === "step") {
@@ -684,6 +710,7 @@ function buildAttempts(projectionRecords, delegationNodes, runtimeGroups, thread
684
710
  threadId,
685
711
  runId,
686
712
  agentId: attempt.agentId,
713
+ agentName: attempt.agentName,
687
714
  groupId: attempt.groupId,
688
715
  sequenceStart: attempt.sequenceStart,
689
716
  sequenceEnd: attempt.sequenceEnd,
@@ -730,15 +757,47 @@ export function buildFlowGraph(input) {
730
757
  const { nodes: runtimeNodes, edges: runtimeEdges, groups: runtimeGroups } = buildRuntimeNodes(runtimeTimeline);
731
758
  for (const node of runtimeNodes) {
732
759
  node.agentId = typeof node.detail.agentId === "string" ? node.detail.agentId : initialAgentId;
760
+ node.agentName = deriveAgentName(node.agentId);
733
761
  }
734
762
  const projectionRecords = input.upstreamProjections
735
763
  ? input.upstreamProjections.map((projection, index) => ({
736
764
  projection,
737
765
  agentId: initialAgentId,
766
+ agentName: deriveAgentName(initialAgentId),
738
767
  sourceEventId: "key" in projection ? projection.key : `projection:${index + 1}`,
739
768
  }))
740
769
  : upstreamContext.projections;
741
770
  const { nodes: attemptNodes, edges: attemptEdges, groups: attemptGroups } = buildAttempts(projectionRecords, upstreamContext.delegationNodes, runtimeGroups, sessionId, requestId);
771
+ const runtimeSurface = [...upstreamContext.delegationNodes, ...attemptNodes]
772
+ .filter((node) => node.layer !== "detail"
773
+ && (node.kind === "agent" || node.kind === "llm" || node.kind === "memory" || node.kind === "skill" || node.kind === "tool"))
774
+ .map((node) => ({
775
+ kind: node.kind,
776
+ id: buildSurfaceId(node.kind, node.kind === "agent" ? (node.agentId ?? node.agentName ?? "agent") : node.label),
777
+ name: node.kind === "agent"
778
+ ? (node.agentName ?? node.agentId ?? "Agent")
779
+ : normalizeLabel(node.label)
780
+ .replace(/^Delegate to\s+/i, "")
781
+ .replace(/^Calling LLM\s+/i, "")
782
+ .replace(/^Completed LLM\s+/i, "")
783
+ .replace(/^Calling tool\s+/i, "")
784
+ .replace(/^Completed tool\s+/i, "")
785
+ .replace(/^Tool\s+/i, "")
786
+ .replace(/\s+failed$/i, "")
787
+ .replace(/^Calling skill\s+/i, "")
788
+ .replace(/^Completed skill\s+/i, "")
789
+ .replace(/^Accessing memory\s+/i, "")
790
+ .replace(/^Completed memory\s+/i, ""),
791
+ label: node.label,
792
+ status: node.status === "resolved" ? "completed" : node.status,
793
+ ownerAgentId: node.kind === "agent"
794
+ ? (typeof node.detail.fromAgentId === "string" ? node.detail.fromAgentId : node.agentId)
795
+ : node.agentId,
796
+ ownerAgentName: node.kind === "agent"
797
+ ? (typeof node.detail.fromAgentName === "string" ? node.detail.fromAgentName : node.agentName)
798
+ : node.agentName,
799
+ sourceEventId: node.sourceEventIds[0],
800
+ }));
742
801
  return {
743
802
  graphId: `${sessionId}/${requestId}`,
744
803
  scope: input.scope ?? "run",
@@ -753,6 +812,7 @@ export function buildFlowGraph(input) {
753
812
  runtimeTimelineCount: runtimeTimeline.length,
754
813
  upstreamProjectionCount: upstreamProjections.length,
755
814
  delegationCount: upstreamContext.delegationNodes.length,
815
+ runtimeSurface,
756
816
  ...(input.metadata ?? {}),
757
817
  },
758
818
  };
@@ -5,6 +5,13 @@ function sanitizeAlias(value) {
5
5
  function escapeLabel(value) {
6
6
  return value.replace(/"/g, '\\"');
7
7
  }
8
+ function formatAgentParticipantLabel(node) {
9
+ const agentId = node.agentId ?? "agent";
10
+ if (node.agentName && node.agentName !== agentId) {
11
+ return `Agent:${agentId} (${node.agentName})`;
12
+ }
13
+ return `Agent:${agentId}`;
14
+ }
8
15
  function selectNodes(graph, options) {
9
16
  const includedKinds = options.includeKinds
10
17
  ? new Set(options.includeKinds)
@@ -33,7 +40,7 @@ function getParticipantsForNode(node) {
33
40
  const agentParticipant = {
34
41
  id: `agent:${node.agentId ?? "agent"}`,
35
42
  alias: sanitizeAlias(`Agent_${node.agentId ?? "agent"}`),
36
- label: `Agent${node.agentId ? `:${node.agentId}` : ""}`,
43
+ label: formatAgentParticipantLabel(node),
37
44
  };
38
45
  if (node.kind === "run") {
39
46
  return [
@@ -48,7 +55,9 @@ function getParticipantsForNode(node) {
48
55
  {
49
56
  id: `agent:${fromAgentId}`,
50
57
  alias: sanitizeAlias(`Agent_${fromAgentId}`),
51
- label: `Agent:${fromAgentId}`,
58
+ label: typeof node.detail.fromAgentName === "string" && node.detail.fromAgentName !== fromAgentId
59
+ ? `Agent:${fromAgentId} (${node.detail.fromAgentName})`
60
+ : `Agent:${fromAgentId}`,
52
61
  },
53
62
  agentParticipant,
54
63
  ];
@@ -16,6 +16,7 @@ export type FlowNode = {
16
16
  threadId: string;
17
17
  runId: string;
18
18
  agentId?: string;
19
+ agentName?: string;
19
20
  startedAt?: string;
20
21
  endedAt?: string;
21
22
  sequenceStart?: number;
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.247";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.249";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.247";
1
+ export const AGENT_HARNESS_VERSION = "0.0.249";
@@ -67,10 +67,12 @@ export class FilePersistence {
67
67
  const runDir = this.runDir(input.threadId, input.runId);
68
68
  await ensureDir(path.join(runDir, "events"));
69
69
  await ensureDir(path.join(runDir, "approvals"));
70
+ const threadMeta = await this.getThreadMeta(input.threadId);
70
71
  const meta = {
71
72
  runId: input.runId,
72
73
  threadId: input.threadId,
73
74
  agentId: input.agentId,
75
+ parentRunId: threadMeta?.latestRunId ?? null,
74
76
  executionMode: input.executionMode,
75
77
  adapterKind: input.adapterKind ?? input.executionMode,
76
78
  createdAt: input.createdAt,
@@ -266,6 +268,7 @@ export class FilePersistence {
266
268
  runId: meta.runId,
267
269
  threadId: meta.threadId,
268
270
  agentId: meta.agentId,
271
+ ...(meta.parentRunId ? { parentRunId: meta.parentRunId } : {}),
269
272
  executionMode: meta.executionMode,
270
273
  adapterKind: meta.adapterKind ?? meta.executionMode,
271
274
  createdAt: meta.createdAt,
@@ -4,7 +4,7 @@ import { createClient } from "@libsql/client";
4
4
  import { fileExists, readJson, writeJson } from "../utils/fs.js";
5
5
  import { SqliteRunContextStore } from "./sqlite-run-context-store.js";
6
6
  import { SqliteRunQueueStore } from "./sqlite-run-queue-store.js";
7
- const RUNTIME_SQLITE_SCHEMA_VERSION = 4;
7
+ const RUNTIME_SQLITE_SCHEMA_VERSION = 5;
8
8
  const RUNTIME_SQLITE_SCHEMA_FAMILY = "agent-harness-runtime";
9
9
  function asRow(value) {
10
10
  return value;
@@ -168,6 +168,7 @@ export class SqlitePersistence {
168
168
  run_id TEXT PRIMARY KEY,
169
169
  thread_id TEXT NOT NULL,
170
170
  agent_id TEXT NOT NULL,
171
+ parent_run_id TEXT,
171
172
  execution_mode TEXT NOT NULL,
172
173
  adapter_kind TEXT,
173
174
  created_at TEXT NOT NULL,
@@ -403,6 +404,7 @@ export class SqlitePersistence {
403
404
  runId: asString(row.run_id),
404
405
  threadId: asString(row.thread_id),
405
406
  agentId: asString(row.agent_id),
407
+ ...(asNullableString(row.parent_run_id) ? { parentRunId: asNullableString(row.parent_run_id) } : {}),
406
408
  executionMode: asString(row.execution_mode),
407
409
  adapterKind: asNullableString(row.adapter_kind) ?? undefined,
408
410
  createdAt: asString(row.created_at),
@@ -473,12 +475,19 @@ export class SqlitePersistence {
473
475
  SELECT run_id, thread_id, created_at, NULL, updated_at, agent_id, json_array(agent_id), NULL, '[]'
474
476
  FROM runs
475
477
  `);
478
+ await this.rawExecute("ALTER TABLE runs ADD COLUMN parent_run_id TEXT");
476
479
  await this.rawExecute("INSERT OR REPLACE INTO runtime_metadata (key, value) VALUES (?, ?)", ["schema_version", String(RUNTIME_SQLITE_SCHEMA_VERSION)]);
477
480
  return;
478
481
  }
479
482
  if (version === "3") {
480
483
  await this.rawExecute("ALTER TABLE run_inspection ADD COLUMN upstream_events_json TEXT NOT NULL DEFAULT '[]'");
481
484
  await this.rawExecute("UPDATE run_inspection SET upstream_events_json = '[]' WHERE upstream_events_json IS NULL");
485
+ await this.rawExecute("ALTER TABLE runs ADD COLUMN parent_run_id TEXT");
486
+ await this.rawExecute("INSERT OR REPLACE INTO runtime_metadata (key, value) VALUES (?, ?)", ["schema_version", String(RUNTIME_SQLITE_SCHEMA_VERSION)]);
487
+ return;
488
+ }
489
+ if (version === "4") {
490
+ await this.rawExecute("ALTER TABLE runs ADD COLUMN parent_run_id TEXT");
482
491
  await this.rawExecute("INSERT OR REPLACE INTO runtime_metadata (key, value) VALUES (?, ?)", ["schema_version", String(RUNTIME_SQLITE_SCHEMA_VERSION)]);
483
492
  return;
484
493
  }
@@ -493,6 +502,7 @@ export class SqlitePersistence {
493
502
  VALUES (?, ?, ?, ?, ?, ?, ?)`, [input.threadId, "default", input.agentId, input.status, input.runId, input.createdAt, input.createdAt]);
494
503
  }
495
504
  async bootstrapRun(input) {
505
+ const parentRunId = input.createThread ? null : (await this.getThreadMeta(input.threadId))?.latestRunId ?? null;
496
506
  await mkdir(this.threadDir(input.threadId), { recursive: true });
497
507
  await mkdir(path.join(this.runDir(input.threadId, input.runId), "events"), { recursive: true });
498
508
  const steps = [];
@@ -506,12 +516,13 @@ export class SqlitePersistence {
506
516
  }
507
517
  steps.push({
508
518
  sql: `INSERT OR REPLACE INTO runs
509
- (run_id, thread_id, agent_id, execution_mode, adapter_kind, created_at, updated_at, state, previous_state, state_entered_at, last_transition_at, resumable, checkpoint_ref)
510
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
519
+ (run_id, thread_id, agent_id, parent_run_id, execution_mode, adapter_kind, created_at, updated_at, state, previous_state, state_entered_at, last_transition_at, resumable, checkpoint_ref)
520
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
511
521
  args: [
512
522
  input.runId,
513
523
  input.threadId,
514
524
  input.agentId,
525
+ parentRunId,
515
526
  input.executionMode,
516
527
  input.adapterKind ?? input.executionMode ?? null,
517
528
  input.createdAt,
@@ -562,13 +573,15 @@ export class SqlitePersistence {
562
573
  await this.executeTransaction(steps);
563
574
  }
564
575
  async createRun(input) {
576
+ const parentRunId = (await this.getThreadMeta(input.threadId))?.latestRunId ?? null;
565
577
  await mkdir(path.join(this.runDir(input.threadId, input.runId), "events"), { recursive: true });
566
578
  await this.execute(`INSERT OR REPLACE INTO runs
567
- (run_id, thread_id, agent_id, execution_mode, adapter_kind, created_at, updated_at, state, previous_state, state_entered_at, last_transition_at, resumable, checkpoint_ref)
568
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
579
+ (run_id, thread_id, agent_id, parent_run_id, execution_mode, adapter_kind, created_at, updated_at, state, previous_state, state_entered_at, last_transition_at, resumable, checkpoint_ref)
580
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
569
581
  input.runId,
570
582
  input.threadId,
571
583
  input.agentId,
584
+ parentRunId,
572
585
  input.executionMode,
573
586
  input.adapterKind ?? input.executionMode ?? null,
574
587
  input.createdAt,
@@ -698,7 +711,7 @@ export class SqlitePersistence {
698
711
  ["runs.thread_id = ?", filter.threadId],
699
712
  ["runs.state = ?", filter.state],
700
713
  ]);
701
- const rows = await this.selectAll(`SELECT runs.run_id, runs.thread_id, runs.agent_id, runs.execution_mode, runs.adapter_kind, runs.created_at, runs.updated_at, runs.state, runs.checkpoint_ref, runs.resumable,
714
+ const rows = await this.selectAll(`SELECT runs.run_id, runs.thread_id, runs.agent_id, runs.parent_run_id, runs.execution_mode, runs.adapter_kind, runs.created_at, runs.updated_at, runs.state, runs.checkpoint_ref, runs.resumable,
702
715
  run_inspection.started_at, run_inspection.ended_at, run_inspection.last_activity_at, run_inspection.current_agent_id, run_inspection.delegation_chain_json, run_inspection.runtime_snapshot_json
703
716
  FROM runs
704
717
  LEFT JOIN run_inspection ON run_inspection.run_id = runs.run_id
@@ -707,7 +720,7 @@ export class SqlitePersistence {
707
720
  return rows.map((row) => this.mapRunSummary(row));
708
721
  }
709
722
  async getRun(runId) {
710
- const row = await this.selectOne(`SELECT runs.run_id, runs.thread_id, runs.agent_id, runs.execution_mode, runs.adapter_kind, runs.created_at, runs.updated_at, runs.state, runs.checkpoint_ref, runs.resumable,
723
+ const row = await this.selectOne(`SELECT runs.run_id, runs.thread_id, runs.agent_id, runs.parent_run_id, runs.execution_mode, runs.adapter_kind, runs.created_at, runs.updated_at, runs.state, runs.checkpoint_ref, runs.resumable,
711
724
  run_inspection.started_at, run_inspection.ended_at, run_inspection.last_activity_at, run_inspection.current_agent_id, run_inspection.delegation_chain_json, run_inspection.runtime_snapshot_json
712
725
  FROM runs
713
726
  LEFT JOIN run_inspection ON run_inspection.run_id = runs.run_id
@@ -737,7 +750,7 @@ export class SqlitePersistence {
737
750
  };
738
751
  }
739
752
  async listThreadRuns(threadId) {
740
- const rows = await this.selectAll(`SELECT runs.run_id, runs.thread_id, runs.agent_id, runs.execution_mode, runs.adapter_kind, runs.created_at, runs.updated_at, runs.state, runs.checkpoint_ref, runs.resumable,
753
+ const rows = await this.selectAll(`SELECT runs.run_id, runs.thread_id, runs.agent_id, runs.parent_run_id, runs.execution_mode, runs.adapter_kind, runs.created_at, runs.updated_at, runs.state, runs.checkpoint_ref, runs.resumable,
741
754
  run_inspection.started_at, run_inspection.ended_at, run_inspection.last_activity_at, run_inspection.current_agent_id, run_inspection.delegation_chain_json, run_inspection.runtime_snapshot_json
742
755
  FROM runs
743
756
  LEFT JOIN run_inspection ON run_inspection.run_id = runs.run_id
@@ -770,7 +783,7 @@ export class SqlitePersistence {
770
783
  return rows.map((row) => this.mapApproval(row));
771
784
  }
772
785
  async getRunMeta(threadId, runId) {
773
- const row = await this.selectOne(`SELECT run_id, thread_id, agent_id, execution_mode, adapter_kind, created_at, updated_at
786
+ const row = await this.selectOne(`SELECT run_id, thread_id, agent_id, parent_run_id, execution_mode, adapter_kind, created_at, updated_at
774
787
  FROM runs
775
788
  WHERE thread_id = ? AND run_id = ?`, [threadId, runId]);
776
789
  if (!row) {
@@ -780,6 +793,7 @@ export class SqlitePersistence {
780
793
  runId: asString(row.run_id),
781
794
  threadId: asString(row.thread_id),
782
795
  agentId: asString(row.agent_id),
796
+ parentRunId: asNullableString(row.parent_run_id),
783
797
  executionMode: asString(row.execution_mode),
784
798
  adapterKind: asNullableString(row.adapter_kind) ?? undefined,
785
799
  createdAt: asString(row.created_at),
@@ -12,6 +12,7 @@ export type PersistenceRunMeta = {
12
12
  runId: string;
13
13
  threadId: string;
14
14
  agentId: string;
15
+ parentRunId: string | null;
15
16
  executionMode: string;
16
17
  adapterKind?: string;
17
18
  createdAt: string;
@@ -46,6 +46,8 @@ export async function dispatchRunListeners(stream, listeners, options) {
46
46
  threadId: item.threadId,
47
47
  runId: item.runId,
48
48
  agentId: item.agentId,
49
+ agentName: item.agentName,
50
+ surfaceItems: item.surfaceItems,
49
51
  event: item.event,
50
52
  });
51
53
  continue;
@@ -1,4 +1,4 @@
1
- import type { CompiledAgentBinding, RuntimeSnapshot } from "../../../contracts/types.js";
1
+ import type { CompiledAgentBinding, RuntimeSnapshot, RuntimeSurfaceItem } from "../../../contracts/types.js";
2
2
  export declare function buildRunRuntimeSnapshot(binding: CompiledAgentBinding, options?: {
3
3
  runId?: string;
4
4
  }): RuntimeSnapshot;
@@ -11,3 +11,19 @@ export declare function consumeRunInspectionUpstreamEvent(input: {
11
11
  currentAgentId: string;
12
12
  delegationChain: string[];
13
13
  };
14
+ export declare function projectRuntimeSurfaceFromUpstreamEvents(input: {
15
+ upstreamEvents: unknown[];
16
+ binding?: CompiledAgentBinding;
17
+ initialAgentId: string;
18
+ }): RuntimeSurfaceItem[];
19
+ export declare function projectRuntimeSurfaceFromSingleUpstreamEvent(input: {
20
+ event: unknown;
21
+ binding?: CompiledAgentBinding;
22
+ currentAgentId: string;
23
+ currentAgentName?: string;
24
+ sourceEventId: string;
25
+ }): {
26
+ currentAgentId: string;
27
+ currentAgentName: string;
28
+ items: RuntimeSurfaceItem[];
29
+ };
@@ -1,4 +1,6 @@
1
1
  import { readSkillMetadata } from "../../support/skill-metadata.js";
2
+ import { createUpstreamTimelineReducer } from "../../../upstream-events.js";
3
+ import { formatAgentName } from "../../../utils/agent-display.js";
2
4
  import { getBindingMemorySources, getBindingPrimaryModel, getBindingPrimaryTools, getBindingSkills, getBindingSubagents, } from "../../support/compiled-binding.js";
3
5
  import { buildRuntimeGovernanceBundles } from "./governance.js";
4
6
  function asObject(value) {
@@ -7,6 +9,32 @@ function asObject(value) {
7
9
  function readStringArray(value) {
8
10
  return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
9
11
  }
12
+ function normalizeLabel(value) {
13
+ return value.replace(/\s+/g, " ").trim();
14
+ }
15
+ function slugify(value) {
16
+ return value
17
+ .toLowerCase()
18
+ .replace(/[^a-z0-9]+/g, "-")
19
+ .replace(/^-+|-+$/g, "")
20
+ .slice(0, 80) || "item";
21
+ }
22
+ function stripStepPrefix(label) {
23
+ return normalizeLabel(label)
24
+ .replace(/^Calling LLM\s+/i, "")
25
+ .replace(/^Completed LLM\s+/i, "")
26
+ .replace(/^Calling tool\s+/i, "")
27
+ .replace(/^Completed tool\s+/i, "")
28
+ .replace(/^Tool\s+/i, "")
29
+ .replace(/\s+failed$/i, "")
30
+ .replace(/^Calling skill\s+/i, "")
31
+ .replace(/^Completed skill\s+/i, "")
32
+ .replace(/^Accessing memory\s+/i, "")
33
+ .replace(/^Completed memory\s+/i, "");
34
+ }
35
+ function buildSurfaceId(kind, value) {
36
+ return `${kind}:${slugify(value)}`;
37
+ }
10
38
  function readTracingConfig(binding) {
11
39
  const deepAgentTracing = asObject(binding.harnessRuntime?.deepagent?.passthrough)?.tracing;
12
40
  const langchainTracing = asObject(binding.harnessRuntime?.langchain?.passthrough)?.tracing;
@@ -158,3 +186,121 @@ export function consumeRunInspectionUpstreamEvent(input) {
158
186
  delegationChain: maybeAppendAgent(input.delegationChain, delegatedAgentId),
159
187
  };
160
188
  }
189
+ function unwrapUpstreamEvent(event) {
190
+ const typed = asObject(event);
191
+ if (!typed) {
192
+ return { event };
193
+ }
194
+ const nestedEvent = asObject(typed.event);
195
+ if (nestedEvent) {
196
+ return {
197
+ event: nestedEvent,
198
+ ...(typeof typed.agentId === "string" ? { agentId: typed.agentId } : {}),
199
+ ...(typeof typed.agentName === "string" ? { agentName: typed.agentName } : {}),
200
+ };
201
+ }
202
+ return { event };
203
+ }
204
+ function toSurfaceKind(category) {
205
+ switch (category) {
206
+ case "llm":
207
+ case "memory":
208
+ case "skill":
209
+ case "tool":
210
+ return category;
211
+ default:
212
+ return null;
213
+ }
214
+ }
215
+ export function projectRuntimeSurfaceFromUpstreamEvents(input) {
216
+ const items = [];
217
+ let currentAgentId = input.initialAgentId;
218
+ let currentAgentName = formatAgentName(currentAgentId);
219
+ for (const [index, rawEvent] of input.upstreamEvents.entries()) {
220
+ const projected = projectRuntimeSurfaceFromSingleUpstreamEvent({
221
+ event: rawEvent,
222
+ binding: input.binding,
223
+ currentAgentId,
224
+ currentAgentName,
225
+ sourceEventId: `upstream:${index + 1}`,
226
+ });
227
+ currentAgentId = projected.currentAgentId;
228
+ currentAgentName = projected.currentAgentName;
229
+ items.push(...projected.items);
230
+ }
231
+ return items;
232
+ }
233
+ export function projectRuntimeSurfaceFromSingleUpstreamEvent(input) {
234
+ const reducer = createUpstreamTimelineReducer();
235
+ const items = [];
236
+ let currentAgentId = input.currentAgentId;
237
+ let currentAgentName = input.currentAgentName ?? formatAgentName(currentAgentId);
238
+ const unwrapped = unwrapUpstreamEvent(input.event);
239
+ if (unwrapped.agentId) {
240
+ if (unwrapped.agentId !== currentAgentId) {
241
+ const nextAgentName = unwrapped.agentName ?? formatAgentName(unwrapped.agentId);
242
+ items.push({
243
+ kind: "agent",
244
+ id: buildSurfaceId("agent", unwrapped.agentId),
245
+ name: nextAgentName,
246
+ label: `Delegate to ${nextAgentName}`,
247
+ status: "completed",
248
+ ownerAgentId: currentAgentId,
249
+ ownerAgentName: currentAgentName,
250
+ sourceEventId: input.sourceEventId,
251
+ });
252
+ }
253
+ currentAgentId = unwrapped.agentId;
254
+ currentAgentName = unwrapped.agentName ?? formatAgentName(currentAgentId);
255
+ }
256
+ else if (unwrapped.agentName) {
257
+ currentAgentName = unwrapped.agentName;
258
+ }
259
+ const typed = asObject(unwrapped.event);
260
+ if (typed && input.binding) {
261
+ const next = consumeRunInspectionUpstreamEvent({
262
+ event: typed,
263
+ currentAgentId,
264
+ delegationChain: [currentAgentId],
265
+ binding: input.binding,
266
+ });
267
+ if (next.currentAgentId !== currentAgentId) {
268
+ currentAgentId = next.currentAgentId;
269
+ currentAgentName = formatAgentName(currentAgentId);
270
+ items.push({
271
+ kind: "agent",
272
+ id: buildSurfaceId("agent", currentAgentId),
273
+ name: currentAgentName,
274
+ label: `Delegate to ${currentAgentName}`,
275
+ status: "completed",
276
+ ownerAgentId: next.delegationChain.at(-2),
277
+ ownerAgentName: next.delegationChain.at(-2) ? formatAgentName(next.delegationChain.at(-2)) : undefined,
278
+ sourceEventId: input.sourceEventId,
279
+ });
280
+ }
281
+ }
282
+ for (const projection of reducer.consume(unwrapped.event)) {
283
+ if (projection.type !== "step") {
284
+ continue;
285
+ }
286
+ const kind = toSurfaceKind(projection.category);
287
+ if (!kind) {
288
+ continue;
289
+ }
290
+ items.push({
291
+ kind,
292
+ id: buildSurfaceId(kind, stripStepPrefix(projection.step) || projection.step),
293
+ name: stripStepPrefix(projection.step) || formatAgentName(kind),
294
+ label: projection.step,
295
+ status: projection.status,
296
+ ownerAgentId: currentAgentId,
297
+ ownerAgentName: currentAgentName,
298
+ sourceEventId: input.sourceEventId,
299
+ });
300
+ }
301
+ return {
302
+ currentAgentId,
303
+ currentAgentName,
304
+ items,
305
+ };
306
+ }
@@ -2,7 +2,8 @@ import { AGENT_INTERRUPT_SENTINEL_PREFIX, RuntimeOperationTimeoutError } from ".
2
2
  import { renderRuntimeFailure, renderToolFailure } from "../../support/harness-support.js";
3
3
  import { getBindingPrimaryModel } from "../../support/compiled-binding.js";
4
4
  import { createContentBlocksItem, createToolResultKey, } from "../events/streaming.js";
5
- import { consumeRunInspectionUpstreamEvent } from "./inspection.js";
5
+ import { consumeRunInspectionUpstreamEvent, projectRuntimeSurfaceFromSingleUpstreamEvent } from "./inspection.js";
6
+ import { formatAgentName } from "../../../utils/agent-display.js";
6
7
  function normalizeStreamChunk(chunk) {
7
8
  if (typeof chunk === "string") {
8
9
  if (chunk.startsWith(AGENT_INTERRUPT_SENTINEL_PREFIX)) {
@@ -45,7 +46,9 @@ export async function* streamHarnessRun(options) {
45
46
  let streamActivityObserved = false;
46
47
  let nonUpstreamStreamActivityObserved = false;
47
48
  let currentAgentId = options.selectedAgentId;
49
+ let currentAgentName = formatAgentName(options.selectedAgentId);
48
50
  let delegationChain = [options.selectedAgentId];
51
+ let upstreamEventOrdinal = 0;
49
52
  let syntheticFallback;
50
53
  try {
51
54
  const [priorHistory, acquiredReleaseRunSlot] = await Promise.all([
@@ -69,6 +72,18 @@ export async function* streamHarnessRun(options) {
69
72
  streamActivityObserved = true;
70
73
  const normalizedChunk = normalizeStreamChunk(rawChunk);
71
74
  if (normalizedChunk.kind === "upstream-event") {
75
+ upstreamEventOrdinal += 1;
76
+ const surfaceProjection = projectRuntimeSurfaceFromSingleUpstreamEvent({
77
+ event: {
78
+ agentId: currentAgentId,
79
+ agentName: currentAgentName,
80
+ event: normalizedChunk.event,
81
+ },
82
+ binding: options.binding,
83
+ currentAgentId,
84
+ currentAgentName,
85
+ sourceEventId: `upstream:${upstreamEventOrdinal}`,
86
+ });
72
87
  const inspection = consumeRunInspectionUpstreamEvent({
73
88
  event: normalizedChunk.event,
74
89
  currentAgentId,
@@ -76,17 +91,24 @@ export async function* streamHarnessRun(options) {
76
91
  binding: options.binding,
77
92
  });
78
93
  currentAgentId = inspection.currentAgentId;
94
+ currentAgentName = formatAgentName(currentAgentId);
79
95
  delegationChain = inspection.delegationChain;
80
96
  await options.updateRunInspection(options.threadId, options.runId, {
81
97
  currentAgentId,
82
98
  delegationChain,
83
- appendUpstreamEvent: normalizedChunk.event,
99
+ appendUpstreamEvent: {
100
+ agentId: currentAgentId,
101
+ agentName: currentAgentName,
102
+ event: normalizedChunk.event,
103
+ },
84
104
  });
85
105
  yield {
86
106
  type: "upstream-event",
87
107
  threadId: options.threadId,
88
108
  runId: options.runId,
89
109
  agentId: currentAgentId,
110
+ agentName: currentAgentName,
111
+ surfaceItems: surfaceProjection.items,
90
112
  event: normalizedChunk.event,
91
113
  };
92
114
  continue;
@@ -1,6 +1,20 @@
1
1
  import { isTerminalRunState, toInspectableApprovalRecord } from "./helpers.js";
2
2
  import { projectRuntimeTimeline } from "../events/timeline.js";
3
3
  import { createUpstreamTimelineReducer } from "../../../upstream-events.js";
4
+ import { projectRuntimeSurfaceFromUpstreamEvents } from "./inspection.js";
5
+ function unwrapPersistedUpstreamEvent(event) {
6
+ if (typeof event !== "object" || event === null || Array.isArray(event)) {
7
+ return event;
8
+ }
9
+ const typed = event;
10
+ if (Object.prototype.hasOwnProperty.call(typed, "event")
11
+ && typeof typed.event === "object"
12
+ && typed.event !== null
13
+ && !Array.isArray(typed.event)) {
14
+ return typed.event;
15
+ }
16
+ return event;
17
+ }
4
18
  function selectLatestPendingApproval(approvals) {
5
19
  return approvals
6
20
  .filter((approval) => approval.status === "pending")
@@ -8,7 +22,7 @@ function selectLatestPendingApproval(approvals) {
8
22
  }
9
23
  function buildRunInspectionProjection(upstreamEvents) {
10
24
  const reducer = createUpstreamTimelineReducer();
11
- const history = upstreamEvents.flatMap((event) => reducer.consume(event));
25
+ const history = upstreamEvents.flatMap((event) => reducer.consume(unwrapPersistedUpstreamEvent(event)));
12
26
  return {
13
27
  upstreamEvents,
14
28
  history,
@@ -18,10 +32,15 @@ export async function buildRequestInspectionRecord(persistence, request) {
18
32
  const inspection = await persistence.getRunInspection(request.threadId, request.runId);
19
33
  const runtimeEvents = await persistence.listRunEvents(request.threadId, request.runId);
20
34
  const { upstreamEvents, history } = buildRunInspectionProjection(inspection.upstreamEvents);
35
+ const runtimeSurface = projectRuntimeSurfaceFromUpstreamEvents({
36
+ upstreamEvents,
37
+ initialAgentId: request.agentId ?? inspection.currentAgentId ?? "agent",
38
+ });
21
39
  return {
22
40
  requestId: request.runId,
23
41
  sessionId: request.threadId,
24
42
  agentId: request.agentId,
43
+ parentRunId: request.parentRunId,
25
44
  executionMode: request.executionMode,
26
45
  adapterKind: request.adapterKind,
27
46
  createdAt: request.createdAt,
@@ -37,6 +56,7 @@ export async function buildRequestInspectionRecord(persistence, request) {
37
56
  runtimeSnapshot: request.runtimeSnapshot,
38
57
  upstreamEvents,
39
58
  history,
59
+ runtimeSurface,
40
60
  runtimeTimeline: projectRuntimeTimeline(runtimeEvents, {
41
61
  threadId: request.threadId,
42
62
  runId: request.runId,
@@ -48,6 +68,7 @@ function toRunRecord(request) {
48
68
  runId: request.requestId,
49
69
  threadId: request.sessionId,
50
70
  agentId: request.agentId,
71
+ parentRunId: request.parentRunId,
51
72
  executionMode: request.executionMode,
52
73
  adapterKind: request.adapterKind,
53
74
  createdAt: request.createdAt,
@@ -63,6 +84,7 @@ function toRunRecord(request) {
63
84
  runtimeSnapshot: request.runtimeSnapshot,
64
85
  upstreamEvents: request.upstreamEvents,
65
86
  history: request.history,
87
+ runtimeSurface: request.runtimeSurface,
66
88
  runtimeTimeline: request.runtimeTimeline,
67
89
  };
68
90
  }
@@ -385,6 +385,7 @@ export class AgentHarnessRuntime {
385
385
  requestId: request.runId,
386
386
  sessionId: request.threadId,
387
387
  agentId: request.agentId,
388
+ parentRunId: request.parentRunId,
388
389
  executionMode: request.executionMode,
389
390
  adapterKind: request.adapterKind,
390
391
  createdAt: request.createdAt,
@@ -410,6 +411,7 @@ export class AgentHarnessRuntime {
410
411
  runId: request.requestId,
411
412
  threadId: request.sessionId,
412
413
  agentId: request.agentId,
414
+ parentRunId: request.parentRunId,
413
415
  executionMode: request.executionMode,
414
416
  adapterKind: request.adapterKind,
415
417
  createdAt: request.createdAt,
@@ -1800,6 +1802,7 @@ function toRequestSummary(summary) {
1800
1802
  requestId: summary.runId,
1801
1803
  sessionId: summary.threadId,
1802
1804
  agentId: summary.agentId,
1805
+ parentRunId: summary.parentRunId,
1803
1806
  executionMode: summary.executionMode,
1804
1807
  adapterKind: summary.adapterKind,
1805
1808
  createdAt: summary.createdAt,
@@ -1834,6 +1837,7 @@ function toRequestRecord(record) {
1834
1837
  ...toRequestSummary(record),
1835
1838
  upstreamEvents: record.upstreamEvents,
1836
1839
  history: record.history,
1840
+ runtimeSurface: record.runtimeSurface,
1837
1841
  runtimeTimeline: record.runtimeTimeline,
1838
1842
  };
1839
1843
  }
@@ -0,0 +1 @@
1
+ export declare function formatAgentName(agentId: string): string;
@@ -0,0 +1,18 @@
1
+ function normalizeSegment(value) {
2
+ return value
3
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
4
+ .replace(/[_-]+/g, " ")
5
+ .replace(/\s+/g, " ")
6
+ .trim();
7
+ }
8
+ export function formatAgentName(agentId) {
9
+ const normalized = normalizeSegment(agentId);
10
+ if (!normalized) {
11
+ return "Agent";
12
+ }
13
+ return normalized
14
+ .split(" ")
15
+ .filter(Boolean)
16
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
17
+ .join(" ");
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.248",
3
+ "version": "0.0.250",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "license": "MIT",
6
6
  "type": "module",