UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

337 lines (336 loc) • 24.5 kB
import type { ThinkingLevel } from "../internal/harness-types.js"; /** * Unified reasoning-intensity abstraction (design/96 S3). * * The intensity a caller requests IS a {@link ThinkingLevel} — we reuse the existing 7-tier vendor scale * (`off | minimal | low | medium | high | xhigh | max`) rather than forking a parallel "fast/standard/deep" * enum, so there is ONE knob, not two that must be kept in sync. This module is the single place that maps a * requested intensity to what a given endpoint can ACTUALLY honor, and reports the EFFECTIVE intensity — so a * request that can't be honored as asked ("high" on a binary enable-only provider like qwen/zai, or * "xhigh"/"max" on an endpoint whose `reasoning_effort` only accepts up to "high") is observable rather than * silently swallowed or 422'd (design/96 §E honesty red-line; design/46 anti-drift). * * The brain (openai.ts / anthropic.ts) consumes this. Surfacing the effective intensity to a task result is a * later config-domain slice (design/96 S6); here it is a pure, tested mapping. */ export type ReasoningIntensity = ThinkingLevel; /** Type guard: is `v` one of the 7 {@link ThinkingLevel} tiers? (A legacy/unknown reasoning string is not.) */ export declare function isThinkingLevel(v: unknown): v is ThinkingLevel; /** * THE thinking-request ENTRY predicate — does this request enter an applier's emission path at all? * One conjunction, three arms: the model declares reasoning (TRUTHINESS — the adapters' own * judgment, see the r5 note in {@link resolveReasoning}), the requested value is a real * {@link ThinkingLevel} (an out-of-contract value — a typo'd tier, a caller's own enum — reads as * ABSENCE, never as a declared tier), and it is not the explicit `"off"`. * * ENTRY gate, deliberately NOT an emitted-a-key guarantee: past this gate the per-FORMAT arms still * decide what (if anything) lands on the wire — `supportsReasoningEffort:false` on a format whose * only carrier is the effort key, a `null` levelmap entry, the binary enable keys. Those arms have * their own reporting shape (the `graded:false` intent-echo family, each pinned with its rationale * where it lives); this predicate only closes the gate the three appliers used to re-spell. * * Single-sourced here because every wire applier (openai.ts `applyThinking`, anthropic.ts's thinking * + effort arms, open-responses.ts `resolveWireEffort`) would otherwise re-spell the same three * conjuncts while the REPORTING resolver ({@link resolveReasoning}) mirrored one of them, so a * garbage tier puts ZERO parameters on the wire while the resolution claims a clamped-to-minimal * gradient. A predicate the appliers and the reporter both call cannot drift. */ export declare function reasoningRequestCarried(model: { reasoning?: boolean; }, reasoning: unknown): reasoning is ThinkingLevel; /** Ordinal rank of a level (`off`=0 … `max`=6). */ export declare function rankOf(level: ThinkingLevel): number; /** * The `reasoning_effort` tiers an OpenAI-family endpoint accepts by DEFAULT. Conservative on purpose: a value * the endpoint rejects 422s the whole request, so we ship the set stock OpenAI takes * (`minimal | low | medium | high`) and clamp anything higher ("xhigh"/"max") down to "high". An endpoint that * genuinely accepts a higher tier declares it via `compat.reasoningEffortLevels`. */ export declare const DEFAULT_EFFORT_LEVELS: readonly ThinkingLevel[]; /** * The per-provider thinking wire format (mirrors `compat.thinkingFormat`). `"budget"` is the synthetic tag for * Anthropic's extended-thinking budget path (no `thinkingFormat` — the anthropic brain doesn't read one), kept * here so {@link resolveReasoning} can report ONE endpoint discriminant across both brain families. */ export type ReasoningFormat = "openai" | "openrouter" | "deepseek" | "together" | "zai" | "qwen" | "qwen-chat-template" | "budget" /** Anthropic `output_config.effort` (CC 2.1.198 / Fable-5 class, design/119): the tier is a named * effort string, clamped to `compat.effortLevels` — the anthropic-side twin of the OpenAI * `reasoning_effort` path (thinking FORM is `thinkingMode`; effort carries the intensity). */ | "effort" /** OpenAI Responses API (`openai-responses` / `azure-openai-responses`, design/163): the tier rides * the NESTED `reasoning: { effort }` request field (open-responses.ts `resolveWireEffort`), clamped * by the same {@link resolveEffort} against `compat.reasoningEffortLevels` — NOT the completions * top-level `reasoning_effort` the `"openai"` tag names. #325 review r3: before this member the * resolver fell through to the completions default and labeled Responses resolutions `"openai"`. */ | "responses"; /** * The Open Responses API family — every `Api` value `createOpenResponsesBrain` serves (design/163). * The SINGLE family discriminant: request-params.ts (reserved-key set selection) and * {@link resolveReasoning} (format dispatch, #325 review r4) both key off THIS set, so a new * Responses api value added here reaches every family-scoped behavior instead of one of two twins. */ export declare const RESPONSES_APIS: ReadonlySet<string>; /** How a requested intensity resolved against one endpoint's real capability. */ export interface ReasoningResolution { /** The level the caller asked for. */ requested: ThinkingLevel; /** The level actually in effect after mapping to what the endpoint accepts. */ effective: ThinkingLevel; /** * True when the endpoint varies behavior by intensity (effort tiers / token budget). False = binary * enable-only (qwen / zai / qwen-chat-template, an effort endpoint that declares * `supportsReasoningEffort:false`, or a tier `Model.thinkingLevelMap` maps to `null` — the wire then * carries NO effort value for it): thinking is on or off and the tier is NOT honored — read this * alongside {@link effective}, which then echoes the request as intent only. */ graded: boolean; /** True when {@link effective} differs from {@link requested} (the request couldn't be honored exactly). */ clamped: boolean; } /** Anthropic's hard floor for an extended-thinking budget. Lives HERE (not the anthropic brain) so * the cap-wins predicate below and the brain's budget-window math read ONE constant. */ export declare const MIN_THINKING_TOKENS = 1024; /** * The anthropic BUDGET path's cap-wins arm (#346, single-sourced; design/119 #2 review): * a HARD per-request output cap (an engine-imposed override or the caller's explicit * `options.maxTokens` — the two lanes the brain refuses to raise) too small to host a legal thinking * budget (≥ {@link MIN_THINKING_TOKENS}) plus answer room means the CAP WINS and thinking is skipped * for the request. Shared by the wire arm (anthropic.ts, which acts on it) and * {@link resolveReasoning}'s budget arm (which mirrors it when the caller supplies the request * facts), so the skip decision and its report are one predicate — the reporter's budget arm used to * claim an unconditional `graded:true` gradient while this arm deleted the thinking block from the * very request it described. A soft (model/config-sourced) cap never skips: the brain raises it to * host the budget instead (`hardCap === false`). */ export declare function budgetCapSkipsThinking(outputCapTokens: number, hardCap: boolean): boolean; /** The model slice the off-capability reads: the API family, the reasoning bit, the compat declaration and the levelmap. */ export interface OffCapabilityModel { api?: string; reasoning?: boolean; compat?: unknown; thinkingLevelMap?: Readonly<Partial<Record<ThinkingLevel, string | null>>>; } /** * The DEPLOYMENT-declared spelling of the explicit "off" tier on an effort-only wire (stock openai * completions, the Responses lane): `thinkingLevelMap.off` as a non-empty string (a provider's own * "none" tier), on an endpoint that takes an effort value at all (`supportsReasoningEffort` not * false) — else `undefined`: there is nothing to send. The effort formats have no disable key, and an * effort VALUE is never off (it enables reasoning at that tier), so off on those wires is spelled only * when the deployment says how. The two appliers and {@link thinkingOffSpelled} read this ONE * function, so what the wire carries and what the capability table claims cannot disagree. */ export declare function declaredOffSpelling(model: OffCapabilityModel): string | undefined; /** * Whether the wire will CARRY a disable key for this model — off is SPELLED, never assumed. Per wire: the * anthropic budget form spells `{type:"disabled"}` (the ADAPTIVE form rejects it — those models think on * every request); deepseek `{type:"disabled"}`, the binary enable keys at `false`, openrouter/together * `reasoning:{enabled:false}` all spell it; the effort-only wires (stock openai completions, the Responses * lane) spell it only through a deployment-declared off spelling ({@link declaredOffSpelling}). * * A model whose `reasoning` is FALSY spells NOTHING: every applier returns before writing any key for it, so * the request is the gateway's own default — and a default-on gateway thinks regardless (three of four live * endpoints, 2026-09-15). This arm used to answer `true` ("a non-reasoning model never thinks"): the engine * may size a request by what it SENT, never by what it hopes a gateway does when told nothing. What this * table cannot see is a gateway that IGNORES the key it was sent — the classifier's re-ask on a no-verdict * reply is the seat that learns that from the reply. */ export declare function thinkingOffSpelled(model: OffCapabilityModel): boolean; /** * OPTIONAL per-request facts for {@link resolveReasoning} — what the wire's budget arm knows at * request build that a per-leg eager resolution cannot: the resolved output cap and whether it is a * HARD bound. Supplied ⇒ the anthropic budget arm mirrors the wire's cap-wins skip * ({@link budgetCapSkipsThinking}); absent ⇒ the budget arm reports the cap-blind gradient it always * did (an eager mint that has not yet seen a request has no facts). * * Who supplies them, and why it is never the reporting caller's own arithmetic: the adapter * that BUILT the request reports the pair it judged, through the runner's reasoning-wire-facts sink * (`status-sink.ts`), from its `onCommitted` hook so a speculatively-built request the overflow * recovery declined can never be reported as sent. A consumer re-deriving the cap would have to * re-implement the adapter's precedence chain (engine override > caller `options.maxTokens` > * `model.maxTokens` > brain-construction default) — a second source of truth for a number only the * minter can state. The runner then re-resolves with them: the skip and its report stay ONE predicate * reading ONE set of facts. */ export interface ReasoningWireFacts { /** The request's resolved output cap (the wire `max_tokens` at the moment the thinking arm judges). */ outputCapTokens: number; /** True when the cap is HARD (engine override / caller `options.maxTokens`) — the lanes the brain * refuses to raise; a soft model/config cap is raised to host the budget instead. */ hardOutputCap: boolean; } /** * The anthropic-`effortLevels` PRESENCE/SHAPE gate, shared by the wire arm (anthropic.ts) and the * reporting dispatch below so the two cannot disagree about which family a request rides (#335): the * old twin predicates were `x && x.length > 0` on both sides, so a truthy NON-ARRAY (a string — * `.length > 0` holds) entered the wire arm and threw a bare TypeError from its `.filter` pre-clean * BEFORE {@link resolveEffort}'s centralized non-array fallback could read it as undeclared — while * the reporter happily described an effort resolution for the same config. Only the container shape * is judged here; MEMBER validity stays {@link resolveEffort}'s job (its element-level sanitization * is the single bad-value seat, which is also why the returned array is not member-checked — the cast * is checked at runtime by every consumer's `resolveEffort` call). */ export declare function declaredEffortLevels(v: unknown): readonly ThinkingLevel[] | undefined; /** * The effort-lane WIRE-VALUE mint — clamp ({@link resolveEffort}) + the `Model.thinkingLevelMap` * translation in ONE place, consumed by both completions-family appliers (openai.ts `applyThinking`, * open-responses.ts `resolveWireEffort`) AND the reporting dispatch ({@link resolveReasoning}'s * arms), so the value that reaches the wire and the resolution a trace/result face claims are two * reads of one computation, never parallel re-derivations. Map semantics (the field's contract): a * MISSING key ⇒ provider default (the clamped tier name as-is); a STRING ⇒ that provider-specific * spelling (same tier, still honored); `null` ⇒ the tier is UNSUPPORTED on this model — * `wireValue: undefined`, no effort value on the wire (thinking still enables via a format's own * enable key where one exists), which is exactly the tier-not-honored shape the reporter must echo. */ export declare function mintEffortWireValue(requested: ThinkingLevel, model: { thinkingLevelMap?: Readonly<Partial<Record<ThinkingLevel, string | null>>>; }, allowed: readonly ThinkingLevel[] | undefined): { resolution: ReasoningResolution; wireValue: string | undefined; }; /** * A {@link ReasoningResolution} enriched with the endpoint discriminant, for observability (design/96 S6). * The brain consumes only the {@link ReasoningResolution} fields to shape the request; `format`/`endpoint` * are reported (e.g. a `reasoning.resolved` trace event) so a deployment can see WHY a tier was downgraded * (binary provider) or clamped (effort-set cap) rather than the resolution being silently swallowed (§E honesty). */ export interface ResolvedReasoning extends ReasoningResolution { /** The wire format the request will use (the load-bearing reason for `graded`/`clamped`). */ format: ReasoningFormat; /** A coarse endpoint label for the trace (`model.api` — e.g. `openai-completions`, `anthropic-messages`). */ endpoint: string; /** * Present (true) only when the request never CARRIES at all: {@link reasoningRequestCarried} false * on a non-"off" request — the model declares NO reasoning capability (`Model.reasoning` falsy), * OR the requested value is not a valid {@link ThinkingLevel} (an out-of-contract tier every wire * applier reads as ABSENCE — the same three-conjunct gate all of them share) — or, with * {@link ReasoningWireFacts} supplied, the anthropic budget path's cap-wins skip * ({@link budgetCapSkipsThinking}), so NO thinking * parameter reaches the wire at all — the requested tier is DROPPED entirely, not clamped or * downgraded-to-binary. `effective:"off"` here states the ENGINE side of that fact (nothing was * requested), NOT a measured gateway state: on the binary enable-only formats (qwen / zai / * qwen-chat-template) parameter ABSENCE is provider-default, and a default-on gateway may still * reason at its own tier — the brains deliberately send no disable key for a model declared * non-reasoning, so the engine cannot claim a measured off there; `format` tells a consumer which * family applies (on every other format, absence is the closest the wire has to off — see * applyThinking's own contract). `format`/`endpoint` report the family that WOULD have carried the * tier (the why-shape a consumer diagnoses with, not a claim about the request). Absent on every * reasoning-capable resolution, so existing consumers see byte-identical objects. Loud-bad-values * rule: the one arm where the request evaporates must not be the one arm with no report. */ dropped?: true; } /** * Resolve a requested intensity against a model END-TO-END, dispatching on its wire format the SAME way the * brains do — so a trace event and the actual provider request can never drift (design/96 S6). This is the * single observability-side resolver: the runner calls it at task start to emit `reasoning.resolved`; the * brains keep calling {@link resolveEffort}/{@link resolveBinary}/{@link reasoningBudgetShare} on the hot path. * * - Anthropic (`api === "anthropic-messages"`) → budget-based: the tier sets a budget share, so it's a real * gradient (`graded:true`) and never tier-clamped (`clamped:false`); reported as `format:"budget"`. With * the optional {@link ReasoningWireFacts} the arm additionally mirrors the wire's cap-wins skip * ({@link budgetCapSkipsThinking}: a hard per-request output cap < 2·{@link MIN_THINKING_TOKENS} deletes * the thinking block) as the drop shape — facts absent keeps the historic cap-blind gradient. * - Binary enable-only formats (qwen / zai / qwen-chat-template) → `graded:false` (tier not honored). * - An effort endpoint with `supportsReasoningEffort:false` → `graded:false` (enable key only, no effort tier). * - Otherwise effort-based → clamp DOWN to the endpoint's `reasoningEffortLevels` (default minimal|low|medium|high). * - A CLAMPED tier that `model.thinkingLevelMap` maps to `null` (declared unsupported on this model) → * `graded:false`: both effort-wire appliers key the map by the post-clamp tier and then carry NO effort * value (thinking still enables via the format's own enable key where one exists), so the tier is not * honored. A resolver blind to the map reports such a request as exactly honored while the wire * drops the value — trace/result-face drift. * * - A request that never CARRIES ({@link reasoningRequestCarried} false on a non-"off" value: the model's * `reasoning` capability flag is FALSY, or the requested value is not a valid tier — no brain emits any * thinking parameter for either, whatever the format) → `effective:"off"`, `graded:false`, `clamped:true`, * `dropped:true` — the loud-drop arm. Describing the capability dispatch instead would report a * resolution the request never carried, leaving the one arm where the request evaporates as the one * arm with no disclosure (and an out-of-contract tier reported as a clamped gradient, or echoed as * `effective` on the binary formats, while the wire carried nothing). * * `off`/falsy never enables thinking, so it resolves trivially (no clamp, graded:true) — the caller decides * whether to emit at all. */ export declare function resolveReasoning(requested: ThinkingLevel, model: { api?: string; reasoning?: boolean; compat?: unknown; thinkingLevelMap?: Readonly<Partial<Record<ThinkingLevel, string | null>>>; }, facts?: ReasoningWireFacts): ResolvedReasoning; /** * Resolve a requested intensity for an effort-based endpoint (`reasoning_effort` / `reasoning.effort`). Picks * the requested tier when supported; otherwise the highest supported tier ≤ requested (clamp DOWN, never * silently up); if none is ≤ requested (the endpoint's set is entirely higher — unusual), the lowest supported * tier. Never returns a value outside `allowed`, so the brain can't emit a tier the endpoint would 422. * A malformed `allowed` reads as UNDECLARED → the conservative default set (non-array, empty, or no * valid member); individually invalid MEMBERS (garbage strings, sparse holes, `"off"` — not a legal * member of the 6-tier compat effort sets) are dropped, so they can never become a clamp target. */ /** * #692 C-c — the effort tiers that actually REACH THE WIRE for this model: the table {@link resolveEffort} clamps * against, keyed by the model's API family exactly as {@link resolveReasoning} dispatches, gated by the request * entry predicate (a `reasoning: false` model accepts no tier) and minus the tiers the model's `thinkingLevelMap` * suppresses (`null` = no effort value on the wire) — the same arms, so a host reading this and the trace's * `effective` can never disagree: * - `anthropic-messages` with declared `compat.effortLevels` → that set (the `output_config.effort` lane); * without it → `undefined` (the BUDGET lane is a token gradient, there is no tier set to clamp to); * - the Responses family and the completions family → `compat.reasoningEffortLevels` sanitized * (invalid members dropped, an empty/garbage declaration reads as undeclared) or the conservative * {@link DEFAULT_EFFORT_LEVELS}; `supportsReasoningEffort: false` (openrouter excepted, whose unified * knob ignores it) and the binary formats (`qwen` / `zai` / `qwen-chat-template`) → `undefined` (the tier * is not honored as a gradient on that wire). * `undefined` therefore means "this wire clamps no tier set", never "unknown": a host that offers a person the * supported tiers reads THIS, per model, instead of a global default it cannot vouch for. Per API, not per * model id: the acceptance set is a property of the endpoint's request schema; `Model` grows no key for it. */ export declare function reasoningEffortLevelsOf(model: { api?: string; reasoning?: boolean; compat?: unknown; thinkingLevelMap?: Readonly<Partial<Record<ThinkingLevel, string | null>>>; }): readonly ThinkingLevel[] | undefined; export declare function resolveEffort(requested: ThinkingLevel, allowed?: readonly ThinkingLevel[]): ReasoningResolution; /** * Resolve a requested intensity for a binary enable-only endpoint (qwen / zai / qwen-chat-template): the * provider only takes an on/off switch, so the intensity tier is NOT honored as a gradient. `graded: false` * is the load-bearing honesty signal. */ export declare function resolveBinary(requested: ThinkingLevel): ReasoningResolution; /** * Extended-thinking budget as a FRACTION of `max_tokens` per intensity tier, for budget-based endpoints * (Anthropic). Lets a higher intensity actually buy a larger thinking budget — before this, the budget was a * fixed share regardless of the requested tier, so the intensity had no effect on Anthropic (design/96 §D/§E). * The brain clamps the resulting budget to Anthropic's `[1024, max_tokens-1]` window; an explicit * `thinkingBudgetTokens` / `thinkingBudgetShare` in config still overrides this table. */ export declare const REASONING_BUDGET_SHARE: Record<ThinkingLevel, number>; /** The extended-thinking budget share for a given intensity tier (see {@link REASONING_BUDGET_SHARE}). */ export declare function reasoningBudgetShare(level: ThinkingLevel): number; /** * design/96 §D (S4) — a user-facing NAMED reasoning tier, DISTINCT from the raw 7-tier {@link ThinkingLevel} * ({@link ReasoningIntensity}). A profile maps it to engine flags via {@link resolveReasoningProfile}. This is * the "ultracode" parity axis: a high tier (`ultra`) buys both a higher thinking level AND a thoroughness/ * self-verify prompt nudge. */ export type ReasoningTier = "off" | "fast" | "standard" | "deep" | "ultra"; /** The engine flags a {@link ReasoningTier} resolves to (design/96 §D table). A profile may override the * default; core ships sane defaults. */ export interface ReasoningProfileFlags { /** The vendor thinking level to run at (clamped per-endpoint downstream by {@link resolveEffort}). */ thinking: ThinkingLevel; /** * Compose `ORCHESTRATION_AWARENESS` — a high-tier thoroughness/self-verify nudge. 🔴 NOT the `run_workflow` * tool (that is gated by a hard sandbox, S8 §6.3); this is purely about how the model reasons. Independent * of self-orchestration (a task can be both). */ awarenessGuidance: boolean; /** Advisory HINT that goal mode suits this tier. Core NEVER forces intensity→mode (constitution: the * mapping is profile data); a profile/caller decides whether to actually run `runGoal`. */ goalSuggested: boolean; } /** * design/96 §D (S4) — map a named {@link ReasoningTier} to its engine {@link ReasoningProfileFlags} (default * mapping). G2: the intensity→capability mapping is PROFILE-layer data — core gives this primitive + * a sane default; a profile/scenario layer calls it (and may override the table) to set a task's `thinking` + * prompt flags. Core never forces it. Returns a fresh COPY (callers may mutate). */ export declare function resolveReasoningProfile(tier: ReasoningTier): ReasoningProfileFlags;