@sema-agent/core
Version:
Stateless, task-oriented AI agent core
85 lines (84 loc) • 5.58 kB
TypeScript
import { type RepetitionEvent } from "./repetition.js";
import { type AssistantMessage, type StreamDropCause } from "../internal/llm.js";
/**
* design/157 A10 — stream-parser pieces that the anthropic and openai SSE lanes carried BYTE-IDENTICALLY
* (verified by diff at 1469481: a 16-line repetition-telemetry closure). Provider-agnostic policy, so it
* lives here once and each lane keeps a pointer comment.
*
* REF-B1 extends that: the fail-closed ARGUMENT PARSE inside each lane's `closeToolCall*` (identical down to
* its comment) is hoisted here as {@link parseStreamedToolArgs}. Only the parse — each lane keeps its own
* accumulator fields, its `closedTc` cache and its own id prefix, which are real wire-format differences.
*
* Deliberately NOT hoisted: each lane's own degenerate-cut bookkeeping (`degenTextBlock` / `degenFace` are
* lane-specific content shapes).
*
* Internal module: not re-exported from `src/index.ts`.
*/
/**
* REF-B1 — turn a streamed tool call's accumulated argument bytes into its arguments object, FAIL-CLOSED.
*
* - no bytes (`""`) → `{}`: genuinely a no-argument call (the provider's frame carried no arguments);
* - valid JSON **object** → that object;
* - anything else (truncated/invalid JSON, or a top-level string/array/number/null) → `undefined`.
*
* bug-hunt #6 (kept from finalize): a non-object must never reach `tool.execute` / the MCP wire. `undefined`
* means "do not emit a `toolcall_end` for this call"; callers must NOT cache that outcome — finalize retries
* the parse against the complete accumulation.
*/
/** The CC-verbatim warning stamped beside `partialFinalized` when a stream ended before its close — by the
* mid-stream promotion (the body threw) and by the parsers' own finalize (content arrived, no finish frame). */
export declare const STREAM_TORN_WARNING = "Connection closed mid-response. The response above may be incomplete.";
export declare function parseStreamedToolArgs(json: string): Record<string, unknown> | undefined;
/** One drop note of a turn: what was dropped, how many, and the sentence the model reads. */
export interface TurnNote {
readonly cause: StreamDropCause;
readonly count: number;
readonly text: string;
}
/**
* Place a turn's engine notes in its final content: AHEAD of the first tool call, in order, or at the end of a turn
* that has none. Every dropped call (cut arguments, no tool name) and dropped stream frames are stated so the model
* re-issues them — also when text survived and no call did; a partial batch still executes its valid calls (a brain
* `error` is a terminal task failure, not a re-prompt). No block may follow the calls: the Anthropic Messages API
* rejects a `tool_use` followed by anything but another `tool_use`. The same notes are stamped on the turn's message as
* its stream drops (`stampStreamDrops`), the run's source for the person's line (`task.tool_turn_incomplete`): the
* model's sentence and the person's line come from one list. One rule for the three lanes.
*/
export declare function placeTurnNotes(message: Pick<AssistantMessage, "diagnostics">, content: AssistantMessage["content"], notes: readonly TurnNote[]): void;
/**
* The cause clause of a dropped call's "not valid JSON" message. The output cap is named only when the stream says
* the cap cut it — the lane's own cap stop, or a counted output at or over the cap on the wire; a stream that ended
* without its stop frame names the transport instead; any other stop adds nothing (naming the cap there sent a reader
* after a budget that was never reached). One rule for the lanes that carry the clause.
*/
export declare function truncationCauseClause(facts: {
capStop: boolean;
outputTokens: number | undefined;
capTokens: number | undefined;
terminalFrameSeen: boolean;
}): string;
/** Repetition telemetry for ONE stream — the cut event plus the allowance-spared hits. */
export interface RepetitionPoll {
/** Inspect the accumulated text; returns whether it is degenerate (the caller trips its own cut). */
poll(text: string): boolean;
/** The cut event, once a poll tripped. */
readonly cut: RepetitionEvent | undefined;
/** Allowance-spared hits, deduped by rule+segment with the loop's PEAK depth kept. */
readonly spared: RepetitionEvent[];
}
/**
* Repetition telemetry (2026-07-10): the cut event + allowance-spared hits for THIS stream.
* Polls re-inspect the same growing text every ~64 chars — dedupe spared by rule+segment so one
* boot block doesn't log once per poll. Attached to the final message as `repetition`.
*/
export declare function createRepetitionPoll(): RepetitionPoll;
/** Record the provider's OWN name for the model that answered, on the shared partial (hence on the final
* message every lane spreads `...partial` into) — ONE rule for the three lanes' spellings (`chunk.model` /
* `message_start.message.model` / `response.model`), WRITE twin of `providerReportedModel`
* (`core/message-utils.ts`). The LAST frame that NAMES a model decides and the seat carries it only when it
* differs; naming nothing (no name, or the empty string — {@link providerEchoReading}) is not an update, naming the
* REQUESTED id RETRACTS an earlier substitution. A name outside {@link isProviderEchoShape} is dropped — not an
* update — and counted once per response (`echo_dropped`, `field: "responseModel"`). */
export declare function noteResponseModel(partial: {
responseModel?: string;
}, reported: unknown, requestedId: string): void;