import type { Message } from "@mariozechner/pi-ai"; const IMPLEMENTATION_KEYWORDS = [ "implement", "fix", "update", "apply", "write", "change", "refactor", "create", "add", "remove", "delete", "patch", "debug", ]; const RESEARCH_KEYWORDS = [ "research", "analyze", "explore", "find", "investigate", "check", "is it possible", "where is", "list", "audit", "review", "understand", "map", "scout", ]; const MUTATING_TOOLS = ["edit", "write", "bash", "exec", "shell", "patch", "mvt", "mv", "rm", "mkdir"]; /** * Returns true if the task sounds like it requires implementation/edits. */ function isImplementationTask(task: string): boolean { const t = task.toLowerCase(); // If it contains research keywords, it might be purely informational. // But if it ALSO contains implementation keywords, we should be cautious. const hasImpl = IMPLEMENTATION_KEYWORDS.some((k) => t.includes(k)); const hasRes = RESEARCH_KEYWORDS.some((k) => t.includes(k)); if (hasImpl && !hasRes) return true; if (hasImpl && hasRes) { // Look for strong implementation signals that override research return /\b(apply|fix|implement|patch)\b/.test(t); } return false; } /** * Returns true if any provided message is a tool call to a mutating tool. */ function usedMutatingTools(messages: Message[]): boolean { for (const msg of messages) { if (msg.role === "assistant" && Array.isArray(msg.content)) { for (const part of msg.content) { if (part.type === "toolCall") { const name = (part as any).name || (part as any).toolName; if (MUTATING_TOOLS.includes(name)) { // Note: bash/exec are considered mutating because they CAN mutate. // In a better implementation, we'd check if they actually changed anything. return true; } } } } } return false; } export interface GuardResult { triggered: boolean; error?: string; } /** * Evaluates whether a completed subagent successfully fulfilled an implementation task. * If it seems to have just talked about it without applying changes, it returns an error. */ export function evaluateCompletionMutationGuard(input: { agent: string; task: string; messages: Message[]; }): GuardResult { if (!isImplementationTask(input.task)) { return { triggered: false }; } if (usedMutatingTools(input.messages)) { return { triggered: false }; } return { triggered: true, error: `Subagent "${input.agent}" completed without using any mutating tools for an implementation task. ` + `It appears to have returned a plan or scratchpad output instead of applying changes.`, }; }