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