/** * The AG-UI event VOCABULARY and the Cotal frame envelope. * * This is the file that makes the change an abolition rather than a rename. Renaming `tr-` to * `events.` while the connectors went on publishing `condense()` output would have moved * glyph-prefixed text to a new channel and changed nothing a consumer can do with it — the channel * name was never the complaint. The mirror is now gone from the tree rather than renamed, and this * vocabulary is what replaced it. * * **The vocabulary is adopted; the SDK is not.** `@ag-ui/core` is a `devDependency`, * pinned EXACT at `0.0.57`, and this file imports from it with `import type` ONLY. The reason is * measured rather than stylistic: `0.0.57` declares `dependencies: { zod: "^3.22.4" }` (verified * against the registry, not remembered), and `connector-core` is esbuild-bundled into every seeded * connector — so a runtime dependency would ship a second zod major to every customer in order to * validate events we construct ourselves. The conformance smoke imports the real schemas and * validates against them; production code carries types and string literals and no zod. * * **Promotion trigger, conditional and NOT scheduled:** if a 0.1.x ships stable with zod moved to * `peerDependencies`, promote to a runtime dependency and use the schemas directly. As of this * writing `latest` is `0.0.57`, the active `canary` still carries the zod-3 runtime dep, and the * release that moves zod to a peer sits on no dist-tag at all. * * ## What is here, and what is deliberately NOT * * Here: the event constructors, the frame envelope, the routing/validity split * ({@link isAguiFramePart} / {@link parseAguiFrame}), the {@link AguiBrackets} stream machine, the * `cotal.*` `CUSTOM` table (empty in v1), the preview plane's {@link splitFrames}, and the egress * policy that decides which events may leave for the event channel. * * Not here: the emitter that publishes, with the channel derivation and the durable source, WAL and * subject frontier it drives. Those are in `agui-emitter.ts`, which imports this module and is never * imported by it, so a change to the delivery machine cannot reach the vocabulary. * * **This module publishes nothing and does no I/O.** A reader deciding whether a change here can * reach the wire on its own needs that to be accurate, so it is maintained rather than left to rot. */ import type { CustomEvent, ReasoningMessageContentEvent, ReasoningMessageEndEvent, ReasoningMessageStartEvent, RunErrorEvent, RunFinishedEvent, RunStartedEvent, TextMessageContentEvent, TextMessageEndEvent, TextMessageStartEvent, ToolCallArgsEvent, ToolCallEndEvent, ToolCallResultEvent, ToolCallStartEvent } from "@ag-ui/core"; export { AGUI_FRAME_KIND, AGUI_EVENT_TYPE, isAguiFramePart } from "@cotal-ai/core"; import { AGUI_FRAME_KIND } from "@cotal-ai/core"; /** * The AG-UI events this plane emits — the MAPPED SUBSET, not the whole protocol. * * Absent by decision, each recorded so its absence is not read as an oversight: * `*_CHUNK` (all three sources are settled observations, so we emit the START/CONTENT/END * triple, which is the subset every consumer implements: raw CHUNK needs a client transformer); * `STATE_*` (reserved for a later lane, and ask state left this plane entirely); * `MESSAGES_SNAPSHOT` (dropped as a compaction anchor, because a * windowed snapshot DELETES the prefix); `THINKING_*` (deprecated at 0.0.57 in favour of * `REASONING_*`, which is what we emit). */ export type AguiEvent = RunStartedEvent | RunFinishedEvent | RunErrorEvent | TextMessageStartEvent | TextMessageContentEvent | TextMessageEndEvent | ToolCallStartEvent | ToolCallArgsEvent | ToolCallEndEvent | ToolCallResultEvent | ReasoningMessageStartEvent | ReasoningMessageContentEvent | ReasoningMessageEndEvent | CustomEvent; /** * Cotal metadata rides ONE key on a standard event. * * Legal because every AG-UI event schema is `.passthrough()` — asserted by the conformance smoke * against the real schemas rather than trusted, since this whole vehicle collapses if a future * release tightens it. * * **It does NOT work everywhere.** `RunFinishedOutcomeSchema` is STRICT: it refuses unrecognized * keys, measured. So `cotal` may ride an EVENT and never an `outcome`. Recorded here because * "AG-UI is passthrough" is the kind of sentence that gets generalized one level too far. */ export interface CotalMeta { /** Where the `timestamp` came from. Absent means the source carried a real one. */ tsSource?: "arrival"; /** Set when the connector minted the `runId` rather than reading one from the harness. */ runIdSource?: "connector"; /** The provider's own message id — preserved for correlation, never spent as `messageId`. */ providerMessageId?: string; /** The harness's stop reason, on the event that carries one. */ stopReason?: string; /** * Model usage for the whole run, on the event that closes it (`RUN_FINISHED` or `RUN_ERROR`). * The union has no step events, so the run terminal is the only boundary every connector shares. * * Set only by a connector whose harness reports the numbers, and a count the harness does not * report is omitted rather than zeroed, so a reader can tell "none" from "unknown". The cache and * reasoning counts are PARTS of the totals beside them: harnesses disagree on whether their input * count includes cached tokens, and one stated rule is what keeps two connectors from publishing * different quantities under the same name. */ usage?: { /** Every prompt token the model read, cached or not. */ inputTokens?: number; /** The part of `inputTokens` read from the provider's prompt cache. */ cacheReadTokens?: number; /** The part of `inputTokens` written to the provider's prompt cache. */ cacheWriteTokens?: number; /** Every token the model generated, reasoning included. */ outputTokens?: number; /** The part of `outputTokens` spent on reasoning. */ reasoningTokens?: number; /** The run's cost in US dollars. */ costUsd?: number; }; /** `tool_result.is_error` — AG-UI's result event has no error field of its own. */ isError?: boolean; /** Subagent linkage. Deliberately NOT `parentRunId`, which is retry/edit lineage. */ delegation?: { agentId: string; toolCallId: string; }; /** * WHAT BEGAN THIS RUN. Attribution, and deliberately NOT a gate on run-opening. * * Run-opening ("did a turn begin?") and attribution ("who began it?") are two questions, and an * earlier revision used one provenance predicate to answer both — so a turn started by a peer * produced no run at all, and an agent-driven session mapped to nothing. Provenance ANNOTATES; * it does not SELECT. A run with `"channel"` attribution is still a run. * * `"unknown"` is never written by the mapper: an unrecognised provenance FAILS LOUD instead, so a * future harness value produces an error rather than a confident wrong attribution. It exists for * consumers that must render something for a producer which did not set the field. * * **`"auto-continuation"` was added because a fail-loud branch is only safe if you know what is on * the other side of it.** It is the harness re-injecting a standing goal into an unattended * session, and it is a turn: work begins, and an observer asking what triggered it is owed the * real answer rather than `"human"`. Measured over 237 real session files and 531,882 records on * one machine, where the whole provenance universe is four values: `channel` 30,385, `human` * 1,505, `task-notification` 706, `auto-continuation` 4. The rare one is exactly the one an * enumeration built from a smaller sample misses, and missing it throws in production. */ turnSource?: "human" | "channel" | "notification" | "sdk" | "auto-continuation" | "unknown"; /** What was cut to fit the wire, and how big it was. Set only by the sizing path. */ truncated?: { field: string; originalBytes: number; }; } /** An event carrying Cotal metadata. Kept structural so it composes with any member of the union. */ export type WithCotal = E & { cotal?: CotalMeta; }; /** * The `cotal.*` `CUSTOM` event table — the second and ONLY other vehicle for Cotal-specific data. * * **The v1 table is EMPTY, and that is the specification, not an unfinished state.** Earlier * revisions described a two-member table holding `cotal.ask.opened` / `cotal.ask.settled`. Both * were removed when ask state left this plane, on the operative ground that there is no consumer: * the board answers what is owed, the plane carries what is happening. Leaving the count in the * prose invited re-adding them "because the table has two slots", so the table ships with no slots. * * It exists as the GATE: adding a member is a decision that touches this declaration, rather than * a `CUSTOM` name invented at a call site where nobody reviews the vocabulary. */ export declare const COTAL_CUSTOM_EVENTS: readonly string[]; /** The envelope version. One frame declares the AG-UI vocabulary version it was built against. */ export declare const AGUI_PROTOCOL = "ag-ui/0.0.57"; /** * One Cotal message = one frame. * * `threadId` is the native harness session and `runId` is ONE native harness turn, and nothing * else may claim either of them. `epoch` is the writer-identity fence recovered from the WAL * (never re-minted on restart), and `seq` is this writer's frame counter, which is what lets a * consumer detect a gap rather than merely fail to notice one. * * **A frame carries no text part, by design.** That is why the renderers are a binding precondition * on the cutover rather than a follow-up. * * **RE-DERIVED, because core changed underneath this sentence.** It used to end "a viewer that does * not understand this part shows nothing, and an empty pane is indistinguishable from a * correctly-empty one." That is now true of some surfaces and false of others, and the split is * exactly which ones adopted core's shared `partsToText`: * * - **3 ADOPTED IT** — `connector-core/src/agent.ts`, `cli/src/commands/join.ts`, * `cli/src/view/mesh-view.ts`. These now render a marker naming the kind. * - **4 DID NOT** — `implementations/web/src/web/app.js`, `.../graph.js`, * `examples/02-self-improving-console/harness/observer.ts`, * `examples/04-frontier-faces/tools/studio.mjs`. The two stringify-form copies still leave a * stray separator; the two filter-form ones still leave no trace at all. * * Measured on a real frame from `aguiFrame` below, placed between two text parts: the adopted * renderer produced `"before after"` before the core change and names the kind after it. **The * worse half of that defect was never the missing frame — it was that `"before after"` is a * well-formed sentence with a silent hole in it, so it prompts no question at all.** * * **THE PRECONDITION IS UNCHANGED AND THE MARKER IS NOT A LOOPHOLE IN IT.** A named marker proves a * frame ARRIVED; it does not display one. Cutting a connector over on the strength of it would * still ship events nothing can render. */ export interface AguiFrame { kind: typeof AGUI_FRAME_KIND; protocol: typeof AGUI_PROTOCOL; threadId: string; runId: string; epoch: string; seq: number; events: AguiEvent[]; } /** Raised when a frame or an event sequence violates a structural rule of the vocabulary. */ export declare class AguiVocabularyError extends Error { constructor(message: string); } /** * The bracket machine's persisted form (WAL v2). * * It is a plain, JSON-round-trippable record on purpose: it is written into the write-ahead log, so * it must survive `JSON.stringify`/`parse` unchanged and must be readable by a human staring at a * WAL trying to work out why an emitter refused something. */ export interface BracketState { /** The run currently open, or `undefined` when the stream is at a legal stopping point. */ run: string | undefined; text: string[]; reasoning: string[]; tools: string[]; } /** * Bracketing, checked INCREMENTALLY over the event sequence — deliberately not over one frame. * * **A frame is not guaranteed to be self-bracketed, and a validator demanding that it be would * forbid a split this design requires.** An oversized frame splits on event boundaries with each * part carrying its own `seq`, so a run can legally open in one frame and close in the next. The * unit that must balance is the WRITER'S STREAM, not the message. Feeding this machine frame after * frame is therefore the only way to check the property that is actually claimed. * * What it enforces: * - one run open at a time; nothing may be emitted outside an open run * - `TEXT_MESSAGE_*`, `REASONING_MESSAGE_*` and `TOOL_CALL_*` open and close by their own id, and an * id may not be opened twice while already open * - a run may not close while any message or tool call it opened is still open * * The id-reuse rule is the one with a measured defect behind it: `message.id` is a PROVIDER REQUEST * id, and over one real session 833 of 1243 assistant `message.id` values appeared in more than one * JSONL entry. Keying identity on it would open and close the same `messageId` repeatedly, which * the AG-UI verifier rejects and the reference reducer collapses. This machine refuses that rather * than letting it reach a consumer. */ export declare class AguiBrackets { private run; private readonly text; private readonly reasoning; private readonly tools; /** * The machine's whole state, as plain JSON — what the WAL persists so a restart does not lose it. * * Sorted, because this value is written to disk and compared BY A HUMAN reading two documents. * A `Set`'s iteration order is insertion order, so two machines that are semantically identical * would serialize differently depending on the order events happened to arrive, and a diff of two * WALs would show a change where there is none. */ snapshot(): BracketState; /** Rebuild a machine from a snapshot. The inverse of {@link snapshot}, and the reason a mid-run * restart can continue instead of refusing its first event. */ static restore(s: BracketState): AguiBrackets; /** An independent machine at the same state — used to VALIDATE a batch without advancing the * machine that is in step with the disk. */ clone(): AguiBrackets; /** True while a run is open — i.e. the stream is mid-turn and not at a legal stopping point. */ get open(): boolean; /** The run currently open, for diagnostics and for checking a frame's envelope against it. */ get runId(): string | undefined; /** Feed one event. Throws {@link AguiVocabularyError} on the first violation. */ accept(event: AguiEvent): void; /** * Assert the stream is at a legal stopping point. * * Called at the end of a synthesized sequence and by any consumer checking a writer closed * cleanly. NOT called per frame: mid-turn frames are legally unbalanced. */ assertClosed(): void; private openId; private requireOpen; private closeId; } /** * Build a frame, validating the envelope's own fields. * * Bracketing is NOT checked here — see {@link AguiBrackets} for why a single frame cannot be * required to balance. The emitter holds one `AguiBrackets` across the whole stream and feeds it as * it builds; that is the placement that checks the property actually claimed. */ export declare function aguiFrame(opts: { threadId: string; runId: string; epoch: string; seq: number; events: AguiEvent[]; }): AguiFrame; /** * **THE CONSUMER-SIDE ENFORCEMENT POINT.** `aguiFrame` above validates on the way OUT; this pair * validates on the way IN, and they are separate functions because they are separate trust domains. * * It exists because the renderers cannot enforce a contract expressed as TypeScript types. * `implementations/web/tsconfig.json` carries `"include": ["src"]` with `"exclude": ["src/web"]`, * and the build copies `src/web` into `dist` verbatim — so the renderer is plain JavaScript that * `tsc` never reads. A prose contract with no enforcement point is the defect, so the contract ships * as **a function a consumer executes**, not as a shape a consumer is trusted to have read. * * **THE TWO ANSWERS ARE DIFFERENT AND MUST NOT BE FUSED.** "This part is not mine" is a routing * decision a consumer makes constantly and quietly — every non-frame part on a channel it also * reads. "This part claims to be mine and is malformed" is a defect that must be LOUD. One function * returning `null` for both would make a version skew look exactly like someone else's message, and * a renderer would show an empty pane for a stream it is actively failing to parse. So: * {@link isAguiFramePart} answers the routing question with a boolean and never throws, and * {@link parseAguiFrame} answers the validity question and throws with the field named. */ /** * Validate an incoming frame, or throw {@link AguiVocabularyError} naming the field that failed. * * Call it only on a part {@link isAguiFramePart} accepted. Everything after that check is a defect * rather than a routing outcome, including an unknown `protocol` — **a version skew must fail loud * rather than render partially**, because a consumer that drops the fields it does not recognise * shows a confidently incomplete transcript, which is worse than showing nothing. * * It deliberately does NOT check bracketing. A frame is not guaranteed to be self-bracketed — an * oversized frame splits on event boundaries — so the unit that must balance is the writer's stream, * and {@link AguiBrackets} is the machine for that, fed frame after frame. A validator demanding a * frame balance on its own would forbid a split the plan mandates. */ export declare function parseAguiFrame(part: unknown): AguiFrame; /** `RUN_STARTED` — `threadId` is the native session, `runId` one native harness turn. */ export declare function runStarted(o: { threadId: string; runId: string; timestamp: number; cotal?: CotalMeta; }): WithCotal; /** * One entry of the `interrupt` outcome. * * Both fields are REQUIRED strings, measured against the real schema rather than read off its * types: an entry missing either is refused naming `outcome.interrupts..`. Extra keys on * an entry are STRIPPED rather than refused, which is why nothing here polices them. */ export interface AguiInterrupt { id: string; reason: string; } /** * `RUN_FINISHED`. * * `outcome` is OPTIONAL — measured against the real schema, which accepts a `RUN_FINISHED` carrying * none. That matters because the Claude `Stop` hook reports that a turn ended and nothing more, so * manufacturing a `success` outcome would be asserting something the source never said. When an * outcome IS supplied its discriminator key is `type`, not `status` (measured: the schema refuses * `{status:"success"}` naming `outcome.type`), and the object is STRICT. * * The `interrupt` outcome is REPRESENTABLE and VALIDATED here, and UNSPENT: no source on this plane * constructs one, because a harness-native park is what would justify it and none of the three * sources reports one. It is checked anyway because the arm exists, and an arm that builds an event * the schema refuses is worse than an arm that does not exist. Its first draft took `unknown[]` and * passed it through, so `[]` and `[{}]` both produced a refused event with nothing looking. */ export declare function runFinished(o: { threadId: string; runId: string; timestamp: number; outcome?: { type: "success"; } | { type: "interrupt"; interrupts: AguiInterrupt[]; }; cotal?: CotalMeta; }): WithCotal; /** `RUN_ERROR` — carries `message` and an optional `code`, and NO `runId` of its own. */ export declare function runError(o: { message: string; timestamp: number; code?: string; cotal?: CotalMeta; }): WithCotal; /** * `TEXT_MESSAGE_START`. * * `messageId` must be unique per OBSERVATION, not per provider message. The specified form is * `${entry.uuid}#${blockIndex}` — the provider's own id is preserved as `cotal.providerMessageId` * and never spent here, because it does not have the cardinality the field needs. */ export declare function textMessageStart(o: { messageId: string; timestamp: number; role?: "assistant" | "user"; cotal?: CotalMeta; }): WithCotal; /** `TEXT_MESSAGE_CONTENT` — one settled observation, never a token-level delta. */ export declare function textMessageContent(o: { messageId: string; delta: string; timestamp: number; cotal?: CotalMeta; }): WithCotal; /** `TEXT_MESSAGE_END`. */ export declare function textMessageEnd(o: { messageId: string; timestamp: number; cotal?: CotalMeta; }): WithCotal; /** `TOOL_CALL_START` — `toolCallId` is the harness's own id, carried rather than re-minted. */ export declare function toolCallStart(o: { toolCallId: string; toolCallName: string; timestamp: number; parentMessageId?: string; cotal?: CotalMeta; }): WithCotal; /** * `TOOL_CALL_ARGS` — `delta` is the FULL `JSON.stringify(input)`. * * The constructor still builds the event. The durable emitter suppresses TOOL_CALL_ARGS before * beginSend (and refuses a frozen pre-fix body that still carries it), because `events..` * has a different read ACL from the channel a mesh read tool ran on. Mappers keep constructing it so * their own shape smokes stay about mapping, not about egress. * * This is where `tr-`'s `salient()` died: it guessed which argument mattered and dropped the rest, * so a reader could not reconstruct what the agent actually did. The whole input is still the * constructor's job; the events plane no longer carries it. */ export declare function toolCallArgs(o: { toolCallId: string; delta: string; timestamp: number; cotal?: CotalMeta; }): WithCotal; /** `TOOL_CALL_END`. */ export declare function toolCallEnd(o: { toolCallId: string; timestamp: number; cotal?: CotalMeta; }): WithCotal; /** * `TOOL_CALL_RESULT`. * * **`messageId` is REQUIRED by the real schema** — measured; a result without one is refused. The * plan's per-connector mapping table names only `toolCallId` for this row, so the identity of the * result MESSAGE is unstated there and is raised as a plan gap rather than guessed at a call site. * The parameter is required here so a mapper cannot omit it and discover the refusal downstream. * * `is_error` has no AG-UI field and rides `cotal.isError`. * * The constructor still builds the event. The durable emitter suppresses TOOL_CALL_RESULT before * beginSend (and refuses a frozen pre-fix body that still carries it). Content is mandatory, so the * event is dropped rather than emptied or placeholdered. Observers lose the tool output they see * today; that is the boundary. */ export declare function toolCallResult(o: { messageId: string; toolCallId: string; content: string; timestamp: number; cotal?: CotalMeta; }): WithCotal; /** * `REASONING_MESSAGE_START` — off by default; the signature is never emitted, ever. * * **`role` is a REQUIRED literal `"reasoning"`**, unlike `TEXT_MESSAGE_START` where `role` is * optional. Measured: the first version of this constructor omitted it and the real schema refused * the event. It is set here rather than exposed as a parameter, because there is exactly one legal * value and a caller-supplied one could only ever be wrong. */ export declare function reasoningMessageStart(o: { messageId: string; timestamp: number; cotal?: CotalMeta; }): WithCotal; /** `REASONING_MESSAGE_CONTENT`. */ export declare function reasoningMessageContent(o: { messageId: string; delta: string; timestamp: number; cotal?: CotalMeta; }): WithCotal; /** `REASONING_MESSAGE_END`. */ export declare function reasoningMessageEnd(o: { messageId: string; timestamp: number; cotal?: CotalMeta; }): WithCotal; /** Truncate to `codePoints` code points, never code UNITS. Slicing a JS string by index can cut a * surrogate pair in half and produce a lone surrogate, which is not well-formed UTF-16 — the exact * defect `fix(core)!: refuse names that are not well-formed UTF-16` landed on this branch for. A * splitter that reintroduced it here would emit a frame the wire layer is now obliged to refuse. */ export declare const takeCodePoints: (s: string, codePoints: number) => string; /** * Split `events` into as many frames as the wire requires, truncating only what physically cannot * cross it, and LABELLING every truncation. * * **THIS IS THE PREVIEW PLANE'S SPLITTER, AND IT HAS NO DURABLE-PLANE CALLER BY DESIGN.** * Read that as a boundary, not as an oversight: the durable emitter packs with `packUnits` at * SOURCE-RECORD boundaries and refuses an oversized unit, because one durable emit unit must be one * frame carrying a cursor that resumes after it, and a frame ending mid-record has no cursor it can * honestly store. This event-boundary split and its labelled truncation were specified before the * durable plane had a cursor contract; where the two disagree, the durable plane's rule wins. The * PREVIEW plane has no resume obligation at all, which is exactly where truncate-and-label is the * right answer and why this machinery is worth keeping. * * **Calling this from the durable emitter would be a silent-loss bug**, not a performance choice — * so if you are here looking for the packer, you want `packUnits` in `agui-emitter.ts`. And it is * marked rather than deleted for the reason one module over already demonstrated: * `assertExpectationSemantics()` sat with zero production callers looking exactly like live code, * and unreachable code that looks live is a hazard whichever direction the next reader resolves it * in. * * **Say the uncomfortable thing:** this is a content truncation, which is one of the * sins `tr-` is being abolished for. The difference is not that we are gentler about it. `tr-` cut * *every* result at 700 characters, silently and unconditionally, as a design choice; this cuts only * what cannot physically be sent, three orders of magnitude higher, and records what it cut and how * big it was. If routine results start tripping the ceiling the honest response is a * content-addressed side channel, not a quieter limit. * * **Splitting happens on EVENT boundaries**, each part carrying its own `seq`, so a run may legally * open in one frame and close in the next. That is why {@link AguiBrackets} checks the writer's * stream and not the frame — a per-frame balance check would forbid the split this function * performs. * * **`seq` is measured, not assumed.** Each candidate is measured at the `seq` it will actually carry, * because `seq` is a header-adjacent value in the encoded body: sizing at 9 and publishing at 10 is * one byte, and a frame one byte over the ceiling is refused. The same reason `encodedSize` takes * `expectedLastSubjectSeq` as a parameter rather than sizing at zero. * * @param measure the EXACT encoded size of a candidate, headers included — `CotalEndpoint.encodedSize` * in production. Never re-implement it here. * @param limit the broker's `max_payload`. * @throws {AguiVocabularyError} if a single event cannot be made to fit even fully truncated, or * carries no truncatable field. Failing loud is required: the alternative is looping forever or * dropping the event, and a dropped event on this plane is the silent loss the plane exists to * make impossible. */ export declare function splitFrames(opts: { threadId: string; runId: string; epoch: string; /** The `seq` the FIRST emitted frame carries; each subsequent part takes the next. */ firstSeq: number; events: AguiEvent[]; measure: (frame: AguiFrame) => number; limit: number; }): AguiFrame[]; /** * Find the first extra property on a parsed frame, returning its JSON-path * string, or `undefined` when the envelope is closed. * * Checks two levels: * 1. Frame-level keys against {@link KNOWN_FRAME_KEYS}. * 2. Per-event keys against {@link KNOWN_EVENT_KEYS_BY_TYPE} for the event's * `type`. An event whose `type` is not in the map has already been refused * by `parseAguiFrame`; if it somehow reaches here it is reported as * `events[i]` with no key. * * Returns the dotted path of the first unknown property found, e.g. * `"recovery"` for a frame-level extra or `"events[0].leaked"` for an * event-level extra. The caller turns this into a named verdict. */ export declare function extraPropertyPath(part: Record): string | undefined; export declare function isForbiddenEgressEventType(type: unknown): boolean; /** * The only `message` a published `RUN_ERROR` carries. * * `RUN_ERROR` is the terminal a renderer waits on, so the kind cannot be withheld the way tool * arguments and results are. Its `message`, `code` and `rawEvent` are upstream values that no * connector controls: a harness can echo the prompt, a peer message or tool output into any of them, * and the events channel has a different read ACL from wherever that text was read (#1431). So the * content is fixed instead: every `RUN_ERROR` leaving through the emitter carries this text and no * `code` or `rawEvent`, and the upstream detail stays in the seat's own log. The failure kind a * connector classified is published on presence, as the agent's condition. */ export declare const RUN_ERROR_EGRESS_MESSAGE = "run failed"; /** * Drop forbidden kinds from a mapped unit, and rebuild every `RUN_ERROR` as * {@link egressRunError}. Sibling lifecycle and text events stay. */ export declare function applyAguiEgressPolicy(events: readonly AguiEvent[]): AguiEvent[]; /** What a frozen body is, as far as the egress policy can tell. */ export type FrozenBodyEgressVerdict = "clean" | "forbidden-kind" | "unreadable" | "extra-property" | "run-error-content"; /** * Classify a frozen body for egress. Used on retry, where the body is already on disk and must not * be rewritten. A part that is not a frame is not this policy's to judge. * * THREE ANSWERS, BECAUSE TWO WERE NOT ENOUGH. A boolean forced every frame this policy could not * read into one of two wrong buckets. Reading it through the strict {@link parseAguiFrame} made an * unreadable frame throw a bare vocabulary error out of a machine whose every other abnormal * outcome is a named halt. Skipping it instead PUBLISHED the frame: measured, a body whose `events` * is the string `"TOOL_CALL_RESULT: "` survives a JSON round-trip, carries the bytes as its * event list, and reaches the wire. The confidentiality boundary is the wire, not what a renderer * folds, so a frame this policy cannot interpret is `unreadable` and the caller halts on it by name. * * THE INVARIANT, AND IT IS THE WHOLE DESIGN: this function's READ PATTERN IS ITS PREDECESSOR'S. It * validates the part through the same `parseAguiFrame` and scans the events of that call's result, * in that order, exactly as the strict read it replaces did. Every difference between them is a * throw becoming a named answer. Nothing else moved, so no input can make this weaker than the code * it replaced — not a malformed envelope, not a stateful accessor, not a representation nobody has * thought of. Equivalence is structural here rather than enumerated, and that is the point. * * WHY IT IS WRITTEN THIS WAY AND NOT AS A DIRECT SCAN. Four repairs tried to answer the forbidden- * kind question by reading `events` off the part itself and validating separately. Each one shipped * a classifier that was weaker than the strict read on some input the author had not pictured: a * non-array `events` that used to throw (`1698fe253`), an empty array the scan looped over zero * times (`350b8eb7a`), an envelope whose `protocol`, `threadId`, `runId`, `epoch` and `seq` were * never checked at all (`071233483`), and a check/use split where the scan and the validator read * `events` at different times and could disagree (`6502b213e`). The last of those tried to close the * split by DETECTING the objects that could exhibit it, which held for an own accessor and lost * immediately to an inherited one and to a Proxy reporting a data descriptor while its `get` trap * returned something else. A guard that enumerates representations loses to the next representation. * Reading the way the predecessor read cannot lose that way, because there is no second reader whose * answer could differ. * * ORDERING, STATED BECAUSE IT IS OBSERVABLE. The parse runs before the scan, so a frame that both * fails to parse and carries a forbidden event answers `unreadable`, not `forbidden-kind`. That is * the predecessor's order too — it threw before it ever reached its scan — so this is equivalence, * not a new preference. Both answers halt the publish and neither claims a cause it did not see. * `forbidden-kind` still wins over `unreadable` ACROSS parts: an earlier unparseable part does not * stop a later part's forbidden event from being named as the more specific diagnosis. * * `run-error-content` (#1431) is read off the same parse result as the forbidden-kind scan: a * `RUN_ERROR` other than the fixed shape {@link applyAguiEgressPolicy} writes was frozen before that * fix and may carry upstream text, so it halts like a pre-fix tool event does. * * A part whose `kind` is not `AGUI_FRAME_KIND` is skipped, as it was before. That is the * pre-existing non-frame gap and this function neither widens nor closes it. * * TOTAL: no input makes this throw. That covers iterating `body` itself, not only reading a part's * properties: a Proxy whose `Symbol.iterator` traps, or an array with a throwing index accessor, * raises before any per-part catch can see it, and an abort there is fail-closed rather than * `clean`. `isAguiFramePart` in core makes the same promise for the same stated reason, that a * function accepting `unknown` and throwing on some of it is a trap for its next caller. This takes * `readonly unknown[]`. */ export declare function frozenBodyEgressVerdict(body: readonly unknown[]): FrozenBodyEgressVerdict; //# sourceMappingURL=agui.d.ts.map