@cotal-ai/connector-core 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +12 -2
- package/dist/agent.js.map +1 -1
- package/dist/agui-holder.d.ts +89 -0
- package/dist/agui-holder.d.ts.map +1 -0
- package/dist/agui-holder.js +135 -0
- package/dist/agui-holder.js.map +1 -0
- package/dist/agui-render.d.ts +100 -0
- package/dist/agui-render.d.ts.map +1 -0
- package/dist/agui-render.js +283 -0
- package/dist/agui-render.js.map +1 -0
- package/dist/agui-wal-path.d.ts +103 -0
- package/dist/agui-wal-path.d.ts.map +1 -0
- package/dist/agui-wal-path.js +324 -0
- package/dist/agui-wal-path.js.map +1 -0
- package/dist/agui.d.ts +727 -0
- package/dist/agui.d.ts.map +1 -0
- package/dist/agui.js +1114 -0
- package/dist/agui.js.map +1 -0
- package/dist/docs-bundle.generated.d.ts.map +1 -1
- package/dist/docs-bundle.generated.js +16 -9
- package/dist/docs-bundle.generated.js.map +1 -1
- package/dist/durable-source.d.ts +127 -0
- package/dist/durable-source.d.ts.map +1 -0
- package/dist/durable-source.js +219 -0
- package/dist/durable-source.js.map +1 -0
- package/dist/event-wal.d.ts +265 -0
- package/dist/event-wal.d.ts.map +1 -0
- package/dist/event-wal.js +698 -0
- package/dist/event-wal.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/launch.d.ts +78 -2
- package/dist/launch.d.ts.map +1 -1
- package/dist/launch.js +120 -0
- package/dist/launch.js.map +1 -1
- package/dist/tool-specs.d.ts +40 -2
- package/dist/tool-specs.d.ts.map +1 -1
- package/dist/tool-specs.js +51 -1
- package/dist/tool-specs.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +5 -6
- package/dist/tools.js.map +1 -1
- package/package.json +8 -2
package/dist/agui.d.ts
ADDED
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AG-UI event VOCABULARY and the Cotal frame envelope.
|
|
3
|
+
*
|
|
4
|
+
* This is the file that makes the change an abolition rather than a rename. Renaming `tr-<name>` to
|
|
5
|
+
* `events.<name>` while the connectors still publish `condense()` output would move glyph-prefixed
|
|
6
|
+
* text to a new channel and change nothing a consumer can do with it — the channel name was never
|
|
7
|
+
* the complaint.
|
|
8
|
+
*
|
|
9
|
+
* **The vocabulary is adopted; the SDK is not.** `@ag-ui/core` is a `devDependency`,
|
|
10
|
+
* pinned EXACT at `0.0.57`, and this file imports from it with `import type` ONLY. The reason is
|
|
11
|
+
* measured rather than stylistic: `0.0.57` declares `dependencies: { zod: "^3.22.4" }` (verified
|
|
12
|
+
* against the registry, not remembered), and `connector-core` is esbuild-bundled into every seeded
|
|
13
|
+
* connector — so a runtime dependency would ship a second zod major to every customer in order to
|
|
14
|
+
* validate events we construct ourselves. The conformance smoke imports the real schemas and
|
|
15
|
+
* validates against them; production code carries types and string literals and no zod.
|
|
16
|
+
*
|
|
17
|
+
* **Promotion trigger, conditional and NOT scheduled:** if a 0.1.x ships stable with zod moved to
|
|
18
|
+
* `peerDependencies`, promote to a runtime dependency and use the schemas directly. As of this
|
|
19
|
+
* writing `latest` is `0.0.57`, the active `canary` still carries the zod-3 runtime dep, and the
|
|
20
|
+
* release that moves zod to a peer sits on no dist-tag at all.
|
|
21
|
+
*
|
|
22
|
+
* ## What is here, and what is deliberately NOT
|
|
23
|
+
*
|
|
24
|
+
* Here: the event constructors, the frame envelope, the routing/validity split
|
|
25
|
+
* ({@link isAguiFramePart} / {@link parseAguiFrame}), the {@link AguiBrackets} stream machine, and
|
|
26
|
+
* the `cotal.*` `CUSTOM` table, which is empty in v1.
|
|
27
|
+
*
|
|
28
|
+
* Not here, and NAMED rather than stubbed, because a stub is a claim that the shape is known: the
|
|
29
|
+
* channel derivation, the `max_payload` split, and the emitter that publishes. Each of those talks
|
|
30
|
+
* to something outside this module, and each lands with the surface that calls it.
|
|
31
|
+
*
|
|
32
|
+
* **Nothing constructs a frame outside a smoke.** No connector emits, and this module publishes
|
|
33
|
+
* nothing: every export below is a pure function of its arguments. That is a statement about the
|
|
34
|
+
* tree, not a disclaimer — a reader deciding whether a change here can reach a customer needs it to
|
|
35
|
+
* be accurate, so it is maintained rather than left to rot.
|
|
36
|
+
*/
|
|
37
|
+
import type { CustomEvent, ReasoningMessageContentEvent, ReasoningMessageEndEvent, ReasoningMessageStartEvent, RunErrorEvent, RunFinishedEvent, RunStartedEvent, TextMessageContentEvent, TextMessageEndEvent, TextMessageStartEvent, ToolCallArgsEvent, ToolCallEndEvent, ToolCallResultEvent, ToolCallStartEvent } from "@ag-ui/core";
|
|
38
|
+
export { AGUI_FRAME_KIND, AGUI_EVENT_TYPE, isAguiFramePart } from "@cotal-ai/core";
|
|
39
|
+
import { AGUI_FRAME_KIND } from "@cotal-ai/core";
|
|
40
|
+
import { type Part } from "@cotal-ai/core";
|
|
41
|
+
import type { DurableSource } from "./durable-source.js";
|
|
42
|
+
import type { EventWal } from "./event-wal.js";
|
|
43
|
+
/**
|
|
44
|
+
* The AG-UI events this plane emits — the MAPPED SUBSET, not the whole protocol.
|
|
45
|
+
*
|
|
46
|
+
* Absent by decision, each recorded so its absence is not read as an oversight:
|
|
47
|
+
* `*_CHUNK` (all three sources are settled observations, so we emit the START/CONTENT/END
|
|
48
|
+
* triple, which is the subset every consumer implements: raw CHUNK needs a client transformer);
|
|
49
|
+
* `STATE_*` (reserved for a later lane, and ask state left this plane entirely);
|
|
50
|
+
* `MESSAGES_SNAPSHOT` (dropped as a compaction anchor, because a
|
|
51
|
+
* windowed snapshot DELETES the prefix); `THINKING_*` (deprecated at 0.0.57 in favour of
|
|
52
|
+
* `REASONING_*`, which is what we emit).
|
|
53
|
+
*/
|
|
54
|
+
export type AguiEvent = RunStartedEvent | RunFinishedEvent | RunErrorEvent | TextMessageStartEvent | TextMessageContentEvent | TextMessageEndEvent | ToolCallStartEvent | ToolCallArgsEvent | ToolCallEndEvent | ToolCallResultEvent | ReasoningMessageStartEvent | ReasoningMessageContentEvent | ReasoningMessageEndEvent | CustomEvent;
|
|
55
|
+
/**
|
|
56
|
+
* Cotal metadata rides ONE key on a standard event.
|
|
57
|
+
*
|
|
58
|
+
* Legal because every AG-UI event schema is `.passthrough()` — asserted by the conformance smoke
|
|
59
|
+
* against the real schemas rather than trusted, since this whole vehicle collapses if a future
|
|
60
|
+
* release tightens it.
|
|
61
|
+
*
|
|
62
|
+
* **It does NOT work everywhere.** `RunFinishedOutcomeSchema` is STRICT: it refuses unrecognized
|
|
63
|
+
* keys, measured. So `cotal` may ride an EVENT and never an `outcome`. Recorded here because
|
|
64
|
+
* "AG-UI is passthrough" is the kind of sentence that gets generalized one level too far.
|
|
65
|
+
*/
|
|
66
|
+
export interface CotalMeta {
|
|
67
|
+
/** Where the `timestamp` came from. Absent means the source carried a real one. */
|
|
68
|
+
tsSource?: "arrival";
|
|
69
|
+
/** Set when the connector minted the `runId` rather than reading one from the harness. */
|
|
70
|
+
runIdSource?: "connector";
|
|
71
|
+
/** The provider's own message id — preserved for correlation, never spent as `messageId`. */
|
|
72
|
+
providerMessageId?: string;
|
|
73
|
+
/** The harness's stop reason, on the event that carries one. */
|
|
74
|
+
stopReason?: string;
|
|
75
|
+
/** `tool_result.is_error` — AG-UI's result event has no error field of its own. */
|
|
76
|
+
isError?: boolean;
|
|
77
|
+
/** Subagent linkage. Deliberately NOT `parentRunId`, which is retry/edit lineage. */
|
|
78
|
+
delegation?: {
|
|
79
|
+
agentId: string;
|
|
80
|
+
toolCallId: string;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* WHAT BEGAN THIS RUN. Attribution, and deliberately NOT a gate on run-opening.
|
|
84
|
+
*
|
|
85
|
+
* Run-opening ("did a turn begin?") and attribution ("who began it?") are two questions, and an
|
|
86
|
+
* earlier revision used one provenance predicate to answer both — so a turn started by a peer
|
|
87
|
+
* produced no run at all, and an agent-driven session mapped to nothing. Provenance ANNOTATES;
|
|
88
|
+
* it does not SELECT. A run with `"channel"` attribution is still a run.
|
|
89
|
+
*
|
|
90
|
+
* `"unknown"` is never written by the mapper: an unrecognised provenance FAILS LOUD instead, so a
|
|
91
|
+
* future harness value produces an error rather than a confident wrong attribution. It exists for
|
|
92
|
+
* consumers that must render something for a producer which did not set the field.
|
|
93
|
+
*/
|
|
94
|
+
turnSource?: "human" | "channel" | "notification" | "sdk" | "unknown";
|
|
95
|
+
/** What was cut to fit the wire, and how big it was. Set only by the sizing path. */
|
|
96
|
+
truncated?: {
|
|
97
|
+
field: string;
|
|
98
|
+
originalBytes: number;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** An event carrying Cotal metadata. Kept structural so it composes with any member of the union. */
|
|
102
|
+
export type WithCotal<E> = E & {
|
|
103
|
+
cotal?: CotalMeta;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* The `cotal.*` `CUSTOM` event table — the second and ONLY other vehicle for Cotal-specific data.
|
|
107
|
+
*
|
|
108
|
+
* **The v1 table is EMPTY, and that is the specification, not an unfinished state.** Earlier
|
|
109
|
+
* revisions described a two-member table holding `cotal.ask.opened` / `cotal.ask.settled`. Both
|
|
110
|
+
* were removed when ask state left this plane, on the operative ground that there is no consumer:
|
|
111
|
+
* the board answers what is owed, the plane carries what is happening. Leaving the count in the
|
|
112
|
+
* prose invited re-adding them "because the table has two slots", so the table ships with no slots.
|
|
113
|
+
*
|
|
114
|
+
* It exists as the GATE: adding a member is a decision that touches this declaration, rather than
|
|
115
|
+
* a `CUSTOM` name invented at a call site where nobody reviews the vocabulary.
|
|
116
|
+
*/
|
|
117
|
+
export declare const COTAL_CUSTOM_EVENTS: readonly string[];
|
|
118
|
+
/** The envelope version. One frame declares the AG-UI vocabulary version it was built against. */
|
|
119
|
+
export declare const AGUI_PROTOCOL = "ag-ui/0.0.57";
|
|
120
|
+
/**
|
|
121
|
+
* One Cotal message = one frame.
|
|
122
|
+
*
|
|
123
|
+
* `threadId` is the native harness session and `runId` is ONE native harness turn, and nothing
|
|
124
|
+
* else may claim either of them. `epoch` is the writer-identity fence recovered from the WAL
|
|
125
|
+
* (never re-minted on restart), and `seq` is this writer's frame counter, which is what lets a
|
|
126
|
+
* consumer detect a gap rather than merely fail to notice one.
|
|
127
|
+
*
|
|
128
|
+
* **A frame carries no text part, by design.** That is why the renderers are a binding precondition
|
|
129
|
+
* on the cutover rather than a follow-up.
|
|
130
|
+
*
|
|
131
|
+
* **RE-DERIVED, because core changed underneath this sentence.** It used to end "a viewer that does
|
|
132
|
+
* not understand this part shows nothing, and an empty pane is indistinguishable from a
|
|
133
|
+
* correctly-empty one." That is now true of some surfaces and false of others, and the split is
|
|
134
|
+
* exactly which ones adopted core's shared `partsToText`:
|
|
135
|
+
*
|
|
136
|
+
* - **3 ADOPTED IT** — `connector-core/src/agent.ts`, `cli/src/commands/join.ts`,
|
|
137
|
+
* `cli/src/view/mesh-view.ts`. These now render a marker naming the kind.
|
|
138
|
+
* - **4 DID NOT** — `implementations/web/src/web/app.js`, `.../graph.js`,
|
|
139
|
+
* `examples/02-self-improving-console/harness/observer.ts`,
|
|
140
|
+
* `examples/04-frontier-faces/tools/studio.mjs`. The two stringify-form copies still leave a
|
|
141
|
+
* stray separator; the two filter-form ones still leave no trace at all.
|
|
142
|
+
*
|
|
143
|
+
* Measured on a real frame from `aguiFrame` below, placed between two text parts: the adopted
|
|
144
|
+
* renderer produced `"before after"` before the core change and names the kind after it. **The
|
|
145
|
+
* worse half of that defect was never the missing frame — it was that `"before after"` is a
|
|
146
|
+
* well-formed sentence with a silent hole in it, so it prompts no question at all.**
|
|
147
|
+
*
|
|
148
|
+
* **THE PRECONDITION IS UNCHANGED AND THE MARKER IS NOT A LOOPHOLE IN IT.** A named marker proves a
|
|
149
|
+
* frame ARRIVED; it does not display one. Cutting a connector over on the strength of it would
|
|
150
|
+
* still ship events nothing can render.
|
|
151
|
+
*/
|
|
152
|
+
export interface AguiFrame {
|
|
153
|
+
kind: typeof AGUI_FRAME_KIND;
|
|
154
|
+
protocol: typeof AGUI_PROTOCOL;
|
|
155
|
+
threadId: string;
|
|
156
|
+
runId: string;
|
|
157
|
+
epoch: string;
|
|
158
|
+
seq: number;
|
|
159
|
+
events: AguiEvent[];
|
|
160
|
+
}
|
|
161
|
+
/** Raised when a frame or an event sequence violates a structural rule of the vocabulary. */
|
|
162
|
+
export declare class AguiVocabularyError extends Error {
|
|
163
|
+
constructor(message: string);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The bracket machine's persisted form (WAL v2).
|
|
167
|
+
*
|
|
168
|
+
* It is a plain, JSON-round-trippable record on purpose: it is written into the write-ahead log, so
|
|
169
|
+
* it must survive `JSON.stringify`/`parse` unchanged and must be readable by a human staring at a
|
|
170
|
+
* WAL trying to work out why an emitter refused something.
|
|
171
|
+
*/
|
|
172
|
+
export interface BracketState {
|
|
173
|
+
/** The run currently open, or `undefined` when the stream is at a legal stopping point. */
|
|
174
|
+
run: string | undefined;
|
|
175
|
+
text: string[];
|
|
176
|
+
reasoning: string[];
|
|
177
|
+
tools: string[];
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Bracketing, checked INCREMENTALLY over the event sequence — deliberately not over one frame.
|
|
181
|
+
*
|
|
182
|
+
* **A frame is not guaranteed to be self-bracketed, and a validator demanding that it be would
|
|
183
|
+
* forbid a split this design requires.** An oversized frame splits on event boundaries with each
|
|
184
|
+
* part carrying its own `seq`, so a run can legally open in one frame and close in the next. The
|
|
185
|
+
* unit that must balance is the WRITER'S STREAM, not the message. Feeding this machine frame after
|
|
186
|
+
* frame is therefore the only way to check the property that is actually claimed.
|
|
187
|
+
*
|
|
188
|
+
* What it enforces:
|
|
189
|
+
* - one run open at a time; nothing may be emitted outside an open run
|
|
190
|
+
* - `TEXT_MESSAGE_*`, `REASONING_MESSAGE_*` and `TOOL_CALL_*` open and close by their own id, and an
|
|
191
|
+
* id may not be opened twice while already open
|
|
192
|
+
* - a run may not close while any message or tool call it opened is still open
|
|
193
|
+
*
|
|
194
|
+
* The id-reuse rule is the one with a measured defect behind it: `message.id` is a PROVIDER REQUEST
|
|
195
|
+
* id, and over one real session 833 of 1243 assistant `message.id` values appeared in more than one
|
|
196
|
+
* JSONL entry. Keying identity on it would open and close the same `messageId` repeatedly, which
|
|
197
|
+
* the AG-UI verifier rejects and the reference reducer collapses. This machine refuses that rather
|
|
198
|
+
* than letting it reach a consumer.
|
|
199
|
+
*/
|
|
200
|
+
export declare class AguiBrackets {
|
|
201
|
+
private run;
|
|
202
|
+
private readonly text;
|
|
203
|
+
private readonly reasoning;
|
|
204
|
+
private readonly tools;
|
|
205
|
+
/**
|
|
206
|
+
* The machine's whole state, as plain JSON — what the WAL persists so a restart does not lose it.
|
|
207
|
+
*
|
|
208
|
+
* Sorted, because this value is written to disk and compared BY A HUMAN reading two documents.
|
|
209
|
+
* A `Set`'s iteration order is insertion order, so two machines that are semantically identical
|
|
210
|
+
* would serialize differently depending on the order events happened to arrive, and a diff of two
|
|
211
|
+
* WALs would show a change where there is none.
|
|
212
|
+
*/
|
|
213
|
+
snapshot(): BracketState;
|
|
214
|
+
/** Rebuild a machine from a snapshot. The inverse of {@link snapshot}, and the reason a mid-run
|
|
215
|
+
* restart can continue instead of refusing its first event. */
|
|
216
|
+
static restore(s: BracketState): AguiBrackets;
|
|
217
|
+
/** An independent machine at the same state — used to VALIDATE a batch without advancing the
|
|
218
|
+
* machine that is in step with the disk. */
|
|
219
|
+
clone(): AguiBrackets;
|
|
220
|
+
/** True while a run is open — i.e. the stream is mid-turn and not at a legal stopping point. */
|
|
221
|
+
get open(): boolean;
|
|
222
|
+
/** The run currently open, for diagnostics and for checking a frame's envelope against it. */
|
|
223
|
+
get runId(): string | undefined;
|
|
224
|
+
/** Feed one event. Throws {@link AguiVocabularyError} on the first violation. */
|
|
225
|
+
accept(event: AguiEvent): void;
|
|
226
|
+
/**
|
|
227
|
+
* Assert the stream is at a legal stopping point.
|
|
228
|
+
*
|
|
229
|
+
* Called at the end of a synthesized sequence and by any consumer checking a writer closed
|
|
230
|
+
* cleanly. NOT called per frame: mid-turn frames are legally unbalanced.
|
|
231
|
+
*/
|
|
232
|
+
assertClosed(): void;
|
|
233
|
+
private openId;
|
|
234
|
+
private requireOpen;
|
|
235
|
+
private closeId;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Build a frame, validating the envelope's own fields.
|
|
239
|
+
*
|
|
240
|
+
* Bracketing is NOT checked here — see {@link AguiBrackets} for why a single frame cannot be
|
|
241
|
+
* required to balance. The emitter holds one `AguiBrackets` across the whole stream and feeds it as
|
|
242
|
+
* it builds; that is the placement that checks the property actually claimed.
|
|
243
|
+
*/
|
|
244
|
+
export declare function aguiFrame(opts: {
|
|
245
|
+
threadId: string;
|
|
246
|
+
runId: string;
|
|
247
|
+
epoch: string;
|
|
248
|
+
seq: number;
|
|
249
|
+
events: AguiEvent[];
|
|
250
|
+
}): AguiFrame;
|
|
251
|
+
/**
|
|
252
|
+
* **THE CONSUMER-SIDE ENFORCEMENT POINT.** `aguiFrame` above validates on the way OUT; this pair
|
|
253
|
+
* validates on the way IN, and they are separate functions because they are separate trust domains.
|
|
254
|
+
*
|
|
255
|
+
* It exists because the renderers cannot enforce a contract expressed as TypeScript types.
|
|
256
|
+
* `implementations/web/tsconfig.json` carries `"include": ["src"]` with `"exclude": ["src/web"]`,
|
|
257
|
+
* and the build copies `src/web` into `dist` verbatim — so the renderer is plain JavaScript that
|
|
258
|
+
* `tsc` never reads. A prose contract with no enforcement point is the defect, so the contract ships
|
|
259
|
+
* as **a function a consumer executes**, not as a shape a consumer is trusted to have read.
|
|
260
|
+
*
|
|
261
|
+
* **THE TWO ANSWERS ARE DIFFERENT AND MUST NOT BE FUSED.** "This part is not mine" is a routing
|
|
262
|
+
* decision a consumer makes constantly and quietly — every non-frame part on a channel it also
|
|
263
|
+
* reads. "This part claims to be mine and is malformed" is a defect that must be LOUD. One function
|
|
264
|
+
* returning `null` for both would make a version skew look exactly like someone else's message, and
|
|
265
|
+
* a renderer would show an empty pane for a stream it is actively failing to parse. So:
|
|
266
|
+
* {@link isAguiFramePart} answers the routing question with a boolean and never throws, and
|
|
267
|
+
* {@link parseAguiFrame} answers the validity question and throws with the field named.
|
|
268
|
+
*/
|
|
269
|
+
/**
|
|
270
|
+
* Validate an incoming frame, or throw {@link AguiVocabularyError} naming the field that failed.
|
|
271
|
+
*
|
|
272
|
+
* Call it only on a part {@link isAguiFramePart} accepted. Everything after that check is a defect
|
|
273
|
+
* rather than a routing outcome, including an unknown `protocol` — **a version skew must fail loud
|
|
274
|
+
* rather than render partially**, because a consumer that drops the fields it does not recognise
|
|
275
|
+
* shows a confidently incomplete transcript, which is worse than showing nothing.
|
|
276
|
+
*
|
|
277
|
+
* It deliberately does NOT check bracketing. A frame is not guaranteed to be self-bracketed — an
|
|
278
|
+
* oversized frame splits on event boundaries — so the unit that must balance is the writer's stream,
|
|
279
|
+
* and {@link AguiBrackets} is the machine for that, fed frame after frame. A validator demanding a
|
|
280
|
+
* frame balance on its own would forbid a split the plan mandates.
|
|
281
|
+
*/
|
|
282
|
+
export declare function parseAguiFrame(part: unknown): AguiFrame;
|
|
283
|
+
/** `RUN_STARTED` — `threadId` is the native session, `runId` one native harness turn. */
|
|
284
|
+
export declare function runStarted(o: {
|
|
285
|
+
threadId: string;
|
|
286
|
+
runId: string;
|
|
287
|
+
timestamp: number;
|
|
288
|
+
cotal?: CotalMeta;
|
|
289
|
+
}): WithCotal<RunStartedEvent>;
|
|
290
|
+
/**
|
|
291
|
+
* One entry of the `interrupt` outcome.
|
|
292
|
+
*
|
|
293
|
+
* Both fields are REQUIRED strings, measured against the real schema rather than read off its
|
|
294
|
+
* types: an entry missing either is refused naming `outcome.interrupts.<i>.<field>`. Extra keys on
|
|
295
|
+
* an entry are STRIPPED rather than refused, which is why nothing here polices them.
|
|
296
|
+
*/
|
|
297
|
+
export interface AguiInterrupt {
|
|
298
|
+
id: string;
|
|
299
|
+
reason: string;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* `RUN_FINISHED`.
|
|
303
|
+
*
|
|
304
|
+
* `outcome` is OPTIONAL — measured against the real schema, which accepts a `RUN_FINISHED` carrying
|
|
305
|
+
* none. That matters because the Claude `Stop` hook reports that a turn ended and nothing more, so
|
|
306
|
+
* manufacturing a `success` outcome would be asserting something the source never said. When an
|
|
307
|
+
* outcome IS supplied its discriminator key is `type`, not `status` (measured: the schema refuses
|
|
308
|
+
* `{status:"success"}` naming `outcome.type`), and the object is STRICT.
|
|
309
|
+
*
|
|
310
|
+
* The `interrupt` outcome is REPRESENTABLE and VALIDATED here, and UNSPENT: no source on this plane
|
|
311
|
+
* constructs one, because a harness-native park is what would justify it and none of the three
|
|
312
|
+
* sources reports one. It is checked anyway because the arm exists, and an arm that builds an event
|
|
313
|
+
* the schema refuses is worse than an arm that does not exist. Its first draft took `unknown[]` and
|
|
314
|
+
* passed it through, so `[]` and `[{}]` both produced a refused event with nothing looking.
|
|
315
|
+
*/
|
|
316
|
+
export declare function runFinished(o: {
|
|
317
|
+
threadId: string;
|
|
318
|
+
runId: string;
|
|
319
|
+
timestamp: number;
|
|
320
|
+
outcome?: {
|
|
321
|
+
type: "success";
|
|
322
|
+
} | {
|
|
323
|
+
type: "interrupt";
|
|
324
|
+
interrupts: AguiInterrupt[];
|
|
325
|
+
};
|
|
326
|
+
cotal?: CotalMeta;
|
|
327
|
+
}): WithCotal<RunFinishedEvent>;
|
|
328
|
+
/** `RUN_ERROR` — carries `message` and an optional `code`, and NO `runId` of its own. */
|
|
329
|
+
export declare function runError(o: {
|
|
330
|
+
message: string;
|
|
331
|
+
timestamp: number;
|
|
332
|
+
code?: string;
|
|
333
|
+
cotal?: CotalMeta;
|
|
334
|
+
}): WithCotal<RunErrorEvent>;
|
|
335
|
+
/**
|
|
336
|
+
* `TEXT_MESSAGE_START`.
|
|
337
|
+
*
|
|
338
|
+
* `messageId` must be unique per OBSERVATION, not per provider message. The specified form is
|
|
339
|
+
* `${entry.uuid}#${blockIndex}` — the provider's own id is preserved as `cotal.providerMessageId`
|
|
340
|
+
* and never spent here, because it does not have the cardinality the field needs.
|
|
341
|
+
*/
|
|
342
|
+
export declare function textMessageStart(o: {
|
|
343
|
+
messageId: string;
|
|
344
|
+
timestamp: number;
|
|
345
|
+
role?: "assistant" | "user";
|
|
346
|
+
cotal?: CotalMeta;
|
|
347
|
+
}): WithCotal<TextMessageStartEvent>;
|
|
348
|
+
/** `TEXT_MESSAGE_CONTENT` — one settled observation, never a token-level delta. */
|
|
349
|
+
export declare function textMessageContent(o: {
|
|
350
|
+
messageId: string;
|
|
351
|
+
delta: string;
|
|
352
|
+
timestamp: number;
|
|
353
|
+
cotal?: CotalMeta;
|
|
354
|
+
}): WithCotal<TextMessageContentEvent>;
|
|
355
|
+
/** `TEXT_MESSAGE_END`. */
|
|
356
|
+
export declare function textMessageEnd(o: {
|
|
357
|
+
messageId: string;
|
|
358
|
+
timestamp: number;
|
|
359
|
+
cotal?: CotalMeta;
|
|
360
|
+
}): WithCotal<TextMessageEndEvent>;
|
|
361
|
+
/** `TOOL_CALL_START` — `toolCallId` is the harness's own id, carried rather than re-minted. */
|
|
362
|
+
export declare function toolCallStart(o: {
|
|
363
|
+
toolCallId: string;
|
|
364
|
+
toolCallName: string;
|
|
365
|
+
timestamp: number;
|
|
366
|
+
parentMessageId?: string;
|
|
367
|
+
cotal?: CotalMeta;
|
|
368
|
+
}): WithCotal<ToolCallStartEvent>;
|
|
369
|
+
/**
|
|
370
|
+
* `TOOL_CALL_ARGS` — `delta` is the FULL `JSON.stringify(input)`.
|
|
371
|
+
*
|
|
372
|
+
* This is where `tr-`'s `salient()` died: it guessed which argument mattered and dropped the rest,
|
|
373
|
+
* so a reader could not reconstruct what the agent actually did. The whole input goes on the wire,
|
|
374
|
+
* and if it physically cannot fit, the sizing path truncates it with a label rather than silently.
|
|
375
|
+
*/
|
|
376
|
+
export declare function toolCallArgs(o: {
|
|
377
|
+
toolCallId: string;
|
|
378
|
+
delta: string;
|
|
379
|
+
timestamp: number;
|
|
380
|
+
cotal?: CotalMeta;
|
|
381
|
+
}): WithCotal<ToolCallArgsEvent>;
|
|
382
|
+
/** `TOOL_CALL_END`. */
|
|
383
|
+
export declare function toolCallEnd(o: {
|
|
384
|
+
toolCallId: string;
|
|
385
|
+
timestamp: number;
|
|
386
|
+
cotal?: CotalMeta;
|
|
387
|
+
}): WithCotal<ToolCallEndEvent>;
|
|
388
|
+
/**
|
|
389
|
+
* `TOOL_CALL_RESULT`.
|
|
390
|
+
*
|
|
391
|
+
* **`messageId` is REQUIRED by the real schema** — measured; a result without one is refused. The
|
|
392
|
+
* plan's per-connector mapping table names only `toolCallId` for this row, so the identity of the
|
|
393
|
+
* result MESSAGE is unstated there and is raised as a plan gap rather than guessed at a call site.
|
|
394
|
+
* The parameter is required here so a mapper cannot omit it and discover the refusal downstream.
|
|
395
|
+
*
|
|
396
|
+
* `is_error` has no AG-UI field and rides `cotal.isError`.
|
|
397
|
+
*/
|
|
398
|
+
export declare function toolCallResult(o: {
|
|
399
|
+
messageId: string;
|
|
400
|
+
toolCallId: string;
|
|
401
|
+
content: string;
|
|
402
|
+
timestamp: number;
|
|
403
|
+
cotal?: CotalMeta;
|
|
404
|
+
}): WithCotal<ToolCallResultEvent>;
|
|
405
|
+
/**
|
|
406
|
+
* `REASONING_MESSAGE_START` — off by default; the signature is never emitted, ever.
|
|
407
|
+
*
|
|
408
|
+
* **`role` is a REQUIRED literal `"reasoning"`**, unlike `TEXT_MESSAGE_START` where `role` is
|
|
409
|
+
* optional. Measured: the first version of this constructor omitted it and the real schema refused
|
|
410
|
+
* the event. It is set here rather than exposed as a parameter, because there is exactly one legal
|
|
411
|
+
* value and a caller-supplied one could only ever be wrong.
|
|
412
|
+
*/
|
|
413
|
+
export declare function reasoningMessageStart(o: {
|
|
414
|
+
messageId: string;
|
|
415
|
+
timestamp: number;
|
|
416
|
+
cotal?: CotalMeta;
|
|
417
|
+
}): WithCotal<ReasoningMessageStartEvent>;
|
|
418
|
+
/** `REASONING_MESSAGE_CONTENT`. */
|
|
419
|
+
export declare function reasoningMessageContent(o: {
|
|
420
|
+
messageId: string;
|
|
421
|
+
delta: string;
|
|
422
|
+
timestamp: number;
|
|
423
|
+
cotal?: CotalMeta;
|
|
424
|
+
}): WithCotal<ReasoningMessageContentEvent>;
|
|
425
|
+
/** `REASONING_MESSAGE_END`. */
|
|
426
|
+
export declare function reasoningMessageEnd(o: {
|
|
427
|
+
messageId: string;
|
|
428
|
+
timestamp: number;
|
|
429
|
+
cotal?: CotalMeta;
|
|
430
|
+
}): WithCotal<ReasoningMessageEndEvent>;
|
|
431
|
+
/**
|
|
432
|
+
* Split `events` into as many frames as the wire requires, truncating only what physically cannot
|
|
433
|
+
* cross it, and LABELLING every truncation.
|
|
434
|
+
*
|
|
435
|
+
* **THIS IS THE PREVIEW PLANE'S SPLITTER, AND IT HAS NO DURABLE-PLANE CALLER BY DESIGN.**
|
|
436
|
+
* Read that as a boundary, not as an oversight: the durable emitter packs with {@link packUnits} at
|
|
437
|
+
* SOURCE-RECORD boundaries and refuses an oversized unit, because one durable emit unit must be one
|
|
438
|
+
* frame carrying a cursor that resumes after it, and a frame ending mid-record has no cursor it can
|
|
439
|
+
* honestly store. This event-boundary split and its labelled truncation were specified before the
|
|
440
|
+
* durable plane had a cursor contract; where the two disagree, the durable plane's rule wins. The
|
|
441
|
+
* PREVIEW plane has no resume obligation at all, which is exactly where truncate-and-label is the
|
|
442
|
+
* right answer and why this machinery is worth keeping.
|
|
443
|
+
*
|
|
444
|
+
* **Calling this from the durable emitter would be a silent-loss bug**, not a performance choice —
|
|
445
|
+
* so if you are here looking for the packer, you want `packUnits`. And it is marked rather than
|
|
446
|
+
* deleted for the reason one module over already demonstrated: `assertExpectationSemantics()` sat
|
|
447
|
+
* with zero production callers looking exactly like live code, and unreachable code that looks live
|
|
448
|
+
* is a hazard whichever direction the next reader resolves it in.
|
|
449
|
+
*
|
|
450
|
+
* **Say the uncomfortable thing:** this is a content truncation, which is one of the
|
|
451
|
+
* sins `tr-` is being abolished for. The difference is not that we are gentler about it. `tr-` cut
|
|
452
|
+
* *every* result at 700 characters, silently and unconditionally, as a design choice; this cuts only
|
|
453
|
+
* what cannot physically be sent, three orders of magnitude higher, and records what it cut and how
|
|
454
|
+
* big it was. If routine results start tripping the ceiling the honest response is a
|
|
455
|
+
* content-addressed side channel, not a quieter limit.
|
|
456
|
+
*
|
|
457
|
+
* **Splitting happens on EVENT boundaries**, each part carrying its own `seq`, so a run may legally
|
|
458
|
+
* open in one frame and close in the next. That is why {@link AguiBrackets} checks the writer's
|
|
459
|
+
* stream and not the frame — a per-frame balance check would forbid the split this function
|
|
460
|
+
* performs.
|
|
461
|
+
*
|
|
462
|
+
* **`seq` is measured, not assumed.** Each candidate is measured at the `seq` it will actually carry,
|
|
463
|
+
* because `seq` is a header-adjacent value in the encoded body: sizing at 9 and publishing at 10 is
|
|
464
|
+
* one byte, and a frame one byte over the ceiling is refused. The same reason `encodedSize` takes
|
|
465
|
+
* `expectedLastSubjectSeq` as a parameter rather than sizing at zero.
|
|
466
|
+
*
|
|
467
|
+
* @param measure the EXACT encoded size of a candidate, headers included — `CotalEndpoint.encodedSize`
|
|
468
|
+
* in production. Never re-implement it here.
|
|
469
|
+
* @param limit the broker's `max_payload`.
|
|
470
|
+
* @throws {AguiVocabularyError} if a single event cannot be made to fit even fully truncated, or
|
|
471
|
+
* carries no truncatable field. Failing loud is required: the alternative is looping forever or
|
|
472
|
+
* dropping the event, and a dropped event on this plane is the silent loss the plane exists to
|
|
473
|
+
* make impossible.
|
|
474
|
+
*/
|
|
475
|
+
export declare function splitFrames(opts: {
|
|
476
|
+
threadId: string;
|
|
477
|
+
runId: string;
|
|
478
|
+
epoch: string;
|
|
479
|
+
/** The `seq` the FIRST emitted frame carries; each subsequent part takes the next. */
|
|
480
|
+
firstSeq: number;
|
|
481
|
+
events: AguiEvent[];
|
|
482
|
+
measure: (frame: AguiFrame) => number;
|
|
483
|
+
limit: number;
|
|
484
|
+
}): AguiFrame[];
|
|
485
|
+
/**
|
|
486
|
+
* The endpoint surface the emitter needs, declared STRUCTURALLY rather than as `CotalEndpoint`.
|
|
487
|
+
*
|
|
488
|
+
* Not for testability as an end in itself — for a specific one. A cell that needs a live broker to
|
|
489
|
+
* exercise the duplicate-ack halt cannot be written at all before a broker exists, and a cell that
|
|
490
|
+
* re-implements `encodedSize` is measuring a copy. This interface is the exact set of methods the
|
|
491
|
+
* emitter calls, so a cell substitutes an instrument and the production path substitutes the real
|
|
492
|
+
* endpoint, and neither one is a re-implementation of the other.
|
|
493
|
+
*/
|
|
494
|
+
export interface EmitterEndpoint {
|
|
495
|
+
readonly principal: {
|
|
496
|
+
owner: string;
|
|
497
|
+
actor: string;
|
|
498
|
+
};
|
|
499
|
+
readonly actorIsEphemeral: boolean;
|
|
500
|
+
/** The broker's live `max_payload`. Throws when not connected — never guesses a default. */
|
|
501
|
+
readonly maxPayload: number;
|
|
502
|
+
/** The single-replica preflight. The emitter calls this at startup, before anything can publish. */
|
|
503
|
+
assertExpectationSemantics(): Promise<void>;
|
|
504
|
+
encodedSize(o: {
|
|
505
|
+
channel: string;
|
|
506
|
+
parts: Part[];
|
|
507
|
+
id: string;
|
|
508
|
+
expectedLastSubjectSeq: number;
|
|
509
|
+
}): number;
|
|
510
|
+
multicastExpecting(o: {
|
|
511
|
+
channel: string;
|
|
512
|
+
parts: Part[];
|
|
513
|
+
id: string;
|
|
514
|
+
expectedLastSubjectSeq: number;
|
|
515
|
+
}): Promise<{
|
|
516
|
+
ack: {
|
|
517
|
+
seq: number;
|
|
518
|
+
duplicate: boolean;
|
|
519
|
+
};
|
|
520
|
+
}>;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* One source record's worth of events, with the cursor that resumes AFTER that record.
|
|
524
|
+
*
|
|
525
|
+
* One durable emit unit is one frame: the emitter splits only at source-observation boundaries that
|
|
526
|
+
* are independently reconstructable from the durable source. A frame therefore ends where a record
|
|
527
|
+
* ends, and carries that record's cursor, so folding it means exactly "every record here is
|
|
528
|
+
* consumed".
|
|
529
|
+
*/
|
|
530
|
+
export interface EmitUnit {
|
|
531
|
+
/** The run these events belong to. A frame's envelope names ONE run, so units are never
|
|
532
|
+
* mixed across runs in one frame. */
|
|
533
|
+
runId: string;
|
|
534
|
+
events: AguiEvent[];
|
|
535
|
+
cursor: string;
|
|
536
|
+
}
|
|
537
|
+
/** What the mapper returns for one source record: its run and its events, or `null` for a record
|
|
538
|
+
* this plane deliberately drops, and it drops many. `null` is NOT an error: a deliberate drop and
|
|
539
|
+
* a failed map are kept apart, because conflating them turns a parser bug into skipped history. */
|
|
540
|
+
export type RecordMapper<T> = (record: T) => {
|
|
541
|
+
runId: string;
|
|
542
|
+
events: AguiEvent[];
|
|
543
|
+
} | null;
|
|
544
|
+
/**
|
|
545
|
+
* The emitter has stopped and will not publish again without operator action.
|
|
546
|
+
*
|
|
547
|
+
* Halting is a SUCCESS of this design, not a failure of it: every halt below is a case where the
|
|
548
|
+
* alternative is to report success for a message that was not stored, or to fold an ack for a body
|
|
549
|
+
* we did not write. A halt is loud, bounded and recoverable by a human; the alternative is silent
|
|
550
|
+
* and permanent.
|
|
551
|
+
*/
|
|
552
|
+
/**
|
|
553
|
+
* A bracket violation that is OURS, not the writer's: the machine that tracks open runs and messages
|
|
554
|
+
* was lost across a process restart.
|
|
555
|
+
*
|
|
556
|
+
* **This exists because two halts that both say "unbalanced" prove nothing about which produced
|
|
557
|
+
* one.** The WAL persists `epoch`, `frontier` and the pending frame, and NOT the set of open
|
|
558
|
+
* runs and messages, so a process that dies mid-run restarts with an empty {@link AguiBrackets},
|
|
559
|
+
* resumes from `sourceCursor` at events whose `RUN_STARTED` was already published, and refuses the
|
|
560
|
+
* first of them. Without this class the operator sees "nothing may be emitted outside an open run"
|
|
561
|
+
* and files a bug against a writer that did nothing wrong.
|
|
562
|
+
*
|
|
563
|
+
* It is deliberately a SUBCLASS: every existing catch of {@link AguiVocabularyError} still catches
|
|
564
|
+
* it, and only code that wants to tell the two apart has to know it exists.
|
|
565
|
+
*/
|
|
566
|
+
export declare class AguiBracketStateLost extends AguiVocabularyError {
|
|
567
|
+
readonly cause: Error;
|
|
568
|
+
constructor(message: string, cause: Error);
|
|
569
|
+
}
|
|
570
|
+
export declare class AguiEmitterHalted extends Error {
|
|
571
|
+
readonly reason: "duplicate-ack" | "cas-loss";
|
|
572
|
+
constructor(reason: "duplicate-ack" | "cas-loss", message: string);
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Pack units into frames, splitting ONLY at unit boundaries and never inside one.
|
|
576
|
+
*
|
|
577
|
+
* Deliberately NOT {@link splitFrames}, and the difference is the durable plane's one-unit-one-frame
|
|
578
|
+
* rule. `splitFrames` splits at EVENT boundaries, which is the right answer for a frame considered
|
|
579
|
+
* on its own, but a frame that ends mid-record has no cursor it can honestly store: the only value
|
|
580
|
+
* available says the whole record was consumed, and folding that after a crash skips the rest of the
|
|
581
|
+
* record's events with no `seq` gap for a consumer to notice.
|
|
582
|
+
*
|
|
583
|
+
* **So the event-boundary split and the one-unit-one-frame rule are in tension, and this resolves it
|
|
584
|
+
* in the direction the durable plane requires: a single unit that does not fit FAILS LOUD rather
|
|
585
|
+
* than being truncated at a frame boundary.** That leaves `splitFrames`'s truncation path with no
|
|
586
|
+
* caller on the durable plane, which is reported as a design conflict rather than decided here.
|
|
587
|
+
*
|
|
588
|
+
* @throws {AguiVocabularyError} when one unit cannot fit in a frame alone.
|
|
589
|
+
*/
|
|
590
|
+
export declare function packUnits(opts: {
|
|
591
|
+
threadId: string;
|
|
592
|
+
epoch: string;
|
|
593
|
+
firstSeq: number;
|
|
594
|
+
units: readonly EmitUnit[];
|
|
595
|
+
measure: (frame: AguiFrame) => number;
|
|
596
|
+
limit: number;
|
|
597
|
+
}): {
|
|
598
|
+
frame: AguiFrame;
|
|
599
|
+
cursor: string;
|
|
600
|
+
}[];
|
|
601
|
+
/**
|
|
602
|
+
* The event emitter: one per principal, one thread at a time.
|
|
603
|
+
*
|
|
604
|
+
* **BRACKET STATE SURVIVES A RESTART, AND THIS PARAGRAPH USED TO SAY THE OPPOSITE.** It described a
|
|
605
|
+
* declared gap — an emitter coming back with an empty machine, resuming at events whose
|
|
606
|
+
* `RUN_STARTED` had already been published, and refusing the first of them — long after the WAL
|
|
607
|
+
* started persisting the machine. The words were true when they were written and stayed on the page
|
|
608
|
+
* through the change that falsified them, which is the failure mode a class header is worst at
|
|
609
|
+
* showing: it is the first thing a cutover author reads about recovery, and it was telling them to
|
|
610
|
+
* expect a halt the code no longer produces.
|
|
611
|
+
*
|
|
612
|
+
* What actually happens: {@link AguiBrackets} is a property of the WRITER'S STREAM across frames, so
|
|
613
|
+
* the WAL freezes the machine's state WITH each pending frame and promotes it on fold. A restart
|
|
614
|
+
* therefore reopens knowing exactly which run, messages and tool calls were open at the last FOLDED
|
|
615
|
+
* position, and {@link AguiBrackets.restore} continues from there rather than from empty.
|
|
616
|
+
*
|
|
617
|
+
* **The lost-state path still exists, and it is now the narrow case it should always have been:** a
|
|
618
|
+
* document that CANNOT SAY what was open. That is a WAL migrated from v1, which recorded no bracket
|
|
619
|
+
* state at all, and it loads as `null` rather than as an empty machine precisely so the difference
|
|
620
|
+
* stays visible. Only there does the emitter start empty, resume into an already-open run, and
|
|
621
|
+
* refuse the first event with {@link AguiBracketStateLost} — a halt rather than a loss, which is the
|
|
622
|
+
* safe direction, and diagnosed by name rather than surfacing as an anonymous protocol violation.
|
|
623
|
+
*/
|
|
624
|
+
export declare class AguiEmitter<T> {
|
|
625
|
+
private readonly ep;
|
|
626
|
+
private readonly wal;
|
|
627
|
+
private readonly source;
|
|
628
|
+
private readonly map;
|
|
629
|
+
/** Derived from the endpoint's OWN principal, never from a config name or the launch env. */
|
|
630
|
+
readonly channel: string;
|
|
631
|
+
readonly threadId: string;
|
|
632
|
+
/**
|
|
633
|
+
* The bracket machine AT THE FOLDED POSITION — deliberately not "wherever validation got to".
|
|
634
|
+
*
|
|
635
|
+
* It advances one frame at a time, immediately before that frame's `beginSend`, so the state
|
|
636
|
+
* frozen with a pending frame is the state that belongs to it. A machine advanced by the whole
|
|
637
|
+
* batch up front would freeze a state describing events that had not been sent.
|
|
638
|
+
*/
|
|
639
|
+
private brackets;
|
|
640
|
+
private halted;
|
|
641
|
+
/** True once THIS process has fed an event through the bracket machine. It is the half of the
|
|
642
|
+
* restart diagnosis that keeps a genuine mid-stream violation from being blamed on a restart. */
|
|
643
|
+
private fedAnyEvent;
|
|
644
|
+
private constructor();
|
|
645
|
+
/**
|
|
646
|
+
* Start an emitter: resolve the channel, run the single-replica preflight, and settle any pending
|
|
647
|
+
* frame.
|
|
648
|
+
*
|
|
649
|
+
* **THIS IS THAT PREFLIGHT'S PRODUCTION CALL SITE, AND UNTIL THIS FUNCTION EXISTED THERE WAS
|
|
650
|
+
* NONE.** `CotalEndpoint.assertExpectationSemantics()` had zero production callers: it was a
|
|
651
|
+
* check that shipped, was covered by its own suite, and never ran outside one. That is why it is
|
|
652
|
+
* called HERE, before recovery and therefore before any publish — a serialized append on an
|
|
653
|
+
* unverified stream is the exact case it exists to prevent, and doing it after recovery would
|
|
654
|
+
* leave the one publish that matters most, the re-publish of a frozen frame, outside the guard.
|
|
655
|
+
*/
|
|
656
|
+
static start<T>(opts: {
|
|
657
|
+
endpoint: EmitterEndpoint;
|
|
658
|
+
/** Already open, so the caller owns `space`, the WAL path, and the `subjectMayExist` judgement
|
|
659
|
+
* — none of which the emitter can make honestly on the caller's behalf. */
|
|
660
|
+
wal: EventWal;
|
|
661
|
+
source: DurableSource<T>;
|
|
662
|
+
map: RecordMapper<T>;
|
|
663
|
+
}): Promise<AguiEmitter<T>>;
|
|
664
|
+
/** True once the emitter has stopped for good. */
|
|
665
|
+
get stopped(): boolean;
|
|
666
|
+
/**
|
|
667
|
+
* Boot recovery, branching on the WAL's tag.
|
|
668
|
+
*
|
|
669
|
+
* `acked` NEVER republishes: the frame landed and we know it, so the only remaining work is to
|
|
670
|
+
* fold. `sent_unacked` is the genuinely uncertain case and republishes with the SAME frozen `id`
|
|
671
|
+
* and `E` — never the current tip, because re-deriving either is what turns an uncertain publish
|
|
672
|
+
* into a second, different message.
|
|
673
|
+
*/
|
|
674
|
+
private recover;
|
|
675
|
+
/**
|
|
676
|
+
* Read forward, map, pack, and publish. Returns what it did, so a caller can distinguish "nothing
|
|
677
|
+
* to do" from "did work" without inspecting the WAL.
|
|
678
|
+
*/
|
|
679
|
+
pump(): Promise<{
|
|
680
|
+
frames: number;
|
|
681
|
+
events: number;
|
|
682
|
+
}>;
|
|
683
|
+
/** Measure a candidate frame EXACTLY as the wire will, at an upper bound over id and expectation. */
|
|
684
|
+
private measure;
|
|
685
|
+
/** Transition 1 then the first network attempt. */
|
|
686
|
+
private publish;
|
|
687
|
+
/**
|
|
688
|
+
* One publish attempt — first or retry — with the FROZEN id and the FROZEN `E`. Never the tip.
|
|
689
|
+
*
|
|
690
|
+
* The three outcomes are not symmetric and the asymmetry is the design:
|
|
691
|
+
* - `!duplicate` → transition 2 then 3. Success becomes durable before the frontier moves.
|
|
692
|
+
* - `duplicate` → HALT. On a first attempt it means a body WE DID NOT WRITE holds our id, and
|
|
693
|
+
* folding its `ackSeq` would advance the frontier and the source cursor past events that were
|
|
694
|
+
* never published. On a retry it cannot happen on a single-replica stream at all, because such a
|
|
695
|
+
* stream evaluates the expectation before the dedup cache, so observing it proves the stream is
|
|
696
|
+
* not single-replica. Both are
|
|
697
|
+
* fail-loud, and neither is a case where guessing is better than stopping.
|
|
698
|
+
* - CAS loss → HALT. Someone else moved the tip on a subject only this principal may write, or
|
|
699
|
+
* the subject was purged. Uncertainty plus a moved tip is exactly what must not be guessed at.
|
|
700
|
+
*
|
|
701
|
+
* A NETWORK error is deliberately none of these: it leaves `pending` as `sent_unacked`, which is
|
|
702
|
+
* the state that means "we do not know", and the next boot retries the same frozen frame.
|
|
703
|
+
*/
|
|
704
|
+
private attempt;
|
|
705
|
+
/**
|
|
706
|
+
* Decide whether a bracket refusal is the WRITER's fault or OURS, and say which.
|
|
707
|
+
*
|
|
708
|
+
* Ours iff ALL THREE hold, and each is load-bearing:
|
|
709
|
+
* - this process has fed NO event through the machine yet, so the machine cannot have been put
|
|
710
|
+
* into a bad state by anything we did in this run; and
|
|
711
|
+
* - the frontier is non-virgin, so frames — and therefore possibly an open `RUN_STARTED` — were
|
|
712
|
+
* published by a PREVIOUS process; and
|
|
713
|
+
* - the WAL cannot say what was open. Since v2 the machine is PERSISTED, so an ordinary restart
|
|
714
|
+
* restores it and never reaches here at all; `null` means the document was migrated from v1 and
|
|
715
|
+
* genuinely never recorded the state. Without this condition the diagnosis would survive as a
|
|
716
|
+
* permanent excuse for a case the migration fixed.
|
|
717
|
+
*
|
|
718
|
+
* Drop the first condition and a genuine mid-stream violation by the writer gets blamed on a
|
|
719
|
+
* restart that happened an hour ago. Drop the second and a violation on a virgin thread, where
|
|
720
|
+
* nothing was ever published and nothing could have been lost, gets blamed on a restart that never
|
|
721
|
+
* happened. Each condition alone produces a confident, wrong diagnosis — which is worse than the
|
|
722
|
+
* undiagnosed error it replaced, because a named cause stops the search.
|
|
723
|
+
*/
|
|
724
|
+
private diagnoseBracket;
|
|
725
|
+
private halt;
|
|
726
|
+
}
|
|
727
|
+
//# sourceMappingURL=agui.d.ts.map
|