import type { Agent, Scope } from "./agents.js"; import { discoverAgents } from "./agents.js"; function escapeXmlText(value: string): string { return value .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">"); } function escapeXmlAttribute(value: string): string { return escapeXmlText(value) .replaceAll('"', """) .replaceAll("'", "'"); } function sortAgentsForWorkflowPrompt(agents: Agent[]): Agent[] { return [...agents].sort((left, right) => { if (left.source !== right.source) { return left.source === "project" ? -1 : 1; } return left.name.localeCompare(right.name); }); } function formatWorkflowAgentExample(agentName?: string): { valid?: string; invalidCasing?: string; invalidInvented: string; } { if (!agentName) { return { invalidInvented: '{"agent":"invented-agent","task":"Map the public API."}', }; } const invalidCasing = agentName.toLowerCase() === agentName.toUpperCase() ? undefined : `{"agent":"${agentName.toUpperCase()}","task":"Map the public API."}`; return { valid: `{"agent":"${agentName}","task":"Map the public API."}`, invalidCasing, invalidInvented: '{"agent":"invented-agent","task":"Map the public API."}', }; } export function formatWorkflowAgentsXml(cwd: string, scope: Scope): string { const discovery = discoverAgents(cwd, scope); const agents = sortAgentsForWorkflowPrompt(discovery.agents); const examples = formatWorkflowAgentExample(agents[0]?.name); const lines = [ ``, " ", " Use only agent names listed in this block for workflow agent fields.", " Any other name is invalid. Do not invent agent names.", " If none of these agents fit, explain the limitation instead of fabricating a new one.", " Agent names are exact identifiers. Prefer them verbatim.", " Agent metadata below is loaded from the discovered agent markdown files for the current cwd and scope.", " ", " ", ]; if (agents.length === 0) { lines.push( " No workflow agents were discovered for this cwd and scope.", ); } else { for (const agent of agents) { lines.push( ` `, " ", ` ${escapeXmlText(agent.description)}`, ); if (agent.model) { lines.push(` ${escapeXmlText(agent.model)}`); } if (agent.thinking) { lines.push( ` ${escapeXmlText(agent.thinking)}`, ); } lines.push(" "); if (agent.skills.length === 0) { lines.push(" (none)"); } else { for (const skill of agent.skills) { lines.push(` ${escapeXmlText(skill)}`); } } lines.push(" ", " ", " "); } } lines.push(" "); if (discovery.diagnostics.length > 0) { lines.push(" "); for (const diagnostic of discovery.diagnostics) { lines.push( ` ${escapeXmlText(diagnostic.message)}`, ); } lines.push(" "); } lines.push(" "); if (examples.valid) { lines.push(` ${escapeXmlText(examples.valid)}`); } if (examples.invalidCasing) { lines.push( ` ${escapeXmlText(examples.invalidCasing)}`, ); } lines.push( ` ${escapeXmlText(examples.invalidInvented)}`, " ", "", ); return lines.join("\n"); } export function shouldInjectWorkflowAgentsPrompt( prompt: string, options?: { workflowUsedInPreviousTurn?: boolean }, ): boolean { if (options?.workflowUsedInPreviousTurn) return true; const normalized = prompt.toLowerCase(); const patterns = [ /\bworkflow\b/, /\bmulti[-\s]?agent\b/, /\bspawn\s+\d+\s+(?:agents?|workers?)\b/, /\bfork\b/, /\bjoin\b/, /\bloop\b/, /\bparallel(?:ly)?\b.*\b(?:agents?|workers?|branches?)\b/, /\b(?:agents?|workers?|branches?)\b.*\bparallel(?:ly)?\b/, ]; return patterns.some((pattern) => pattern.test(normalized)); }