/**
* agent-tree.ts — Visual and JSON representations for Agent swarms.
*
* Provides three export formats (all using parentId for hierarchy):
* - `buildAgentTreeMermaid` — Mermaid flowchart diagram
* - `buildAgentTreeText` — Unicode box-drawing text tree
* - `buildAgentTreeJson` — Structured JSON hierarchy
*/
import type { AgentRecord } from "./types.js";
/**
* Formats a clean label string for Mermaid/visual charts.
*/
function label(record: AgentRecord): string {
const safeDescription = record.description.replace(/["\n\r]/g, " ").trim();
return `${record.id}
${record.type}
${record.status}
${safeDescription}`;
}
/**
* Build a tree of AgentRecords using parentId relationships.
* Returns root nodes and a children map for efficient traversal.
*/
function buildTree(records: AgentRecord[]): {
roots: AgentRecord[];
childrenMap: Map;
nodeMap: Map;
} {
const roots: AgentRecord[] = [];
const childrenMap = new Map();
const nodeMap = new Map();
for (const r of records) {
nodeMap.set(r.id, r);
}
for (const r of records) {
if (r.parentId && nodeMap.has(r.parentId)) {
if (!childrenMap.has(r.parentId)) {
childrenMap.set(r.parentId, []);
}
childrenMap.get(r.parentId)!.push(r);
} else {
roots.push(r);
}
}
return { roots, childrenMap, nodeMap };
}
/**
* Builds a Mermaid Flowchart definition mapping the agent hierarchy.
* Uses parentId relationships (same as text and JSON formats).
*/
export function buildAgentTreeMermaid(records: AgentRecord[]): string {
const sorted = [...records].sort((a, b) => a.spawnedAt - b.spawnedAt);
const lines = ["flowchart TD"];
if (sorted.length === 0) {
lines.push(' empty["No agents in this session"]');
return lines.join("\n");
}
// Use the shared buildTree helper for parentId-based hierarchy
const { roots, childrenMap, nodeMap } = buildTree(sorted);
// Node declarations for all agents
for (const record of sorted) {
lines.push(` ${record.id.replace(/-/g, "_")}["${label(record)}"]`);
}
// Edges: parent → child (using parentId from buildTree)
for (const [parentId, children] of childrenMap) {
for (const child of children) {
lines.push(` ${parentId.replace(/-/g, "_")} --> ${child.id.replace(/-/g, "_")}`);
}
}
// Also add edges for groupId (swarm/group membership) as dashed lines
for (const record of sorted) {
if (record.groupId && nodeMap.has(record.groupId)) {
// Avoid duplicate edges when parentId === groupId
const alreadyHasParentEdge = record.parentId === record.groupId;
if (!alreadyHasParentEdge) {
lines.push(` ${record.groupId.replace(/-/g, "_")} -.-> ${record.id.replace(/-/g, "_")}`);
}
}
}
// Show root nodes that have no groupId — link from a virtual "session" node
if (roots.length > 1) {
lines.push(' session["Session"]');
for (const root of roots) {
lines.push(` session --> ${root.id.replace(/-/g, "_")}`);
}
}
return lines.join("\n");
}
/**
* Build a plain-text Unicode box-drawing tree of the agent hierarchy.
* Uses parentId relationships to determine the tree structure.
*
* Example output:
* agent-1 (Explore) [running]
* ├─ agent-2 (Plan) [completed]
* │ └─ agent-4 (Explore) [running]
* └─ agent-3 (Analysis) [queued]
*/
export function buildAgentTreeText(records: AgentRecord[]): string {
if (records.length === 0) return "No execution tree available.";
const { roots, childrenMap, nodeMap } = buildTree(records);
let out = "";
const render = (nodeId: string, indent: string, isLast: boolean): void => {
const r = nodeMap.get(nodeId);
if (!r) return;
const branch = indent ? (isLast ? "\u2514\u2500 " : "\u251C\u2500 ") : "";
out += `${indent}${branch}${r.id} (${r.type}) [${r.status}]\n`;
const children = childrenMap.get(nodeId) || [];
for (let i = 0; i < children.length; i++) {
// Continuation indent: " " (last sibling) or "│ " (more siblings follow).
// Always compute it — children of a root (indent="") still need indent.
const cont = indent + (isLast ? " " : "\u2502 ");
render(children[i].id, cont, i === children.length - 1);
}
};
for (let i = 0; i < roots.length; i++) {
render(roots[i].id, "", i === roots.length - 1);
}
return out;
}
/**
* Generates a clean JSON array of agent tree nodes with children.
* Each root node recursively contains its descendants.
*/
export function buildAgentTreeJson(records: AgentRecord[]): string {
interface TreeNode {
id: string;
type: string;
status: string;
description: string;
children: TreeNode[];
}
const { roots, childrenMap } = buildTree(records);
const toNode = (r: AgentRecord): TreeNode => {
const childRecords = childrenMap.get(r.id) || [];
return {
id: r.id,
type: r.type,
status: r.status,
description: r.description,
children: childRecords.map(toNode),
};
};
return JSON.stringify(roots.map(toNode), null, 2);
}