UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

123 lines (122 loc) • 9.64 kB
/** * The request view's TOOL-TURN LAW — the structure every provider lane requires of a tool turn, restored on the * way to the brain (never in the transcript). * * For every assistant message that carries tool calls: * (i) the calls are a CONTIGUOUS TAIL of DISTINCT ids — every block that is not a call and stands after the first * call (between two calls or after the last) moves ahead of the calls; blocks ahead of the first call, thinking * first, keep their place (a stable partition: non-call blocks in their order, then the calls in theirs); a * second call carrying an id the message already used is left out (every lane requires a request's call ids to * be unique), so the message keeps the FIRST call per id and every result for that id answers it; * (ii) the messages right after it are EXACTLY one result per call — a result recorded later in the transcript * (behind a user message, a wake frame, another turn) is moved up into the batch, a second result for the * same call is folded into the first (no recorded result is withheld), an interrupted close * ({@link isInterruptedClose}) stands in only for a call with no other result (beside a recorded result it * is void: it says only that none was recorded), and a call with no result at all is closed with the reconcile * family's synthetic interrupted result ({@link interruptedResults}: the same text and `errorKind` a persisted * reconcile writes); * (iii) every result answers a call — a result with no EARLIER assistant message emitting its id (its call is gone, * or it was recorded ahead of its call) answers nothing in this view and is left out. This is the one rule for * that shape on every request builder (the harness's request lane and the side query alike). * * The three lanes carry the same requirement in three spellings — the Anthropic Messages API rejects a `tool_use` * followed by any non-`tool_use` block (between two calls as after the last), or whose `tool_result` is not in the * very next message; chat completions requires the `tool` messages right after the assistant's `tool_calls`; the * Responses API requires a `function_call_output` for every `function_call` — and each brain's own builder only ever * merged ADJACENT results. One transcript that * breaks the structure (a result recorded late, a note written after the calls) was therefore rejected on every * later request of its session. Holding the law here, once, at the provider boundary means the three brains read * the canonical form, and a session already carrying a broken turn is repaired on its next request. * * Attribution (some gateways number calls per request, so an id legitimately recurs across turns): a result belongs * to the NEAREST EARLIER assistant message that emitted its id — and to nothing else: its recorded tool name is not a * key. That is the persisted reconcile's positional reading (a call is answered by a result standing after it and * before the id's next use), so the two readers never disagree about which occurrence a result answers; a name-based * reading (7.37.1) reached back past turns the model had already answered. * An earlier occurrence of a reused id that has no result before the next use is therefore closed as * interrupted (outcome unknown), never handed a later occurrence's success; every result after the last use is that * use's (two or more are folded). A result with no earlier call carrying its id is left out (iii). Pure and * deterministic (a moved result keeps its object; a synthesized one takes its assistant's timestamp), so the same * transcript yields the same request bytes and the prefix cache holds. * The close the view synthesizes carries NO tool-effect refinement: the view holds no tool table and no record of * which calls started, so every call it closes reads the reconcile family's conservative "outcome unknown — verify" * wording, whatever the tool. (The persisted reconcile refines with the run's tool effects — a read-only or * idempotent call's close says it may simply be re-issued — and, on a live abort, with the calls it saw start.) * Returns the SAME array when nothing needed restoring. * * RUN OWNERSHIP. The caller may hand over the assistant messages that stood in its transcript before its run began * (`priorMessages`, by object). A `missing_result` repair on one of them is marked `beforeRun`: the call went * unanswered before this run started, so closing it on the wire is a fact about the transcript the run inherited, not * an event of the run. Membership is by message, never by position — a request projection that inserts, drops or * replaces other messages cannot move it. The mark says that and nothing else: the repair, the synthesized result and * the operator's account of them are the same with or without it. No `priorMessages` ⇒ no mark. */ import type { AssertAllKeysHandled } from "../../core/ask-origin.js"; import type { AssistantMessageDiagnostic } from "./diagnostics.js"; import type { Message } from "./types.js"; /** What the law restored — the `forms` of a `context.tool_turn_repaired` notice. */ export declare const TOOL_TURN_REPAIR_FORMS: readonly ["trailing_text", "late_result", "missing_result", "duplicate_result", "duplicate_call", "orphan_result"]; export type ToolTurnRepairForm = (typeof TOOL_TURN_REPAIR_FORMS)[number]; /** Does the form leave recorded content out of the request (a call, a result), rather than move, fold or add it? * A request that did is a lossy projection of the session — the harness never seeds a fork summary from it. */ export declare const TOOL_TURN_REPAIR_DROPS: { readonly trailing_text: false; readonly late_result: false; readonly missing_result: false; readonly duplicate_result: false; readonly duplicate_call: true; readonly orphan_result: true; }; export type DropsTableCoversEveryForm = AssertAllKeysHandled<Exclude<ToolTurnRepairForm, keyof typeof TOOL_TURN_REPAIR_DROPS>>; /** * Why a tool turn reached the model incomplete — the `task.tool_turn_incomplete` notice's `detail.cause`, the person's * view of facts the model is already told in the turn itself. Closed: * · `truncated` — calls whose argument bytes were cut before they parsed: dropped, the model asked to re-issue them; * · `unnamed` — calls that arrived with no tool name: dropped the same way; * · `bad_frame` — provider stream frames that were not valid JSON: dropped, so the response may be missing content; * · `missing_result_synthesized` — a call with no recorded result, closed in the request view as interrupted with its * outcome unknown (the `missing_result` repair). * The first three are a brain's stream facts ({@link StreamDropCause}), stamped on the turn's own message; the fourth is * the request view's. */ export declare const TOOL_TURN_INCOMPLETE_CAUSES: readonly ["truncated", "unnamed", "bad_frame", "missing_result_synthesized"]; export type ToolTurnIncompleteCause = (typeof TOOL_TURN_INCOMPLETE_CAUSES)[number]; /** The causes a brain's stream parse finds — what a turn's own message can report. */ export type StreamDropCause = Exclude<ToolTurnIncompleteCause, "missing_result_synthesized">; /** One cause a turn's stream dropped content for, and how many calls (or frames) it dropped. */ export interface StreamDrop { readonly cause: StreamDropCause; readonly count: number; } /** Stamp a turn's stream drops on its message (`AssistantMessage.diagnostics`); nothing for a turn that dropped none. */ export declare function stampStreamDrops(message: { diagnostics?: AssistantMessageDiagnostic[]; }, drops: readonly StreamDrop[]): void; /** The stream drops a message reports. A host brain's message is not trusted to be well-formed: an entry counts only * with a closed-set cause and a positive integer count; anything else reads as no drop. */ export declare function streamDropsOf(message: { readonly diagnostics?: unknown; }): StreamDrop[]; /** One restoration: on one assistant message, or — for `orphan_result` — on the view as a whole. */ export interface ToolTurnRepair { readonly form: ToolTurnRepairForm; /** The calls concerned, in call order — for `trailing_text` every call of the message; for `duplicate_call` the * ids a later call of the message repeated; for `orphan_result` the ids of the results left out, once each, in * transcript order; otherwise the calls whose result was moved up, synthesized, or folded with a second one. */ readonly toolCallIds: readonly string[]; /** `missing_result` only, and only when the caller handed over its run's prior messages * ({@link ensureToolTurnInvariants}'s `priorMessages`): the message holding these calls is one of them. */ readonly beforeRun?: true; } export interface ToolTurnView { /** The request view — the input array itself when {@link repairs} is empty. */ readonly messages: Message[]; readonly repairs: readonly ToolTurnRepair[]; } /** The text standing between two results recorded for one call when the view folds them into one. */ export declare const SECOND_RESULT_SEPARATOR = "[another result was recorded for this same call:]"; /** Restore the tool-turn law on a provider-bound message list (see the module header). `priorMessages` = the calling * run's inherited assistant messages (module header, RUN OWNERSHIP). */ export declare function ensureToolTurnInvariants(messages: Message[], priorMessages?: { has(message: object): boolean; }): ToolTurnView;