/** * CompletionPromise — 可验证的完成承诺。 * * CompletionPromise 是 agent 承诺完成的可验证目标,不是聊天中的泛泛意图。 * 每个 run 必须有 CompletionPromise;PLAN、VERIFY、SHIP 都引用同一个 CompletionPromise。 * CompletionPromise 不满足时,agent 不得自称完成。 * * @see docs/AGENT_GATE_LOOP_REFACTOR_PLAN.md — "Completion Promise" */ import type { GateCheckpoint, GateSeverity } from "./action-plan.ts"; // --------------------------------------------------------------------------- // Stop condition types // --------------------------------------------------------------------------- /** 停止条件类型——完成侧。 */ export type DoneConditionType = | "acceptance-met" | "gate-passed" | "evidence-present" | "user-confirmed"; /** 停止条件类型——中止侧。 */ export type AbortConditionType = | "budget-exhausted" | "unsafe" | "scope-drift" | "stuck" | "user-aborted"; /** 停止条件类型联合。 */ export type StopConditionType = DoneConditionType | AbortConditionType; /** * StopCondition — 停止条件。 * * 用于 doneWhen 和 abortWhen,描述触发停止的条件。 */ export interface StopCondition { /** 条件类型。 */ type: StopConditionType; /** * 条件目标——语义由 type 决定: * - "acceptance-met": 验收标准 ID * - "gate-passed": 门禁检查点名称 * - "evidence-present": 证据 ID * - "user-confirmed": 确认描述 * - "budget-exhausted": 预算类型(如 "token", "time", "tool-calls") * - "unsafe": 风险类型描述 * - "scope-drift": 偏离描述 * - "stuck": 重复检测描述 * - "user-aborted": 不需要 target */ target?: string; } // --------------------------------------------------------------------------- // Acceptance criterion // --------------------------------------------------------------------------- /** * AcceptanceCriterion — 验收标准。 * * 每个验收标准定义一个可验证的目标, * agent 必须提供 requiredEvidence 才能声称该标准已满足。 */ export interface AcceptanceCriterion { /** 唯一标识。 */ id: string; /** 人类可读描述——说明该验收标准要求什么。 */ description: string; /** 满足该标准所需的证据类型或 ID 列表。 */ evidenceRequired: string[]; } // --------------------------------------------------------------------------- // CompletionPromise // --------------------------------------------------------------------------- /** * CompletionPromise — 可验证的完成承诺。 * * agent 承诺完成的结构化目标。所有行动(ActionPlan)、验证(VerifyPlan)、 * 委派(DelegationPlan)都必须绑定到一个 CompletionPromise。 */ export interface CompletionPromise { /** 唯一标识,格式:cp-{timestamp}-{random_hex_4}。 */ id: string; /** 可验证的目标描述。 */ objective: string; /** 验收标准列表。 */ acceptance: AcceptanceCriterion[]; /** 允许的文件路径/glob 模式——修改范围白名单。 */ allowedScope: string[]; /** 禁止的文件路径/glob 模式——修改范围黑名单。 */ disallowedScope: string[]; /** 必须存在的证据 ID 列表。 */ requiredEvidence: string[]; /** 完成停止条件——所有条件都满足时视为完成。 */ doneWhen: StopCondition[]; /** 中止停止条件——任一条件触发时必须中止。 */ abortWhen: StopCondition[]; } // --------------------------------------------------------------------------- // Validation // --------------------------------------------------------------------------- /** CompletionPromise 验证问题。 */ export interface CompletionPromiseValidationIssue { severity: GateSeverity; checkpoint: GateCheckpoint; message: string; } /** 验证结果。 */ export interface CompletionPromiseValidationResult { valid: boolean; issues: CompletionPromiseValidationIssue[]; } // --------------------------------------------------------------------------- // Fulfillment check // --------------------------------------------------------------------------- /** Fulfillment 检查结果。 */ export interface CompletionPromiseFulfillmentResult { /** 是否完全满足。 */ fulfilled: boolean; /** 缺失的证据 ID 列表。 */ missing: string[]; /** 阻断原因列表。 */ blockers: string[]; } // --------------------------------------------------------------------------- // Gate result input (for isFulfilled) // --------------------------------------------------------------------------- /** * GateResult — 门禁检查结果的简化接口。 * * isFulfilled 消费此结构来判断 gate-passed 条件。 * 不导入 GateDecision 全类型,保持模块解耦。 */ export interface GateResult { /** 门禁是否通过。 */ allowed: boolean; /** 检查点。 */ checkpoint: GateCheckpoint; /** 追踪 ID。 */ traceId: string; } // --------------------------------------------------------------------------- // ID generation // --------------------------------------------------------------------------- /** * 生成 CompletionPromise ID:cp-{timestamp}-{random_hex_4}。 * * @example "cp-1717980000000-a3f1" */ export function generateCompletionPromiseId(): string { const timestamp = Date.now(); const randomHex = Math.floor(Math.random() * 0x10000) .toString(16) .padStart(4, "0"); return `cp-${timestamp}-${randomHex}`; } // --------------------------------------------------------------------------- // Factory // --------------------------------------------------------------------------- /** * 创建 CompletionPromise 实例。 * * @param objective - 可验证的目标描述。 * @param acceptance - 验收标准列表。 * @param options - 可选配置。 * @param options.allowedScope - 允许的文件路径/glob 模式。 * @param options.disallowedScope - 禁止的文件路径/glob 模式。 * @param options.requiredEvidence - 必须存在的证据 ID。 * @param options.doneWhen - 完成停止条件。 * @param options.abortWhen - 中止停止条件。 * @returns 新建的 CompletionPromise。 * @throws 若 objective 为空或 acceptance 为空。 */ export function createCompletionPromise( objective: string, acceptance: AcceptanceCriterion[], options?: { allowedScope?: string[]; disallowedScope?: string[]; requiredEvidence?: string[]; doneWhen?: StopCondition[]; abortWhen?: StopCondition[]; }, ): CompletionPromise { if (!objective.trim()) { throw new Error("CompletionPromise objective 不能为空"); } if (acceptance.length === 0) { throw new Error("CompletionPromise 至少需要一个验收标准(acceptance)"); } // 默认 doneWhen:所有验收标准满足 const defaultDoneWhen: StopCondition[] = [ { type: "acceptance-met", target: "*" }, ]; // 默认 abortWhen:预算耗尽、不安全、范围漂移、卡住、用户中止 const defaultAbortWhen: StopCondition[] = [ { type: "budget-exhausted" }, { type: "unsafe" }, { type: "scope-drift" }, { type: "stuck" }, { type: "user-aborted" }, ]; return { id: generateCompletionPromiseId(), objective, acceptance, allowedScope: options?.allowedScope ?? [], disallowedScope: options?.disallowedScope ?? [], requiredEvidence: options?.requiredEvidence ?? [], doneWhen: options?.doneWhen ?? defaultDoneWhen, abortWhen: options?.abortWhen ?? defaultAbortWhen, }; } // --------------------------------------------------------------------------- // Validation function // --------------------------------------------------------------------------- /** * 校验 CompletionPromise 的结构完整性。 * * @param cp - 待校验的 CompletionPromise。 * @returns 验证结果,valid=true 表示通过。 */ export function validateCompletionPromise(cp: CompletionPromise): CompletionPromiseValidationResult { const issues: CompletionPromiseValidationIssue[] = []; // id 格式校验 if (!/^cp-\d+-[0-9a-f]{4}$/.test(cp.id)) { issues.push({ severity: "hard-deny", checkpoint: "pre-plan", message: `CompletionPromise id 格式非法:${cp.id},应为 cp-{timestamp}-{hex4}`, }); } // objective 非空 if (!cp.objective.trim()) { issues.push({ severity: "hard-deny", checkpoint: "pre-plan", message: "CompletionPromise objective 不能为空", }); } // acceptance 非空 if (cp.acceptance.length === 0) { issues.push({ severity: "hard-deny", checkpoint: "pre-plan", message: "CompletionPromise 至少需要一个验收标准(acceptance)", }); } // 每个 acceptance criterion 必须有 id 和 description for (const criterion of cp.acceptance) { if (!criterion.id.trim()) { issues.push({ severity: "hard-deny", checkpoint: "pre-plan", message: `验收标准缺少 id:${criterion.description}`, }); } if (!criterion.description.trim()) { issues.push({ severity: "hard-deny", checkpoint: "pre-plan", message: `验收标准 ${criterion.id} 缺少 description`, }); } } // doneWhen 非空 if (cp.doneWhen.length === 0) { issues.push({ severity: "warning", checkpoint: "pre-plan", message: "CompletionPromise doneWhen 为空,将无法判断完成条件", }); } // abortWhen 非空(建议有) if (cp.abortWhen.length === 0) { issues.push({ severity: "warning", checkpoint: "pre-plan", message: "CompletionPromise abortWhen 为空,缺少中止保护", }); } // allowedScope 和 disallowedScope 不能有重叠 if (cp.allowedScope.length > 0 && cp.disallowedScope.length > 0) { const allowedSet = new Set(cp.allowedScope); const overlap = cp.disallowedScope.filter((s) => allowedSet.has(s)); if (overlap.length > 0) { issues.push({ severity: "soft-deny", checkpoint: "pre-plan", message: `allowedScope 和 disallowedScope 存在重叠:${overlap.join(", ")}`, }); } } // acceptance ID 唯一性 const acceptanceIds = cp.acceptance.map((c) => c.id); const duplicateIds = acceptanceIds.filter((id, i) => acceptanceIds.indexOf(id) !== i); if (duplicateIds.length > 0) { issues.push({ severity: "soft-deny", checkpoint: "pre-plan", message: `验收标准 ID 重复:${[...new Set(duplicateIds)].join(", ")}`, }); } return { valid: issues.filter((i) => i.severity === "hard-deny").length === 0, issues, }; } // --------------------------------------------------------------------------- // Fulfillment checker // --------------------------------------------------------------------------- /** * 检查 CompletionPromise 是否已满足。 * * 判断逻辑: * 1. requiredEvidence 中的每个 ID 必须在 evidenceIds 中存在。 * 2. 每个 AcceptanceCriterion 的 evidenceRequired 中至少有一个在 evidenceIds 中存在。 * 3. doneWhen 中 "gate-passed" 类型的条件必须在 gateResults 中有对应的 allowed=true。 * * @param cp - 待检查的 CompletionPromise。 * @param evidenceIds - 已收集的证据 ID 集合。 * @param gateResults - 门禁检查结果列表。 * @returns 满足检查结果。 */ export function isCompletionFulfilled( cp: CompletionPromise, evidenceIds: ReadonlySet, gateResults: ReadonlyArray, ): CompletionPromiseFulfillmentResult { const missing: string[] = []; const blockers: string[] = []; // 1. 检查 requiredEvidence for (const evidenceId of cp.requiredEvidence) { if (!evidenceIds.has(evidenceId)) { missing.push(evidenceId); } } // 2. 检查每个 acceptance criterion 的 evidenceRequired for (const criterion of cp.acceptance) { if (criterion.evidenceRequired.length === 0) { // 没有 evidence 要求则跳过(由上层决定是否允许) continue; } const hasAny = criterion.evidenceRequired.some((e) => evidenceIds.has(e)); if (!hasAny) { blockers.push( `验收标准 "${criterion.id}" 缺少证据:${criterion.evidenceRequired.join(", ")}`, ); } } // 3. 检查 doneWhen 中 gate-passed 条件 const gateMap = new Map(); for (const gr of gateResults) { gateMap.set(gr.checkpoint, gr.allowed); } for (const condition of cp.doneWhen) { if (condition.type === "gate-passed" && condition.target) { const checkpoint = condition.target as GateCheckpoint; const passed = gateMap.get(checkpoint); if (passed !== true) { blockers.push(`门禁 "${checkpoint}" 未通过`); } } } // fulfilled = 无 missing 且无 blockers return { fulfilled: missing.length === 0 && blockers.length === 0, missing, blockers, }; }