/** * 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, and * the `cotal.*` `CUSTOM` table, which is empty in v1. * * Not here, and NAMED rather than stubbed, because a stub is a claim that the shape is known: the * channel derivation, the `max_payload` split, and the emitter that publishes. Each of those talks * to something outside this module, and each lands with the surface that calls it. * * **Nothing constructs a frame outside a smoke.** No connector emits, and this module publishes * nothing: every export below is a pure function of its arguments. That is a statement about the * tree, not a disclaimer — a reader deciding whether a change here can reach a customer needs it 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"; import { type Part } from "@cotal-ai/core"; import type { DurableSource } from "./durable-source.js"; import type { SubjectFrontier } from "./subject-frontier.js"; import type { EventWal } from "./event-wal.js"; /** * 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; /** `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; /** * 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 {@link 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`. 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[]; /** * The endpoint surface the emitter needs, declared STRUCTURALLY rather than as `CotalEndpoint`. * * Not for testability as an end in itself — for a specific one. A cell that needs a live broker to * exercise the duplicate-ack halt cannot be written at all before a broker exists, and a cell that * re-implements `encodedSize` is measuring a copy. This interface is the exact set of methods the * emitter calls, so a cell substitutes an instrument and the production path substitutes the real * endpoint, and neither one is a re-implementation of the other. */ export interface EmitterEndpoint { readonly principal: { owner: string; actor: string; }; readonly actorIsEphemeral: boolean; /** The broker's live `max_payload`. Throws when not connected — never guesses a default. */ readonly maxPayload: number; /** The single-replica preflight. The emitter calls this at startup, before anything can publish. */ assertExpectationSemantics(): Promise; encodedSize(o: { channel: string; parts: Part[]; id: string; expectedLastSubjectSeq: number; }): number; multicastExpecting(o: { channel: string; parts: Part[]; id: string; expectedLastSubjectSeq: number; }): Promise<{ ack: { seq: number; duplicate: boolean; }; }>; } /** * One source record's worth of events, with the cursor that resumes AFTER that record. * * One durable emit unit is one frame: the emitter splits only at source-observation boundaries that * are independently reconstructable from the durable source. A frame therefore ends where a record * ends, and carries that record's cursor, so folding it means exactly "every record here is * consumed". */ export interface EmitUnit { /** The run these events belong to. A frame's envelope names ONE run, so units are never * mixed across runs in one frame. */ runId: string; events: AguiEvent[]; cursor: string; } /** What the mapper returns for one source record: its run and its events, or `null` for a record * this plane deliberately drops, and it drops many. `null` is NOT an error: a deliberate drop and * a failed map are kept apart, because conflating them turns a parser bug into skipped history. */ export type RecordMapper = (record: T) => { runId: string; events: AguiEvent[]; } | null; /** * The emitter has stopped and will not publish again without operator action. * * Halting is a SUCCESS of this design, not a failure of it: every halt below is a case where the * alternative is to report success for a message that was not stored, or to fold an ack for a body * we did not write. A halt is loud, bounded and recoverable by a human; the alternative is silent * and permanent. */ /** * A bracket violation that is OURS, not the writer's: the machine that tracks open runs and messages * was lost across a process restart. * * **This exists because two halts that both say "unbalanced" prove nothing about which produced * one.** The WAL persists `epoch`, `frontier` and the pending frame, and NOT the set of open * runs and messages, so a process that dies mid-run restarts with an empty {@link AguiBrackets}, * resumes from `sourceCursor` at events whose `RUN_STARTED` was already published, and refuses the * first of them. Without this class the operator sees "nothing may be emitted outside an open run" * and files a bug against a writer that did nothing wrong. * * It is deliberately a SUBCLASS: every existing catch of {@link AguiVocabularyError} still catches * it, and only code that wants to tell the two apart has to know it exists. */ export declare class AguiBracketStateLost extends AguiVocabularyError { readonly cause: Error; constructor(message: string, cause: Error); } export declare class AguiEmitterHalted extends Error { readonly reason: "duplicate-ack" | "cas-loss" | "egress-policy" | "egress-unreadable" | "egress-extra-property"; constructor(reason: "duplicate-ack" | "cas-loss" | "egress-policy" | "egress-unreadable" | "egress-extra-property", message: string); } /** * Pack units into frames, splitting ONLY at unit boundaries and never inside one. * * Deliberately NOT {@link splitFrames}, and the difference is the durable plane's one-unit-one-frame * rule. `splitFrames` splits at EVENT boundaries, which is the right answer for a frame considered * on its own, but a frame that ends mid-record has no cursor it can honestly store: the only value * available says the whole record was consumed, and folding that after a crash skips the rest of the * record's events with no `seq` gap for a consumer to notice. * * **So the event-boundary split and the one-unit-one-frame rule are in tension, and this resolves it * in the direction the durable plane requires: a single unit that does not fit FAILS LOUD rather * than being truncated at a frame boundary.** That leaves `splitFrames`'s truncation path with no * caller on the durable plane, which is reported as a design conflict rather than decided here. * * @throws {AguiVocabularyError} when one unit cannot fit in a frame alone. */ export declare function packUnits(opts: { threadId: string; epoch: string; firstSeq: number; units: readonly EmitUnit[]; measure: (frame: AguiFrame) => number; limit: number; }): { frame: AguiFrame; cursor: string; }[]; /** * 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; /** Drop forbidden kinds from a mapped unit. 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"; /** * 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. * * 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; /** * The event emitter: one per principal, one thread at a time. * * **BRACKET STATE SURVIVES A RESTART, AND THIS PARAGRAPH USED TO SAY THE OPPOSITE.** It described a * declared gap — an emitter coming back with an empty machine, resuming at events whose * `RUN_STARTED` had already been published, and refusing the first of them — long after the WAL * started persisting the machine. The words were true when they were written and stayed on the page * through the change that falsified them, which is the failure mode a class header is worst at * showing: it is the first thing a cutover author reads about recovery, and it was telling them to * expect a halt the code no longer produces. * * What actually happens: {@link AguiBrackets} is a property of the WRITER'S STREAM across frames, so * the WAL freezes the machine's state WITH each pending frame and promotes it on fold. A restart * therefore reopens knowing exactly which run, messages and tool calls were open at the last FOLDED * position, and {@link AguiBrackets.restore} continues from there rather than from empty. * * **The lost-state path still exists, and it is now the narrow case it should always have been:** a * document that CANNOT SAY what was open. That is a WAL migrated from v1, which recorded no bracket * state at all, and it loads as `null` rather than as an empty machine precisely so the difference * stays visible. Only there does the emitter start empty, resume into an already-open run, and * refuse the first event with {@link AguiBracketStateLost} — a halt rather than a loss, which is the * safe direction, and diagnosed by name rather than surfacing as an anonymous protocol violation. */ export declare class AguiEmitter { private readonly ep; private readonly wal; private readonly source; private readonly map; /** Derived from the endpoint's OWN principal, never from a config name or the launch env. */ readonly channel: string; readonly threadId: string; /** * The bracket machine AT THE FOLDED POSITION — deliberately not "wherever validation got to". * * It advances one frame at a time, immediately before that frame's `beginSend`, so the state * frozen with a pending frame is the state that belongs to it. A machine advanced by the whole * batch up front would freeze a state describing events that had not been sent. */ private brackets; private halted; /** True once THIS process has fed an event through the bracket machine. It is the half of the * restart diagnosis that keeps a genuine mid-stream violation from being blamed on a restart. */ private fedAnyEvent; private constructor(); /** * Start an emitter: resolve the channel, run the single-replica preflight, and settle any pending * frame. * * **THIS IS THAT PREFLIGHT'S PRODUCTION CALL SITE, AND UNTIL THIS FUNCTION EXISTED THERE WAS * NONE.** `CotalEndpoint.assertExpectationSemantics()` had zero production callers: it was a * check that shipped, was covered by its own suite, and never ran outside one. That is why it is * called HERE, before recovery and therefore before any publish — a serialized append on an * unverified stream is the exact case it exists to prevent, and doing it after recovery would * leave the one publish that matters most, the re-publish of a frozen frame, outside the guard. */ static start(opts: { endpoint: EmitterEndpoint; /** Already open, so the caller owns `space`, the WAL path, and the `subjectMayExist` judgement * — none of which the emitter can make honestly on the caller's behalf. */ wal: EventWal; /** * The PRINCIPAL-scoped subject frontier. **Required, and not optional with a zero default.** * * The subject is shared by every thread of one principal, so the expectation a publish carries * is a fact about the principal and not about the thread. An optional parameter here would let * a new connector omit it and reintroduce, silently, the defect where an agent's second session * expects an empty subject its own first session filled. There is one thing to pass and there * is no legal way to not pass it. */ subjectFrontier: SubjectFrontier; source: DurableSource; map: RecordMapper; }): Promise>; /** True once the emitter has stopped for good. */ get stopped(): boolean; /** * Boot recovery, branching on the WAL's tag. * * `acked` NEVER republishes: the frame landed and we know it, so the only remaining work is to * fold. `sent_unacked` is the genuinely uncertain case and republishes with the SAME frozen `id` * and `E` — never the current tip, because re-deriving either is what turns an uncertain publish * into a second, different message. */ private recover; /** * Read forward, map, pack, and publish. Returns what it did, so a caller can distinguish "nothing * to do" from "did work" without inspecting the WAL. */ pump(): Promise<{ frames: number; events: number; }>; /** * Close the run this stream currently has open, at a boundary the RECORD STREAM CANNOT SEE. * * **This exists because the two halves of the mapping were specified against different inputs.** * The plan sources `RUN_FINISHED` from a harness lifecycle hook, and the durable plane reads a * FILE: a hook fires in another process and writes no record, so a hook-sourced terminal has no * vehicle into a record-sourced stream. Deriving the terminal from records instead is possible but * lies about time in two ways that matter to a live view: the finish lands only when the NEXT turn * starts, so a finished agent renders as still running, and the last run of a session never closes * at all, because there is no later record to close it on. This is that vehicle. * * It is a FRAME LIKE ANY OTHER: same epoch, same `seq` line, same write-ahead discipline, same * halt rules. The single thing that differs is the cursor, which is republished UNCHANGED, because * this frame consumes no source record. A frame that advanced the cursor here would mark records * consumed that were never mapped. * * Idempotent by construction rather than by a flag: the bracket machine is the only state it * reads, so once the run is closed there is nothing open to close and it answers `null`. That also * makes it safe on a stream whose run was opened by a PREVIOUS process, since the machine is * restored from the WAL. * * **AN `error` CLOSES THE SAME RUN WITH `RUN_ERROR` INSTEAD, and it is one method rather than two * ON PURPOSE.** `RUN_ERROR` closes a run on its own, so a run that emitted one must never also * emit a `RUN_FINISHED`. With a second method that invariant would be a rule someone has to * remember; with one method and one branch it is a property of the shape: exactly one terminal is * built, and the bracket machine has closed the run by the time anything could ask for another, so * a following close answers `null` like any other close on a settled stream. Which harness signals * mean a turn FAILED is a connector's decision and is stated at each connector's own mapping site; * this file only carries the answer to the wire. * * **AN OVERSIZED `error.message` IS BOUNDED HERE, ONCE, FOR EVERY CONNECTOR.** The message is * upstream free text. If it cannot fit in the one closing frame, the close rebuilds the event so * it does, keeps the `code`, and the emitted message says the original detail was omitted or * shortened because of the bound. A short message is unchanged. The alternative is `packUnits` * refusing before `beginSend`, which leaves the run with no terminal and kills the holder. * * @returns the run that was closed, or `null` when the stream was already at a stopping point. */ closeRun(o: { timestamp: number; cotal?: CotalMeta; /** Close with `RUN_ERROR` carrying these instead of `RUN_FINISHED`. */ error?: { message: string; code?: string; }; }): Promise; /** Measure a candidate frame EXACTLY as the wire will, at an upper bound over id and expectation. */ private measure; /** Transition 1 then the first network attempt. */ private publish; /** * One publish attempt — first or retry — with the FROZEN id and the FROZEN `E`. Never the tip. * * The three outcomes are not symmetric and the asymmetry is the design: * - `!duplicate` → transition 2 then 3. Success becomes durable before the frontier moves. * - `duplicate` → HALT. On a first attempt it means a body WE DID NOT WRITE holds our id, and * folding its `ackSeq` would advance the frontier and the source cursor past events that were * never published. On a retry it cannot happen on a single-replica stream at all, because such a * stream evaluates the expectation before the dedup cache, so observing it proves the stream is * not single-replica. Both are * fail-loud, and neither is a case where guessing is better than stopping. * - CAS loss → HALT. Someone else moved the tip on a subject only this principal may write, or * the subject was purged. Uncertainty plus a moved tip is exactly what must not be guessed at. * * A NETWORK error is deliberately none of these: it leaves `pending` as `sent_unacked`, which is * the state that means "we do not know", and the next boot retries the same frozen frame. */ private attempt; /** * Decide whether a bracket refusal is the WRITER's fault or OURS, and say which. * * Ours iff ALL THREE hold, and each is load-bearing: * - this process has fed NO event through the machine yet, so the machine cannot have been put * into a bad state by anything we did in this run; and * - the frontier is non-virgin, so frames — and therefore possibly an open `RUN_STARTED` — were * published by a PREVIOUS process; and * - the WAL cannot say what was open. Since v2 the machine is PERSISTED, so an ordinary restart * restores it and never reaches here at all; `null` means the document was migrated from v1 and * genuinely never recorded the state. Without this condition the diagnosis would survive as a * permanent excuse for a case the migration fixed. * * Drop the first condition and a genuine mid-stream violation by the writer gets blamed on a * restart that happened an hour ago. Drop the second and a violation on a virgin thread, where * nothing was ever published and nothing could have been lost, gets blamed on a restart that never * happened. Each condition alone produces a confident, wrong diagnosis — which is worse than the * undiagnosed error it replaced, because a named cause stops the search. */ private diagnoseBracket; private halt; } //# sourceMappingURL=agui.d.ts.map