UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

121 lines (120 loc) • 9.71 kB
/** * 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;