@vincemakes/kiso-core 0.1.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/LICENSE +21 -0
- package/README.md +9 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +29 -0
- package/dist/governance/delivery.d.ts +35 -0
- package/dist/governance/delivery.js +47 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/kernel/compaction.d.ts +60 -0
- package/dist/kernel/compaction.js +117 -0
- package/dist/kernel/event-log.d.ts +44 -0
- package/dist/kernel/event-log.js +51 -0
- package/dist/kernel/hooks.d.ts +52 -0
- package/dist/kernel/hooks.js +16 -0
- package/dist/kernel/ledger.d.ts +43 -0
- package/dist/kernel/ledger.js +88 -0
- package/dist/kernel/loop.d.ts +97 -0
- package/dist/kernel/loop.js +793 -0
- package/dist/kernel/mode.d.ts +26 -0
- package/dist/kernel/mode.js +21 -0
- package/dist/kernel/permission.d.ts +27 -0
- package/dist/kernel/permission.js +20 -0
- package/dist/kernel/project.d.ts +43 -0
- package/dist/kernel/project.js +287 -0
- package/dist/protocol/adapter.d.ts +77 -0
- package/dist/protocol/adapter.js +44 -0
- package/dist/protocol/events.d.ts +408 -0
- package/dist/protocol/events.js +230 -0
- package/dist/protocol/index.d.ts +3 -0
- package/dist/protocol/index.js +3 -0
- package/dist/protocol/messages.d.ts +113 -0
- package/dist/protocol/messages.js +17 -0
- package/dist/tools/registry.d.ts +28 -0
- package/dist/tools/registry.js +53 -0
- package/dist/tools/tool.d.ts +72 -0
- package/dist/tools/tool.js +21 -0
- package/dist/tools/validate.d.ts +11 -0
- package/dist/tools/validate.js +28 -0
- package/package.json +53 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L1 — the event sum type.
|
|
3
|
+
*
|
|
4
|
+
* The kernel emits a stream of these. Adapters translate provider-specific
|
|
5
|
+
* wire events INTO these. Surfaces (TUI, web, logs) consume these.
|
|
6
|
+
*
|
|
7
|
+
* WHY a discriminated union instead of `{type: string, ...unknown}`:
|
|
8
|
+
* every consumer gets exhaustiveness checking for free. Add a variant and
|
|
9
|
+
* TypeScript breaks every `switch` that forgot it — which is exactly the
|
|
10
|
+
* moment you want to be interrupted. A loose record defers that failure
|
|
11
|
+
* to production.
|
|
12
|
+
*
|
|
13
|
+
* See ADR-0003. Read it before changing anything in this file.
|
|
14
|
+
*
|
|
15
|
+
* `seq` — every event carries a monotonically increasing sequence number,
|
|
16
|
+
* assigned by the kernel's EventLog at append time. Consumers (surfaces,
|
|
17
|
+
* persistence, eval) sync by `seq`; a trajectory is the complete replay of
|
|
18
|
+
* `seq` 0..N. Without `seq`, "what happened" can only be reconstructed by
|
|
19
|
+
* array-shape heuristics — the exact failure Claude Code's transcript sync
|
|
20
|
+
* lives in. See ADR-0002.
|
|
21
|
+
*
|
|
22
|
+
* This module is almost types-only: the only runtime value it emits is
|
|
23
|
+
* `isKisoEvent`, the type guard the session store validates records with.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Why the model stopped producing. Provider-specific reasons are mapped at
|
|
27
|
+
* the ADAPTER boundary into this closed union (Area 6) — `refusal`,
|
|
28
|
+
* `pause_turn`, `content_filter`, and `context_window` are never allowed
|
|
29
|
+
* to degrade into a normal `end_turn`. A new SDK enum that lacks a mapping
|
|
30
|
+
* is a compile error in the adapters' exhaustive switches.
|
|
31
|
+
*/
|
|
32
|
+
export type StopReason = "end_turn" | "tool_use" | "max_tokens" | "stop_sequence" | "abort" | "error" | "refusal" | "pause_turn" | "content_filter" | "context_window" | "function_call";
|
|
33
|
+
/**
|
|
34
|
+
* Structured failure classification, layered ON TOP OF `isError: boolean`.
|
|
35
|
+
* It carries WHY a tool failed, not merely THAT it failed.
|
|
36
|
+
* Only meaningful when `isError` is true.
|
|
37
|
+
*
|
|
38
|
+
* - `invalid_input` — arguments are malformed or fail the schema.
|
|
39
|
+
* - `precondition` — the tool REFUSED to run because a gate was not met.
|
|
40
|
+
* It never attempted the work. This is the slot that
|
|
41
|
+
* separates "refused" from "ran and produced nothing" —
|
|
42
|
+
* a distinction most harnesses collapse, and then cannot
|
|
43
|
+
* tell a blocked agent from an unproductive one.
|
|
44
|
+
* - `transient` — retriable (network blip, rate limit).
|
|
45
|
+
* - `fatal` — unrecoverable (handler threw, invariant broken).
|
|
46
|
+
*
|
|
47
|
+
* The kernel never branches on this value. It is a pass-through signal for
|
|
48
|
+
* the harness above; retry and re-route policy stay product-side.
|
|
49
|
+
*
|
|
50
|
+
* See ADR-0020.
|
|
51
|
+
*/
|
|
52
|
+
export type ToolErrorKind = "invalid_input" | "precondition" | "transient" | "fatal";
|
|
53
|
+
/**
|
|
54
|
+
* A new assistant text block begins.
|
|
55
|
+
* Surfaces should open a new paragraph. Subsequent `TextDelta` events with no
|
|
56
|
+
* intervening `TextStart` belong to the same block.
|
|
57
|
+
*/
|
|
58
|
+
export interface TextStart {
|
|
59
|
+
readonly seq: number;
|
|
60
|
+
readonly type: "text_start";
|
|
61
|
+
/** Provenance of the assistant message this block belongs to (Area 6). */
|
|
62
|
+
readonly source?: import("./messages.js").MessageSource;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The explicit boundary of an assistant message (D 组). Adapters never emit
|
|
66
|
+
* these (their implicit boundaries — tool_result/user_input/terminal — are
|
|
67
|
+
* enough); the seed encoder uses them so ADJACENT assistant messages and
|
|
68
|
+
* EMPTY assistant messages round-trip losslessly.
|
|
69
|
+
*/
|
|
70
|
+
export interface AssistantStart {
|
|
71
|
+
readonly seq: number;
|
|
72
|
+
readonly type: "assistant_start";
|
|
73
|
+
readonly source?: import("./messages.js").MessageSource;
|
|
74
|
+
}
|
|
75
|
+
export interface AssistantEnd {
|
|
76
|
+
readonly seq: number;
|
|
77
|
+
readonly type: "assistant_end";
|
|
78
|
+
}
|
|
79
|
+
export interface TextDelta {
|
|
80
|
+
readonly seq: number;
|
|
81
|
+
readonly type: "text_delta";
|
|
82
|
+
readonly text: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The assistant text block closes. Surfaces flush the paragraph here; a
|
|
86
|
+
* block without an explicit end is closed by the next `TextStart` or the
|
|
87
|
+
* terminal. Adapters may omit it; the union admits it so fixtures and
|
|
88
|
+
* replayable trajectories can carry the boundary explicitly (design v3 §4.1).
|
|
89
|
+
*/
|
|
90
|
+
export interface TextEnd {
|
|
91
|
+
readonly seq: number;
|
|
92
|
+
readonly type: "text_end";
|
|
93
|
+
}
|
|
94
|
+
export interface ToolCallStart {
|
|
95
|
+
readonly seq: number;
|
|
96
|
+
readonly type: "tool_call_start";
|
|
97
|
+
readonly callId: string;
|
|
98
|
+
readonly name: string;
|
|
99
|
+
/** Provenance of the assistant message this call belongs to (Area 6). */
|
|
100
|
+
readonly source?: import("./messages.js").MessageSource;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Incremental JSON characters for one call's arguments.
|
|
104
|
+
*
|
|
105
|
+
* Concatenating every delta for the same `callId` yields the JSON document the
|
|
106
|
+
* model emitted. It is NOT valid JSON until `ToolCallEnd` — consumers that
|
|
107
|
+
* parse mid-stream must tolerate failure, or wait.
|
|
108
|
+
*/
|
|
109
|
+
export interface ToolCallInputDelta {
|
|
110
|
+
readonly seq: number;
|
|
111
|
+
readonly type: "tool_call_input_delta";
|
|
112
|
+
readonly callId: string;
|
|
113
|
+
readonly inputJsonDelta: string;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The model finished describing this call.
|
|
117
|
+
* `input` is the parsed document, or `null` when parsing failed — the kernel
|
|
118
|
+
* then decides whether to repair or reject. Adapters never repair silently:
|
|
119
|
+
* a null here is a fact about the model's output, not a defect to hide.
|
|
120
|
+
*/
|
|
121
|
+
export interface ToolCallEnd {
|
|
122
|
+
readonly seq: number;
|
|
123
|
+
readonly type: "tool_call_end";
|
|
124
|
+
readonly callId: string;
|
|
125
|
+
/** The tool being called — the registry lookup key. */
|
|
126
|
+
readonly name: string;
|
|
127
|
+
readonly input: Readonly<Record<string, unknown>> | null;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Emitted by the KERNEL (never by an adapter) once a handler returns.
|
|
131
|
+
* The next `adapter.stream()` call translates it back into a provider message.
|
|
132
|
+
*/
|
|
133
|
+
export interface ToolResultEvent {
|
|
134
|
+
readonly seq: number;
|
|
135
|
+
readonly type: "tool_result";
|
|
136
|
+
readonly callId: string;
|
|
137
|
+
/** Full content — blocks preserved losslessly (D 组). */
|
|
138
|
+
readonly content: string | readonly import("./messages.js").ContentBlock[];
|
|
139
|
+
readonly isError: boolean;
|
|
140
|
+
/** Present only when `isError` is true and the handler classified it. */
|
|
141
|
+
readonly errorKind?: ToolErrorKind;
|
|
142
|
+
/** The execution that produced this result (B 组) — receipt pairing key. */
|
|
143
|
+
readonly executionId?: string;
|
|
144
|
+
/** Provenance + product tags — preserved losslessly (Area 6). */
|
|
145
|
+
readonly source?: import("./messages.js").MessageSource;
|
|
146
|
+
readonly tags?: readonly string[];
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* A human/user input entered the run. Emitted by the harness (session layer)
|
|
150
|
+
* or by the loop's seed encoder — never by a provider. This is what makes a
|
|
151
|
+
* trajectory self-contained: ADR-0002's replay of `seq` 0..N must include the
|
|
152
|
+
* prompts, or the run cannot be rebuilt from its own log.
|
|
153
|
+
*/
|
|
154
|
+
export interface UserInputEvent {
|
|
155
|
+
readonly seq: number;
|
|
156
|
+
readonly type: "user_input";
|
|
157
|
+
readonly content: string | readonly import("./messages.js").ContentBlock[];
|
|
158
|
+
/** Provenance of the prompt (Area 6) — preserved losslessly. */
|
|
159
|
+
readonly source?: import("./messages.js").MessageSource;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Compaction happened at this point in the trajectory. The EXACT
|
|
163
|
+
* replacements are persisted; the projection applies them verbatim — it
|
|
164
|
+
* never re-runs a future version of the compaction algorithm (A 组/D 组).
|
|
165
|
+
* The replay therefore equals the live run byte for byte, independent of
|
|
166
|
+
* algorithm drift.
|
|
167
|
+
*
|
|
168
|
+
* 五: `eventSeq` is the STABLE identity — the seq of the specific
|
|
169
|
+
* `tool_result` event that was replaced. The provider callId may repeat
|
|
170
|
+
* across runs and is correlation-only; `callId` is kept for traceability.
|
|
171
|
+
* Only THIS turn's NEWLY cleared results are listed, never a cumulative
|
|
172
|
+
* set of already-cleared markers (五).
|
|
173
|
+
*
|
|
174
|
+
* 第四轮: `eventSeq` is OPTIONAL because sessions written by round three
|
|
175
|
+
* (v1) carry `{callId, content}` entries without it. Those are legal and
|
|
176
|
+
* replay with v1 semantics (replace every tool result with that callId,
|
|
177
|
+
* exactly as the old framework did); records written from now on always
|
|
178
|
+
* carry the eventSeq and replace exactly one result.
|
|
179
|
+
*/
|
|
180
|
+
export interface CompactedEvent {
|
|
181
|
+
readonly seq: number;
|
|
182
|
+
readonly type: "compacted";
|
|
183
|
+
readonly cleared: readonly {
|
|
184
|
+
readonly eventSeq?: number;
|
|
185
|
+
readonly callId: string;
|
|
186
|
+
readonly content: string;
|
|
187
|
+
}[];
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* A tool execution is about to run — the durable START of the side effect
|
|
191
|
+
* (Phase D / Area 3). `executionId` is the framework-generated, persistent
|
|
192
|
+
* identity of THIS logical execution; the provider's `callId` only
|
|
193
|
+
* correlates messages and may repeat across runs. Written BEFORE the
|
|
194
|
+
* handler is invoked, so an interruption between this event and its result
|
|
195
|
+
* leaves an auditably UNCERTAIN state that requires a human decision.
|
|
196
|
+
*/
|
|
197
|
+
export interface ToolExecutionStarted {
|
|
198
|
+
readonly seq: number;
|
|
199
|
+
readonly type: "tool_execution_started";
|
|
200
|
+
readonly executionId: string;
|
|
201
|
+
readonly callId: string;
|
|
202
|
+
readonly name: string;
|
|
203
|
+
readonly input: Readonly<Record<string, unknown>>;
|
|
204
|
+
}
|
|
205
|
+
/** The side effect completed successfully. A confirmed success never re-runs. */
|
|
206
|
+
export interface ToolExecutionSucceeded {
|
|
207
|
+
readonly seq: number;
|
|
208
|
+
readonly type: "tool_execution_succeeded";
|
|
209
|
+
readonly executionId: string;
|
|
210
|
+
readonly callId: string;
|
|
211
|
+
readonly result: {
|
|
212
|
+
readonly content: string;
|
|
213
|
+
readonly isError: false;
|
|
214
|
+
};
|
|
215
|
+
/** 八: the tags ride on the durable RECEIPT so a crash-window repair
|
|
216
|
+
* of the tool_result can reproduce the normal path losslessly. */
|
|
217
|
+
readonly tags?: readonly string[];
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The side effect ran and FAILED. `safeToRetry` is the tool's own proof
|
|
221
|
+
* (declared idempotent): only then is a failure a clean "failed"; a
|
|
222
|
+
* non-idempotent failure may have produced a side effect and is UNCERTAIN
|
|
223
|
+
* until a human decides (Area 3).
|
|
224
|
+
*/
|
|
225
|
+
export interface ToolExecutionFailed {
|
|
226
|
+
readonly seq: number;
|
|
227
|
+
readonly type: "tool_execution_failed";
|
|
228
|
+
readonly executionId: string;
|
|
229
|
+
readonly callId: string;
|
|
230
|
+
readonly error: string;
|
|
231
|
+
readonly errorKind?: ToolErrorKind;
|
|
232
|
+
readonly safeToRetry: boolean;
|
|
233
|
+
/** 八: tags on the durable receipt, preserved across crash-window repair. */
|
|
234
|
+
readonly tags?: readonly string[];
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* A human resolved an execution: "rerun" (the human takes responsibility —
|
|
238
|
+
* the side effect may run again) or "abandoned" (the attempt is treated as
|
|
239
|
+
* failed; the trajectory continues with a recorded denial).
|
|
240
|
+
*/
|
|
241
|
+
export interface ToolExecutionResolved {
|
|
242
|
+
readonly seq: number;
|
|
243
|
+
readonly type: "tool_execution_resolved";
|
|
244
|
+
readonly executionId: string;
|
|
245
|
+
readonly callId: string;
|
|
246
|
+
readonly resolution: "rerun" | "abandoned";
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* A permission `defer` became a real pause (Phase D): the run yields this
|
|
250
|
+
* event, persists the request, and waits for the human decision. The
|
|
251
|
+
* decision id is the durable handle `session.approve(decisionId, ...)`
|
|
252
|
+
* resolves.
|
|
253
|
+
*/
|
|
254
|
+
export interface PermissionRequested {
|
|
255
|
+
readonly seq: number;
|
|
256
|
+
readonly type: "permission_requested";
|
|
257
|
+
readonly decisionId: string;
|
|
258
|
+
readonly callId: string;
|
|
259
|
+
readonly name: string;
|
|
260
|
+
readonly input: Readonly<Record<string, unknown>>;
|
|
261
|
+
}
|
|
262
|
+
/** The durable answer to a PermissionRequested. */
|
|
263
|
+
export interface PermissionDecided {
|
|
264
|
+
readonly seq: number;
|
|
265
|
+
readonly type: "permission_decided";
|
|
266
|
+
readonly decisionId: string;
|
|
267
|
+
/** The invocation this decision binds to (B 组). */
|
|
268
|
+
readonly callId?: string;
|
|
269
|
+
readonly decision: "approved" | "denied";
|
|
270
|
+
readonly reason?: string;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* A permission request was CLOSED because its run terminated first (B 组):
|
|
274
|
+
* an aborted/completed/error run's dangling approval is dead — it is never
|
|
275
|
+
* re-presented and a late approve() cannot resurrect the run.
|
|
276
|
+
*/
|
|
277
|
+
export interface PermissionExpired {
|
|
278
|
+
readonly seq: number;
|
|
279
|
+
readonly type: "permission_expired";
|
|
280
|
+
readonly decisionId: string;
|
|
281
|
+
readonly reason: string;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* A non-idempotent execution FAILED and the run PAUSES until a human
|
|
285
|
+
* decides (C 组): no next model turn, no sibling tool, no auto-retry. The
|
|
286
|
+
* verdict is recorded by the session (resolveUncertain) and the ledger
|
|
287
|
+
* transitions uncertain → rerun/abandoned; the event itself is the durable
|
|
288
|
+
* pause marker.
|
|
289
|
+
*/
|
|
290
|
+
export interface UncertainPending {
|
|
291
|
+
readonly seq: number;
|
|
292
|
+
readonly type: "uncertain_pending";
|
|
293
|
+
readonly executionId: string;
|
|
294
|
+
readonly callId: string;
|
|
295
|
+
readonly name: string;
|
|
296
|
+
readonly error: string;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* A user input was VETOED or REWRITTEN by the harness (C 组). `replaces`
|
|
300
|
+
* is the seq of the original user_input; the projection skips the original
|
|
301
|
+
* and, when `content` is non-null, produces the replacement instead — the
|
|
302
|
+
* rewritten fact is the ONLY fact every later turn sees. null content = a
|
|
303
|
+
* true veto (the model never receives the message).
|
|
304
|
+
*/
|
|
305
|
+
export interface UserInputReplaced {
|
|
306
|
+
readonly seq: number;
|
|
307
|
+
readonly type: "user_input_replaced";
|
|
308
|
+
readonly replaces: number;
|
|
309
|
+
readonly content: string | readonly import("./messages.js").ContentBlock[] | null;
|
|
310
|
+
/** Provenance of the replacement — preserved from the hook (三). */
|
|
311
|
+
readonly source?: import("./messages.js").MessageSource;
|
|
312
|
+
}
|
|
313
|
+
/** Extended-thinking content. Providers without it emit nothing here. */
|
|
314
|
+
export interface Thinking {
|
|
315
|
+
readonly seq: number;
|
|
316
|
+
readonly type: "thinking";
|
|
317
|
+
readonly text: string;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Token accounting.
|
|
321
|
+
* INVARIANT: at least one `Usage` MUST precede each `Stop`. A turn that cannot
|
|
322
|
+
* report its cost is a turn you cannot bill, cap, or trust.
|
|
323
|
+
*
|
|
324
|
+
* `known: false` means the provider reported NO usage — the token fields
|
|
325
|
+
* are null, never faked as zero (Area 6).
|
|
326
|
+
*/
|
|
327
|
+
export interface Usage {
|
|
328
|
+
readonly seq: number;
|
|
329
|
+
readonly type: "usage";
|
|
330
|
+
readonly inputTokens: number | null;
|
|
331
|
+
readonly outputTokens: number | null;
|
|
332
|
+
readonly cacheRead: number | null;
|
|
333
|
+
readonly cacheWrite: number | null;
|
|
334
|
+
readonly known: boolean;
|
|
335
|
+
}
|
|
336
|
+
export interface Stop {
|
|
337
|
+
readonly seq: number;
|
|
338
|
+
readonly type: "stop";
|
|
339
|
+
readonly reason: StopReason;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Structured failure classification for MODEL / TRANSPORT errors.
|
|
343
|
+
*
|
|
344
|
+
* Distinct from `ToolErrorKind` (which classifies a tool's own failure and
|
|
345
|
+
* which the kernel never branches on): a `StructuredError` is an error the
|
|
346
|
+
* LOOP itself may branch on — `retryable: true` means the loop may retry
|
|
347
|
+
* (backoff, max attempts, state entirely inside the loop — see ADR-0005).
|
|
348
|
+
* Adapters translate provider wire errors into this shape. No regex over
|
|
349
|
+
* error strings anywhere: classification happens at the adapter boundary.
|
|
350
|
+
*/
|
|
351
|
+
export type ErrorCode = "rate_limit" | "overloaded" | "network" | "timeout" | "quota" | "api_5xx" | "context_overflow" | "invalid_request" | "unknown";
|
|
352
|
+
export interface StructuredError {
|
|
353
|
+
readonly code: ErrorCode;
|
|
354
|
+
readonly status?: number;
|
|
355
|
+
readonly retryable: boolean;
|
|
356
|
+
readonly message: string;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Why the whole run ended. The ONE terminal shape every run converges to.
|
|
360
|
+
*
|
|
361
|
+
* Every consumer switches on `kind`; with `exactOptionalPropertyTypes` and
|
|
362
|
+
* `strictNullChecks` on, a terminal that nobody handles is a compile error,
|
|
363
|
+
* not a production mystery. CC's query() returns 11 different reasons that
|
|
364
|
+
* every consumer discards — here the terminal is an event like any other,
|
|
365
|
+
* so it cannot be lost. See ADR-0004.
|
|
366
|
+
*
|
|
367
|
+
* - `completed` — the loop ended on its own terms (no tool call, or the
|
|
368
|
+
* mode's stop predicate fired).
|
|
369
|
+
* - `max_tokens` — the provider stopped on its output budget; the model's
|
|
370
|
+
* turn is truncated, NOT a clean completion (Phase B).
|
|
371
|
+
* - `max_turns` — the round budget was consumed.
|
|
372
|
+
* - `error` — a `StructuredError` the loop could not retry past.
|
|
373
|
+
* - `aborted` — a human (user) or the parent agent stopped it.
|
|
374
|
+
* - `hook_stopped` — a Stop-hook prevented continuation.
|
|
375
|
+
*/
|
|
376
|
+
export type Terminal = {
|
|
377
|
+
kind: "completed";
|
|
378
|
+
} | {
|
|
379
|
+
kind: "max_tokens";
|
|
380
|
+
} | {
|
|
381
|
+
kind: "max_turns";
|
|
382
|
+
turns: number;
|
|
383
|
+
} | {
|
|
384
|
+
kind: "error";
|
|
385
|
+
error: StructuredError;
|
|
386
|
+
} | {
|
|
387
|
+
kind: "aborted";
|
|
388
|
+
by: "user" | "parent";
|
|
389
|
+
} | {
|
|
390
|
+
kind: "hook_stopped";
|
|
391
|
+
hook: string;
|
|
392
|
+
};
|
|
393
|
+
/** The kernel yields exactly one of these per run, as its last event. */
|
|
394
|
+
export interface TerminalEvent {
|
|
395
|
+
readonly seq: number;
|
|
396
|
+
readonly type: "terminal";
|
|
397
|
+
readonly outcome: Terminal;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* The union. Consume it with a `switch (event.type)`; with
|
|
401
|
+
* `strictNullChecks` on, an unhandled variant is a compile error.
|
|
402
|
+
*/
|
|
403
|
+
export type Event = AssistantStart | AssistantEnd | TextStart | TextDelta | TextEnd | ToolCallStart | ToolCallInputDelta | ToolCallEnd | ToolResultEvent | Thinking | Usage | Stop | UserInputEvent | CompactedEvent | ToolExecutionStarted | ToolExecutionSucceeded | ToolExecutionFailed | ToolExecutionResolved | PermissionRequested | PermissionDecided | PermissionExpired | UncertainPending | UserInputReplaced | TerminalEvent;
|
|
404
|
+
/**
|
|
405
|
+
* Runtime type guard for the union. The store validates every JSONL record
|
|
406
|
+
* with it: valid JSON that is not a kiso event is corruption, not history.
|
|
407
|
+
*/
|
|
408
|
+
export declare function isKisoEvent(value: unknown): value is Event;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L1 — the event sum type.
|
|
3
|
+
*
|
|
4
|
+
* The kernel emits a stream of these. Adapters translate provider-specific
|
|
5
|
+
* wire events INTO these. Surfaces (TUI, web, logs) consume these.
|
|
6
|
+
*
|
|
7
|
+
* WHY a discriminated union instead of `{type: string, ...unknown}`:
|
|
8
|
+
* every consumer gets exhaustiveness checking for free. Add a variant and
|
|
9
|
+
* TypeScript breaks every `switch` that forgot it — which is exactly the
|
|
10
|
+
* moment you want to be interrupted. A loose record defers that failure
|
|
11
|
+
* to production.
|
|
12
|
+
*
|
|
13
|
+
* See ADR-0003. Read it before changing anything in this file.
|
|
14
|
+
*
|
|
15
|
+
* `seq` — every event carries a monotonically increasing sequence number,
|
|
16
|
+
* assigned by the kernel's EventLog at append time. Consumers (surfaces,
|
|
17
|
+
* persistence, eval) sync by `seq`; a trajectory is the complete replay of
|
|
18
|
+
* `seq` 0..N. Without `seq`, "what happened" can only be reconstructed by
|
|
19
|
+
* array-shape heuristics — the exact failure Claude Code's transcript sync
|
|
20
|
+
* lives in. See ADR-0002.
|
|
21
|
+
*
|
|
22
|
+
* This module is almost types-only: the only runtime value it emits is
|
|
23
|
+
* `isKisoEvent`, the type guard the session store validates records with.
|
|
24
|
+
*/
|
|
25
|
+
// ── deep-shape helpers (五): every variant is validated field by field —
|
|
26
|
+
// legal enums, Terminal union members, Usage known/token combos, content
|
|
27
|
+
// blocks, plain-object inputs, optional fields when present. A record that
|
|
28
|
+
// parses as JSON but violates its variant's shape is corruption, never
|
|
29
|
+
// history.
|
|
30
|
+
const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
31
|
+
/** 九: counts (seq, tokens, turns, status) are non-negative SAFE integers —
|
|
32
|
+
* negative values, NaN, Infinity, and fractional values are rejected. */
|
|
33
|
+
const isNonNegativeInt = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
34
|
+
const STOP_REASONS = new Set([
|
|
35
|
+
"end_turn",
|
|
36
|
+
"tool_use",
|
|
37
|
+
"max_tokens",
|
|
38
|
+
"stop_sequence",
|
|
39
|
+
"abort",
|
|
40
|
+
"error",
|
|
41
|
+
"refusal",
|
|
42
|
+
"pause_turn",
|
|
43
|
+
"content_filter",
|
|
44
|
+
"context_window",
|
|
45
|
+
"function_call",
|
|
46
|
+
]);
|
|
47
|
+
const TOOL_ERROR_KINDS = new Set(["invalid_input", "precondition", "transient", "fatal"]);
|
|
48
|
+
const ERROR_CODES = new Set([
|
|
49
|
+
"rate_limit",
|
|
50
|
+
"overloaded",
|
|
51
|
+
"network",
|
|
52
|
+
"timeout",
|
|
53
|
+
"quota",
|
|
54
|
+
"api_5xx",
|
|
55
|
+
"context_overflow",
|
|
56
|
+
"invalid_request",
|
|
57
|
+
"unknown",
|
|
58
|
+
]);
|
|
59
|
+
const MESSAGE_SOURCES = new Set([
|
|
60
|
+
"user",
|
|
61
|
+
"suggestion",
|
|
62
|
+
"tool_result",
|
|
63
|
+
"subagent",
|
|
64
|
+
"system",
|
|
65
|
+
"model",
|
|
66
|
+
]);
|
|
67
|
+
const MEDIA_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
68
|
+
/**
|
|
69
|
+
* A ContentBlock: text (text: string) or image (sourceType with the
|
|
70
|
+
* documented payload — url for "url", data + mediaType for "base64").
|
|
71
|
+
* 九: the two payload kinds are STRICTLY EXCLUSIVE — a url block must not
|
|
72
|
+
* carry data, a base64 block must not carry url.
|
|
73
|
+
*/
|
|
74
|
+
function isContentBlock(v) {
|
|
75
|
+
if (!isPlainObject(v))
|
|
76
|
+
return false;
|
|
77
|
+
if (v.type === "text")
|
|
78
|
+
return typeof v.text === "string";
|
|
79
|
+
if (v.type !== "image")
|
|
80
|
+
return false;
|
|
81
|
+
if (v.sourceType === "url") {
|
|
82
|
+
return typeof v.url === "string" && v.data === undefined;
|
|
83
|
+
}
|
|
84
|
+
if (v.sourceType === "base64") {
|
|
85
|
+
return typeof v.data === "string" && typeof v.mediaType === "string" && MEDIA_TYPES.has(v.mediaType) && v.url === undefined;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
/** string content, or an array whose EVERY element is a legal ContentBlock. */
|
|
90
|
+
function isContent(v) {
|
|
91
|
+
return typeof v === "string" || (Array.isArray(v) && v.every(isContentBlock));
|
|
92
|
+
}
|
|
93
|
+
function isSource(v) {
|
|
94
|
+
return v.source === undefined || MESSAGE_SOURCES.has(v.source);
|
|
95
|
+
}
|
|
96
|
+
function isTags(v) {
|
|
97
|
+
return v.tags === undefined || (Array.isArray(v.tags) && v.tags.every((t) => typeof t === "string"));
|
|
98
|
+
}
|
|
99
|
+
function isErrorKind(v) {
|
|
100
|
+
return v.errorKind === undefined || TOOL_ERROR_KINDS.has(v.errorKind);
|
|
101
|
+
}
|
|
102
|
+
function isExecutionId(v) {
|
|
103
|
+
return v.executionId === undefined || typeof v.executionId === "string";
|
|
104
|
+
}
|
|
105
|
+
/** A StructuredError — the shape the `error` terminal carries. 九: a
|
|
106
|
+
* status, when present, is a non-negative safe integer (no negatives,
|
|
107
|
+
* NaN, Infinity, or fractions). */
|
|
108
|
+
function isStructuredError(v) {
|
|
109
|
+
if (!isPlainObject(v))
|
|
110
|
+
return false;
|
|
111
|
+
return (ERROR_CODES.has(v.code) &&
|
|
112
|
+
typeof v.retryable === "boolean" &&
|
|
113
|
+
typeof v.message === "string" &&
|
|
114
|
+
(v.status === undefined || isNonNegativeInt(v.status)));
|
|
115
|
+
}
|
|
116
|
+
/** Terminal union members validated by their own required fields. */
|
|
117
|
+
function isTerminal(v) {
|
|
118
|
+
if (!isPlainObject(v))
|
|
119
|
+
return false;
|
|
120
|
+
switch (v.kind) {
|
|
121
|
+
case "completed":
|
|
122
|
+
case "max_tokens":
|
|
123
|
+
return true;
|
|
124
|
+
case "max_turns":
|
|
125
|
+
return isNonNegativeInt(v.turns); // 九: a turn count is a safe integer
|
|
126
|
+
case "error":
|
|
127
|
+
return isStructuredError(v.error);
|
|
128
|
+
case "aborted":
|
|
129
|
+
return v.by === "user" || v.by === "parent";
|
|
130
|
+
case "hook_stopped":
|
|
131
|
+
return typeof v.hook === "string";
|
|
132
|
+
default:
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Usage invariant (Area 6/九): `known: false` means the provider reported
|
|
138
|
+
* NO usage — every token field is null, never faked as zero. `known: true`
|
|
139
|
+
* means SOME usage was reported — AT LEAST ONE field is a non-negative
|
|
140
|
+
* safe integer; the others are null when the provider did not report
|
|
141
|
+
* them.
|
|
142
|
+
*/
|
|
143
|
+
function isUsage(v) {
|
|
144
|
+
if (typeof v.known !== "boolean")
|
|
145
|
+
return false;
|
|
146
|
+
const tokens = [v.inputTokens, v.outputTokens, v.cacheRead, v.cacheWrite];
|
|
147
|
+
if (v.known === false)
|
|
148
|
+
return tokens.every((t) => t === null);
|
|
149
|
+
return tokens.some((t) => isNonNegativeInt(t)) && tokens.every((t) => t === null || isNonNegativeInt(t));
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Per-variant runtime validation (五): the table is
|
|
153
|
+
* `satisfies Record<Event["type"], ...>` — adding a variant without a
|
|
154
|
+
* validator here is a compile error (ADR-0003).
|
|
155
|
+
*/
|
|
156
|
+
const EVENT_VALIDATORS = {
|
|
157
|
+
assistant_start: (v) => isSource(v),
|
|
158
|
+
assistant_end: () => true,
|
|
159
|
+
text_start: (v) => isSource(v),
|
|
160
|
+
text_delta: (v) => typeof v.text === "string",
|
|
161
|
+
text_end: () => true,
|
|
162
|
+
tool_call_start: (v) => typeof v.callId === "string" && typeof v.name === "string" && isSource(v),
|
|
163
|
+
tool_call_input_delta: (v) => typeof v.callId === "string" && typeof v.inputJsonDelta === "string",
|
|
164
|
+
tool_call_end: (v) => typeof v.callId === "string" && typeof v.name === "string" && (v.input === null || isPlainObject(v.input)),
|
|
165
|
+
tool_result: (v) => typeof v.callId === "string" &&
|
|
166
|
+
isContent(v.content) &&
|
|
167
|
+
typeof v.isError === "boolean" &&
|
|
168
|
+
// 九: an errorKind is only meaningful on an ERROR result.
|
|
169
|
+
(v.isError === true || v.errorKind === undefined) &&
|
|
170
|
+
isErrorKind(v) &&
|
|
171
|
+
isExecutionId(v) &&
|
|
172
|
+
isSource(v) &&
|
|
173
|
+
isTags(v),
|
|
174
|
+
thinking: (v) => typeof v.text === "string",
|
|
175
|
+
usage: isUsage,
|
|
176
|
+
stop: (v) => STOP_REASONS.has(v.reason),
|
|
177
|
+
user_input: (v) => isContent(v.content) && isSource(v),
|
|
178
|
+
compacted: (v) => Array.isArray(v.cleared) &&
|
|
179
|
+
v.cleared.every((c) => isPlainObject(c) &&
|
|
180
|
+
// 第四轮: eventSeq is optional — v1 (round three) entries are
|
|
181
|
+
// {callId, content} and remain legal; v2 entries must carry a
|
|
182
|
+
// valid eventSeq.
|
|
183
|
+
(c.eventSeq === undefined || isNonNegativeInt(c.eventSeq)) &&
|
|
184
|
+
typeof c.callId === "string" &&
|
|
185
|
+
typeof c.content === "string"),
|
|
186
|
+
tool_execution_started: (v) => typeof v.executionId === "string" &&
|
|
187
|
+
typeof v.callId === "string" &&
|
|
188
|
+
typeof v.name === "string" &&
|
|
189
|
+
isPlainObject(v.input),
|
|
190
|
+
tool_execution_succeeded: (v) => typeof v.executionId === "string" &&
|
|
191
|
+
typeof v.callId === "string" &&
|
|
192
|
+
isPlainObject(v.result) &&
|
|
193
|
+
typeof v.result.content === "string" &&
|
|
194
|
+
v.result.isError === false &&
|
|
195
|
+
isTags(v),
|
|
196
|
+
tool_execution_failed: (v) => typeof v.executionId === "string" &&
|
|
197
|
+
typeof v.callId === "string" &&
|
|
198
|
+
typeof v.error === "string" &&
|
|
199
|
+
typeof v.safeToRetry === "boolean" &&
|
|
200
|
+
isErrorKind(v) &&
|
|
201
|
+
isTags(v),
|
|
202
|
+
tool_execution_resolved: (v) => typeof v.executionId === "string" &&
|
|
203
|
+
typeof v.callId === "string" &&
|
|
204
|
+
(v.resolution === "rerun" || v.resolution === "abandoned"),
|
|
205
|
+
permission_requested: (v) => typeof v.decisionId === "string" &&
|
|
206
|
+
typeof v.callId === "string" &&
|
|
207
|
+
typeof v.name === "string" &&
|
|
208
|
+
isPlainObject(v.input),
|
|
209
|
+
permission_decided: (v) => typeof v.decisionId === "string" &&
|
|
210
|
+
(v.decision === "approved" || v.decision === "denied") &&
|
|
211
|
+
(v.callId === undefined || typeof v.callId === "string") &&
|
|
212
|
+
(v.reason === undefined || typeof v.reason === "string"),
|
|
213
|
+
permission_expired: (v) => typeof v.decisionId === "string" && typeof v.reason === "string",
|
|
214
|
+
uncertain_pending: (v) => typeof v.executionId === "string" && typeof v.callId === "string" && typeof v.name === "string" && typeof v.error === "string",
|
|
215
|
+
user_input_replaced: (v) => isNonNegativeInt(v.replaces) && (v.content === null || isContent(v.content)) && isSource(v),
|
|
216
|
+
terminal: (v) => isTerminal(v.outcome),
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* Runtime type guard for the union. The store validates every JSONL record
|
|
220
|
+
* with it: valid JSON that is not a kiso event is corruption, not history.
|
|
221
|
+
*/
|
|
222
|
+
export function isKisoEvent(value) {
|
|
223
|
+
if (typeof value !== "object" || value === null)
|
|
224
|
+
return false;
|
|
225
|
+
const v = value;
|
|
226
|
+
if (typeof v.type !== "string" || !isNonNegativeInt(v.seq))
|
|
227
|
+
return false;
|
|
228
|
+
const validate = EVENT_VALIDATORS[v.type];
|
|
229
|
+
return validate !== undefined && validate(v);
|
|
230
|
+
}
|