@sema-agent/core
Version:
Stateless, task-oriented AI agent core
121 lines (120 loc) • 9.71 kB
TypeScript
/**
* Is this an Error, including one minted in ANOTHER REALM?
*
* `instanceof` compares against THIS realm's `Error.prototype`, so a genuine `Error` minted outside it — by
* a script the engine ran through a deployment-supplied runner, or a duplicated module instance — fails and would be
* described as an anonymous object, losing the message that is the entire point of reporting it. The brand
* check is the realm-independent one: an object with the `[[ErrorData]]` internal slot answers `[object Error]`
* no matter where it was made, and a proxy forwards it. A plain object can forge the tag through
* `Symbol.toStringTag`, which costs nothing — the only thing this decides is whether we look for a `message`,
* and a forger that supplies a string message has told us what it wanted rendered anyway.
*
* Guarded, because both operations are attacker-reachable: `instanceof` walks a prototype chain a revoked
* proxy traps, and the tag is a getter that can throw.
*/
export declare function isErrorLike(value: unknown): boolean;
/** `value instanceof ctor`, TOTAL — the class half of {@link readMember}'s rule. The prototype-chain walk
* runs two proxy traps and a revoked proxy throws on both, while every caller tests a value someone else
* threw inside the handler that decides how a failure settles. Unrecognizable is not that class, and
* {@link isErrorLike} is this same rule for `Error`. */
export declare function thrownIs<T>(value: unknown, ctor: abstract new (...args: never[]) => T): value is T;
/** The NAME a thrown value carries (`Error.prototype.name` and its subclasses' overrides), read
* realm-independently and guarded — `undefined` when the value is not error-like or its `name` is not a
* string. Callers that compare against a name CONSTANT need exactly this: the comparison is on the WORD
* precisely because the value crossed a seam, so binding the test to this realm's `Error.prototype`
* defeats it. Whether an EMPTY name folds to `undefined` is policy and stays at the call site. */
export declare function thrownName(err: unknown): string | undefined;
/**
* Turning a value that was THROWN at us into text — the ONE rule, for the whole engine.
*
* The idiom it replaces — `err instanceof Error ? err.message : String(err)` — is PARTIAL on its non-Error
* arm: `String` calls the value's own `toString` / `Symbol.toPrimitive`, so it throws on a value that has
* neither and on one whose hook throws. The thrower is not ours (a deployment hook, a plugin callback, a
* model-supplied tool body may reject with anything), so the rendering step was the one step that could
* destroy the report it exists to produce — and inside a promise rejection handler it took the process down.
*
* TOTALITY is therefore the contract, met the way the structural describer meets it: the value is never
* asked to say what it is. Three arms, ordered so the dominant shapes keep their EXACT previous bytes:
* - a thrown STRING is its own message (a bare `throw "…"`), returned verbatim — the describer would quote
* it, which is right when rejecting a config value and wrong when relaying a message someone threw;
* - an ERROR hands over `message` verbatim — no cap, no one-lining, no quoting. This is the arm nearly all
* production traffic takes, and the text on it is byte-for-byte what the old idiom produced;
* - anything else is DESCRIBED structurally, by the one describer, which guards every read it makes.
*
* Empty is a legitimate answer: `new Error("")` returns `""`, exactly as `err.message` did, because callers
* fold that into their own wording (`… || "unknown"`). Two callers want something else and have their own
* face for it: {@link thrownLabel} when the previous text was `String(err)` (which names the error CLASS),
* and `describeThrown` when the text must be non-empty and contained for a model-facing frame.
*/
export declare function thrownText(err: unknown): string;
/**
* The LABELLED face of the same rule: what `String(err)` used to produce, made total.
*
* `String` on an Error does not yield its message — it yields `"TypeError: boom"`, the class AND the message
* (and just `"TypeError"` when the message is empty). Sites that spelled a bare `String(err)` were therefore
* saying something the message-only face cannot say, and at least one of them is load-bearing rather than
* decorative: a stuck-detector keyed its signature on this text, so collapsing two distinct error classes
* with equal messages into one string would have changed when a run gets abandoned. Preserving those bytes
* is the whole reason this face exists as well as {@link thrownText}.
*
* Fidelity is obtained by calling `String` itself rather than reassembling `name` and `message` by hand —
* the spelling rules for an empty name or an empty message live in `Error.prototype.toString`, and a second
* copy of them here would be a second rule to keep in step. It is called INSIDE the guard, so a subclass
* whose own `toString`/`name`/`message` misbehaves falls back to the structural description instead of
* escaping. Non-Errors never reach `String` at all; they go to {@link thrownText}, which is the fix.
*/
export declare function thrownLabel(err: unknown): string;
/**
* The same rule for callers that need an `Error` OBJECT rather than text — the normalizing half of
* `err instanceof Error ? err : new Error(String(err))`, which carries the identical defect on the identical
* arm and appears wherever an unknown rejection has to be handed to a seat typed `Error` (a deployment's
* error lane, a promise `reject`, a report callback).
*
* An `Error` of THIS realm is passed through by IDENTITY — same object, same stack, same own properties — so
* the arm that carries nearly all traffic is unchanged in every observable way, not merely in its message.
* A cross-realm Error is WRAPPED rather than passed through, which is what the old idiom did too: the seat's
* type says `Error`, and an object that fails `instanceof` here would break the first consumer that trusts
* it. Its message survives the wrap, because the new message is the {@link thrownLabel} of the original —
* the same text `String(err)` produced.
*
* The original value is deliberately NOT attached as `cause`: `cause` chains are walked elsewhere to
* attribute error codes, so attaching one here would let an arbitrary thrown object start supplying codes
* to readers that never saw it before. Preserving it is a worthwhile change on its own terms and belongs
* with those readers, not smuggled in by a rendering fix.
*/
export declare function asThrownError(err: unknown): Error;
/**
* Read ONE member of an untrusted value, once, and hand back an inert snapshot.
*
* Inspecting a thrown value is attacker-reachable in the same way coercing it is: any property can be an
* accessor that throws, or one that answers differently on a second read. Sites that need a member — a
* shell error's `exitCode`, a refusal's `code`, a transport error's `cause` — were each wrapping their own
* `try`, and a guard written per SITE is coarse: one hostile member took the readable ones down with it,
* so a report lost an exit code it had already read because a later `stdout` threw.
*
* One guarded read, per member, is the finer and the smaller rule: the caller keeps what it could read and
* loses only what it could not, and nothing it holds afterwards can run the value's code again. The result
* is `undefined` for BOTH "absent" and "unreadable" — deliberately, because a caller that must tell those
* apart is asking a question an untrusted value is not obliged to answer; what it does instead is fall back
* to describing the value, which {@link thrownText} already does totally.
*
* The read walks the PROTOTYPE CHAIN deliberately — `name` lives on `Error.prototype`. `"__proto__"` is
* the one key where that differs from an own read, and its only caller is {@link readOwnMembers}, which
* passes it only when it IS an own key (a data property shadows the accessor; both readings agree).
*/
export declare function readMember(value: unknown, key: string): unknown;
/** The PLURAL of {@link readMember}: read a value's own enumerable members, once each, into an inert
* snapshot — for sites that want a COPY of a whole bag an untrusted value handed over (a thrown
* error's `details` mark, on its way onto a tool result). The idiom it replaces is `{ ...value }`,
* a second way of running the value's code: the spread asks for its own keys and evaluates every
* getter behind them, so one hostile member took the copy down with it, and a revoked proxy traps the
* enumeration itself (even `Array.isArray` on one throws). Non-objects, arrays (spreading one yields
* index keys) and anything whose keys cannot be enumerated answer `undefined` — "there is no bag
* here"; a member that refuses to be read comes back `undefined` in a PRESENT key. */
export declare function readOwnMembers(value: unknown): Record<string, unknown> | undefined;
/** The MACHINE-READABLE marks a thrown value carries (`details`, with a string `errorKind` folded in) —
* the one extractor for both legs that lift them onto a tool result. The live leg and the resume leg
* each had their own copy, and the resume copy still spread the bag and called `Array.isArray` on it,
* so a hostile `details` failed the whole leg where the live one had stopped losing anything but the
* mark. `undefined` = nothing to carry; a caller that must always present a bag folds that itself. */
export declare function thrownMarksOf(err: unknown): Record<string, unknown> | undefined;