import { spawn } from "node:child_process"; import { readFile } from "node:fs/promises"; import { isAbsolute, join } from "node:path"; import type { LoopAttentionEventV1, LoopStateV1, AttentionStatus } from "./domain.js"; export function buildAttentionEvent(state: LoopStateV1, status: AttentionStatus, session: LoopAttentionEventV1["session"], now: number): LoopAttentionEventV1 { const agent = status === "completed" || status === "failed" || status === "blocked"; const semanticEnd = state.endedAt ?? now; const extensionEvidence: Record, string> = { duration_limit: "O prazo de calendário do loop foi atingido.", iteration_limit: "O orçamento máximo de iterações foi atingido.", protocol_error: "A iteração terminou sem uma decisão loop_control válida.", }; return { version: 1, status, summary: state.lastDecision?.summary ?? extensionEvidence[status as keyof typeof extensionEvidence] ?? status, evidence: { source: agent ? "agent" : "extension", text: agent ? (state.lastDecision?.evidence ?? "") : extensionEvidence[status as keyof typeof extensionEvidence] }, objective: state.objective, iterationCount: state.iterationCount, maxIterations: state.maxIterations, protocolRetryCount: state.protocolRetryCount, calendarElapsedMs: Math.max(0, semanticEnd - state.startedAt), deadlineRemainingMs: Math.max(0, state.startedAt + state.forMs - now), session, endedAt: new Date(semanticEnd).toISOString(), observedAt: new Date(state.observedAt ?? now).toISOString() }; } export async function readHookPath(home = process.env.HOME): Promise { if (!home) return undefined; try { const config = JSON.parse(await readFile(join(home, ".pi", "agent", "loop.json"), "utf8")) as { onAttention?: unknown }; if (typeof config.onAttention !== "string" || !isAbsolute(config.onAttention)) return undefined; return config.onAttention; } catch { return undefined; } } export interface HookRunner { (path: string, input: string, timeoutMs: number): Promise; } export const nodeHookRunner: HookRunner = (path, input, timeoutMs) => new Promise((resolve, reject) => { const child = spawn(path, [], { shell: false, stdio: ["pipe", "ignore", "pipe"] }); let stderr = ""; const timer = setTimeout(() => { child.kill("SIGKILL"); reject(new Error("hook excedeu o timeout")); }, timeoutMs); child.stderr.setEncoding("utf8"); child.stderr.on("data", (chunk: string) => { if (stderr.length < 8_192) stderr += chunk.slice(0, 8_192 - stderr.length); }); child.once("error", (error) => { clearTimeout(timer); reject(error); }); child.once("close", (code) => { clearTimeout(timer); code === 0 ? resolve() : reject(new Error(stderr.trim() || `hook encerrou com código ${code}`)); }); child.stdin.end(input); }); export async function runAttentionHook(runner: HookRunner, path: string, event: LoopAttentionEventV1): Promise { await runner(path, `${JSON.stringify(event)}\n`, 5_000); }