/** * pause — runner-level pause/resume primitives. * * Pattern: Control-flow exception (PauseRequest) + typed outcome (RunnerPauseOutcome). * Role: core/ layer. Bridges footprintjs.s pause signal into the agentfootprint * Runner contract: tools call `pauseHere(data)` to raise a pause * intent; runners detect the paused executor result and return a * `RunnerPauseOutcome` instead of `TOut`. Consumers call * `runner.resume(checkpoint, input)` to continue. * Emits: N/A (types + helpers only). Event emission happens in RunnerBase. * * Why a control-flow "throw": tool.execute(args, ctx) doesn't receive the * typed scope, so it cannot call `scope.$pause()` directly. A thrown * `PauseRequest` is caught inside the Agent's tool-call stage, which then * forwards the pause into the flowchart via `scope.$pause()`. This keeps * the tool API clean (tools are pure-ish) while still supporting pause. */ import type { FlowchartCheckpoint } from 'footprintjs'; import type { AskComponent } from './askComponent.js'; import type { CheckInRequest } from './checkin.js'; /** * Outcome returned by `runner.run()` / `runner.resume()` when execution * has paused mid-flow. The shape mirrors footprintjs's `PausedResult` but * surfaces `pauseData` as a first-class field for consumers who don't * want to reach into the checkpoint. */ export interface RunnerPauseOutcome { readonly paused: true; /** Serializable checkpoint — store anywhere (Redis, Postgres, localStorage). */ readonly checkpoint: FlowchartCheckpoint; /** Data passed to `scope.$pause()` / `pauseHere()`. Consumer-typed. */ readonly pauseData: unknown; /** * Present ONLY when this pause is an evidence-carrying check-in (a tool * declared `checkIn`). Carries the typed ask + evidence pack. Absent for * plain `askHuman` / `pauseHere` pauses — that's the clean discriminant * between the two pause kinds. Resume with a `CheckInDecision` * (`checkInApproved` / `checkInDeclined`). */ readonly checkIn?: CheckInRequest; /** * Present ONLY when a `toolMiddleware` answered `ask` — the question it put * to a person, plus the middleware that asked. Absent for every other pause, * which is the discriminant. * * Resume with a `CheckInDecision` (`checkInApproved` / `checkInDeclined`). * That is deliberate rather than a second decision type: a person approving * is a person approving, whether the gate was a tool's `checkIn` or a * middleware's `ask`, and one word for one thing beats a synonym. * * The answer is a DECISION, not a result. Approve and the chain resumes from * the next middleware and the REAL tool runs; decline and the model receives * a denial it can adapt to. Nobody — not the middleware, not the person — * gets to write the tool's answer. */ readonly ask?: MiddlewareAsk; } /** The question a `toolMiddleware` put to a person, as it rides the checkpoint. */ export interface MiddlewareAsk { /** The question, in the middleware author's own words. */ readonly question: string; /** Anything else the answering UI should render. Never interpreted here. */ readonly detail?: unknown; /** * Which REGISTERED screen component collects the answer (9.24.0) — the * typed half of the question, carried from `ask({ question, component })`. * Absent means what it always meant: render the prose. The answer comes * back through the same `CheckInDecision` either way. */ readonly component?: AskComponent; /** `name` of the middleware that asked. */ readonly middleware: string; } /** Type guard — discriminates `RunnerPauseOutcome` from a normal `TOut`. */ export declare function isPaused(result: T | RunnerPauseOutcome): result is RunnerPauseOutcome; /** * Type guard — is this a check-in pause (evidence-carrying human consent), * as opposed to a plain `askHuman` pause? Narrows `checkIn` to present. * * @example * const out = await agent.run({ message }); * if (isCheckInPause(out)) { * showToHuman(out.checkIn.evidence); // the receipts * const decision = checkInApproved({ by: 'alice' }); * await agent.resume(out.checkpoint, decision); * } */ export declare function isCheckInPause(result: unknown): result is RunnerPauseOutcome & { readonly checkIn: CheckInRequest; }; /** * Type guard — is this a middleware-ask pause (a `toolMiddleware` answered * `ask`), as opposed to a check-in or a plain `askHuman` pause? Narrows `ask` * to present. * * @example * const out = await agent.run({ message }); * if (isAskPause(out)) { * const yes = await showToHuman(out.ask.question); // asked by out.ask.middleware * await agent.resume(out.checkpoint, yes * ? checkInApproved({ by: 'alice' }) * : checkInDeclined({ by: 'alice', note: 'not this one' })); * } */ export declare function isAskPause(result: unknown): result is RunnerPauseOutcome & { readonly ask: MiddlewareAsk; }; /** * The two pause kinds whose answer is a DECISION rather than a value. * * A **`'checkIn'`** pause is the tool's own consent demand, and its identity is * the evidence pack. A **`'ask'`** pause is a `toolMiddleware`'s own question. * Both are answered with `checkInApproved()` / `checkInDeclined()`. * * Everything else — `pauseHere()` / `askHuman()`, and the 3LO credential-consent * pause — is NOT a consent gate in this sense: there the human's answer either * IS the tool's result or is ignored entirely, and any value is accepted. */ export type ConsentGateKind = 'checkIn' | 'ask'; /** What {@link pauseDemandsDecision} reports about a pause that is a consent gate. */ export interface ConsentGate { readonly kind: ConsentGateKind; /** The tool the gate is about, when the pause payload named one. */ readonly toolName?: string; /** `'ask'` only — the `name` of the middleware that asked. */ readonly middleware?: string; } /** * Read a pause payload and say whether answering it requires a * `CheckInDecision` — and if so, which gate is outstanding. * * THE ONE reader of that shape. `RunnerBase.detectPause` builds * `outcome.checkIn` / `outcome.ask` from this, and `Agent.resume` refuses a * mis-shaped answer from this, so the surface a consumer is told about and the * surface the library enforces cannot drift apart. * * Keyed on the PAUSE, never on the input: a plain `askHuman` answer is a string * and must stay one, so "is this the right answer?" can only be decided by * knowing what was asked. * * @returns the gate, or `undefined` when this pause takes any value. */ /** * Refuse an answer chosen against different bytes than the question was asked * against. See {@link StaleDecisionError}. * * Silent in every case but one, deliberately: the ask has to have pinned a ref * AND the answer has to name one. Either side absent means nobody claimed the * two were about the same artifact, and inventing that claim here would refuse * answers that are perfectly good. */ export declare function assertDecisionIsNotStale(pauseData: unknown, input: unknown): void; export declare function pauseDemandsDecision(pauseData: unknown): ConsentGate | undefined; /** * Raised by `agent.resume()` when a run paused on a CONSENT GATE and the resume * input is not a `CheckInDecision`. * * Before 8.13.0 that resume silently DECLINED, attributed to `by: 'unknown'` — * a `checkin.decision` record naming a person who was never asked. A governance * layer that invents a decision is worse than one that drops it: the run reads * as consented-and-refused when nobody consented to anything. * * **Nothing was executed and the checkpoint is unchanged** — this refuses at the * API boundary, before the engine is handed the checkpoint. Answer the gate and * resume the same checkpoint again. * * A plain `askHuman()` / `pauseHere()` pause never raises this: there the * human's answer IS the tool's result, so any value is accepted. The 3LO * credential-consent pause never raises it either — it re-asks the provider and * ignores the input by design. */ /** * The person answered about something the ask was not about. * * A typed ask can pin the data the answer is to be chosen FROM — the * `propsRef` on its {@link AskComponent}, a 200-row picker's options living in * the artifact store rather than in every stored session envelope. A * {@link DecisionValue} can say which artifact the choice was made AGAINST. * When both are present and they disagree, the answer is about different bytes * than the question. * * That happens for an ordinary reason: time passes between asking and * answering. A refresh lands, a filter moves, rows re-sort, the ask is re-raised * with new options — and "the third row" now names something else. Accepting it * would resume the run with a value the person never chose, and there is no * later signal that anything went wrong: the id is well-formed, the type checks, * the loop continues. * * So it refuses, and the refusal is the feature. A person asked again is mildly * annoyed; a person whose gesture was silently reinterpreted has been * misrepresented, and in a consent record that is the one failure nothing * downstream can recover from. * * Only ever raised when the ask pinned a ref AND the answer named one. An * approve/decline, a plain `askHuman` value, or an answer that names no * artifact are all byte-identical to every earlier release. */ export declare class StaleDecisionError extends Error { readonly code: "ERR_STALE_DECISION"; /** The artifact the QUESTION was asked against. */ readonly asked: string; /** The artifact the ANSWER was chosen against. */ readonly answered: string; constructor(asked: string, answered: string); } export declare class DecisionRequiredError extends Error { readonly code: "ERR_DECISION_REQUIRED"; /** Which gate is outstanding. */ readonly gate: ConsentGateKind; /** The tool the gate is about, when the pause named one. */ readonly toolName?: string; /** `'ask'` only — the middleware that asked. */ readonly middleware?: string; /** * What arrived instead, as a TYPE NAME only (`'a string'`, `'nothing'`, …). * * Never the value. A resume payload is caller data that may carry anything a * person typed, and an error message is copied into logs, tickets and crash * reporters by default. */ readonly received: string; constructor(gate: ConsentGate, input: unknown); } /** * Raised when an `askHuman()` / `pauseHere()` pause is resumed with NO answer. * * That pause exists to collect a value, and the value it collects becomes the * paused tool's result — the model reads it as what the tool said. Resuming * with nothing has two live readings, "the person gave no answer" and "just * carry on", and they produce different conversations. So the caller says * which; the library does not choose. * * Until 8.18.0 it chose, and chose invisibly: the missing answer became a * `role: 'tool'` message with `content: undefined`, and the run died on the * next turn inside the messages slot with a `TypeError` that named neither the * tool nor the resume. * * Nothing ran before this raised and the checkpoint is unchanged — answer it * and resume the same checkpoint again. */ export declare class PauseAnswerRequiredError extends Error { readonly code: "ERR_PAUSE_ANSWER_REQUIRED"; /** The tool whose `execute()` paused. */ readonly toolName: string; /** The tool call id the pause is filed under. */ readonly toolCallId: string; constructor(ctx: { toolName: string; toolCallId: string; }); } /** * Control-flow error raised by `pauseHere()` inside a tool's `execute()`. * Caught by the Agent's tool-call stage, which forwards to `scope.$pause()`. * Never propagates to the consumer. */ export declare class PauseRequest extends Error { readonly data: unknown; constructor(data: unknown); } /** * Called from inside a tool's `execute()` to request a pause. Throws a * `PauseRequest` that the Agent catches and forwards to the flowchart. * * @example * const approveTool: Tool<{ action: string }, string> = { * schema: { name: 'approve', description: 'Ask human', inputSchema: {...} }, * execute: async (args) => { * pauseHere({ question: `Approve ${args.action}?`, risk: 'high' }); * return ''; // unreachable — pauseHere always throws * }, * }; */ export declare function pauseHere(data: unknown): never; /** Type guard for a thrown `PauseRequest`. */ export declare function isPauseRequest(err: unknown): err is PauseRequest; /** * Ergonomic alias for `pauseHere(data)` — the human-in-the-loop name. * * `pauseHere` describes the mechanism (control-flow throw); `askHuman` * describes the intent (ask a person to decide). Both work identically. * * @example * const approveRefund: Tool<{ amount: number }, string> = { * schema: { name: 'approve_refund', description: '...', inputSchema: {...} }, * execute: async ({ amount }) => { * if (amount > 1000) askHuman({ question: `Approve $${amount}?` }); * return 'auto-approved'; * }, * }; */ export declare const askHuman: typeof pauseHere;