feat(FN-1950): add list_agents and delegate_task agent delegation tools
- Add list_agents tool for discovering agents by role, state, or includeEphemeral filter - Add delegate_task tool for assigning work to a specific agent by ID - Add comprehensive test coverage for both delegation tools in agent-tools-delegation.test.ts - Update executor.ts to wire up delegation tools when agentStore is configured - Document both tools in AGENTS.md and docs/agents.md
This commit is contained in:
@@ -1631,14 +1631,16 @@ describe("HeartbeatMonitor", () => {
|
||||
expect(callArgs.cwd).toBe("/tmp/test");
|
||||
expect(callArgs.systemPrompt).toBe(HEARTBEAT_SYSTEM_PROMPT);
|
||||
expect(callArgs.tools).toBe("readonly");
|
||||
// Tools: task_create, task_log, task_document_write, task_document_read, heartbeat_done
|
||||
expect(callArgs.customTools).toHaveLength(5);
|
||||
// Tools: task_create, task_log, task_document_write, task_document_read, list_agents, delegate_task, heartbeat_done
|
||||
expect(callArgs.customTools).toHaveLength(7);
|
||||
expect(callArgs.customTools![0]!.name).toBe("task_create");
|
||||
expect(callArgs.customTools![1]!.name).toBe("task_log");
|
||||
expect(callArgs.customTools![2]!.name).toBe("task_document_write");
|
||||
expect(callArgs.customTools![3]!.name).toBe("task_document_read");
|
||||
expect(callArgs.customTools![4]!.name).toBe("list_agents");
|
||||
expect(callArgs.customTools![5]!.name).toBe("delegate_task");
|
||||
// heartbeat_done is last (terminal tool)
|
||||
expect(callArgs.customTools![4]!.name).toBe("heartbeat_done");
|
||||
expect(callArgs.customTools![6]!.name).toBe("heartbeat_done");
|
||||
});
|
||||
|
||||
it("includes document tools in heartbeat session", async () => {
|
||||
@@ -2388,11 +2390,13 @@ describe("HeartbeatMonitor", () => {
|
||||
|
||||
const tools = monitor.createHeartbeatTools("agent-001", mockTaskStore, "FN-001");
|
||||
|
||||
expect(tools).toHaveLength(4);
|
||||
expect(tools).toHaveLength(6);
|
||||
expect(tools[0]!.name).toBe("task_create");
|
||||
expect(tools[1]!.name).toBe("task_log");
|
||||
expect(tools[2]!.name).toBe("task_document_write");
|
||||
expect(tools[3]!.name).toBe("task_document_read");
|
||||
expect(tools[4]!.name).toBe("list_agents");
|
||||
expect(tools[5]!.name).toBe("delegate_task");
|
||||
});
|
||||
|
||||
it("task_create tool creates a task in triage via TaskStore", async () => {
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import type { AgentStore, AgentHeartbeatRun, HeartbeatInvocationSource, AgentHeartbeatConfig, AgentBudgetStatus, Message, MessageStore, TaskStore, TaskDetail, AgentRole, Agent, InboxTask, BlockedStateSnapshot, RunMutationContext } from "@fusion/core";
|
||||
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
||||
import { Type, type Static } from "@mariozechner/pi-ai";
|
||||
import { createTaskCreateTool, createTaskLogToolWithContext, createTaskDocumentWriteTool, createTaskDocumentReadTool, taskCreateParams } from "./agent-tools.js";
|
||||
import { createTaskCreateTool, createTaskLogToolWithContext, createTaskDocumentWriteTool, createTaskDocumentReadTool, createListAgentsTool, createDelegateTaskTool, taskCreateParams } from "./agent-tools.js";
|
||||
import { AgentLogger } from "./agent-logger.js";
|
||||
import { heartbeatLog } from "./logger.js";
|
||||
import { createRunAuditor, type EngineRunContext } from "./run-audit.js";
|
||||
@@ -1129,6 +1129,10 @@ export class HeartbeatMonitor {
|
||||
tools.push(createTaskDocumentWriteTool(taskStore, taskId));
|
||||
tools.push(createTaskDocumentReadTool(taskStore, taskId));
|
||||
|
||||
// Agent delegation tools — discover and delegate work to other agents
|
||||
tools.push(createListAgentsTool(this.store));
|
||||
tools.push(createDelegateTaskTool(this.store, taskStore));
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
|
||||
318
packages/engine/src/agent-tools-delegation.test.ts
Normal file
318
packages/engine/src/agent-tools-delegation.test.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import type { Agent, AgentStore, TaskStore, Task } from "@fusion/core";
|
||||
import { createListAgentsTool, createDelegateTaskTool } from "./agent-tools.js";
|
||||
|
||||
function createMockAgentStore(overrides: Partial<AgentStore> = {}): AgentStore {
|
||||
return {
|
||||
listAgents: vi.fn().mockResolvedValue([]),
|
||||
getAgent: vi.fn().mockResolvedValue(null),
|
||||
...overrides,
|
||||
} as unknown as AgentStore;
|
||||
}
|
||||
|
||||
function createMockTaskStore(overrides: Partial<TaskStore> = {}): TaskStore {
|
||||
return {
|
||||
createTask: vi.fn().mockResolvedValue({
|
||||
id: "FN-001",
|
||||
description: "",
|
||||
dependencies: [],
|
||||
column: "triage" as const,
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
}),
|
||||
...overrides,
|
||||
} as unknown as TaskStore;
|
||||
}
|
||||
|
||||
function createAgent(overrides: Partial<Agent> = {}): Agent {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
id: "agent-001",
|
||||
name: "Test Agent",
|
||||
role: "executor",
|
||||
state: "idle",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
metadata: {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("createListAgentsTool", () => {
|
||||
let agentStore: AgentStore;
|
||||
|
||||
beforeEach(() => {
|
||||
agentStore = createMockAgentStore();
|
||||
});
|
||||
|
||||
it("returns formatted list of agents with their details", async () => {
|
||||
const agents = [
|
||||
createAgent({ id: "agent-001", name: "Alice", role: "executor", state: "idle", taskId: undefined }),
|
||||
createAgent({ id: "agent-002", name: "Bob", role: "reviewer", state: "running", taskId: "FN-100" }),
|
||||
];
|
||||
vi.mocked(agentStore.listAgents).mockResolvedValue(agents);
|
||||
|
||||
const tool = createListAgentsTool(agentStore);
|
||||
const result = await tool.execute("session-1", {}, undefined as any, undefined as any, undefined as any);
|
||||
|
||||
expect(result.content[0]).toHaveProperty("text");
|
||||
const text = (result.content[0] as { text: string }).text;
|
||||
expect(text).toContain("Available agents:");
|
||||
expect(text).toContain("ID: agent-001");
|
||||
expect(text).toContain("Name: Alice");
|
||||
expect(text).toContain("Role: executor");
|
||||
expect(text).toContain("State: idle");
|
||||
expect(text).toContain("ID: agent-002");
|
||||
expect(text).toContain("Name: Bob");
|
||||
expect(text).toContain("Role: reviewer");
|
||||
expect(text).toContain("State: running");
|
||||
expect(text).toContain("Current Task: FN-100");
|
||||
});
|
||||
|
||||
it("includes soul truncated to 200 chars when present", async () => {
|
||||
const longSoul = "A".repeat(300);
|
||||
const agent = createAgent({ id: "agent-001", soul: longSoul });
|
||||
vi.mocked(agentStore.listAgents).mockResolvedValue([agent]);
|
||||
|
||||
const tool = createListAgentsTool(agentStore);
|
||||
const result = await tool.execute("session-1", {}, undefined as any, undefined as any, undefined as any);
|
||||
|
||||
const text = (result.content[0] as { text: string }).text;
|
||||
expect(text).toContain("Soul: " + "A".repeat(200));
|
||||
expect(text).not.toContain("Soul: " + "A".repeat(201));
|
||||
});
|
||||
|
||||
it("includes title when present", async () => {
|
||||
const agent = createAgent({ id: "agent-001", title: "Senior Engineer" });
|
||||
vi.mocked(agentStore.listAgents).mockResolvedValue([agent]);
|
||||
|
||||
const tool = createListAgentsTool(agentStore);
|
||||
const result = await tool.execute("session-1", {}, undefined as any, undefined as any, undefined as any);
|
||||
|
||||
const text = (result.content[0] as { text: string }).text;
|
||||
expect(text).toContain("Title: Senior Engineer");
|
||||
});
|
||||
|
||||
it("includes instructionsText summary when present", async () => {
|
||||
const agent = createAgent({ id: "agent-001", instructionsText: "Be thorough and check edge cases." });
|
||||
vi.mocked(agentStore.listAgents).mockResolvedValue([agent]);
|
||||
|
||||
const tool = createListAgentsTool(agentStore);
|
||||
const result = await tool.execute("session-1", {}, undefined as any, undefined as any, undefined as any);
|
||||
|
||||
const text = (result.content[0] as { text: string }).text;
|
||||
expect(text).toContain("Custom Instructions: Be thorough and check edge cases.");
|
||||
});
|
||||
|
||||
it("includes instructionsText truncated to 100 chars with ellipsis", async () => {
|
||||
const longInstructions = "X".repeat(150);
|
||||
const agent = createAgent({ id: "agent-001", instructionsText: longInstructions });
|
||||
vi.mocked(agentStore.listAgents).mockResolvedValue([agent]);
|
||||
|
||||
const tool = createListAgentsTool(agentStore);
|
||||
const result = await tool.execute("session-1", {}, undefined as any, undefined as any, undefined as any);
|
||||
|
||||
const text = (result.content[0] as { text: string }).text;
|
||||
expect(text).toContain("Custom Instructions: " + "X".repeat(100) + "…");
|
||||
expect(text).not.toContain("Custom Instructions: " + "X".repeat(101));
|
||||
});
|
||||
|
||||
it("filters by role when provided", async () => {
|
||||
const tool = createListAgentsTool(agentStore);
|
||||
await tool.execute("session-1", { role: "executor" }, undefined as any, undefined as any, undefined as any);
|
||||
|
||||
expect(agentStore.listAgents).toHaveBeenCalledWith({ role: "executor" });
|
||||
});
|
||||
|
||||
it("filters by state when provided", async () => {
|
||||
const tool = createListAgentsTool(agentStore);
|
||||
await tool.execute("session-1", { state: "idle" }, undefined as any, undefined as any, undefined as any);
|
||||
|
||||
expect(agentStore.listAgents).toHaveBeenCalledWith({ state: "idle" });
|
||||
});
|
||||
|
||||
it("passes includeEphemeral when provided", async () => {
|
||||
const tool = createListAgentsTool(agentStore);
|
||||
await tool.execute("session-1", { includeEphemeral: true }, undefined as any, undefined as any, undefined as any);
|
||||
|
||||
expect(agentStore.listAgents).toHaveBeenCalledWith({ includeEphemeral: true });
|
||||
});
|
||||
|
||||
it("returns no-agents message when list is empty", async () => {
|
||||
vi.mocked(agentStore.listAgents).mockResolvedValue([]);
|
||||
|
||||
const tool = createListAgentsTool(agentStore);
|
||||
const result = await tool.execute("session-1", {}, undefined as any, undefined as any, undefined as any);
|
||||
|
||||
const text = (result.content[0] as { text: string }).text;
|
||||
expect(text).toContain("No agents found matching the specified filters.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createDelegateTaskTool", () => {
|
||||
let agentStore: AgentStore;
|
||||
let taskStore: TaskStore;
|
||||
|
||||
beforeEach(() => {
|
||||
agentStore = createMockAgentStore();
|
||||
taskStore = createMockTaskStore();
|
||||
});
|
||||
|
||||
it("creates task with correct assignedAgentId, column todo, and description", async () => {
|
||||
const agent = createAgent({ id: "agent-001", name: "Bob" });
|
||||
vi.mocked(agentStore.getAgent).mockResolvedValue(agent);
|
||||
vi.mocked(taskStore.createTask).mockResolvedValue({
|
||||
id: "FN-050",
|
||||
description: "Write tests",
|
||||
dependencies: [],
|
||||
column: "todo" as const,
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
|
||||
const tool = createDelegateTaskTool(agentStore, taskStore);
|
||||
const result = await tool.execute("session-1", {
|
||||
agent_id: "agent-001",
|
||||
description: "Write tests",
|
||||
}, undefined as any, undefined as any, undefined as any);
|
||||
|
||||
expect(taskStore.createTask).toHaveBeenCalledWith({
|
||||
description: "Write tests",
|
||||
dependencies: undefined,
|
||||
column: "todo",
|
||||
assignedAgentId: "agent-001",
|
||||
});
|
||||
|
||||
const text = (result.content[0] as { text: string }).text;
|
||||
expect(text).toContain("Delegated to Bob (agent-001)");
|
||||
expect(text).toContain("Created FN-050");
|
||||
expect(text).toContain("picked up by Bob on their next heartbeat cycle");
|
||||
});
|
||||
|
||||
it("returns success message with task ID and agent name", async () => {
|
||||
const agent = createAgent({ id: "agent-001", name: "Bob" });
|
||||
vi.mocked(agentStore.getAgent).mockResolvedValue(agent);
|
||||
vi.mocked(taskStore.createTask).mockResolvedValue({
|
||||
id: "FN-051",
|
||||
description: "Write tests",
|
||||
dependencies: [],
|
||||
column: "todo" as const,
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
|
||||
const tool = createDelegateTaskTool(agentStore, taskStore);
|
||||
const result = await tool.execute("session-1", {
|
||||
agent_id: "agent-001",
|
||||
description: "Write tests",
|
||||
}, undefined as any, undefined as any, undefined as any);
|
||||
|
||||
const text = (result.content[0] as { text: string }).text;
|
||||
expect(text).toContain("FN-051");
|
||||
expect(text).toContain("Bob");
|
||||
expect(result.details).toEqual({ taskId: "FN-051", agentId: "agent-001", agentName: "Bob" });
|
||||
});
|
||||
|
||||
it("returns error when target agent not found", async () => {
|
||||
vi.mocked(agentStore.getAgent).mockResolvedValue(null);
|
||||
|
||||
const tool = createDelegateTaskTool(agentStore, taskStore);
|
||||
const result = await tool.execute("session-1", {
|
||||
agent_id: "nonexistent-agent",
|
||||
description: "Do something",
|
||||
}, undefined as any, undefined as any, undefined as any);
|
||||
|
||||
const text = (result.content[0] as { text: string }).text;
|
||||
expect(text).toContain("ERROR: Agent nonexistent-agent not found");
|
||||
expect(taskStore.createTask).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns error when target agent is ephemeral", async () => {
|
||||
const ephemeralAgent = createAgent({
|
||||
id: "executor-FN-100",
|
||||
metadata: { agentKind: "task-worker" },
|
||||
});
|
||||
vi.mocked(agentStore.getAgent).mockResolvedValue(ephemeralAgent);
|
||||
|
||||
const tool = createDelegateTaskTool(agentStore, taskStore);
|
||||
const result = await tool.execute("session-1", {
|
||||
agent_id: "executor-FN-100",
|
||||
description: "Do something",
|
||||
}, undefined as any, undefined as any, undefined as any);
|
||||
|
||||
const text = (result.content[0] as { text: string }).text;
|
||||
expect(text).toContain("ERROR: Cannot delegate to ephemeral/runtime agent executor-FN-100");
|
||||
expect(taskStore.createTask).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes dependencies through to task creation", async () => {
|
||||
const agent = createAgent({ id: "agent-001", name: "Bob" });
|
||||
vi.mocked(agentStore.getAgent).mockResolvedValue(agent);
|
||||
vi.mocked(taskStore.createTask).mockResolvedValue({
|
||||
id: "FN-052",
|
||||
description: "Integration test",
|
||||
dependencies: ["FN-010"],
|
||||
column: "todo" as const,
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
|
||||
const tool = createDelegateTaskTool(agentStore, taskStore);
|
||||
const result = await tool.execute("session-1", {
|
||||
agent_id: "agent-001",
|
||||
description: "Integration test",
|
||||
dependencies: ["FN-010"],
|
||||
}, undefined as any, undefined as any, undefined as any);
|
||||
|
||||
expect(taskStore.createTask).toHaveBeenCalledWith({
|
||||
description: "Integration test",
|
||||
dependencies: ["FN-010"],
|
||||
column: "todo",
|
||||
assignedAgentId: "agent-001",
|
||||
});
|
||||
|
||||
const text = (result.content[0] as { text: string }).text;
|
||||
expect(text).toContain("depends on: FN-010");
|
||||
});
|
||||
|
||||
it("creates task without dependencies when none specified", async () => {
|
||||
const agent = createAgent({ id: "agent-001", name: "Bob" });
|
||||
vi.mocked(agentStore.getAgent).mockResolvedValue(agent);
|
||||
vi.mocked(taskStore.createTask).mockResolvedValue({
|
||||
id: "FN-053",
|
||||
description: "Simple task",
|
||||
dependencies: [],
|
||||
column: "todo" as const,
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
});
|
||||
|
||||
const tool = createDelegateTaskTool(agentStore, taskStore);
|
||||
const result = await tool.execute("session-1", {
|
||||
agent_id: "agent-001",
|
||||
description: "Simple task",
|
||||
}, undefined as any, undefined as any, undefined as any);
|
||||
|
||||
expect(taskStore.createTask).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ dependencies: undefined }),
|
||||
);
|
||||
|
||||
const text = (result.content[0] as { text: string }).text;
|
||||
expect(text).not.toContain("depends on:");
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,8 @@
|
||||
* The parameter schemas are canonical here — executor.ts imports and reuses them.
|
||||
*/
|
||||
|
||||
import type { TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext } from "@fusion/core";
|
||||
import type { AgentStore, AgentState, AgentCapability, TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext } from "@fusion/core";
|
||||
import { isEphemeralAgent } from "@fusion/core";
|
||||
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
||||
import { Type, type Static } from "@mariozechner/pi-ai";
|
||||
import type { AgentReflectionService } from "./agent-reflection.js";
|
||||
@@ -46,6 +47,26 @@ export const reflectOnPerformanceParams = Type.Object({
|
||||
),
|
||||
});
|
||||
|
||||
export const listAgentsParams = 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)" }),
|
||||
),
|
||||
});
|
||||
|
||||
export const delegateTaskParams = 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\"])" }),
|
||||
),
|
||||
});
|
||||
|
||||
// ── Tool factory functions ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -293,3 +314,115 @@ export function createReflectOnPerformanceTool(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a `list_agents` tool that lists all available agents.
|
||||
*
|
||||
* @param agentStore - AgentStore for agent discovery
|
||||
* @returns ToolDefinition for the `list_agents` tool
|
||||
*/
|
||||
export function createListAgentsTool(agentStore: AgentStore): ToolDefinition {
|
||||
return {
|
||||
name: "list_agents",
|
||||
label: "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.",
|
||||
parameters: listAgentsParams,
|
||||
execute: async (_id: string, params: Static<typeof listAgentsParams>) => {
|
||||
const filter: { role?: AgentCapability; state?: AgentState; includeEphemeral?: boolean } = {};
|
||||
if (params.role) filter.role = params.role as AgentCapability;
|
||||
if (params.state) filter.state = params.state as AgentState;
|
||||
if (params.includeEphemeral !== undefined) filter.includeEphemeral = params.includeEphemeral;
|
||||
|
||||
const agents = await agentStore.listAgents(filter);
|
||||
|
||||
if (agents.length === 0) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: "No agents found matching the specified filters." }],
|
||||
details: {},
|
||||
};
|
||||
}
|
||||
|
||||
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:\n\n${lines.join("\n\n")}` }],
|
||||
details: { agents },
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a `delegate_task` tool that creates and assigns a task to a specific agent.
|
||||
*
|
||||
* @param agentStore - AgentStore for agent lookup
|
||||
* @param taskStore - TaskStore for task creation
|
||||
* @returns ToolDefinition for the `delegate_task` tool
|
||||
*/
|
||||
export function createDelegateTaskTool(agentStore: AgentStore, taskStore: TaskStore): ToolDefinition {
|
||||
return {
|
||||
name: "delegate_task",
|
||||
label: "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 list_agents first to find available agents and their capabilities.",
|
||||
parameters: delegateTaskParams,
|
||||
execute: async (_id: string, params: Static<typeof delegateTaskParams>) => {
|
||||
// Validate target agent exists
|
||||
const agent = await agentStore.getAgent(params.agent_id);
|
||||
if (!agent) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `ERROR: Agent ${params.agent_id} not found` }],
|
||||
details: {},
|
||||
};
|
||||
}
|
||||
|
||||
// Validate target agent is not ephemeral
|
||||
if (isEphemeralAgent(agent)) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `ERROR: Cannot delegate to ephemeral/runtime agent ${params.agent_id}` }],
|
||||
details: {},
|
||||
};
|
||||
}
|
||||
|
||||
// Create task assigned to the target agent
|
||||
const task = await taskStore.createTask({
|
||||
description: params.description,
|
||||
dependencies: params.dependencies,
|
||||
column: "todo",
|
||||
assignedAgentId: params.agent_id,
|
||||
});
|
||||
|
||||
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 },
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ import type { AgentReflectionService } from "./agent-reflection.js";
|
||||
import { createRunAuditor, generateSyntheticRunId, type EngineRunContext } from "./run-audit.js";
|
||||
import { evaluateSpecStaleness, getPromptPath } from "./spec-staleness.js";
|
||||
import {
|
||||
createDelegateTaskTool,
|
||||
createListAgentsTool,
|
||||
createReflectOnPerformanceTool,
|
||||
createTaskCreateTool as sharedCreateTaskCreateTool,
|
||||
createTaskDocumentReadTool as sharedCreateTaskDocumentReadTool,
|
||||
@@ -43,10 +45,14 @@ import { getTaskCompletionBlockerForStore } from "./task-completion.js";
|
||||
// Re-export for backward compatibility (tests import from executor.ts)
|
||||
export { summarizeToolArgs } from "./agent-logger.js";
|
||||
export {
|
||||
createDelegateTaskTool,
|
||||
createListAgentsTool,
|
||||
createTaskCreateTool,
|
||||
createTaskDocumentReadTool,
|
||||
createTaskDocumentWriteTool,
|
||||
createTaskLogTool,
|
||||
delegateTaskParams,
|
||||
listAgentsParams,
|
||||
taskCreateParams,
|
||||
taskLogParams,
|
||||
} from "./agent-tools.js";
|
||||
@@ -1404,6 +1410,11 @@ export class TaskExecutor {
|
||||
this.createTaskDocumentReadTool(task.id),
|
||||
// Conditionally add agent self-reflection when enabled and task has an assigned agent.
|
||||
...reflectionTools,
|
||||
// Agent delegation tools — discover and delegate work to other agents.
|
||||
...(this.options.agentStore ? [
|
||||
createListAgentsTool(this.options.agentStore),
|
||||
createDelegateTaskTool(this.options.agentStore, this.store),
|
||||
] : []),
|
||||
// Add plugin tools from PluginRunner
|
||||
...(this.options.pluginRunner?.getPluginTools() ?? []),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user