feat(FN-3957): add delegate_task override flag to bypass executor role poli

Adds a `delegate_override` flag to the `delegate_task` tool that allows executor-role tasks to be assigned to non-executor agents. The override is persisted as metadata in the extension and honored by the heartbeat inbox guard, with test coverage across engine, core, and CLI packages plus updated do

Fusion-Task-Id: FN-3957
This commit is contained in:
Fusion
2026-05-10 19:19:15 -07:00
committed by gsxdsm
parent a5a794d12e
commit a0c7c3315a
12 changed files with 160 additions and 10 deletions

View File

@@ -256,6 +256,49 @@ describe("createDelegateTaskTool", () => {
expect(taskStore.createTask).not.toHaveBeenCalled();
});
it("rejects non-executor target without override", async () => {
const reviewer = createAgent({ id: "agent-002", name: "Rita", role: "reviewer" });
vi.mocked(agentStore.getAgent).mockResolvedValue(reviewer);
const tool = createDelegateTaskTool(agentStore, taskStore);
const result = await tool.execute("session-1", {
agent_id: "agent-002",
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 agent-002 has role \"reviewer\"");
expect(text).toContain("Pass override=true to bypass");
expect(taskStore.createTask).not.toHaveBeenCalled();
});
it("allows non-executor target with override", async () => {
const reviewer = createAgent({ id: "agent-002", name: "Rita", role: "reviewer" });
vi.mocked(agentStore.getAgent).mockResolvedValue(reviewer);
vi.mocked(taskStore.createTask).mockResolvedValue({
id: "FN-054",
description: "Do something",
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);
await tool.execute("session-1", {
agent_id: "agent-002",
description: "Do something",
override: true,
}, undefined as any, undefined as any, undefined as any);
expect(taskStore.createTask).toHaveBeenCalledWith(expect.objectContaining({
source: { sourceType: "api", sourceMetadata: { executorRoleOverride: true } },
}), expect.objectContaining({ settings: { autoSummarizeTitles: false } }));
});
it("passes dependencies through to task creation", async () => {
const agent = createAgent({ id: "agent-001", name: "Bob" });
vi.mocked(agentStore.getAgent).mockResolvedValue(agent);

View File

@@ -164,6 +164,23 @@ describe("createDelegateTaskTool", () => {
}));
});
it("forwards override=true marker in source metadata", async () => {
const agentStore = {
getAgent: vi.fn().mockResolvedValue({ id: "agent-2", name: "Planner", role: "triage", state: "idle" }),
};
const taskStore = {
getSettings: vi.fn().mockResolvedValue({ autoSummarizeTitles: false }),
createTask: vi.fn().mockResolvedValue({ id: "FN-102", dependencies: [], description: "Delegated" }),
};
const tool = createDelegateTaskTool(agentStore as any, taskStore as any);
await tool.execute("call-1", { agent_id: "agent-2", description: "Delegated", override: true } as any, undefined, undefined, {} as any);
expect(taskStore.createTask).toHaveBeenCalledWith(expect.objectContaining({
source: { sourceType: "api", sourceMetadata: { executorRoleOverride: true } },
}), expect.any(Object));
});
it("wires title summarization callback when rootDir is provided", async () => {
const summarizeSpy = vi.spyOn(core, "summarizeTitle").mockResolvedValue("Short title");
const agentStore = {

View File

@@ -1707,7 +1707,11 @@ describe("executeHeartbeat", () => {
});
describe("executeHeartbeat - inbox selection", () => {
const makeInboxSelection = (taskId: string, priority: "in_progress" | "todo" | "blocked" = "todo") => {
const makeInboxSelection = (
taskId: string,
priority: "in_progress" | "todo" | "blocked" = "todo",
sourceMetadata?: Record<string, unknown>,
) => {
const now = new Date().toISOString();
return {
task: {
@@ -1720,6 +1724,7 @@ describe("executeHeartbeat", () => {
log: [],
createdAt: now,
updatedAt: now,
...(sourceMetadata ? { sourceMetadata } : {}),
},
priority,
reason: `selected:${priority}`,
@@ -1791,6 +1796,40 @@ describe("executeHeartbeat", () => {
expect(mockTaskStore.getTask).toHaveBeenCalledWith("FN-EXISTING");
});
it("allows non-executor inbox selection when override metadata is present", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined, role: "reviewer" });
const selectNextTaskForAgent = vi.fn().mockResolvedValue(
makeInboxSelection("FN-INBOX", "todo", { executorRoleOverride: true }),
);
mockTaskStore = createMockTaskStore({
selectNextTaskForAgent,
getTask: vi.fn().mockResolvedValue({
id: "FN-INBOX",
title: "Inbox Task",
description: "Inbox-selected task",
prompt: "",
steps: [],
column: "todo",
dependencies: [],
log: [],
attachments: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
sourceMetadata: { executorRoleOverride: true },
} as unknown as TaskDetail),
});
const mockSession = createMockAgentSession();
mockedCreateFnAgent.mockResolvedValue({ session: mockSession as any });
const monitor = new HeartbeatMonitor({ store, taskStore: mockTaskStore, rootDir: "/tmp" });
const result = await monitor.executeHeartbeat({ agentId: "agent-001", source: "on_demand" });
expect(selectNextTaskForAgent).toHaveBeenCalledWith("agent-001", { id: "agent-001", role: "reviewer" });
expect(store.assignTask).toHaveBeenCalledWith("agent-001", "FN-INBOX", expect.objectContaining({ agentId: "agent-001" }));
expect(result.resultJson).toEqual(expect.objectContaining({ reason: "inbox_selected", taskId: "FN-INBOX" }));
});
it("when inbox returns null, heartbeat completes with no_assignment", async () => {
const store = createStoreWithAgentForExec({ taskId: undefined });
const selectNextTaskForAgent = vi.fn().mockResolvedValue(null);

View File

@@ -1491,10 +1491,13 @@ export class HeartbeatMonitor {
if (!taskId) {
inboxSelection = await taskStore.selectNextTaskForAgent(agentId, { id: agent.id, role: agent.role });
if (inboxSelection && !canAgentTakeImplementationTask(agent, inboxSelection.task)) {
heartbeatLog.log(
`Agent ${agentId} (role=${agent.role}) skipped inbox-selected task ${inboxSelection.task.id} due to executor-role assignment policy`,
);
inboxSelection = null;
const hasRoleOverride = inboxSelection.task.sourceMetadata?.executorRoleOverride === true;
if (!hasRoleOverride) {
heartbeatLog.log(
`Agent ${agentId} (role=${agent.role}) skipped inbox-selected task ${inboxSelection.task.id} due to executor-role assignment policy`,
);
inboxSelection = null;
}
}
if (inboxSelection) {
taskId = inboxSelection.task.id;

View File

@@ -12,7 +12,7 @@ import { existsSync } from "node:fs";
import { createHash } from "node:crypto";
import { join, relative, resolve } from "node:path";
import type { AgentStore, AgentState, AgentCapability, AgentUpdateInput, TaskDocument, TaskDocumentCreateInput, TaskStore, RunMutationContext, MessageStore, Message, SourceType, Settings, ResearchRun, ResearchRunStatus, TaskCreateInput, ReflectionStore, ApprovalRequestStore, ProjectSettings } from "@fusion/core";
import { DASHBOARD_USER_ID, dailyMemoryPath, ensureOpenClawMemoryFiles, extractAgentProvisioningRequest, getMemoryBackendCapabilities, getProjectMemory, isEphemeralAgent, memoryLongTermPath, normalizeMessageParticipant, resolveAgentProvisioningPolicy, resolveMemoryBackend, resolveResearchSettings, resolveTitleSummarizerSettingsModel, scheduleQmdProjectMemoryRefresh, searchProjectMemory, shouldSkipBackgroundQmdRefresh, summarizeTitle } from "@fusion/core";
import { DASHBOARD_USER_ID, canAgentTakeImplementationTask, dailyMemoryPath, ensureOpenClawMemoryFiles, extractAgentProvisioningRequest, formatRoleMismatchReason, getMemoryBackendCapabilities, getProjectMemory, isEphemeralAgent, memoryLongTermPath, normalizeMessageParticipant, resolveAgentProvisioningPolicy, resolveMemoryBackend, resolveResearchSettings, resolveTitleSummarizerSettingsModel, scheduleQmdProjectMemoryRefresh, searchProjectMemory, shouldSkipBackgroundQmdRefresh, summarizeTitle } from "@fusion/core";
import { ResearchOrchestrator } from "./research-orchestrator.js";
import { ResearchProviderRegistry } from "./research/provider-registry.js";
import { ResearchStepRunner } from "./research-step-runner.js";
@@ -83,6 +83,7 @@ export const delegateTaskParams = Type.Object({
dependencies: Type.Optional(
Type.Array(Type.String(), { description: "Task IDs this new task depends on (e.g. [\"KB-001\"])" }),
),
override: Type.Optional(Type.Boolean({ description: "Set true to bypass executor-role assignment policy" })),
});
export const getAgentConfigParams = Type.Object({
@@ -1732,13 +1733,25 @@ export function createDelegateTaskTool(
};
}
const override = params.override === true;
const newTaskRef = { id: "<new>", column: "todo" } as const;
if (!override && !canAgentTakeImplementationTask(agent, { column: newTaskRef.column })) {
return {
content: [{ type: "text" as const, text: `ERROR: ${formatRoleMismatchReason(agent, newTaskRef)}` }],
details: {},
};
}
// Create task assigned to the target agent
const task = await createAgentTask(taskStore, {
description: params.description,
dependencies: params.dependencies,
column: "todo",
assignedAgentId: params.agent_id,
source: { sourceType: "api" },
source: {
sourceType: "api",
...(override ? { sourceMetadata: { executorRoleOverride: true } } : {}),
},
}, options);
const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";