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:
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user