export type LoopStatus = "waiting" | "queued" | "running" | "paused" | "completed" | "failed" | "stopped"; export type PauseReason = "user" | "blocked" | "duration_limit" | "iteration_limit" | "protocol_error" | "session_closed" | "branch_changed" | "unknown_version"; export type AttentionStatus = "completed" | "failed" | "blocked" | "duration_limit" | "iteration_limit" | "protocol_error"; export type LoopPolicy = { kind: "fixed"; delayMs: number } | { kind: "auto" }; export interface LoopStartOptions { objective: string; every: LoopPolicy; afterMs: number; forMs: number; maxIterations: number; minDelayMs: number; maxDelayMs: number; } export interface LoopUpdatePatch { every?: LoopPolicy; forMs?: number; maxIterations?: number; minDelayMs?: number; maxDelayMs?: number; } export type LoopDecision = | { decision: "continue"; summary: string; evidence: string; nextDelayMs?: number } | { decision: "complete"; summary: string; evidence: string } | { decision: "fail"; summary: string; evidence: string } | { decision: "block"; summary: string; evidence: string }; export interface LastDecision { decision: LoopDecision["decision"] | "protocol_error"; summary: string; evidence: string; at: number; } export interface LoopStateV1 { version: 1; status: LoopStatus; pauseReason?: PauseReason; objective: string; policy: LoopPolicy; startedAt: number; forMs: number; maxIterations: number; iterationCount: number; minDelayMs: number; maxDelayMs: number; nextWakeAt?: number; pendingIntent?: "pause" | "stop"; protocolRetryCount: number; protocolRetryUsed: boolean; decisionReceived: boolean; lastDecision?: LastDecision; endedAt?: number; observedAt?: number; } export interface LoopAttentionEventV1 { version: 1; status: AttentionStatus; summary: string; evidence: { source: "agent" | "extension"; text: string }; objective: string; iterationCount: number; maxIterations: number; protocolRetryCount: number; calendarElapsedMs: number; deadlineRemainingMs: number; session: { id: string; file?: string; cwd: string }; endedAt: string; observedAt: string; } export const deadlineOf = (state: Pick) => state.startedAt + state.forMs; export const isTerminal = (status: LoopStatus) => status === "completed" || status === "failed" || status === "stopped"; export function startLoop(options: LoopStartOptions, now: number): LoopStateV1 { return { version: 1, status: "waiting", objective: options.objective, policy: options.every, startedAt: now, forMs: options.forMs, maxIterations: options.maxIterations, iterationCount: 0, minDelayMs: options.minDelayMs, maxDelayMs: options.maxDelayMs, nextWakeAt: options.afterMs < options.forMs ? now + options.afterMs : undefined, protocolRetryCount: 0, protocolRetryUsed: false, decisionReceived: false }; } export function materializeDeadline(state: LoopStateV1, now: number): LoopStateV1 { const deadline = deadlineOf(state); if (isTerminal(state.status) || now < deadline || state.pauseReason === "duration_limit") return state; return { ...state, status: "paused", pauseReason: "duration_limit", nextWakeAt: undefined, endedAt: deadline, observedAt: now }; } export function queueIteration(state: LoopStateV1, now: number): LoopStateV1 { const current = materializeDeadline(state, now); if (current.status !== "waiting" || current.iterationCount >= current.maxIterations) { if (current.status === "waiting" && current.iterationCount >= current.maxIterations) return pauseForLimit(current, "iteration_limit", now); return current; } return { ...current, status: "queued", nextWakeAt: undefined }; } export function beginIteration(state: LoopStateV1, now: number): LoopStateV1 { const current = materializeDeadline(state, now); if (current.status !== "queued") return current; if (current.iterationCount >= current.maxIterations) return pauseForLimit(current, "iteration_limit", now); return { ...current, status: "running", iterationCount: current.iterationCount + 1, protocolRetryUsed: false, decisionReceived: false }; } function pauseForLimit(state: LoopStateV1, reason: "iteration_limit" | "protocol_error", now: number): LoopStateV1 { return { ...state, status: "paused", pauseReason: reason, nextWakeAt: undefined, endedAt: now, observedAt: now }; } export function applyDecision(state: LoopStateV1, decision: LoopDecision, now: number): LoopStateV1 { if (state.status !== "running") throw new Error("loop_control só é válido durante uma iteração."); if (!decision.summary.trim() || !decision.evidence.trim()) throw new Error("summary e evidence são obrigatórios."); let decided: LoopStateV1 = { ...state, decisionReceived: true, lastDecision: { decision: decision.decision, summary: decision.summary, evidence: decision.evidence, at: now } }; if (state.pendingIntent === "stop") return { ...decided, status: "stopped", pendingIntent: undefined, nextWakeAt: undefined, endedAt: now, observedAt: now }; if (state.pendingIntent === "pause") return { ...decided, status: "paused", pauseReason: "user", pendingIntent: undefined, nextWakeAt: undefined }; if (decision.decision === "complete" || decision.decision === "fail") return { ...decided, status: decision.decision === "complete" ? "completed" : "failed", nextWakeAt: undefined, endedAt: now, observedAt: now }; if (decision.decision === "block") return { ...decided, status: "paused", pauseReason: "blocked", nextWakeAt: undefined, endedAt: now, observedAt: now }; const delay = state.policy.kind === "fixed" ? state.policy.delayMs : decision.nextDelayMs; if (state.policy.kind === "fixed" && decision.nextDelayMs !== undefined) throw new Error("nextDelay é proibido no ritmo fixo."); if (delay === undefined) throw new Error("nextDelay é obrigatório no auto-ritmo."); if (delay < state.minDelayMs || delay > state.maxDelayMs) throw new Error("nextDelay está fora da faixa min/max."); if (state.iterationCount >= state.maxIterations) return pauseForLimit(decided, "iteration_limit", now); if (now + delay >= deadlineOf(state)) return { ...decided, status: "waiting", nextWakeAt: undefined }; return { ...decided, status: "waiting", nextWakeAt: now + delay }; } export function requestPause(state: LoopStateV1): LoopStateV1 { if (state.status === "running" || state.status === "queued") return { ...state, pendingIntent: "pause" }; if (state.status === "waiting") return { ...state, status: "paused", pauseReason: "user", nextWakeAt: undefined }; throw new Error("O loop não está ativo."); } export function requestStop(state: LoopStateV1, now: number): LoopStateV1 { if (state.status === "running" || state.status === "queued") return { ...state, pendingIntent: "stop" }; if (!isTerminal(state.status)) return { ...state, status: "stopped", nextWakeAt: undefined, endedAt: now, observedAt: now }; throw new Error("O loop já está encerrado."); } export function settleWithoutDecision(state: LoopStateV1, now: number): { state: LoopStateV1; retry: boolean } { if (state.status !== "running" || state.decisionReceived) return { state, retry: false }; if (!state.protocolRetryUsed && state.protocolRetryCount < 5) return { state: { ...state, protocolRetryUsed: true, protocolRetryCount: state.protocolRetryCount + 1 }, retry: true }; return { state: pauseForLimit({ ...state, lastDecision: { decision: "protocol_error", summary: "A iteração terminou sem decisão válida.", evidence: "loop_control não foi chamado corretamente.", at: now } }, "protocol_error", now), retry: false }; } export function resumeLoop(state: LoopStateV1, now: number): LoopStateV1 { const current = materializeDeadline(state, now); if (current.status !== "paused") throw new Error("O loop não está pausado."); if (current.pauseReason === "duration_limit") throw new Error("Amplie --for antes de retomar."); if (current.iterationCount >= current.maxIterations) throw new Error("Amplie --max-iterations antes de retomar."); return { ...current, status: "waiting", pauseReason: undefined, pendingIntent: undefined, protocolRetryUsed: false, decisionReceived: false, nextWakeAt: now }; } export function updateLoop(state: LoopStateV1, patch: LoopUpdatePatch, now: number): LoopStateV1 { let next = { ...state, ...patch }; if (next.minDelayMs > next.maxDelayMs) throw new Error("minDelay não pode exceder maxDelay."); if (next.policy.kind === "fixed" && (next.policy.delayMs < next.minDelayMs || next.policy.delayMs > next.maxDelayMs)) throw new Error("Intervalo fixo fora da faixa."); if (patch.forMs !== undefined && deadlineOf(next) > now && next.pauseReason === "duration_limit") next = { ...next, pauseReason: "user", endedAt: undefined, observedAt: undefined }; if (patch.maxIterations !== undefined && next.iterationCount < next.maxIterations && next.pauseReason === "iteration_limit") next = { ...next, pauseReason: "user", endedAt: undefined, observedAt: undefined }; return materializeDeadline(next, now); }