@sema-agent/core
Version:
Stateless, task-oriented AI agent core
123 lines (122 loc) • 9.64 kB
TypeScript
/**
* 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;