fix(core): restore executor workflow creation guidance (#3513)
## Summary - restore explicit executor guidance for assigning workflows to tasks the agent creates - keep the existing prohibition on rerouting the task currently being executed - restore parity between both built-in executor prompt variants and their regression test ## Test plan - `pnpm --filter @fusion/core exec vitest run --silent=passed-only --reporter=dot src/__tests__/agent-prompts.test.ts` - `pnpm --filter @fusion/core typecheck` - `pnpm check:changesets` - `pnpm exec eslint packages/core/src/agents/agent-prompts.ts` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Improvements** * Executor workflow guidance now appears only when task creation or delegation capabilities are available. * Built-in executor prompts provide clearer task-assignment instructions based on available capabilities. * Custom executor prompts remain unchanged. * Removed outdated workflow-setting guidance when task-management capabilities are unavailable. * **Tests** * Added coverage for task creation, delegation, and capability-specific workflow guidance scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
7
.changeset/executor-workflow-prompt-parity.md
Normal file
7
.changeset/executor-workflow-prompt-parity.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Match executor workflow guidance to the task-creation tools available in each session.
|
||||
category: fix
|
||||
dev: Built-in executor variants render created-task workflow guidance only on creator-capable tool surfaces.
|
||||
@@ -240,6 +240,30 @@ describe("resolveAgentPrompt", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("renders created-task workflow guidance only for creation tools on the resolved surface", () => {
|
||||
const unavailable = resolveAgentPrompt("executor", undefined, {
|
||||
taskCreateToolAvailable: false,
|
||||
delegateTaskToolAvailable: false,
|
||||
});
|
||||
expect(unavailable).not.toContain("set the workflow on tasks you create");
|
||||
|
||||
const taskCreateOnly = resolveAgentPrompt("executor", undefined, {
|
||||
taskCreateToolAvailable: true,
|
||||
delegateTaskToolAvailable: false,
|
||||
});
|
||||
expect(taskCreateOnly).toContain("set the workflow on tasks you create via `fn_task_create`");
|
||||
expect(taskCreateOnly).not.toContain("set the workflow on tasks you create via `fn_delegate_task`");
|
||||
|
||||
const seniorDelegateOnly = resolveAgentPrompt("executor", {
|
||||
roleAssignments: { executor: "senior-engineer" },
|
||||
}, {
|
||||
taskCreateToolAvailable: false,
|
||||
delegateTaskToolAvailable: true,
|
||||
});
|
||||
expect(seniorDelegateOnly).toContain("set the workflow on tasks you create via `fn_delegate_task`");
|
||||
expect(seniorDelegateOnly).not.toContain("set the workflow on tasks you create via `fn_task_create`");
|
||||
});
|
||||
|
||||
it("senior-engineer prompt limits fixes to impacted failures and follow-ups unrelated broad-suite failures", () => {
|
||||
const config: AgentPromptsConfig = {
|
||||
roleAssignments: {
|
||||
|
||||
@@ -1381,6 +1381,34 @@ export const BUILTIN_AGENT_PROMPTS: readonly AgentPromptTemplate[] = [
|
||||
*/
|
||||
export interface ResolveAgentPromptOptions {
|
||||
plannerHeartbeatPatrolEnabled?: boolean;
|
||||
taskCreateToolAvailable?: boolean;
|
||||
delegateTaskToolAvailable?: boolean;
|
||||
}
|
||||
|
||||
const EXECUTOR_WORKFLOW_SELECTION_GUARDRAIL = "- Do not call `fn_workflow_select` to change the workflow of the task you are executing; you did not create that task, the user or triage did. The only exception is when the user explicitly requested a specific workflow for this task in a steering comment, task instruction, or similar direct instruction.";
|
||||
|
||||
/*
|
||||
FNXC:WorkflowRouting 2026-08-23-18:49:
|
||||
Workflow assignment guidance must match the executor's real tool surface. Built-in creator-capable
|
||||
personas may set workflows on tasks they create, while task-execution sessions omit this permission
|
||||
because they structurally withhold both creation tools. Render the clause at resolution time so the
|
||||
default and senior-engineer variants cannot drift from session capabilities.
|
||||
*/
|
||||
function renderExecutorWorkflowRoutingToolSurface(
|
||||
prompt: string,
|
||||
options: ResolveAgentPromptOptions,
|
||||
): string {
|
||||
const availableCreationTools = [
|
||||
...(options.taskCreateToolAvailable !== false ? ["`fn_task_create`"] : []),
|
||||
...(options.delegateTaskToolAvailable !== false ? ["`fn_delegate_task`"] : []),
|
||||
];
|
||||
if (availableCreationTools.length === 0) return prompt;
|
||||
|
||||
const toolList = availableCreationTools.join(" or ");
|
||||
return prompt.replace(
|
||||
EXECUTOR_WORKFLOW_SELECTION_GUARDRAIL,
|
||||
`${EXECUTOR_WORKFLOW_SELECTION_GUARDRAIL}\n- You may still set the workflow on tasks you create via ${toolList}.`,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveAgentPrompt(
|
||||
@@ -1410,6 +1438,9 @@ export function resolveAgentPrompt(
|
||||
if (role === PLANNER_AGENT_ROLE && template.builtIn && template.id === "concise-triage") {
|
||||
return `${CONCISE_TRIAGE_PROMPT_TEXT}\n\n${buildConciseTriageHeartbeatGuidance(options)}`;
|
||||
}
|
||||
if (role === "executor" && template.builtIn) {
|
||||
return renderExecutorWorkflowRoutingToolSurface(template.prompt, options);
|
||||
}
|
||||
return template.prompt;
|
||||
}
|
||||
|
||||
@@ -1418,6 +1449,9 @@ export function resolveAgentPrompt(
|
||||
if (role === PLANNER_AGENT_ROLE && builtIn?.id === "default-triage") {
|
||||
return `${TRIAGE_PROMPT_TEXT}\n\n${buildTriageHeartbeatGuidance(options)}`;
|
||||
}
|
||||
if (role === "executor" && builtIn) {
|
||||
return renderExecutorWorkflowRoutingToolSurface(builtIn.prompt, options);
|
||||
}
|
||||
return builtIn?.prompt ?? "";
|
||||
}
|
||||
|
||||
|
||||
@@ -3080,6 +3080,9 @@ describe("executor base prompt runtime self-awareness", () => {
|
||||
});
|
||||
|
||||
describe("completion recommendation prompt contract", () => {
|
||||
const getCreatedTaskWorkflowRoutingClause = (prompt: string): string | undefined =>
|
||||
prompt.split("\n").find((line) => line.startsWith("- You may still set the workflow on tasks you create via "));
|
||||
|
||||
it.each([
|
||||
["built-in default", { agentPrompts: undefined }],
|
||||
["custom executor prompt", {
|
||||
@@ -3121,6 +3124,48 @@ describe("completion recommendation prompt contract", () => {
|
||||
expect(customDisabledPrompt).toMatch(/Always send recommendations\.[\s\S]*Ignore any earlier generic recommendation guidance/);
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:WorkflowRouting 2026-08-23-13:25:
|
||||
The production executor prompt resolver treats creation tools as available by default. Only an
|
||||
explicit withheld=true removes that tool's created-task workflow guidance; omitted and empty
|
||||
availability inputs preserve both clauses, while one withheld tool preserves the other clause.
|
||||
*/
|
||||
it.each([
|
||||
["task creation only", { taskCreateWithheld: false, delegateWithheld: true }, "`fn_task_create`", "`fn_delegate_task`"],
|
||||
["delegation only", { taskCreateWithheld: true, delegateWithheld: false }, "`fn_delegate_task`", "`fn_task_create`"],
|
||||
])("renders workflow routing for the %s surface", async (_label, availability, presentTool, absentTool) => {
|
||||
const { getExecutorSystemPrompt } = await import("../executor.js");
|
||||
const prompt = getExecutorSystemPrompt({ agentPrompts: undefined } as any, availability);
|
||||
|
||||
const clause = getCreatedTaskWorkflowRoutingClause(prompt);
|
||||
expect(clause).toBe(`- You may still set the workflow on tasks you create via ${presentTool}.`);
|
||||
expect(clause).not.toContain(absentTool);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["omitted availability", undefined],
|
||||
["empty availability", {}],
|
||||
])("keeps both workflow creation clauses for %s", async (_label, availability) => {
|
||||
const { getExecutorSystemPrompt } = await import("../executor.js");
|
||||
const prompt = getExecutorSystemPrompt({ agentPrompts: undefined } as any, availability);
|
||||
|
||||
expect(getCreatedTaskWorkflowRoutingClause(prompt)).toBe(
|
||||
"- You may still set the workflow on tasks you create via `fn_task_create` or `fn_delegate_task`.",
|
||||
);
|
||||
expect(prompt).not.toContain("Follow-up task creation is disabled for this session");
|
||||
});
|
||||
|
||||
it("removes created-task workflow guidance when both creation tools are withheld", async () => {
|
||||
const { getExecutorSystemPrompt } = await import("../executor.js");
|
||||
const prompt = getExecutorSystemPrompt(
|
||||
{ agentPrompts: undefined } as any,
|
||||
{ taskCreateWithheld: true, delegateWithheld: true },
|
||||
);
|
||||
|
||||
expect(getCreatedTaskWorkflowRoutingClause(prompt)).toBeUndefined();
|
||||
expect(prompt).toContain("Task-execution sessions structurally withhold `fn_task_create` and `fn_delegate_task`");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["only task creation is withheld", { taskCreateWithheld: true }],
|
||||
["only delegation is withheld", { delegateWithheld: true }],
|
||||
|
||||
@@ -570,7 +570,8 @@ describe("Code review verdict enforcement - fn_task_update blocking", () => {
|
||||
expect(capturedSystemPrompt).toContain("Do not call `fn_workflow_select` to change the workflow of the task you are executing");
|
||||
expect(capturedSystemPrompt).toContain("The only exception is when the user explicitly requested a specific workflow for this task");
|
||||
expect(capturedSystemPrompt).toContain("Implement required in-scope work directly here");
|
||||
expect(capturedSystemPrompt).not.toContain("You may still set the workflow on tasks you create via `fn_task_create` or `fn_delegate_task`");
|
||||
expect(capturedSystemPrompt).not.toContain("set the workflow on tasks you create");
|
||||
expect(capturedSystemPrompt).toContain("Task-execution sessions structurally withhold `fn_task_create` and `fn_delegate_task`");
|
||||
});
|
||||
|
||||
// Note: The EXECUTOR_SYSTEM_PROMPT constant is tested indirectly via the buildExecutionPrompt test.
|
||||
|
||||
@@ -318,7 +318,16 @@ export function getExecutorSystemPrompt(
|
||||
settings: Settings,
|
||||
toolAvailability?: { taskCreateWithheld?: boolean; delegateWithheld?: boolean },
|
||||
): string {
|
||||
const customPrompt = resolveAgentPrompt("executor", settings.agentPrompts);
|
||||
/*
|
||||
FNXC:WorkflowRouting 2026-08-23-13:25:
|
||||
Tool availability is opt-out at this production boundary: omitted availability, an empty object,
|
||||
and omitted fields all preserve the core resolver's available-by-default contract. Only an
|
||||
explicit withheld=true removes that tool's created-task workflow guidance.
|
||||
*/
|
||||
const customPrompt = resolveAgentPrompt("executor", settings.agentPrompts, {
|
||||
taskCreateToolAvailable: toolAvailability?.taskCreateWithheld !== true,
|
||||
delegateTaskToolAvailable: toolAvailability?.delegateWithheld !== true,
|
||||
});
|
||||
const basePrompt = customPrompt || EXECUTOR_SYSTEM_PROMPT;
|
||||
/*
|
||||
FNXC:TaskRecommendations 2026-08-10-01:15:
|
||||
|
||||
Reference in New Issue
Block a user