import { readArtifact } from "./artifacts.ts"; import { buildHandoffCapsule } from "./handoff-capsule.ts"; import type { ActiveRun, KdPhase } from "./types.ts"; import { phaseOrderForRun } from "./types.ts"; import { isAbsolute, relative } from "node:path"; export type DelegationRole = "research" | "doc" | "code" | "review" | "verify"; export interface DelegationRequest { role: DelegationRole; task: string; } export type DelegationMode = "single" | "parallel" | "chain"; export interface ParsedDelegationArgs extends DelegationRequest { dryRun: boolean; } const ROLE_GUIDANCE: Record = { research: [ "只读调研项目、文档、SDK 线索和现有实现。", "必须基于真实读取或搜索结果定位文件;禁止猜测路径。", "输出压缩结论、证据文件/代码位置、风险和下一步指令。", "禁止修改文件;禁止推进 Harness 状态。", ], doc: [ "只写当前任务明确要求的文档或阶段产物。", "修改阶段文档时,内容必须匹配当前 Harness 阶段。", "禁止修改产品源码;禁止推进 Harness 状态。", ], code: [ "只有当前 run 处于 execute 阶段时才允许修改产品代码。", "只修改 PLAN.md 批准的文件;发现新文件需求时停止并说明必须回到 plan。", "完成后列出变更文件、执行步骤证据和主 agent 必须执行的验证命令。", ], review: [ "只读交叉自查,不修改文件。", "审查重点:bug、状态机漏洞、门禁绕过、证据缺口和测试缺口。", "输出 findings,按严重程度排序并带文件/函数引用;明确是否阻止发布。", ], verify: [ "只读分析计划中的验证命令、失败证据和风险。", "实际运行验证由主 agent 执行;子 agent 输出命令和原因。", "验证结果由主 agent 通过 kd_verify_result 记录。", "禁止绕过 Harness 的 VERIFY/evidence 记录闭环。", ], }; const ROLE_ALLOWED_WRITES: Record = { research: "none", doc: "docs-and-current-phase-artifact", code: "plan-approved-files-only", review: "none", verify: "evidence-through-kd_verify_result", }; export const DEFAULT_REVIEW_TASK = "审查当前 run 和最近变更,重点找状态机漏洞、门禁绕过、证据缺口、工程指令分散和测试缺口。"; export const CHILD_AGENT_USER_TASK = "执行 KCode 子 agent 委派任务。"; export const KD_SUBAGENT_TOOL_DESCRIPTION = "将调研、文档、代码、验证或交叉审查任务委派给隔离 Pi 子进程。主 Harness 仍负责阶段推进、证据和门禁。"; export const KD_DELEGATE_USAGE = "用法:/kd-delegate <任务> [--dry-run]"; export const KD_SUBAGENT_INVALID_PARAMS = "kd_subagent 参数要求:role=research|doc|code|review|verify 且 task 非空,或提供 tasks/chain。"; export const KD_SUBAGENT_PARALLEL_ROLE_ERROR = "kd_subagent 并行模式只允许 research、review、verify 这类只读角色。doc/code 必须串行执行。"; export const KD_REVIEW_COMMAND_DESCRIPTION = "启动只读交叉自查子 agent:/kd-review [审查重点]"; export const KD_DELEGATE_COMMAND_DESCRIPTION = "委派任务给隔离子 agent:/kd-delegate <任务> [--dry-run]"; export const KD_SUBAGENT_SCHEMA_DESCRIPTIONS = { role: "单任务角色:research、doc、code、review、verify。", task: "单任务内容。", taskItemRole: "任务角色。", taskItemTask: "任务内容。", tasks: "并行任务数组,最多 8 个,最多 4 个同时运行。", chain: "链式任务数组,按顺序运行;后一步会收到上一步输出。", dryRun: "只预览上下文包,不启动子进程。", maxOutputChars: "返回给主 agent 的最大输出字符数,默认 30000。", } as const; const READ_ONLY_ROLES = new Set(["research", "review", "verify"]); const WRITE_TOOL_NAMES = new Set(["write", "edit"]); const SHELL_TOOL_NAMES = new Set(["bash", "shell", "shell_command"]); export function isDelegationRole(value: string): value is DelegationRole { return value === "research" || value === "doc" || value === "code" || value === "review" || value === "verify"; } export function parseDelegationArgs(args: string): ParsedDelegationArgs | undefined { const tokens = tokenizeArgs(args); if (tokens.length === 0) return undefined; const roleText = tokens[0]?.toLowerCase(); if (!roleText || !isDelegationRole(roleText)) return undefined; const dryRun = tokens.includes("--dry-run"); const task = tokens.slice(1).filter((token) => token !== "--dry-run").join(" ").trim(); if (!task) return undefined; return { role: roleText, task, dryRun }; } export function delegationBlockReason(role: DelegationRole, run: ActiveRun | undefined): string | undefined { if (role === "code") { if (!run) return "code 子 agent 要求 active Harness run,且必须处于 execute 阶段。"; if (run.phase !== "execute") return `code 子 agent 只能在 execute 阶段运行;当前阶段:${run.phase}。`; } if (role === "verify" && !run) return "verify 子 agent 要求 active Harness run。"; return undefined; } export function isReadOnlyDelegationRole(role: DelegationRole): boolean { return READ_ONLY_ROLES.has(role); } export function parallelDelegationBlockReason(requests: DelegationRequest[]): string | undefined { return requests.some((request) => !isReadOnlyDelegationRole(request.role)) ? KD_SUBAGENT_PARALLEL_ROLE_ERROR : undefined; } export function isSubagentChild(env: NodeJS.ProcessEnv = process.env): boolean { return env.KCODE_SUBAGENT_CHILD === "1"; } export function subagentRoleFromEnv(env: NodeJS.ProcessEnv = process.env): DelegationRole | undefined { const role = env.KCODE_SUBAGENT_ROLE; return typeof role === "string" && isDelegationRole(role) ? role : undefined; } export function shouldInjectDelegationGuidance(env: NodeJS.ProcessEnv = process.env): boolean { return !isSubagentChild(env); } export function subagentAllowedTools(role: DelegationRole): string[] { const readTools = ["read", "grep", "find", "ls"]; if (role === "doc" || role === "code") return [...readTools, "write", "edit"]; return readTools; } export function subagentToolCallBlockReason(input: { role: DelegationRole; toolName: string; path?: string; cwd: string; run?: ActiveRun; sourceLike?: boolean; planWriteBlockReason?: string; sourceWriteBlockReason?: string; }): string | undefined { if (SHELL_TOOL_NAMES.has(input.toolName)) { return "子 agent 禁止调用 shell 类工具;运行命令由主 agent 执行,子 agent 只输出命令和原因。"; } if (!WRITE_TOOL_NAMES.has(input.toolName)) return undefined; if (isReadOnlyDelegationRole(input.role)) { return `${input.role} 子 agent 是只读角色,不能写入文件。`; } if (input.role === "doc") { if (isDocWritablePath(input.cwd, input.run, input.path)) return undefined; return "doc 子 agent 只能写 README、docs/ 或当前 run 的阶段文档。"; } if (input.role === "code") { if (!input.sourceLike) return "code 子 agent 只能写产品源码,其他文件改动必须交回主 agent。"; if (input.sourceWriteBlockReason) return input.sourceWriteBlockReason; if (input.planWriteBlockReason) return input.planWriteBlockReason; return undefined; } return undefined; } export function buildDelegationCommandPrompt(request: DelegationRequest, dryRun: boolean): string { return [ "执行 kd_subagent 委派任务。", `role: ${request.role}`, `dryRun: ${dryRun ? "true" : "false"}`, "task:", request.task, "", "子 agent 返回后,主 agent 负责采纳结论、修改文件、记录 evidence 或推进 Harness。", ].join("\n"); } export function delegationGuidanceForWorkflow(): string { return [ "- 任务包含大量调研、独立交叉审查、长上下文复盘或可并行拆分内容时,使用 kd_subagent。", "- 自动委派只做旁路工作;主 agent 仍负责采纳结论、修改文件、记录 evidence 和推进阶段。", "- code 委派只能在 execute 阶段且限于 PLAN.md 批准文件;review/research 默认只读。", "- kd_subagent 不是 shell/read/grep 失败的替代方案;基础文件搜索失败时,主 agent 必须报告阻塞原因或改用当前环境可用工具。", ].join("\n"); } export function buildChainedDelegationRequest(request: DelegationRequest, previousOutput: string): DelegationRequest { if (!previousOutput.trim()) return request; return { role: request.role, task: [ request.task.trim(), "", "上一子 agent 输出:", trimForPrompt(previousOutput, 6000), "", "基于上一输出继续当前步骤;仅交付本步骤结论。", ].join("\n"), }; } export function buildDelegationPrompt(cwd: string, run: ActiveRun | undefined, request: DelegationRequest): string { const roleGuidance = ROLE_GUIDANCE[request.role]; const capsule = buildHandoffCapsule({ cwd, run, audience: "subagent", task: request.task, maxChars: 16_000, }); return [ "KCode 子 agent 委派任务。", "", "角色:", request.role, "", "任务:", request.task.trim(), "", "写入边界:", ROLE_ALLOWED_WRITES[request.role], "", "角色规则:", ...roleGuidance.map((item) => `- ${item}`), "- 子 agent 不是主状态机;禁止调用 /kd phase、/kd done 或改变需求生命周期。", "- 禁止创建子 agent;输出结果交回主 agent 统一决策。", "- 禁止假设 bash、Linux 路径或猜测文件位置;Windows 项目按 PowerShell/Windows 路径语义描述命令。", "", "Handoff Capsule:", capsule, "", "输出格式:", "- 结论", "- 证据/引用", "- 风险", "- 主 agent 下一步指令", ].join("\n"); } export function formatDelegationPreview(cwd: string, run: ActiveRun | undefined, request: DelegationRequest): string { return [ `角色:${request.role}`, `写入边界:${ROLE_ALLOWED_WRITES[request.role]}`, run ? `Run:${run.id} | 阶段:${run.phase}` : "Run:无 active run", "", "任务:", request.task.trim(), "", "将发送给隔离子 agent 的上下文包:", buildDelegationPrompt(cwd, run, request), ].join("\n"); } function delegationPhaseContextForRun(cwd: string, run: ActiveRun): string { const phaseOrder = phaseOrderForRun(run); const currentIndex = phaseOrder.indexOf(run.phase); const nearby = phaseOrder.slice(Math.max(0, currentIndex - 2), currentIndex + 1); const sections = nearby .map((phase) => artifactSection(cwd, run, phase)) .filter((section): section is string => Boolean(section)); return sections.join("\n\n") || `阶段文档路径:.pi/kd/runs/${run.id}/`; } function artifactSection(cwd: string, run: ActiveRun, phase: KdPhase): string | undefined { const content = readArtifact(cwd, run, phase); if (!content) return undefined; return [`## ${phase}`, trimForPrompt(content, 1800)].join("\n"); } function trimForPrompt(content: string, maxLength: number): string { if (content.length <= maxLength) return content; return `${content.slice(0, maxLength)}\n\n[...已截断;完整内容读取本地文件...]`; } function isDocWritablePath(cwd: string, run: ActiveRun | undefined, path: string | undefined): boolean { if (!path) return false; const normalized = normalizeWorkspacePath(cwd, path); if (normalized === "README.md") return true; if (normalized.startsWith("docs/") && /\.md$/i.test(normalized)) return true; if (run && normalized.startsWith(`.pi/kd/runs/${run.id}/`) && /\.md$/i.test(normalized)) return true; return false; } function normalizeWorkspacePath(cwd: string, path: string): string { const relativePath = isAbsolute(path) ? relative(cwd, path) : path; return relativePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, ""); } function tokenizeArgs(args: string): string[] { const tokens: string[] = []; const pattern = /"([^"]*)"|'([^']*)'|(\S+)/g; let match: RegExpExecArray | null; while ((match = pattern.exec(args)) !== null) { const token = match[1] ?? match[2] ?? match[3]; if (token) tokens.push(token); } return tokens; }