feat(FN-3777): add create_agent and delete_agent tools

Adds agent creation and deletion tools to the pi extension, with engine-side implementation in `agent-tools.ts`, core store integration for agent lifecycle management, and corresponding tests and documentation updates across the workspace.

Fusion-Task-Id: FN-3777
This commit is contained in:
Fusion
2026-05-08 19:02:59 -07:00
committed by gsxdsm
parent e04af96d9c
commit 3a91534f98
18 changed files with 352 additions and 19 deletions

View File

@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Agent, AgentStore } from "@fusion/core";
import { createGetAgentConfigTool, createUpdateAgentConfigTool } from "../agent-tools.js";
import { createAgentCreateTool, createAgentDeleteTool, createGetAgentConfigTool, createUpdateAgentConfigTool } from "../agent-tools.js";
function createAgent(overrides: Partial<Agent> = {}): Agent {
const now = new Date().toISOString();
@@ -19,7 +19,10 @@ function createAgent(overrides: Partial<Agent> = {}): Agent {
function createMockAgentStore(overrides: Partial<AgentStore> = {}): AgentStore {
return {
getAgent: vi.fn().mockResolvedValue(null),
createAgent: vi.fn(),
deleteAgent: vi.fn(),
updateAgent: vi.fn(),
updateAgentState: vi.fn(),
...overrides,
} as unknown as AgentStore;
}
@@ -229,3 +232,44 @@ describe("createUpdateAgentConfigTool", () => {
expect(schema.properties.heartbeat_timeout_ms?.minimum).toBe(5000);
});
});
describe("agent lifecycle tools", () => {
it("create tool allows direct-report creation", async () => {
const manager = createAgent({ id: "manager-1", reportsTo: "ceo-1" });
const created = createAgent({ id: "report-1", reportsTo: "manager-1", name: "Report" });
const agentStore = createMockAgentStore();
vi.mocked(agentStore.getAgent).mockResolvedValue(manager);
vi.mocked(agentStore.createAgent).mockResolvedValue(created);
const tool = createAgentCreateTool(agentStore, "manager-1");
const result = await tool.execute("session", { name: "Report", role: "executor" }, undefined as never, undefined as never, undefined as never);
expect((result.content[0] as { text: string }).text).toContain("Created agent Report (report-1)");
expect(agentStore.createAgent).toHaveBeenCalledWith(expect.objectContaining({ reportsTo: "manager-1" }));
});
it("create tool blocks non-privileged cross-manager create", async () => {
const manager = createAgent({ id: "manager-1", reportsTo: "ceo-1" });
const agentStore = createMockAgentStore();
vi.mocked(agentStore.getAgent).mockResolvedValue(manager);
const tool = createAgentCreateTool(agentStore, "manager-1");
const result = await tool.execute("session", { name: "Report", role: "executor", reportsTo: "other" }, undefined as never, undefined as never, undefined as never);
expect((result.content[0] as { text: string }).text).toContain("ERROR: You can only create agents that report to you");
expect(agentStore.createAgent).not.toHaveBeenCalled();
});
it("delete tool blocks non-direct report delete", async () => {
const manager = createAgent({ id: "manager-1", reportsTo: "ceo-1" });
const target = createAgent({ id: "report-1", reportsTo: "other" });
const agentStore = createMockAgentStore();
vi.mocked(agentStore.getAgent).mockResolvedValueOnce(manager).mockResolvedValueOnce(target);
const tool = createAgentDeleteTool(agentStore, "manager-1");
const result = await tool.execute("session", { agent_id: "report-1" }, undefined as never, undefined as never, undefined as never);
expect((result.content[0] as { text: string }).text).toContain("ERROR: You can only delete agents that report to you");
expect(agentStore.deleteAgent).not.toHaveBeenCalled();
});
});

View File

@@ -1919,9 +1919,9 @@ describe("executeHeartbeat", () => {
expect(callArgs.systemPrompt).toContain("fn_task_log");
expect(callArgs.systemPrompt).toContain("fn_task_document_write");
expect(callArgs.tools).toBe("coding");
// Tools: fn_task_create, fn_task_log, fn_task_document_write, fn_task_document_read, fn_list_agents, fn_delegate_task,
// fn_get_agent_config, fn_update_agent_config, fn_read_evaluations, fn_update_identity, fn_web_fetch, fn_memory_search, fn_memory_get, fn_memory_append, fn_heartbeat_done
expect(callArgs.customTools).toHaveLength(15);
// fn_get_agent_config, fn_update_agent_config, fn_agent_create, fn_agent_delete, fn_read_evaluations, fn_update_identity,
// fn_web_fetch, fn_memory_search, fn_memory_get, fn_memory_append, fn_heartbeat_done
expect(callArgs.customTools).toHaveLength(17);
expect(callArgs.customTools![0]!.name).toBe("fn_task_create");
expect(callArgs.customTools![1]!.name).toBe("fn_task_log");
expect(callArgs.customTools![2]!.name).toBe("fn_task_document_write");
@@ -1930,14 +1930,16 @@ describe("executeHeartbeat", () => {
expect(callArgs.customTools![5]!.name).toBe("fn_delegate_task");
expect(callArgs.customTools![6]!.name).toBe("fn_get_agent_config");
expect(callArgs.customTools![7]!.name).toBe("fn_update_agent_config");
expect(callArgs.customTools![8]!.name).toBe("fn_read_evaluations");
expect(callArgs.customTools![9]!.name).toBe("fn_update_identity");
expect(callArgs.customTools![10]!.name).toBe("fn_web_fetch");
expect(callArgs.customTools![11]!.name).toBe("fn_memory_search");
expect(callArgs.customTools![12]!.name).toBe("fn_memory_get");
expect(callArgs.customTools![13]!.name).toBe("fn_memory_append");
expect(callArgs.customTools![8]!.name).toBe("fn_agent_create");
expect(callArgs.customTools![9]!.name).toBe("fn_agent_delete");
expect(callArgs.customTools![10]!.name).toBe("fn_read_evaluations");
expect(callArgs.customTools![11]!.name).toBe("fn_update_identity");
expect(callArgs.customTools![12]!.name).toBe("fn_web_fetch");
expect(callArgs.customTools![13]!.name).toBe("fn_memory_search");
expect(callArgs.customTools![14]!.name).toBe("fn_memory_get");
expect(callArgs.customTools![15]!.name).toBe("fn_memory_append");
// fn_heartbeat_done is last (terminal tool)
expect(callArgs.customTools![14]!.name).toBe("fn_heartbeat_done");
expect(callArgs.customTools![16]!.name).toBe("fn_heartbeat_done");
});
it("loads workspace memory into system prompt and identity snapshot when inline memory is empty", async () => {

View File

@@ -106,7 +106,7 @@ describe("createHeartbeatTools", () => {
const tools = monitor.createHeartbeatTools("agent-001", mockTaskStore, "FN-001");
expect(tools).toHaveLength(10);
expect(tools).toHaveLength(12);
expect(tools[0]!.name).toBe("fn_task_create");
expect(tools[1]!.name).toBe("fn_task_log");
expect(tools[2]!.name).toBe("fn_task_document_write");
@@ -115,8 +115,10 @@ describe("createHeartbeatTools", () => {
expect(tools[5]!.name).toBe("fn_delegate_task");
expect(tools[6]!.name).toBe("fn_get_agent_config");
expect(tools[7]!.name).toBe("fn_update_agent_config");
expect(tools[8]!.name).toBe("fn_read_evaluations");
expect(tools[9]!.name).toBe("fn_update_identity");
expect(tools[8]!.name).toBe("fn_agent_create");
expect(tools[9]!.name).toBe("fn_agent_delete");
expect(tools[10]!.name).toBe("fn_read_evaluations");
expect(tools[11]!.name).toBe("fn_update_identity");
});
it("fn_task_create tool creates a task in triage via TaskStore", async () => {