feat(FN-3327): add agent delegation and org hierarchy tools to pi extension

Adds `list_agents` and `delegate_task` tools to the pi extension for inter-agent delegation, along with supporting reference docs and 248 new tests. The sync script was also updated to keep the skill tools in sync with the extension implementation.

Fusion-Task-Id: FN-3327
This commit is contained in:
Fusion
2026-05-04 02:05:32 -07:00
committed by gsxdsm
parent 94fdbc3481
commit cbb87ae00e
7 changed files with 669 additions and 2 deletions

View File

@@ -30,7 +30,7 @@ Mission → Milestone → Slice → Feature → Task
- **Task tools** — `fn_task_create`, `fn_task_update`, `fn_task_list`, `fn_task_show`, `fn_task_attach`, `fn_task_pause`, `fn_task_unpause`, `fn_task_retry`, `fn_task_duplicate`, `fn_task_refine`, `fn_task_archive`, `fn_task_unarchive`, `fn_task_delete`, `fn_task_plan`
- **GitHub tools** — `fn_task_import_github`, `fn_task_import_github_issue`, `fn_task_browse_github_issues`
- **Mission tools** — `fn_mission_create`, `fn_mission_list`, `fn_mission_show`, `fn_mission_delete`, `fn_milestone_add`, `fn_slice_add`, `fn_feature_add`, `fn_slice_activate`, `fn_feature_link_task`
- **Agent tools** — `fn_agent_stop`, `fn_agent_start`
- **Agent tools** — `fn_agent_stop`, `fn_agent_start`, `fn_list_agents`, `fn_delegate_task`, `fn_agent_show`, `fn_agent_org_chart`
- **Skills tools** — `fn_skills_search`, `fn_skills_install`
- **Insight tools** — `fn_insight_list`, `fn_insight_show`, `fn_insight_run_list`, `fn_insight_run_show`
- **Other tools** — `fn_research_run`, `fn_research_list`, `fn_research_get`, `fn_research_cancel`

View File

@@ -89,3 +89,36 @@ For work larger than L, use missions to break it into phases.
2. `fn_task_import_github_issue` for high-priority issues
3. Tasks enter triage and get AI-specified
4. Monitor board as AI works through them
## Agent Management and Delegation
Agents are AI workers in the Fusion system. Each agent has a role, state, and position in an organizational hierarchy. Use the agent management tools to discover, inspect, delegate to, and manage agents.
**Discovering available agents:**
1. `fn_list_agents` — see all agents, optionally filter by role or state
2. `fn_agent_show` — get full details about a specific agent (including hierarchy)
3. `fn_delegate_task` — create and assign a task to the chosen agent
**Checking team structure before delegation:**
1. `fn_agent_org_chart` — visualize the full org tree
2. `fn_agent_show` — inspect a specific agent's capabilities and reports
3. `fn_delegate_task` — assign work to the appropriate agent
**Delegation patterns:**
- **Delegate to reports** — an agent delegates to agents that report to it (downward delegation)
- **Delegate to peers** — an agent delegates to another agent at the same level (lateral delegation)
- **Delegate to other teams** — use `fn_agent_org_chart` to understand cross-team structure
- Always verify the target agent is not ephemeral/runtime before delegating
**Recovery — re-delegating stalled work:**
1. `fn_agent_stop` — pause the stalled agent
2. `fn_list_agents` — find an available agent to take over
3. `fn_delegate_task` — create a new task for the replacement agent
4. The original task can be refined or the new task can depend on it
**Agent lifecycle states:**
- `idle` — agent is available for work
- `active` — agent is running and available for heartbeat cycles
- `running` — agent is currently executing a task
- `paused` — agent has been stopped (use `fn_agent_start` to resume)
- `error` — agent encountered an error

View File

@@ -263,6 +263,43 @@ Start a stopped agent — resumes its execution. Transitions the agent from paus
|-----------|------|----------|-------------|
| `id` | string | ✓ | Agent ID to start (e.g., agent-abc123) |
### fn_list_agents
List all available agents in the system. Shows each agent's name, role, state, personality (soul), and current assignment. Use this to discover which agents exist and what they specialize in before delegating work.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `role` | string | — | Filter by agent role/capability (e.g., 'executor', 'reviewer', 'qa') |
| `state` | string | — | Filter by agent state (e.g., 'idle', 'active', 'running') |
| `includeEphemeral` | boolean | — | Include ephemeral/runtime agents (default: false) |
### fn_delegate_task
Create a new task and assign it to a specific agent for execution. The task goes to 'todo' and will be picked up by the target agent on their next heartbeat cycle. Use fn_list_agents first to find available agents and their capabilities.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `agent_id` | string | ✓ | The agent ID to delegate work to |
| `description` | string | ✓ | What needs to be done |
| `dependencies` | array | — | Task IDs this new task depends on (e.g. [\"KB-001\"] |
### fn_agent_show
Show detailed information about a single agent, including their role, state, position in the org hierarchy (reports-to, direct reports), skills, and current assignment.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `id` | string | ✓ | Agent ID or resolvable name |
### fn_agent_org_chart
Show the organizational tree of agents, displaying the role hierarchy. Optionally filter to a subtree rooted at a specific agent.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `root_agent_id` | string | — | If provided, show only the subtree rooted at this agent |
| `include_ephemeral` | boolean | — | Include ephemeral/runtime agents (default: false) |
## Skills Tools
### fn_skills_search

View File

@@ -48,6 +48,10 @@ All skill/extension tool invocations in this catalog use the public `fn_*` names
| `fn_feature_link_task` | Link a feature to a fn task for implementation. Updates the feature status to 'triaged' and associates it with the task. |
| `fn_agent_stop` | Stop a running agent — pauses its execution. Transitions the agent from running/active to paused state. |
| `fn_agent_start` | Start a stopped agent — resumes its execution. Transitions the agent from paused to active state. |
| `fn_list_agents` | List all available agents in the system. Shows each agent's name, role, state, personality (soul), and current assignment. Use this to discover which agents exist and what they specialize in before delegating work. |
| `fn_delegate_task` | Create a new task and assign it to a specific agent for execution. The task goes to 'todo' and will be picked up by the target agent on their next heartbeat cycle. Use fn_list_agents first to find available agents and their capabilities. |
| `fn_agent_show` | Show detailed information about a single agent, including their role, state, position in the org hierarchy (reports-to, direct reports), skills, and current assignment. |
| `fn_agent_org_chart` | Show the organizational tree of agents, displaying the role hierarchy. Optionally filter to a subtree rooted at a specific agent. |
| `fn_skills_search` | Search the skills.sh directory for agent skills. Returns matching skills with names, sources (owner/repo), install counts, and install commands. Use fn_skills_install to install a selected skill. |
| `fn_skills_install` | Install an agent skill from skills.sh into the current project. Downloads skill files into the project's skill directories (.fusion/skills/, legacy .pi/skills/, .agents/skills/). The skill becomes available to AI agents in subsequent sessions. |
<!-- END: fusion-capabilities-tool-table -->

View File

@@ -185,6 +185,10 @@ describe.skipIf(!SHOULD_RUN_EXTENSION_INTEGRATION)("fn pi extension", () => {
"fn_feature_link_task",
"fn_agent_stop",
"fn_agent_start",
"fn_list_agents",
"fn_delegate_task",
"fn_agent_show",
"fn_agent_org_chart",
"fn_skills_search",
"fn_skills_install",
] as const;
@@ -1262,4 +1266,248 @@ describe("fn pi extension (runnable structured-output regression slice)", () =>
expect(result.details.error).toContain("ephemeral/runtime agent");
expect(result.content[0].text).toContain(ephemeralId);
});
describe("fn_list_agents", () => {
it("returns agent list", async () => {
await seedAgent(tmpDir, { name: "alpha-agent" });
await seedAgent(tmpDir, { name: "beta-agent" });
const tool = api.tools.get("fn_list_agents")!;
const result = await tool.execute("la-1", {}, undefined, undefined, makeCtx(tmpDir));
expect(result.content[0].text).toContain("alpha-agent");
expect(result.content[0].text).toContain("beta-agent");
expect(result.details.count).toBeGreaterThanOrEqual(2);
});
it("filters by role", async () => {
const agentStore = new AgentStore({ rootDir: join(tmpDir, ".fusion") });
await agentStore.init();
await agentStore.createAgent({ name: "exec-agent", role: "executor", metadata: {} });
await agentStore.createAgent({ name: "review-agent", role: "reviewer", metadata: {} });
const tool = api.tools.get("fn_list_agents")!;
const result = await tool.execute("la-2", { role: "executor" }, undefined, undefined, makeCtx(tmpDir));
expect(result.content[0].text).toContain("exec-agent");
expect(result.content[0].text).not.toContain("review-agent");
});
it("filters by state", async () => {
const agentStore = new AgentStore({ rootDir: join(tmpDir, ".fusion") });
await agentStore.init();
const active = await agentStore.createAgent({ name: "active-agent", role: "executor", metadata: {} });
await agentStore.updateAgentState(active.id, "active");
const tool = api.tools.get("fn_list_agents")!;
const result = await tool.execute("la-3", { state: "active" }, undefined, undefined, makeCtx(tmpDir));
expect(result.content[0].text).toContain("active-agent");
expect(result.details.agents.every((a: any) => a.state === "active")).toBe(true);
});
it("excludes ephemeral agents by default", async () => {
const ephemeralId = await seedAgent(tmpDir, { ephemeral: true, name: "eph-agent" });
await seedAgent(tmpDir, { name: "real-agent" });
const tool = api.tools.get("fn_list_agents")!;
const result = await tool.execute("la-4", {}, undefined, undefined, makeCtx(tmpDir));
expect(result.content[0].text).not.toContain("eph-agent");
expect(result.content[0].text).toContain("real-agent");
expect(result.details.agents.every((a: any) => a.id !== ephemeralId)).toBe(true);
});
it("returns empty list message when no agents", async () => {
const tool = api.tools.get("fn_list_agents")!;
const result = await tool.execute("la-5", {}, undefined, undefined, makeCtx(tmpDir));
expect(result.content[0].text).toContain("No agents found");
expect(result.details.count).toBe(0);
});
});
describe("fn_delegate_task", () => {
it("delegates task to agent", async () => {
const agentId = await seedAgent(tmpDir, { name: "delegate-target" });
const tool = api.tools.get("fn_delegate_task")!;
const result = await tool.execute(
"dt-1",
{ agent_id: agentId, description: "Do important work" },
undefined,
undefined,
makeCtx(tmpDir),
);
expect(result.content[0].text).toContain("delegate-target");
expect(result.content[0].text).toContain(agentId);
expect(result.details.agentId).toBe(agentId);
expect(result.details.agentName).toBe("delegate-target");
expect(result.details.taskId).toBeTruthy();
// Verify task was actually created
const store = new TaskStore(tmpDir);
await store.init();
const task = await store.getTask(result.details.taskId);
expect(task).toBeTruthy();
expect(task!.assignedAgentId).toBe(agentId);
expect(task!.column).toBe("todo");
});
it("rejects unknown agent", async () => {
const tool = api.tools.get("fn_delegate_task")!;
const result = await tool.execute(
"dt-2",
{ agent_id: "agent-no-such", description: "Will fail" },
undefined,
undefined,
makeCtx(tmpDir),
);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("not found");
});
it("rejects ephemeral agent", async () => {
const ephemeralId = await seedAgent(tmpDir, { ephemeral: true, name: "eph-delegate" });
const tool = api.tools.get("fn_delegate_task")!;
const result = await tool.execute(
"dt-3",
{ agent_id: ephemeralId, description: "Will fail" },
undefined,
undefined,
makeCtx(tmpDir),
);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("ephemeral/runtime agent");
});
it("wires dependencies correctly", async () => {
const agentId = await seedAgent(tmpDir, { name: "dep-agent" });
// Create a real task to use as a dependency
const store = new TaskStore(tmpDir);
await store.init();
const depTask = await store.createTask({ description: "Prerequisite", column: "todo" });
const tool = api.tools.get("fn_delegate_task")!;
const result = await tool.execute(
"dt-4",
{ agent_id: agentId, description: "Dependent work", dependencies: [depTask.id] },
undefined,
undefined,
makeCtx(tmpDir),
);
expect(result.content[0].text).toContain(depTask.id);
const task = await store.getTask(result.details.taskId);
expect(task!.dependencies).toEqual([depTask.id]);
});
});
describe("fn_agent_show", () => {
it("shows agent by ID", async () => {
const agentId = await seedAgent(tmpDir, { name: "show-agent" });
const tool = api.tools.get("fn_agent_show")!;
const result = await tool.execute("as-1", { id: agentId }, undefined, undefined, makeCtx(tmpDir));
expect(result.content[0].text).toContain("show-agent");
expect(result.content[0].text).toContain(agentId);
expect(result.details.agent.id).toBe(agentId);
});
it("shows agent by name", async () => {
await seedAgent(tmpDir, { name: "resolve-by-name" });
const tool = api.tools.get("fn_agent_show")!;
const result = await tool.execute("as-2", { id: "resolve-by-name" }, undefined, undefined, makeCtx(tmpDir));
expect(result.content[0].text).toContain("resolve-by-name");
expect(result.details.agent.name).toBe("resolve-by-name");
});
it("returns error for unknown agent", async () => {
const tool = api.tools.get("fn_agent_show")!;
const result = await tool.execute("as-3", { id: "no-such-agent" }, undefined, undefined, makeCtx(tmpDir));
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("not found");
});
it("shows reports-to and direct reports", async () => {
const agentStore = new AgentStore({ rootDir: join(tmpDir, ".fusion") });
await agentStore.init();
const manager = await agentStore.createAgent({ name: "the-manager", role: "executor", metadata: {} });
const report = await agentStore.createAgent({
name: "the-report",
role: "executor",
reportsTo: manager.id,
metadata: {},
});
const tool = api.tools.get("fn_agent_show")!;
// Check manager sees direct reports
const mgrResult = await tool.execute("as-4a", { id: manager.id }, undefined, undefined, makeCtx(tmpDir));
expect(mgrResult.content[0].text).toContain("the-report");
expect(mgrResult.details.directReports.length).toBeGreaterThan(0);
// Check report sees reports-to
const rptResult = await tool.execute("as-4b", { id: report.id }, undefined, undefined, makeCtx(tmpDir));
expect(rptResult.content[0].text).toContain("the-manager");
});
});
describe("fn_agent_org_chart", () => {
it("returns full tree", async () => {
const agentStore = new AgentStore({ rootDir: join(tmpDir, ".fusion") });
await agentStore.init();
await agentStore.createAgent({ name: "ceo", role: "executor", metadata: {} });
await agentStore.createAgent({ name: "worker", role: "executor", metadata: {} });
const tool = api.tools.get("fn_agent_org_chart")!;
const result = await tool.execute("oc-1", {}, undefined, undefined, makeCtx(tmpDir));
expect(result.content[0].text).toContain("ceo");
expect(result.content[0].text).toContain("worker");
expect(result.details.count).toBeGreaterThanOrEqual(2);
});
it("returns subtree by root agent", async () => {
const agentStore = new AgentStore({ rootDir: join(tmpDir, ".fusion") });
await agentStore.init();
const manager = await agentStore.createAgent({ name: "org-manager", role: "executor", metadata: {} });
await agentStore.createAgent({ name: "org-report", role: "executor", reportsTo: manager.id, metadata: {} });
const tool = api.tools.get("fn_agent_org_chart")!;
const result = await tool.execute("oc-2", { root_agent_id: manager.id }, undefined, undefined, makeCtx(tmpDir));
expect(result.content[0].text).toContain("org-manager");
expect(result.content[0].text).toContain("org-report");
});
it("returns empty message when no agents", async () => {
const tool = api.tools.get("fn_agent_org_chart")!;
const result = await tool.execute("oc-3", {}, undefined, undefined, makeCtx(tmpDir));
expect(result.content[0].text).toContain("No agents found");
expect(result.details.count).toBe(0);
});
it("returns single agent for lone agent", async () => {
const agentStore = new AgentStore({ rootDir: join(tmpDir, ".fusion") });
await agentStore.init();
const lone = await agentStore.createAgent({ name: "lone-agent", role: "executor", metadata: {} });
const tool = api.tools.get("fn_agent_org_chart")!;
const result = await tool.execute("oc-4", { root_agent_id: lone.id }, undefined, undefined, makeCtx(tmpDir));
expect(result.content[0].text).toContain("lone-agent");
});
});
});

View File

@@ -2143,6 +2143,345 @@ export default function kbExtension(pi: ExtensionAPI) {
},
});
// ── fn_list_agents ───────────────────────────────────────────────
pi.registerTool({
name: "fn_list_agents",
label: "fn: List Agents",
description:
"List all available agents in the system. Shows each agent's name, role, state, " +
"personality (soul), and current assignment. Use this to discover which agents exist " +
"and what they specialize in before delegating work.",
promptSnippet: "List all available Fusion agents",
promptGuidelines: [
"Use fn_list_agents to discover which agents exist before delegating work",
"Filter by role or state to narrow results",
"Ephemeral/runtime agents are excluded by default",
],
parameters: Type.Object({
role: Type.Optional(
Type.String({ description: "Filter by agent role/capability (e.g., 'executor', 'reviewer', 'qa')" }),
),
state: Type.Optional(
Type.String({ description: "Filter by agent state (e.g., 'idle', 'active', 'running')" }),
),
includeEphemeral: Type.Optional(
Type.Boolean({ description: "Include ephemeral/runtime agents (default: false)" }),
),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const { AgentStore } = await import("@fusion/core");
const agentStore = new AgentStore({ rootDir: getFusionDir(ctx.cwd) });
await agentStore.init();
const filter: Record<string, unknown> = {};
if (params.role) filter.role = params.role;
if (params.state) filter.state = params.state;
if (params.includeEphemeral !== undefined) filter.includeEphemeral = params.includeEphemeral;
const agents = await agentStore.listAgents(filter as Parameters<typeof agentStore.listAgents>[0]);
if (agents.length === 0) {
return {
content: [{ type: "text" as const, text: "No agents found matching the specified filters." }],
details: { agents: [], count: 0 },
};
}
const lines = agents.map((agent) => {
const parts: string[] = [
`ID: ${agent.id}`,
`Name: ${agent.name}`,
`Role: ${agent.role}`,
`State: ${agent.state}`,
];
if (agent.title) parts.push(`Title: ${agent.title}`);
if (agent.soul) parts.push(`Soul: ${agent.soul.slice(0, 200)}`);
if (agent.instructionsText) {
const snippet = agent.instructionsText.slice(0, 100);
parts.push(`Custom Instructions: ${snippet}${agent.instructionsText.length > 100 ? "…" : ""}`);
}
if (agent.taskId) parts.push(`Current Task: ${agent.taskId}`);
return parts.join("\n");
});
return {
content: [{ type: "text" as const, text: `Available agents (${agents.length}):\n\n${lines.join("\n\n")}` }],
details: { agents, count: agents.length },
};
},
});
// ── fn_delegate_task ──────────────────────────────────────────────
pi.registerTool({
name: "fn_delegate_task",
label: "fn: Delegate Task",
description:
"Create a new task and assign it to a specific agent for execution. The task goes to " +
"'todo' and will be picked up by the target agent on their next heartbeat cycle. " +
"Use fn_list_agents first to find available agents and their capabilities.",
promptSnippet: "Delegate a task to a specific Fusion agent",
promptGuidelines: [
"Use fn_list_agents first to find available agents and their capabilities",
"The task is created in 'todo' and assigned to the target agent",
"Cannot delegate to ephemeral/runtime agents",
"Optionally specify dependencies on other tasks",
],
parameters: Type.Object({
agent_id: Type.String({ description: "The agent ID to delegate work to" }),
description: Type.String({ description: "What needs to be done" }),
dependencies: Type.Optional(
Type.Array(Type.String(), { description: "Task IDs this new task depends on (e.g. [\"KB-001\"]" }),
),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
// Validate target agent exists and is not ephemeral
const agentError = await validateAssignableAgentId(ctx.cwd, params.agent_id);
if (agentError) {
return {
content: [{ type: "text", text: `ERROR: ${agentError}` }],
isError: true,
details: { error: agentError },
};
}
const { AgentStore } = await import("@fusion/core");
const agentStore = new AgentStore({ rootDir: getFusionDir(ctx.cwd) });
await agentStore.init();
const agent = await agentStore.getAgent(params.agent_id);
// Create task assigned to the target agent
const store = await getStore(ctx.cwd);
const task = await store.createTask({
description: params.description,
dependencies: params.dependencies,
column: "todo",
assignedAgentId: params.agent_id,
source: { sourceType: "api" },
});
const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
return {
content: [{
type: "text" as const,
text: `Delegated to ${agent!.name} (${agent!.id}): Created ${task.id}${deps}. ` +
`The task will be picked up by ${agent!.name} on their next heartbeat cycle.`,
}],
details: { taskId: task.id, agentId: agent!.id, agentName: agent!.name },
};
},
});
// ── fn_agent_show ─────────────────────────────────────────────────
pi.registerTool({
name: "fn_agent_show",
label: "fn: Show Agent",
description:
"Show detailed information about a single agent, including their role, state, " +
"position in the org hierarchy (reports-to, direct reports), skills, and current assignment.",
promptSnippet: "Show details of a specific Fusion agent",
promptGuidelines: [
"Use to get full details about a specific agent",
"Provide agent ID or a resolvable name",
"Shows the agent's position in the org hierarchy",
],
parameters: Type.Object({
id: Type.String({ description: "Agent ID or resolvable name" }),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const { AgentStore } = await import("@fusion/core");
const agentStore = new AgentStore({ rootDir: getFusionDir(ctx.cwd) });
await agentStore.init();
const agent = await agentStore.resolveAgent(params.id);
if (!agent) {
return {
content: [{ type: "text", text: `Agent '${params.id}' not found` }],
isError: true,
details: { error: "Agent not found" },
};
}
// Get direct reports
const directReports = await agentStore.getAgentsByReportsTo(agent.id);
const parts: string[] = [
`ID: ${agent.id}`,
`Name: ${agent.name}`,
`Role: ${agent.role}`,
`State: ${agent.state}`,
];
if (agent.title) parts.push(`Title: ${agent.title}`);
if (agent.icon) parts.push(`Icon: ${agent.icon}`);
if (agent.reportsTo) {
const manager = await agentStore.getAgent(agent.reportsTo);
if (manager) {
parts.push(`Reports To: ${manager.name} (${manager.id})`);
} else {
parts.push(`Reports To: ${agent.reportsTo}`);
}
}
if (directReports.length > 0) {
parts.push(`Direct Reports: ${directReports.map((r) => `${r.name} (${r.id})`).join(", ")}`);
}
if (agent.taskId) parts.push(`Current Task: ${agent.taskId}`);
if (agent.instructionsText) {
const snippet = agent.instructionsText.slice(0, 100);
parts.push(`Custom Instructions: ${snippet}${agent.instructionsText.length > 100 ? "…" : ""}`);
}
if (agent.soul) {
const snippet = agent.soul.slice(0, 200);
parts.push(`Soul: ${snippet}${agent.soul.length > 200 ? "…" : ""}`);
}
if (agent.metadata?.skills) {
parts.push(`Skills: ${JSON.stringify(agent.metadata.skills)}`);
}
return {
content: [{ type: "text" as const, text: parts.join("\n") }],
details: {
agent,
directReports: directReports.map((r) => ({ id: r.id, name: r.name, role: r.role })),
},
};
},
});
// ── fn_agent_org_chart ────────────────────────────────────────────
pi.registerTool({
name: "fn_agent_org_chart",
label: "fn: Agent Org Chart",
description:
"Show the organizational tree of agents, displaying the role hierarchy. " +
"Optionally filter to a subtree rooted at a specific agent.",
promptSnippet: "Show the Fusion agent org chart",
promptGuidelines: [
"Use to understand the team structure and reporting hierarchy",
"Optionally specify a root agent to see only their subtree",
"Ephemeral/runtime agents are excluded by default",
],
parameters: Type.Object({
root_agent_id: Type.Optional(
Type.String({ description: "If provided, show only the subtree rooted at this agent" }),
),
include_ephemeral: Type.Optional(
Type.Boolean({ description: "Include ephemeral/runtime agents (default: false)" }),
),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const { AgentStore } = await import("@fusion/core");
type OrgTreeNode = { agent: { id: string; icon?: string; name: string; role: string; state: string; taskId?: string }; children: OrgTreeNode[] };
const agentStore = new AgentStore({ rootDir: getFusionDir(ctx.cwd) });
await agentStore.init();
const includeEphemeral = params.include_ephemeral ?? false;
// If root_agent_id specified, show subtree via chain-of-command + reports
if (params.root_agent_id) {
const rootAgent = await agentStore.resolveAgent(params.root_agent_id);
if (!rootAgent) {
return {
content: [{ type: "text", text: `Agent '${params.root_agent_id}' not found` }],
isError: true,
details: { error: "Root agent not found" },
};
}
// Get the full tree, then find the subtree
const fullTree = await agentStore.getOrgTree({ includeEphemeral });
// Find the subtree rooted at the specified agent
const findSubtree = (nodes: OrgTreeNode[]): OrgTreeNode | null => {
for (const node of nodes) {
if (node.agent.id === rootAgent.id) return node;
const found = findSubtree(node.children);
if (found) return found;
}
return null;
};
const subtree = findSubtree(fullTree);
if (!subtree) {
// Agent exists but has no tree position — show just that agent
return {
content: [{
type: "text" as const,
text: `${rootAgent.icon ?? "🤖"} ${rootAgent.name} (${rootAgent.role}) — ${rootAgent.state}${rootAgent.taskId ? ` [${rootAgent.taskId}]` : ""}`,
}],
details: { tree: [{ agent: rootAgent, children: [] }] },
};
}
const lines: string[] = [];
const renderNode = (node: OrgTreeNode, indent: string) => {
const a = node.agent;
lines.push(
`${indent}${a.icon ?? "🤖"} ${a.name} (${a.role}) — ${a.state}${a.taskId ? ` [${a.taskId}]` : ""}`,
);
for (const child of node.children) {
renderNode(child, indent + " ");
}
};
renderNode(subtree, "");
return {
content: [{ type: "text" as const, text: `Agent Org Tree (subtree: ${rootAgent.name}):\n${lines.join("\n")}` }],
details: { tree: [subtree] },
};
}
// Full tree
const tree = await agentStore.getOrgTree({ includeEphemeral });
if (tree.length === 0) {
return {
content: [{ type: "text" as const, text: "No agents found." }],
details: { tree: [], count: 0 },
};
}
const lines: string[] = [];
let count = 0;
const renderNode = (node: OrgTreeNode, indent: string) => {
const a = node.agent;
lines.push(
`${indent}${a.icon ?? "🤖"} ${a.name} (${a.role}) — ${a.state}${a.taskId ? ` [${a.taskId}]` : ""}`,
);
count++;
for (const child of node.children) {
renderNode(child, indent + " ");
}
};
for (const root of tree) {
renderNode(root, "");
}
return {
content: [{ type: "text" as const, text: `Agent Org Tree (${count} agents):\n${lines.join("\n")}` }],
details: { tree, count },
};
},
});
// ── fn_skills_search ─────────────────────────────────────────────
pi.registerTool({