/** Default: `/PROJECTS/loops`. Override: `AGIM_LOOP_REPORTS_DIR`. */ export declare function resolveLoopReportsDir(agentName?: string): string; /** * Find detail-report markdown files for a Loop id under the reports dir. * Matches `.md` and `*-.md` (session-prefixed convention). */ export declare function discoverLoopReportPaths(loopId: string): string[]; /** Merge discovered + stored report paths; optionally persist onto history. */ export declare function resolveLoopReportPaths(run: LoopRun, opts?: { persist?: boolean; }): string[]; /** * Register a report file for a Loop after Agent write_file. * `filePath` may be absolute or workspace-relative (`PROJECTS/loops/…`). */ export declare function noteLoopReportPath(loopId: string, filePath: string): void; /** Read the primary detail report markdown for a Loop (newest matching file). */ export declare function readLoopReportMarkdown(loopId: string): { ok: true; path: string; markdown: string; } | { ok: false; error: string; }; /** L3 statuses; legacy 'running'|'observing' still read as active. */ export type LoopStatus = 'aligning' | 'planning' | 'dispatching' | 'verifying' | 'reflecting' /** met=true claimed — waiting for Portal user confirm (end | continue). */ | 'pending_user_confirm' | 'running' | 'observing' | 'paused' | 'ended'; export type LoopDecision = 'continue' | 'revise' | 'stop'; /** R4 structured verification against mission.objective (+ acceptance). */ export interface LoopVerification { met: boolean; summary: string; gaps?: string[]; nextDirection?: string; } export interface LoopVerdict { decision: LoopDecision; summary: string; reasons?: string[]; /** @deprecated prefer verification.nextDirection; still accepted as alias */ nextHint?: string; /** R4: required for continue/revise; optional on abandon stop */ met?: boolean; gaps?: string[]; nextDirection?: string; } export interface LoopMission { objective: string; /** * Optional longer brief / hints for the Agent (not a hard gate field). * Portal aligning dock 「正文」; slash `/loop body …`. */ body?: string; acceptance?: string; exit: { description: string; /** Hard floor: met=true stop refused while round < minRounds. */ minRounds?: number; maxRounds: number; maxWallMs?: number; onFocusOff: 'cancel'; }; } export interface LoopTimelineEntry { at: number; kind: 'executor' | 'observer' | 'user' | 'system' | 'subagent_terminal'; summary: string; } export interface LoopRun { id: string; /** Stable store key: platform__channel__thread (also the json basename). */ sessionKey: string; platform: string; channelId: string; threadId: string; focusSnapshot: string; openedFocus: boolean; status: LoopStatus; endReason?: 'stop_verdict' | 'user_stop' | 'limit' | 'wall_limit' | 'objective_met' | 'focus_off' | 'verify_skipped' | 'error'; round: number; /** Mirror of mission.exit.maxRounds when present; else provisional. */ maxRounds: number; startNote?: string; /** * Mission SSOT. While ALIGNING may be partial (empty objective and/or * exit.description). After confirm both hard fields are non-empty. */ mission: LoopMission | null; /** Legacy mirrors of mission fields (kept in sync for older readers). */ draftObjective?: string; draftExitDescription?: string; /** R3: subagent spawns accepted this round (call_agent / call_agents). */ dispatchesThisRound: number; /** * Job-board ids of Subagents spawned in the current round. * Round-scoped live wait uses these — not all thread jobs forever. */ roundJobIds: number[]; /** * Cumulative Subagent job ids across **all rounds of this Loop**. * Used by post-end detail reports so prior Loops on the same thread * are not mixed in. Cleared only when a new Loop starts (new run). */ loopJobIds: number[]; /** * Absolute paths of detail reports written for this Loop * (typically `PROJECTS/loops/.md`). Discovered on disk and/or * registered when `agim_write_file` lands under the reports dir. */ reportPaths?: string[]; /** R3: last decompose plan recorded from call_agents (or synthetic single). */ lastPlan?: { at: number; tasks: Array<{ id: string; brief: string; agentHint?: string; }>; }; /** R4: last structured verification */ lastVerification?: LoopVerification & { at: number; }; /** Independent evaluator (cheap) opinion on met=true stop claims */ lastEvaluator?: { at: number; agree: boolean; reason: string; skipped?: boolean; }; /** Cumulative usage for this Loop (C — Loop 用量) */ usage: LoopUsage; createdAt: number; updatedAt: number; lastVerdict?: LoopVerdict; timeline: LoopTimelineEntry[]; } export interface LoopUsage { /** Accepted call_agent(s) across all rounds */ dispatchCount: number; /** Independent evaluator invocations */ evaluatorCount: number; continueCount: number; reviseCount: number; } export declare function emptyLoopUsage(): LoopUsage; /** Readable mission view for status / snippet / aligning prompts. */ export interface LoopMissionView { objective: string; exitDescription: string; /** Optional longer brief / hints (soft). */ body?: string; acceptance?: string; minRounds?: number; maxRounds: number; maxWallMs?: number; confirmed: boolean; partial: boolean; } /** R5 UX buckets — user-facing waiting stage. */ export type LoopUxBucket = 'aligning' | 'dispatch' | 'verify' | 'confirm' | 'ended'; /** * UX stage for Portal / status. * HARD: while this round still has live Subagent jobs, stay in 待派工 — * a single child finishing must NOT open 待验收 / evaluation. */ export declare function getLoopUxBucket(run: LoopRun): LoopUxBucket; export declare function loopUxBucketLabel(bucket: LoopUxBucket, lang?: 'zh' | 'en'): string; /** Compact DTO for Portal / REST (R5). */ export interface LoopStatusDto { id: string; sessionKey: string; status: LoopStatus; bucket: LoopUxBucket; bucketLabel: string; active: boolean; round: number; minRounds?: number; maxRounds: number; objective: string; exitDescription: string; /** Optional longer brief / hints for the Agent. */ body?: string; acceptance?: string; dispatchesThisRound: number; /** In-flight Subagent jobs for the current Loop round (pending|running). */ liveSubagents: number; /** True when dispatches≥1, no live children this round, and bucket is verify. */ canVerify: boolean; endReason?: LoopRun['endReason']; lastVerification?: LoopVerification & { at: number; }; lastEvaluator?: LoopRun['lastEvaluator']; lastPlan?: LoopRun['lastPlan']; /** Present while status=pending_user_confirm (met=true awaiting user). */ pendingConfirmation?: { summary: string; gaps?: string[]; evaluatorReason?: string; minRounds?: number; belowMinRounds: boolean; }; usage: LoopUsage; openedFocus: boolean; createdAt: number; updatedAt: number; maxWallMs?: number; } export declare function toLoopStatusDto(run: LoopRun, lang?: 'zh' | 'en'): LoopStatusDto; /** Markdown summary card when a Loop ends (R5). */ export declare function formatLoopEndedSummary(run: LoopRun): string; /** Always-readable objective + exit (mission first, drafts as fallback). */ export declare function getMissionView(run: LoopRun): LoopMissionView; /** Status / prompt lines for the two hard fields (+ optional acceptance). */ export declare function formatMissionFields(run: LoopRun, opts?: { draftLabel?: boolean; }): string[]; /** Best-effort fans for Portal WS (and tests). Never throw to callers. */ type LoopRunWrittenListener = (run: LoopRun) => void; /** Subscribe to successful thread-snapshot writes (status transitions). */ export declare function onLoopRunWritten(fn: LoopRunWrittenListener): () => void; /** Load one Loop by run id from history (falls back to thread snapshots). */ export declare function getLoopRunById(runId: string): LoopRun | null; export interface ListLoopRunsOpts { status?: LoopStatus | 'active'; platform?: string; threadId?: string; sessionKey?: string; limit?: number; } /** * List Loop runs for Tasks audit — history first, merge thread snapshots * (legacy files written before history existed). Newest `updatedAt` first. */ export declare function listLoopRuns(opts?: ListLoopRunsOpts): LoopRun[]; /** Rich DTO for Tasks audit detail (includes timeline + job ids). */ export interface LoopAuditDto extends LoopStatusDto { platform: string; channelId: string; threadId: string; focusSnapshot?: string; loopJobIds: number[]; roundJobIds: number[]; timeline: LoopTimelineEntry[]; lastVerdict?: LoopRun['lastVerdict']; endedSummary?: string; historyPath?: string; /** Linked detail-report files (absolute paths). */ reportPaths: string[]; hasReport: boolean; } export declare function toLoopAuditDto(run: LoopRun, lang?: 'zh' | 'en'): LoopAuditDto; /** Compact list row for Tasks table. */ export interface LoopListItemDto { id: string; sessionKey: string; platform: string; channelId: string; threadId: string; status: LoopStatus; bucket: LoopUxBucket; bucketLabel: string; active: boolean; objective: string; exitDescription: string; round: number; maxRounds: number; endReason?: LoopRun['endReason']; dispatchCount: number; childJobCount: number; hasReport: boolean; createdAt: number; updatedAt: number; } export declare function toLoopListItemDto(run: LoopRun, lang?: 'zh' | 'en'): LoopListItemDto; /** @internal test helper — expose history path resolution. */ export declare function _historyPathForTests(runId: string): string; export declare function getLoopRun(platform: string, channelId: string, threadId: string): LoopRun | null; export declare function getActiveLoop(platform: string, channelId: string, threadId: string): LoopRun | null; export declare function isLoopAligning(run: LoopRun): boolean; export declare function isLoopDispatchAllowed(run: LoopRun): boolean; /** True when mission.exit.maxWallMs elapsed since run.createdAt. */ export declare function isLoopWallExceeded(run: LoopRun, now?: number): boolean; export type LoopExitEnforceResult = { ended: boolean; run: LoopRun | null; reason?: NonNullable; }; /** @internal */ export declare function _setTestLiveSubagents(value: boolean | null): void; /** * Live Subagent count for the active Loop's current round. * * Only `roundJobIds` count. An empty id list is 0 even when * `dispatchesThisRound ≥ 1` — do not fall back to every child job on the * thread (unrelated / leftover jobs must not block VERIFY). A2A registers * each id in `noteLoopRoundJob` immediately after `createInlineJob`. */ export declare function countRoundLiveSubagents(run: LoopRun): number; /** Count of A2A Subagent jobs still pending/running for this Loop round. */ export declare function countThreadLiveSubagents(platform: string, channelId: string, threadId: string): number; /** True while this Loop round still has pending/running Subagent jobs. */ export declare function threadHasLiveSubagents(platform: string, channelId: string, threadId: string): boolean; /** * Register a Subagent job id on the active Loop's current round. * Called when A2A createInlineJob succeeds so live wait is round-scoped. */ export declare function noteLoopRoundJob(platform: string, channelId: string, threadId: string, jobId: number): void; /** * After an A2A Subagent job reaches a terminal status, touch the Loop * snapshot so `loop-changed` fires and Portal docks recompute * `liveSubagents` / `canVerify` immediately — not only on the next 4s poll. * * Without this, Codex (etc.) can finish while the dock stays on「待派工」 * until a coincidental writeRun (or the parent turn eventually continues). */ export declare function notifyLoopJobTerminal(platform: string, channelId: string, threadId: string, jobId: number, opts?: { outcome?: string; agent?: string; }): void; /** * Count Subagent jobs belonging to this Loop (for Portal report-offer copy). * Prefers cumulative `loopJobIds`; falls back to the last round only. */ export declare function countLoopChildJobs(platform: string, channelId: string, threadId: string): number; /** * Active exit truncation that does NOT wait for loop_verdict. * Currently: wall clock. Call at turn start / after dispatch join / status poll. * * Defers while Subagents are still live — ending the Loop mid-dispatch left * UI cards in「等待子代理」after the Loop already showed ended. */ export declare function maybeEnforceLoopExit(platform: string, channelId: string, threadId: string, now?: number): LoopExitEnforceResult; /** * When VERIFY follow-up still did not produce loop_verdict, force-end the Loop * so it cannot hang forever in「待验收」with a delivered report. * Also defers while Subagents are still live. */ export declare function forceLoopExitSkippedVerify(platform: string, channelId: string, threadId: string): LoopExitEnforceResult; /** Parse exit NL into hard round / wall-clock fields (fail-soft). */ export declare function parseExitConstraints(exitDescription: string): { minRounds?: number; maxRounds?: number; maxWallMs?: number; }; /** Parse `/loop …` payload for objective + exit (NL-friendly). */ export declare function parseLoopStartText(text: string): { objective: string; exitDescription: string; body?: string; acceptance?: string; minRounds?: number; maxRounds?: number; maxWallMs?: number; }; /** * Format pre-existing Focus as a Mission.body appendix. * Path A must NOT seed objective from Focus — append here instead. */ export declare function formatFocusBodyAppendix(focusContent: string): string; export type StartLoopResult = { ok: true; run: LoopRun; path: 'A' | 'B'; aligned: boolean; } | { ok: false; error: string; }; /** * Begin a Loop. Always creates a run; if objective or exit missing → aligning. * Path A: Focus already on. Path B: no Focus → require objective text to set Focus. */ export declare function startLoop(input: { platform: string; channelId: string; threadId: string; userId: string; text?: string; }): StartLoopResult; export declare function setLoopObjective(platform: string, channelId: string, threadId: string, objective: string): { ok: true; run: LoopRun; } | { ok: false; error: string; }; export declare function setLoopExit(platform: string, channelId: string, threadId: string, exitDescription: string): { ok: true; run: LoopRun; } | { ok: false; error: string; }; export declare function setLoopAcceptance(platform: string, channelId: string, threadId: string, acceptance: string): { ok: true; run: LoopRun; } | { ok: false; error: string; }; /** Optional longer brief — aligning or after confirm (refine). */ export declare function setLoopBody(platform: string, channelId: string, threadId: string, body: string): { ok: true; run: LoopRun; } | { ok: false; error: string; }; export declare function confirmLoopMission(platform: string, channelId: string, threadId: string): { ok: true; run: LoopRun; } | { ok: false; error: string; }; export type LoopPlanTask = { id: string; brief: string; agentHint?: string; }; /** * Record that the active Loop accepted ≥1 subagent spawn this round. * Moves planning → dispatching. No-op when aligning / ended / no active loop. */ export declare function noteLoopDispatch(platform: string, channelId: string, threadId: string, input?: { count?: number; tasks?: LoopPlanTask[]; }): LoopRun | null; /** Record independent evaluator result (does not count as R3 dispatch). */ export declare function noteLoopEvaluator(platform: string, channelId: string, threadId: string, result: { agree: boolean; reason: string; skipped?: boolean; }): LoopRun | null; export declare function stopLoop(platform: string, channelId: string, threadId: string, reason?: 'user_stop' | 'stop_verdict' | 'limit' | 'error'): LoopRun | null; export declare function cancelLoopForFocusOff(platform: string, channelId: string, threadId: string): LoopRun | null; export declare function applyLoopVerdict(platform: string, channelId: string, threadId: string, verdict: LoopVerdict): { ok: true; run: LoopRun; } | { ok: false; error: string; }; /** * User resolves met=true confirmation dock: end as objective_met, or continue * another planning round (optional supplement → nextDirection). */ export declare function resolveLoopMetConfirm(platform: string, channelId: string, threadId: string, input: { action: 'end' | 'continue'; supplement?: string; }): { ok: true; run: LoopRun; } | { ok: false; error: string; }; export declare function buildLoopSnippet(platform: string, channelId: string, threadId: string, opts?: { preferredWorkerAgent?: string; }): string; export declare function formatAligningPrompt(run: LoopRun): string; /** Test-only: backdate createdAt for wall-clock exit coverage. */ export declare function _touchLoopCreatedAtForTests(platform: string, channelId: string, threadId: string, createdAt: number): void; export declare function formatLoopStatus(run: LoopRun): string; export {}; //# sourceMappingURL=loop-run.d.ts.map