import type { TurnRecorder } from '../../manager/session/turn-recorder.js'; import type { CheckpointId, HITLDecisionRequest, HITLResumeDecision, ToolCallEscalation, ToolCallSummary } from '../../types/hitl/index.js'; import type { MessageId } from '../../types/ids/index.js'; import type { AssistantMessage, Message, ToolCall } from '../../types/message/index.js'; import type { ChatCompletionResponse } from '../../types/provider/index.js'; import type { Logger } from '../../utils/logger.js'; import type { RestoredCheckpoint } from './checkpoint.js'; import type { PriorToolResults, ToolCallDenials, ToolExecutor } from './executor.js'; import { PendingAnswers } from './question-park.js'; /** * Apply a decision collected out-of-band to the tool calls a turn parked on. * * This is the half of durable HITL that actually pays off. Recording the * park makes the request survive a restart; without this, a resumed turn * still threw the approval away — the restore path repairs the unanswered * `tool_use` blocks and lets the model re-decide, so a human's "yes, delete * that row" became "ask the model again and hope it asks for the same * thing". The tool calls the human approved are right there in the * checkpoint; the decision applies to THOSE, or it means nothing. * * Returns `null` when there is nothing to apply, in which case the caller * keeps the existing repair-and-re-decide behavior. */ export interface PendingResumePlan { /** * What produced this plan, and therefore whether it carries the human's * decision out or stands in for it. * * `'decision'` — the answer, applied to the calls the park was about. * `'recovery'` — the checkpoint's recorded and explicitly unknown outcomes, * replayed so that nothing runs twice. The caller resolves the park in both * cases — recovery answering the batch is what makes the question moot — * but only the first may write the human's decision down as what ended it. * Recording a decision recovery stood in for says the turn carried out * something it did not. */ readonly source: 'decision' | 'recovery'; /** * The checkpoint the park was recorded on, so the caller can clear it * once the decision has actually been applied. Leaving it outstanding * makes an approval queue re-serve a destructive call that already ran. */ readonly checkpointId: CheckpointId; /** The assistant turn whose `tool_use` blocks are unanswered. */ readonly assistant: AssistantMessage; /** Synthesized response the executor consumes. */ readonly response: ChatCompletionResponse; /** Per-call refusals derived from the decision. */ readonly denials: ToolCallDenials; /** Exact projections and gate decisions persisted with a tool review. */ readonly reviewedCalls?: readonly ToolCallSummary[]; /** Calls whose raw input the human replaced in the durable decision. */ readonly modifiedCallIds?: ReadonlySet; /** Calls whose sandbox escape the durable decision confirmed; see `HITLResumeDecision`. */ readonly confirmedEscalations?: readonly string[]; /** * Answers to deliver to tools that parked on a question, keyed by the * asking call's id. Present only on a question resume. */ readonly answers?: PendingAnswers; } /** * Decide whether a restored checkpoint can have `decision` applied to it * directly. * * Only `tool_review` parks qualify. A `plan_approval` or * `iteration_checkpoint` park leaves no unanswered tool calls behind, so * there is nothing to apply a decision TO — those resume by simply * continuing, which the normal path already does correctly. A * `user_question` park happens inside a tool's own execution, so honoring * it across a restart would mean re-entering that tool; it is out of scope * here and says so rather than pretending. */ export declare function planPendingResume(checkpoint: RestoredCheckpoint, decision: HITLResumeDecision, log: Logger): PendingResumePlan | null; /** * The stable marker `supersededByRecovery` puts at the head of its reason. * * `resolvedAt` says a park ENDED; it does not say HOW, and `pause` is the * action both endings share — `CheckpointManager.expire` records one for a * park that ran out of time, this one records another for a park whose * question crash recovery answered instead. A reader that tests * `pending.decision.action` alone can tell neither from a turn still holding * the park, and the reason is the only field left to carry the difference. * * A constant rather than a sentence written at the call site, and a PREFIX * rather than the whole string, because the sentence names which decision was * superseded — informative to a person, unstable to a comparison. A consumer * tests this; the tail is prose. * * Exported for the SDK's own readers. It is not on the package's public * surface: a new field on the recorded decision would be, and this branch * ships as a `patch`. */ export declare const PARK_SUPERSEDED_BY_RECOVERY = "crash-recovery-superseded"; /** Whether a recorded decision is the supersede marker rather than an answer. */ export declare function isSupersededByRecovery(decision: HITLResumeDecision | undefined): boolean; /** * What to record on a park whose batch crash recovery answered instead of the * decision. * * Neither half of the obvious record is honest. Writing the human's decision * down would say the turn carried it out, when the calls it named were answered * with an explicitly UNKNOWN outcome and nothing they asked for happened — * `planPendingResume` refused that decision in the first place, which is why * recovery spoke at all. Writing nothing would lose the fact that somebody * answered, and leave `pending.decision` meaning two different things. * * The vocabulary already has one shape for "this park ended and no decision * was carried out": `CheckpointManager.expire` records a `pause` carrying the * reason, and says why it is not an `abort` ("that would read as somebody * having refused it"). This is that shape, with * {@link PARK_SUPERSEDED_BY_RECOVERY} at the head of the reason so the fact is * comparable rather than prose, and the decision it superseded after it so the * answer a human gave is still on the record. */ export declare function supersededByRecovery(decision: HITLResumeDecision): HITLResumeDecision; /** * Whether the ordinary continue path carries `decision` out for a park that * has no batch of tool calls to apply it to. * * {@link planPendingResume} covers the two arms whose decision has to REACH * something: the calls a `tool_review` park is about, and the tool a * `user_question` park is inside. An `iteration_checkpoint` park has neither, * so it produces no plan — and "no plan" must not be read as "nothing * happened". For this arm the decision IS the turn's next move, and the loop * that resumes carries it out by continuing; the park it answered therefore * has to be resolved exactly as the other arms' are. * * The set is `handleHITLDecision`'s continue arm, deliberately: these are the * decisions a resumed turn carries out by going on. `pause` is not among them * — it is "hold this, I am not answering now", which the live path leaves the * park standing for, and a resumed turn does not honour it either. Neither are * `abort` and `reject_plan`: nothing on the resume path acts on them, so * recording one as the park's answer would say the turn carried out something * it did not. */ export declare function isCarriedOutByContinue(decision: HITLResumeDecision): boolean; /** * Whether `decision` is a verdict on the question a `plan_approval` park asks. * * The plan arm was the one park `isCarriedOutByContinue` did not cover and * nothing else did either, so a turn resumed with `{action: 'approve_plan'}` * completed with its park still outstanding: `findPendingCheckpoint` kept * serving a plan nobody was waiting on, a second resume of the FINISHED run * was refused `awaiting-decision`, and `prune`'s refusal to collect an * unresolved park left the row uncollectable — with no `hitlParkTtlMs` there * is no `deadlineAt`, so `expire` cannot reach it either. * * Both verdicts answer it, and that is the difference from `pause` (which * HOLDS the park rather than answering it, on the live path and here) and * from `abort` (which is not a verdict on the plan at all). What the resumed * run can do about the answer afterwards is a separate question and not this * predicate's: the record's `decision` is what the HUMAN answered, and a park * is not made outstanding again by the new process having no plan to act on. */ export declare function isPlanVerdict(decision: HITLResumeDecision): boolean; /** * Whether a park of `type` is ANSWERED by `decision` on the resume path. * * The map from park to the decision that answers it, in one place, because * each arm was added by a different fix and the two that were missed were * missed by being absent rather than wrong. `tool_review` and `user_question` * are deliberately not here: their decisions have to REACH something — * `planPendingResume` applies them to the parked batch — and their parks are * resolved where that plan is applied, not by this predicate. * * "Answered" is the ordinary continue path carrying the decision out, which * for the cadence arm means the loop going on and for the plan arm means the * verdict having been given. It does not mean the turn did everything the * decision implies — see {@link isPlanVerdict}. */ export declare function answersParkOf(parkType: HITLDecisionRequest['type'] | undefined, decision: HITLResumeDecision): boolean; /** * Preserve a restored batch's recorded and explicitly unknown outcomes. * `completed` comes from recoverCompletedCalls: missing entries are eligible * only after a complete scan establishes that they have no recorded start. * A started tool with no trustworthy completion is carried as an unknown * outcome, not executed again. An untouched batch uses ordinary history repair. */ export declare function planCrashResume(checkpoint: RestoredCheckpoint, completed: ReadonlyMap, log: Logger): PendingResumePlan | null; /** * Execute a resume plan, pushing the assistant turn and its results. * * The assistant message is re-pushed rather than repaired away, because * the `tool_result` blocks about to be produced must answer the `tool_use` * blocks that are in it — a result with no matching call is exactly the * malformed request the repair path exists to prevent. */ export declare function applyPendingResume(plan: PendingResumePlan, recorder: TurnRecorder, executor: ToolExecutor, prior?: PriorToolResults, assistantMessageId?: MessageId): Promise; /** * Why a re-prepared call's escalation may not run on a durable decision, or * `undefined` when the decision covers it. * * The approval was given to what the reviewer was SHOWN. A call re-prepared * in this process may reach further than that — a directory was removed * from the session, a hook rewrote the path — and the decision says nothing * about the difference. So every path it reaches must have been in the * reviewed escalation, an escape must have been shown AND confirmed by id, * and a call nobody reviewed, or whose input was modified afterwards, gets * no escalation at all. */ export declare function escalationRefusal(escalation: ToolCallEscalation | undefined, reviewed: ToolCallSummary | undefined, wasModified: boolean, confirmed: readonly string[] | undefined, id: string): string | undefined; /** * Recover completed results and close interrupted calls with unknown outcomes. * Only a complete execution scan can authorize the absence of a start record. * Unreadable, partial or contradictory evidence never becomes permission to * replay; an explicitly answered durable question may re-enter its own tool. */ export declare function recoverCompletedCalls(recorder: TurnRecorder, toolCalls: readonly ToolCall[], log: Logger, options?: { answers?: PendingAnswers; signal?: AbortSignal; }): Promise>; /** * All calls owned by the last incomplete batch. Applying a resume plan * reconstructs that entire batch, including any partial results already in * the checkpoint. Those answered siblings must also be recovered, or their * missing prior-result entries would let the executor repeat them. */ export declare function interruptedToolCalls(messages: readonly Message[]): ToolCall[]; //# sourceMappingURL=resume-pending.d.ts.map