@sema-agent/core
Version:
Stateless, task-oriented AI agent core
102 lines (101 loc) • 7.67 kB
TypeScript
/** OpenAI request-body keys core owns; `extraBody` cannot override these. */
export declare const OPENAI_RESERVED: ReadonlySet<string>;
/** Anthropic request-body keys core owns; `extraBody` cannot override these. (Note the asymmetry vs
* OpenAI — e.g. `stop`/`stop_sequences` is NOT reserved here because the Anthropic brain never sets it.) */
export declare const ANTHROPIC_RESERVED: ReadonlySet<string>;
/**
* Open Responses request-body keys core owns; `extraBody` cannot override these (design/163).
*
* The last three are NOT keys the adapter sets — they are keys it must be IMPOSSIBLE to set. The
* Open Responses lane is stateless by contract (this engine is the transcript authority), and
* `extraBody` is the only other path to the request body, so `store` / `previous_response_id` /
* `conversation` are reserved purely to close it. A deployment that sets one gets a stripped key on the
* wire and the drop announced, never a silently server-linked turn.
*/
export declare const RESPONSES_RESERVED: ReadonlySet<string>;
/**
* Merge `extraBody` into a brain's already-built request `body`, dropping any key the brain owns so core
* always wins. `body` is a NEW object (passthrough keys first, then core keys) when `extraBody` is set; the
* exact `body` reference (no allocation, byte-identical serialization) when it isn't — so an existing
* deployment with no `extraBody` is unaffected.
*
* `dropped` is the keys of `extraBody` that were stripped, in `extraBody`'s insertion order, names only (a
* value never leaves this function except inside `body`). It is a property of the same walk that strips, so
* what is said and what is dropped cannot disagree; empty when nothing was stripped.
*/
export declare function applyExtraBody(body: Record<string, unknown>, extraBody: Record<string, unknown> | undefined, reserved: ReadonlySet<string>): {
body: Record<string, unknown>;
dropped: readonly string[];
};
/**
* #343 — the shared USER-HEADER merge layer (`model.headers` → construction `config.headers` →
* per-call `options.headers`, later bag wins), CASE-FOLD deduplicated: HTTP header field names are
* case-insensitive (RFC 9110), but a plain-object spread keys by exact spelling, so an `X-Tenant`
* in one bag and an `x-tenant` in another both reach the wire, where fetch's Headers folds them
* into one comma-joined value ("a, b") — neither writer's value, the later layer's documented
* override silently defeated. A later bag's entry replaces an earlier case-variant instead; the
* WINNER'S spelling and value survive (a single-spelling config is byte-identical on the wire).
*
* EXEMPT: the credential headers ({@link isCredentialHeaderName}, the engine's one reading of which
* header carries a credential: `authorization`, `x-api-key`, `api-key`, `cookie`, …, any case) pass
* through with plain spread semantics (same-spelling override only, no case-fold dedup), so that the
* credential enforcement (`applyRouteCredentialHeaders`, route-adjudicator.ts) stays the ONE authority
* over credential spelling and this layer never becomes a second, subtly different one.
*
* The exemption is NOT what keeps a header-only `ANTHROPIC_AUTH_TOKEN` alive: dedup keeps the
* winner's spelling, a lone `Authorization` has nothing to dedup against, and the credential
* enforcement already deletes every spelling present when a per-model credential wins. What the
* exemption really costs is measured, not reasoned:
* `mergeHeaders({Authorization:A},{authorization:B})` keeps BOTH, and the platform `Headers` fold
* sends `authorization: A, B`.
*
* STATED RESIDUAL (deliberately not fixed here), NARROWED by the credential enforcement: where a
* PER-MODEL credential wins (an options- or entry-bag carrier), every carrier is stripped and only
* the winning bag's re-asserted, so a cross-layer double spelling now folds to the winner. What
* remains is the DEPLOYMENT-CONFIG arm — its bytes ride untouched by design — so a config bag that
* itself spells the SAME auth carrier two ways still ships both, comma-folded. Held, not denied, on
* severity: no server accepts a comma-joined credential, so that failure is a LOUD 401 attributable
* to the misconfiguration, while the non-auth case this function exists for produces a silently
* WRONG value (`X-Tenant: a, b`). Tightening it changes which credential reaches the wire, so it
* belongs in a window that discloses an auth-face behavior change.
*/
/**
* #343 — assign a STRUCTURAL locked header under its canonical lowercase name, deleting every
* case-variant spelling first. The brains hard-lock `content-type` / `anthropic-version` AFTER the
* user-bag merge precisely so they "can NEVER be overridden" (council design/40) — and without the
* delete a user bag carrying `Content-Type: text/plain` survives BESIDE the lowercase lock, the
* platform `Headers` fold turning the pair into `text/plain, application/json` on the wire: a lock
* that decides nothing. WHICH credential headers ride is the jurisdiction of `applyRouteCredentialHeaders`
* (route-adjudicator.ts); the key header a brain writes after it (`authorization` / `x-api-key`, from the
* apiKey knob) is a lock like any other — one value per name, whatever spelling a bag carried.
*/
export declare function lockHeader(headers: Record<string, string>, lowerName: string, value: string): void;
/**
* #343 (review r4) — case-fold READ-AND-CLAIM for an AUGMENTABLE structural header (`anthropic-beta`):
* returns the current value under whatever spelling the user bag carried and deletes that spelling,
* so the caller's canonical lowercase write REPLACES it instead of duplicating beside it (the
* read-modify-write used to key the read by the exact lowercase name and miss `Anthropic-Beta`,
* losing the user's betas from the merge AND double-sending the header).
*/
export declare function takeHeaderCasefold(headers: Record<string, string>, lowerName: string): string | undefined;
/** A caller's header bag read ONCE, into a plain copy of its own enumerable entries — what a request build hands both
* the route law (which credential rides, and its identity) and the merge (what goes on the wire), so a bag whose getters
* or keys answer differently on a second read cannot be judged as one bag and sent as another. */
export declare function headerBagOnce(bag: Readonly<Record<string, string>> | undefined): Record<string, string> | undefined;
export declare function mergeHeaders(...bags: Array<Record<string, string> | undefined>): Record<string, string>;
/**
* The output-cap key(s) each lane's wire form uses. A lane's set is exactly the keys THAT lane's
* endpoint reads — a stray cap key belonging to another wire form is inert there and must not be
* mistaken for a bound this request carries.
*/
export declare const OUTPUT_CAP_KEYS: Readonly<Record<"openai" | "anthropic" | "responses", readonly string[]>>;
/**
* The per-request output cap a FINAL (post-`extraBody`) request body actually carries — the SMALLEST
* of the lane's cap keys that is present and usable, or `undefined` when none is.
*
* Read off the merged body rather than off the knob the adapter set, because those are not the same
* thing: `extraBody` can contribute a cap under a spelling the adapter did not choose, and a
* consumer that treats the adapter's own number as "the cap on the wire" would be wrong about a
* request that carries two. Smallest wins so a consumer reasoning about the bound stays conservative.
*/
export declare function effectiveOutputCap(body: Record<string, unknown>, keys: readonly string[]): number | undefined;