/** * @packageDocumentation * [[EXT-178]] — the run ended and nothing went wrong. Say what it was for, what it did, and what * is left. * * `terminationNotice.ts` announces every ending except two, and `outstandingWork.ts` speaks for a * checklist left unfinished. Between them sits the stop this module exists for: `completed`, with * text, no tool calls and no error — the one ending that *looks* like success and need not be one. * Today it says nothing at all, by design, which is right for a run that finished the job and * silent for a run that did not. * * ## IT REPORTS. IT NEVER ACTS. (Andrew, 2026-09-16 — the nudge is out of scope.) * * **Nothing here may continue a run.** No `jumpTo`, no injected `HumanMessage`, no budgeted retry, * no "small opt-in hook". A recap that finds outstanding work says so to the **user** and stops. * Continuing a run automatically was [[EXT-158]]'s question and was ruled there. * * The mechanism is chosen to make that structural rather than promised: the recap is produced by * {@link askStructured}, which is **non-agentic** — one `withStructuredOutput` call against * `config.llm`, no graph, no tools, no message history written back. There is no seam here through * which a tool call could be issued or a turn resumed, so "add a small nudge" is not a change to a * parameter; it is a different mechanism, which is what a future reader should have to confront. * * ## WHY THE RECAP SUBSUMES [[EXT-158]]'s NOTICE, AND WHAT MAKES THAT SAFE * * Both surfaces speak for the **same** population — a clean stop with work outstanding — and two * statements about one stop is the banner problem both nodes are trying to avoid. So when the * recap renders, the notice does not: {@link runEndReport} is the one place that decides, and it * returns exactly one of the two. * * What makes suppressing a shipped feature safe is that the recap is **required to carry what the * notice would have said**. {@link runRecapNotice} renders the outstanding-work counts from the * detector's own {@link GthOutstandingWork} value, in this module's own words — never from the * model's prose. So the load-bearing half of the notice survives verbatim even if the model's * summary is vague, wrong, or steered by a prompt injection in the transcript it was shown. The * model supplies recall the counts cannot; the counts supply a guarantee the model cannot. * * **The detector is consumed, never re-derived.** `detectOutstandingWork` is a pure function * of a message list and its docblock says a second consumer is expected; this is it. Asking the * same question a second way would produce two answers that drift, and would make the suppression * above a lie. * * ## THE QUOTA PROPERTY IS STRUCTURAL, NOT A SUPPRESSION RULE * * {@link shouldRequestRunRecap} gates **positively** on `completed`, exactly as * {@link shouldAnnounceOutstandingWork} does, and never as "any category * `shouldAnnounceTermination` declines" — that reads as the natural complement and is a defect, * because it also declines `suspended`, a run parked on a tool-approval interrupt whose checklist * is outstanding *by construction*. Every gated tool call would have drawn a paid model call. * * The same shape is what keeps this off a rate-limited turn. A run killed by a `rate_limited` * refusal never reaches `completed`, so it never reaches here — excluded by the taxonomy rather * than by a rule someone has to get right. A recap that also fired on error stops would spend a * model call per failure and reintroduce the amplifier EXT-158 was corrected to avoid. */ import type { BaseMessage } from '@langchain/core/messages'; import type { GthConfig } from '#src/config.js'; import type { GthRunRecapRung } from '#src/config/schema.js'; import type { GthTerminationReason } from '#src/core/terminationReason.js'; import { type GthOutstandingWork, type GthOutstandingWorkNotice } from '#src/core/outstandingWork.js'; /** * The rung a config with no `recap` key resolves to. * * **`off`, and the argument is about what a default may spend rather than about the feature.** A * recap is a second model call at the end of a turn: the user's tokens, the user's latency, the * user's money. A default that starts spending those without anyone asking is not a default added * during a config freeze, and its failure direction is the bad one — every run quietly slower and * dearer, discovered late and by someone who never heard of this node. Default-off fails the other * way: a feature nobody switched on, recoverable by one line in a config, by the person who * noticed. * * **The decisive half is [[EXT-158]].** Its notice is deterministic, free, bounded and shipped, and * the ruling above says an enabled recap *suppresses* it. A default-on recap would therefore * silently replace a working guarantee with a paid one on every existing installation — a feature * removal disguised as a feature addition. Off means the floor stays the floor and the recap is * strictly additive. * * This is a deliberate departure from the node's own reading that "firing every time is the feature * working". That is true of the *rendering* and it is why `RUN_RECAP_RUNGS` has an `always` * rung at all; it is not an argument about what an unconfigured install should do. */ export declare const DEFAULT_RUN_RECAP_RUNG: GthRunRecapRung; /** * Wall-clock budget for the recap's model call. * * Shorter than {@link askStructured}'s own 30s default, deliberately. This call happens *after* the * user has their answer, so every millisecond it takes is time a finished run spends looking * unfinished — on the TUI the prompt is already back, and on `gth ask` the process is holding the * terminal. A recap that has not arrived in ten seconds has already cost more than it is worth, and * the failure is soft: {@link runEndReport} falls back to the notice. */ export declare const RUN_RECAP_TIMEOUT_MS = 10000; /** * How many of the newest messages the recap is shown. * * A window rather than the whole conversation, because the input is a cost as well as a context: a * long `code` session would otherwise send its entire history through a second model every turn, * which is the shape of feature that gets switched off for reasons nobody attributes to it. The * goal is extracted separately ({@link buildRunRecapSource}) so a window this small never loses the * one thing the recap is chiefly about. */ export declare const RUN_RECAP_MESSAGE_WINDOW = 24; /** Characters of the rendered transcript window handed to the model, after which it is clipped. */ export declare const RUN_RECAP_TRANSCRIPT_MAX_CHARS = 12000; /** Characters of any single message kept in the transcript window. */ export declare const RUN_RECAP_MESSAGE_MAX_CHARS = 1200; /** Characters of the extracted goal kept. */ export declare const RUN_RECAP_GOAL_MAX_CHARS = 1000; /** Characters of any one model-written recap field rendered to the terminal. */ export declare const RUN_RECAP_FIELD_MAX_CHARS = 600; /** Appended when one of the caps above actually clipped something. */ export declare const RUN_RECAP_TRUNCATION_MARKER = "\u2026 [truncated]"; /** The opening of the recap's title, exported for the reason every other notice prefix here is: * two surfaces put this into a channel that also carries other messages, and a consumer keying on * its own copy of the string finds out too late that the two have drifted. */ export declare const RUN_RECAP_TITLE_PREFIX = "Run recap: "; /** * The sentence that says whose words the recap is. * * **It is not the same claim [[EXT-158]]'s marker makes, and the difference is the point.** That * notice states counts the runtime read out of the message history; this one states a summary the * *model* wrote about its own run, from a transcript that may contain tool output, fetched pages * and anything else an attacker could put in front of it. A reader who mistakes it for the * runtime's own finding has been told something false about how much it can be trusted, so the * marker says model-written in words, once, in every place the recap is carried. */ export declare const RUN_RECAP_AUTOMATED_MARKER: string; /** The line that reports outstanding work, labelled as the runtime's own count. */ export declare const RUN_RECAP_COUNTS_PREFIX = "Checklist: "; /** The bounded, deterministic inputs one recap call is built from. */ export interface GthRunRecapSource { /** What the user last asked for, clipped to {@link RUN_RECAP_GOAL_MAX_CHARS}. */ goal: string; /** The newest {@link RUN_RECAP_MESSAGE_WINDOW} messages, rendered compactly and clipped. */ transcript: string; } /** A recap as the model returned it, plus the runtime's own outstanding-work value. */ export interface GthRunRecap { /** What the run was for, in the model's words. */ goal: string; /** What the run actually did, in the model's words. */ happened: string; /** What the model believes is still outstanding, in its words. */ outstanding: string; /** The model's own answer to the side duty: did this run finish the job? */ complete: boolean; /** * **The runtime's own finding**, carried alongside the prose and rendered from separately. * * This is what makes suppressing [[EXT-158]]'s notice safe: the counts reach the user whatever * the model wrote. `null` means the detector found nothing, which is the ordinary case and not a * defect — a run may simply have kept no checklist. */ work: GthOutstandingWork | null; } /** A rendered recap: a title and the body lines under it. */ export interface GthRunRecapNotice { /** **The carrier.** The fact travels as this value; the strings are derived from it. */ recap: GthRunRecap; title: string; lines: string[]; } /** * What one surface should say about how a run ended, once the recap has been resolved. * * **The subsumption contract lives here and nowhere else.** Every surface renders what this * returns rather than deciding for itself, so six surfaces cannot come to disagree about which of * the two notices speaks — the same reason [[EXT-158]] takes its repeat decision in the agent * instead of at each surface. */ export type GthRunEndReport = { kind: 'recap'; notice: GthRunRecapNotice; } | { kind: 'outstanding'; notice: GthOutstandingWorkNotice; } | { kind: 'silent'; }; /** * The rung this config resolves to. * * **Defaulted at the READ SITE, not in `DEFAULT_CONFIG`**, matching `output.header`, * `injectModelContext`, `debugDump.redact` and `toolLoopGuard`: a default in `DEFAULT_CONFIG` * churns the effective-config snapshot, and a `.default()` in the zod schema would be emitted into * the published JSON Schema as though the schema decided it. */ export declare function resolveRunRecapRung(config: Pick | null | undefined): GthRunRecapRung; /** * Should this stop be given a recap at all — i.e. is a model call warranted here? * * Three gates, each of which excludes a population the others do not: * * 1. **The rung.** `off` never; `always` on every clean stop; `outstanding` only where the detector * found something. `outstanding` is the rung that answers this node's own "banner nobody reads" * constraint without turning the feature off — the recap then fires exactly where the silence * was actually costing something, and a clean run stays clean. * 2. **The ending.** Positively `completed`, for the reason the module doc gives: the complement * form would sweep in `suspended`, and every gated tool call would buy a model call. * 3. **The repeat bound.** A checklist state that has already been announced does not buy a second * recap on the `outstanding` rung. `OUTSTANDING_WORK_NOTICE_MAX_PER_SIGNATURE` is [[EXT-158]]'s * bound on how often one stall episode may be spoken about, and a rung that ignored it would be * a way to defeat that bound rather than a second opinion about it — a stuck model re-emits an * identical checklist turn after turn, which is precisely the state the bound exists for. * * **`always` is deliberately NOT gated on `repeat`**, and the asymmetry is the rung's meaning: * `always` says "recap every clean stop", and a run whose checklist has not moved is still a run * the user asked to have summarised. What `repeat` suppresses is the *warning* about a stall, * and {@link runRecapNotice} does not warn — it reports counts inside a summary the user * requested unconditionally. A rung that fell silent on the second identical turn would be * `outstanding` wearing another name. */ export declare function shouldRequestRunRecap(rung: GthRunRecapRung, work: GthOutstandingWork | null | undefined, reason: GthTerminationReason | null | undefined): boolean; /** * The bounded inputs a recap is built from, or `null` when there is nothing to summarise. * * A **pure function of a message list**, deliberately — no config, no model, no I/O — so it is unit * testable on its own and so the snapshot an agent takes at the end of a turn can outlive the agent * itself. That matters because the non-interactive verbs read the recap after `cleanup()` has * dropped the agent, exactly as they read the outstanding-work value. * * **The goal is the NEWEST human turn, not the first.** In a `chat` or `code` session the first * thing the user typed is often an hour and twenty turns behind; "what the goal was" at the end of * *this* turn is what they last asked for. The window below still carries the surrounding context, * so an earlier goal is not lost, only demoted. * * Everything here is untrusted: the transcript carries tool output, fetched pages and MCP server * text. It is defanged and capped before it is composed into a prompt, so a payload cannot forge * this codebase's own delimiters inside the block the recap model is shown. */ export declare function buildRunRecapSource(messages: readonly BaseMessage[] | undefined): GthRunRecapSource | null; /** * The instructions the recap model is given. * * Exported so the unit suite can assert on what the call was actually made with, and so the two * properties that are easy to lose in an edit are visible to a reader: it asks for a report and * never for a next step, and it tells the model that the transcript it is shown is untrusted data * rather than instructions addressed to it. */ export declare const RUN_RECAP_SYSTEM_PROMPT: string; /** * Ask the configured model for a recap of the run that just ended. * * Returns `null` for every "no recap" outcome — the rung is off, the ending was not `completed`, * there was nothing to summarise, no model is configured, the call timed out, the provider refused, * or the answer failed the schema. That collapse is deliberate: {@link runEndReport} treats any * `null` as "the recap did not speak", which is what restores [[EXT-158]]'s notice, and a caller * that could tell the failures apart would be tempted to render the difference to a user who cannot * act on it. The distinction is written to the debug log, where it is a maintainer's question. * * **Never throws.** Explaining a finished run must never be what breaks it. */ export declare function requestRunRecap(opts: { config: GthConfig; source: GthRunRecapSource | null | undefined; work: GthOutstandingWork | null | undefined; reason: GthTerminationReason | null | undefined; }): Promise; /** * Render a recap for a surface that shows a title and body lines. * * ## THE COUNTS ARE OURS, THE PROSE IS THE MODEL'S, AND THEY ARE RENDERED SEPARATELY * * The outstanding-work line is built from {@link GthRunRecap.work} — the value * `detectOutstandingWork` produced — in this module's own words, using the same counts * {@link outstandingWorkNotice} would have printed. **That is the whole basis for suppressing the * notice.** If this line were derived from the model's `outstanding` string instead, an enabled * recap would silently downgrade a deterministic guarantee to a model's impression of one, and a * vague or injected summary would lose the fact entirely. * * Every model-written field goes through {@link neutralizeUntrustedText} and the defanger first. * `outstandingWorkNotice`'s own docblock hands that obligation to this consumer by name: it renders * counts precisely so it never has to, and says a consumer that wants more must neutralise it * itself. Recap prose is model-authored text on its way to a terminal, summarising a transcript * that may contain anything, so it is treated exactly like tool output. */ export declare function runRecapNotice(recap: GthRunRecap): GthRunRecapNotice; /** * **The subsumption decision.** Given a recap that has already been resolved, say which of the two * surfaces speaks about this stop — and never both. * * Synchronous and total, taking the recap as an **input** rather than awaiting it here. That shape * is the guard: a future edit cannot move the suppression ahead of the model call, so a recap that * timed out, hit an unconfigured model or failed its schema arrives here as `null` and the notice * is restored. A version that decided to suppress *before* knowing whether the recap would arrive * would turn every provider hiccup into a silent regression of a shipped feature. * * The `outstanding` arm delegates entirely to {@link shouldAnnounceOutstandingWork}, so this cannot * come to disagree with [[EXT-158]]'s own gate about `suspended`, about a repeat, or about a `null` * reason. */ export declare function runEndReport(recap: GthRunRecap | null | undefined, work: GthOutstandingWork | null | undefined, reason: GthTerminationReason | null | undefined): GthRunEndReport; /** One line for the debug log, stating what the end-of-run report decided. */ export declare function runEndReportLogLine(report: GthRunEndReport): string; /** * Say it on a console surface — the readline session and the non-interactive verbs — and write what * was decided to the debug log either way. * * Returns the kind that spoke, so a caller can tell a suppressed notice from a silent stop. * * `displayNotice` writes to **stderr** through `su.error`, which is the property [[EXT-158]] was * careful to keep and this must not lose: `gth batch` and `gth eval` assert on the stdout a run * produces and on the `answer` string `runSingleShot` returns, and neither may move because a * recap was switched on. `tone: 'info'` rather than the notice's `'warn'`, since a recap of a run * that finished cleanly is not a warning — and on the stop where it replaces the warning, the line * carrying the counts says what it is. * * Fail-soft in the strongest sense: reporting a finished run must never become a second failure. */ export declare function displayRunEndReport(report: GthRunEndReport): GthRunEndReport['kind'];