// src/core/turn-limiter.ts // // soft/hard turn 限制器。maxTurns 到达 → steer 提醒收尾; // graceTurns 后仍不结束 → abort。 /** steer 提醒消息:要求 agent 总结已完成/未完成/下一步,不得谎报完成。 */ const WRAP_UP_MESSAGE = [ "You have reached your turn limit. Wrap up now:", "1. Summarize what you have completed (with evidence: file paths, command output).", "2. List what remains undone and why.", "3. State the single most important next step for whoever continues.", "Do NOT claim the task is complete if any part remains unfinished.", ].join(" "); /** * 预防性 wrap-up 提示(启动时注入 --append-system-prompt)。 * * spawn 模式下 runSpawn 无法在达 maxTurns 时运行时注入 steer(pi `--mode json` 是 * single-shot,无 stdin 命令通道;见 session-runner.ts limiter 注释)。作为短期补偿, * 在启动时预置此预防性指令,让 agent 在感知接近上限时主动收尾。 * * 长期方案是切到 pi `--mode rpc`(支持运行时 steer 命令),见 follow-up。 */ export const WRAP_UP_HINT = [ `You have a turn limit. As you sense you are approaching it, proactively wrap up:`, "1. Summarize what you have completed (with evidence: file paths, command output).", "2. List what remains undone and why.", "3. State the single most important next step for whoever continues.", "Do NOT claim the task is complete if any part remains unfinished.", ].join(" "); /** turn limiter 配置。 */ export interface TurnLimiterOptions { maxTurns: number; graceTurns: number; steer: (msg: string) => void; abort: () => void; } /** * soft/hard turn 限制。 * * onTurnEnd(currentTurns): * 已 aborted 或 maxTurns<=0(禁用)→ 直接 return * currentTurns >= maxTurns 且未 steer → steer(WRAP_UP_MESSAGE)(仅一次) * 已 steer 且 currentTurns >= maxTurns + graceTurns → abort()(仅一次) * * maxTurns<=0 表示不限(limit=Infinity,永不触发)。 * graceTurns<=0 时 steer 后下一 turn 即 abort。 */ export interface TurnLimiter { /** 每次 turn_end 调用。 */ onTurnEnd(currentTurns: number): void; /** * 重置 steered/aborted 标志(新一轮开始)。 * * chatMode 下 agent_settled 时调用——每轮(message → agent_settled)独立计数, * maxTurns 不跨轮累计(续聊本质是无限轮,累计上限违背 G1)。 * 非 chatMode 不调用,行为不变(全程累计)。 */ reset(): void; /** 是否已发过 steer(诊断用)。 */ readonly didSteer: boolean; /** 是否已 abort(诊断用)。 */ readonly didAbort: boolean; } /** 工厂函数。 */ export function createTurnLimiter(opts: TurnLimiterOptions): TurnLimiter { let steered = false; let aborted = false; const limit = opts.maxTurns > 0 ? opts.maxTurns : Infinity; const grace = opts.graceTurns > 0 ? opts.graceTurns : 0; const onTurnEnd = (turn: number): void => { if (aborted || !Number.isFinite(limit)) return; if (!steered && turn >= limit) { steered = true; opts.steer(WRAP_UP_MESSAGE); } if (steered && turn >= limit + grace) { aborted = true; opts.abort(); } }; const reset = (): void => { steered = false; aborted = false; }; return { onTurnEnd, reset, get didSteer(): boolean { return steered; }, get didAbort(): boolean { return aborted; }, }; }