/** * Agent follow-through guard. * * An agent that tells the owner "I'm doing X now" and then ends its turn has * broken a promise that nothing tracked (2026-10-10, Pia, three videos: two * turns ended on "I haven't started yet — the next step is …" and she was * idle-stopped 24 minutes later). This service remembers the last stated * intent per agent and, at turn end, nudges the agent once when nothing * followed it — no tool call, no WorkItem for the work, no hand-off. * * Cheap by construction: text heuristics ({@link detectStatedIntent}) plus * the runtime's own tool hooks; no model call. Off switch: * `CREWLY_FOLLOW_THROUGH=off`. * * @module services/agent/follow-through.service * @see specs/2026-10-10-agent-follow-through.md */ /** Tunables (ms). */ export declare const FOLLOW_THROUGH_CONSTANTS: { /** A statement older than this is stale: the turn that ended now is not about it */ readonly MAX_AGE_MS: number; /** A statement keeps its agent from being idle-stopped for this long */ readonly HOLD_MS: number; /** Never nudge one agent more often than this, whatever it says next */ readonly NUDGE_COOLDOWN_MS: number; /** Without tool hooks: PTY output for longer than this after the statement counts as work */ readonly OUTPUT_WORK_MS: 45000; /** After a runtime restart the first tool call is the harness's own register-self */ readonly TOOLS_AFTER_RESTART: 2; /** * Tool calls after the statement that count as having started. One is not * enough: the reply itself is followed by housekeeping (status mark, * memory write) that is not the promised work (Pia, 2026-10-11). */ readonly TOOLS_TO_COUNT: 3; /** A statement this old whose agent is quiet is checked without waiting for a turn-end event */ readonly SWEEP_AFTER_MS: 60000; /** How often open statements are swept */ readonly SWEEP_INTERVAL_MS: 20000; /** Quiet for this long = the agent is not mid-turn */ readonly QUIET_MS: 15000; }; /** What the guard needs from the rest of the backend. */ export interface FollowThroughDeps { /** Deliver text into the agent's conversation (wakes nothing; false when not delivered) */ nudgeAgent: (session: string, text: string) => Promise; /** Tool calls started after `since`; null when the runtime reports no hooks */ toolStartsSince: (session: string, since: number) => number | null; /** Epoch ms the agent's runtime process started, when known */ runtimeStartedAt?: (session: string) => number | null; /** Fallback without hooks: ms of PTY output after `since`; null when unknown */ outputSpanSince?: (session: string, since: number) => number | null; /** The agent handed the work to someone or put it on the pool since `since` */ handedOff: (session: string, since: number) => Promise; /** The agent is waiting on an owner card / question */ waitingOnOwner?: (session: string) => Promise; /** Owner-stopped or team-paused agents are never nudged */ isHeldBack?: (session: string) => boolean; /** The agent produced no output for {@link FOLLOW_THROUGH_CONSTANTS.QUIET_MS} (sweep only) */ isQuiet?: (session: string) => boolean; /** Make the statement a durable WorkItem for the agent; returns its id */ commit?: (session: string, sentence: string, at: number) => Promise; /** The statement was answered: close its WorkItem */ settle?: (workItemId: string) => Promise; now?: () => number; } /** What a turn end did. */ export type TurnEndOutcome = 'none' | 'stale' | 'worked' | 'handed_off' | 'waiting' | 'held' | 'cooldown' | 'nudged' | 'not_delivered'; /** * @param sentence - What the agent said * @returns The nudge text (harness text, English) */ export declare function followThroughNudge(sentence: string): string; /** * Tracks the last stated intent per agent and nudges once when a turn ends * without following it. */ export declare class AgentFollowThroughService { private readonly deps; private readonly statements; private readonly lastNudgeAt; private readonly lastReplyAt; private readonly logger; /** @param deps - Injected behaviour */ constructor(deps: FollowThroughDeps); private now; /** * An agent posted to the owner (Slack or chat). A post that states an intent * is remembered; any other post supersedes the earlier statement. * * @param input - Agent, the post's text, whether it was an interim "working on it" mark */ noteAgentPost(input: { agent: string; text: string; interim?: boolean; at?: number; }): void; /** * When the agent last posted something that was not a stated intent (an * answer, a result, a question). Ticket work creation reads it. * * @param agent - Session * @returns Epoch ms, or undefined */ lastRealReplyAt(agent: string): number | undefined; /** * Whether the agent has an unfulfilled stated intent (nudged or not) that * is recent enough to keep it from being idle-stopped. * * @param agent - Session * @returns True while one is open */ holdsIntent(agent: string): boolean; /** * Take the open statement away (the caller turns it into a WorkItem). * * @param agent - Session * @returns The sentence, or null */ takeIntent(agent: string): { sentence: string; at: number; workItemId?: string; } | null; /** * Age of the agent's open stated intent. * * @param agent - Session * @returns Milliseconds, or null when none is open */ intentAgeMs(agent: string): number | null; /** * The agent's turn ended: nudge it once when it stated an intent that * nothing followed. Never throws. * * @param agent - Session * @returns What happened */ onTurnEnd(agent: string): Promise; /** * Check statements whose agent went quiet without a turn-end event reaching * us (the post can land after the turn-end settle ran). Called on a timer. * * @returns Agents nudged */ sweep(): Promise; /** Whether the agent did anything after the statement. */ private followed; } /** @returns The wired guard, or null before boot (and when switched off) */ export declare function getFollowThrough(): AgentFollowThroughService | null; /** @param service - The guard to expose (null clears) */ export declare function setFollowThrough(service: AgentFollowThroughService | null): void; /** * Report an agent's post to the owner, if the guard runs. Never throws. * * @param input - Agent, text, interim flag */ export declare function reportAgentPostForFollowThrough(input: { agent: string; text: string; interim?: boolean; }): void; //# sourceMappingURL=follow-through.service.d.ts.map