@vincemakes/kiso-runtime 0.11.0 → 0.12.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/compose.js CHANGED
@@ -89,8 +89,8 @@ export function composeHooks(existing, extensions) {
89
89
  // message as the PREVIOUS one left it (the existing come first), and a null (veto)
90
90
  // anywhere ends the chain immediately: never "no opinion" for the
91
91
  // next handler to outvote. Adding an extension can therefore never
92
- // make the chain MORE permissive (the approval chain's deny>ask>allow
93
- // monotonicity, on the message side).
92
+ // make the chain MORE permissive (the approval chain's
93
+ // deny > allow > ask monotonicity, on the message side).
94
94
  out.onUserMessage = async (msg, ctx) => {
95
95
  let current = msg;
96
96
  for (const h of messageHandlers) {
package/dist/index.d.ts CHANGED
@@ -12,12 +12,16 @@
12
12
  * (unstable, first-party only — see internal.ts).
13
13
  *
14
14
  * Canonical names: Agent = AgentRuntime, Session = AgentSession. The old
15
- * names are deprecated aliases, removed in the next major (additive,
16
- * ADR-0051 Amendment 1release rounds move the cli minor).
15
+ * names are deprecated aliases, removed in the next major (additive). The
16
+ * cadence is ADR-0051 Amendment 4the LOCKSTEP line: all thirteen public
17
+ * packages share one version, a release round is a minor on the line and a
18
+ * fix-only round a patch. (Amendment 1's per-package counters, which moved
19
+ * the cli minor on its own, are superseded.)
17
20
  */
18
21
  export { AgentRuntime, AgentRuntime as Agent, createAgent } from "./agent.js";
19
22
  export type { AgentDefinition, PermissionPolicy, PermissionRule } from "./agent.js";
20
23
  export { AgentSession, AgentSession as Session } from "./session.js";
24
+ export type { SideQueryOptions } from "./session.js";
21
25
  export { PoisonedSessionError, ResumeBlockedError } from "./session.js";
22
26
  export type { ApprovalRequest, CompactInfo, ContextPolicy, SessionConfig, SummarizeResult } from "./session.js";
23
27
  export { Run } from "./run.js";
package/dist/index.js CHANGED
@@ -12,8 +12,11 @@
12
12
  * (unstable, first-party only — see internal.ts).
13
13
  *
14
14
  * Canonical names: Agent = AgentRuntime, Session = AgentSession. The old
15
- * names are deprecated aliases, removed in the next major (additive,
16
- * ADR-0051 Amendment 1release rounds move the cli minor).
15
+ * names are deprecated aliases, removed in the next major (additive). The
16
+ * cadence is ADR-0051 Amendment 4the LOCKSTEP line: all thirteen public
17
+ * packages share one version, a release round is a minor on the line and a
18
+ * fix-only round a patch. (Amendment 1's per-package counters, which moved
19
+ * the cli minor on its own, are superseded.)
17
20
  */
18
21
  // agent
19
22
  export { AgentRuntime, AgentRuntime as Agent, createAgent } from "./agent.js";
package/dist/ledger.d.ts CHANGED
@@ -14,8 +14,17 @@
14
14
  * provider's `callId` is correlation only and may repeat; two logical calls
15
15
  * with identical (name, input) are two executions.
16
16
  *
17
+ * UNCERTAINTY BELONGS TO THE CRASH WINDOW ALONE (ADR-0038): the ONLY shape
18
+ * that derives "uncertain" here is a `tool_execution_started` with no
19
+ * receipt after it — the process died between the write-ahead and the
20
+ * outcome, so what the side effect did is genuinely unknown and a human
21
+ * must rule. A receipted execution is an outcome, success or failure alike;
22
+ * no tool property and no error kind can make a receipted execution
23
+ * uncertain. ADR-0038 superseded ADR-0025 decision #3, which had routed a
24
+ * failed non-idempotent execution into this bucket.
25
+ *
17
26
  * Status derivation:
18
- * started, no terminal event yet → "uncertain" (interrupted: human)
27
+ * started, no terminal event yet → "uncertain" (the crash window: human)
19
28
  * succeeded → "succeeded" (confirmed, never re-run)
20
29
  * failed (any) → "failed" (a complete receipt IS
21
30
  * the outcome — ruling #12 / ADR-0038;
package/dist/ledger.js CHANGED
@@ -14,8 +14,17 @@
14
14
  * provider's `callId` is correlation only and may repeat; two logical calls
15
15
  * with identical (name, input) are two executions.
16
16
  *
17
+ * UNCERTAINTY BELONGS TO THE CRASH WINDOW ALONE (ADR-0038): the ONLY shape
18
+ * that derives "uncertain" here is a `tool_execution_started` with no
19
+ * receipt after it — the process died between the write-ahead and the
20
+ * outcome, so what the side effect did is genuinely unknown and a human
21
+ * must rule. A receipted execution is an outcome, success or failure alike;
22
+ * no tool property and no error kind can make a receipted execution
23
+ * uncertain. ADR-0038 superseded ADR-0025 decision #3, which had routed a
24
+ * failed non-idempotent execution into this bucket.
25
+ *
17
26
  * Status derivation:
18
- * started, no terminal event yet → "uncertain" (interrupted: human)
27
+ * started, no terminal event yet → "uncertain" (the crash window: human)
19
28
  * succeeded → "succeeded" (confirmed, never re-run)
20
29
  * failed (any) → "failed" (a complete receipt IS
21
30
  * the outcome — ruling #12 / ADR-0038;
@@ -79,6 +79,10 @@ export function deriveRecoveryPlan(events, scope) {
79
79
  // order blocks; the driver throws with the full list. A receipted
80
80
  // execution is an outcome (ruling #12 / the α ruling: the audit keeps
81
81
  // it) — never uncertain.
82
+ // ADR-0038: uncertainty belongs to the crash window ALONE. This filter
83
+ // is the whole of it — `executionLedger` derives "uncertain" only for
84
+ // started-without-receipt, so a FAILED execution never reaches this
85
+ // branch and never blocks a resume, whatever its safeToRetry says.
82
86
  const uncertain = [...executionLedger(events).values()].filter((r) => r.status === "uncertain");
83
87
  if (uncertain.length > 0)
84
88
  return { kind: "RESOLVE_UNCERTAIN", executionId: uncertain[0].executionId };
package/dist/session.d.ts CHANGED
@@ -30,6 +30,21 @@
30
30
  import { EventLog, type AbortSignalLike, type Adapter, type Event, type KisoExtension, type Message, type PermissionDecision, type Tool } from "@vincemakes/kiso-core";
31
31
  import { type SessionStore } from "./store.js";
32
32
  import { Run } from "./run.js";
33
+ /** TUI2-R3v2 ③ — one off-trajectory model request (session.sideQuery).
34
+ * Deliberately tiny: a purpose to mark it in the trace, its own short
35
+ * system prompt, one user message, and an abort. No tools — a side
36
+ * query answers, it never acts. */
37
+ export interface SideQueryOptions {
38
+ /** what this request is FOR — lands in the trace as `purpose`. */
39
+ readonly purpose: string;
40
+ /** the side query's OWN system prompt; the session's is not sent. */
41
+ readonly systemPrompt: string;
42
+ /** the single user message. */
43
+ readonly prompt: string;
44
+ readonly maxTokens?: number;
45
+ /** cancels the request — the CLI wires esc to it. */
46
+ readonly signal?: AbortSignalLike;
47
+ }
33
48
  /** A session whose disk write was rejected (stale handle) is PERMANENTLY
34
49
  * poisoned: its in-memory log no longer matches the disk, so no further
35
50
  * run may proceed — reload the session (round 1). */
@@ -103,6 +118,43 @@ export declare class AgentSession {
103
118
  * per-run tracer reads the SAME #config.provider; one source, one
104
119
  * route — the CLI and the trace can never disagree. */
105
120
  get provider(): "anthropic" | "openai-compat" | undefined;
121
+ /**
122
+ * TUI2-R3v2 ③ — ONE model request that belongs to no run (the
123
+ * safer-options seam, adjudicated 2026-08-18).
124
+ *
125
+ * A side query exists for the case where the human, staring at a
126
+ * paused approval, presses a button and wants an answer NOW. It is
127
+ * not a turn, not a run, and not part of the trajectory.
128
+ *
129
+ * IT WRITES NOTHING DURABLE. No session-log lines, no receipts, no
130
+ * execution records — nothing. This is the design, not an omission,
131
+ * and it is what makes the method safe to call while a run sits
132
+ * paused: it cannot interleave with that run's own event sequence
133
+ * because it produces no events to interleave. The ONLY durable
134
+ * consequence a side query can have is what the human does with the
135
+ * answer, and that lands through an existing channel — for the
136
+ * safer-options flow, the amend channel, exactly as a typed denial
137
+ * would. Nothing here touches the durable approval contract.
138
+ *
139
+ * IT IS VISIBLE. It rides the same traceGuard every run request
140
+ * rides, so it lands one request line in the trace ledger, carrying
141
+ * a FRESH runId (it is not the paused run's work) and the `purpose`
142
+ * marker (schemaVersion 4) that lets a rent audit separate on-demand
143
+ * requests from a run's own without heuristics. A request the ledger
144
+ * cannot see is rent nobody can audit, and the entire argument for
145
+ * an on-demand feature is that its rent is countable.
146
+ *
147
+ * IT PAYS ITS OWN, SMALLER RENT. It sends its own short system
148
+ * prompt and ONE user message, and NO TOOLS AT ALL — it cannot call
149
+ * anything, it can only answer. Its declared prediction arm is
150
+ * predictSideQueryRentLedger (scripts/request-surface.mjs), gated
151
+ * against a real call in rent-ledger-gate.test.ts.
152
+ *
153
+ * The returned string is the model's text, concatenated. The caller
154
+ * parses it and must treat a failure to parse as a failure — see the
155
+ * CLI's honest degradation.
156
+ */
157
+ sideQuery(options: SideQueryOptions): Promise<string>;
106
158
  /** Run one user turn. Iterate to consume; `run.abort()` cancels. */
107
159
  run(input: string, options?: {
108
160
  signal?: AbortSignalLike;
package/dist/session.js CHANGED
@@ -38,6 +38,10 @@ import { estimateTokens } from "@vincemakes/kiso-core";
38
38
  import { StaleWriterError } from "./store.js";
39
39
  import { composeHooks } from "./compose.js";
40
40
  import { Run } from "./run.js";
41
+ // TUI2-R3v2 ③ — the side query rides the SAME tracer the runs ride; that
42
+ // sameness is the whole point (one ledger, one shape, no second path).
43
+ import { RequestTracer, traceGuard } from "./trace/guard.js";
44
+ import { runtimeVersion } from "./trace/writer.js";
41
45
  /** A session whose disk write was rejected (stale handle) is PERMANENTLY
42
46
  * poisoned: its in-memory log no longer matches the disk, so no further
43
47
  * run may proceed — reload the session (round 1). */
@@ -168,6 +172,83 @@ export class AgentSession {
168
172
  get provider() {
169
173
  return this.#config.provider;
170
174
  }
175
+ /**
176
+ * TUI2-R3v2 ③ — ONE model request that belongs to no run (the
177
+ * safer-options seam, adjudicated 2026-08-18).
178
+ *
179
+ * A side query exists for the case where the human, staring at a
180
+ * paused approval, presses a button and wants an answer NOW. It is
181
+ * not a turn, not a run, and not part of the trajectory.
182
+ *
183
+ * IT WRITES NOTHING DURABLE. No session-log lines, no receipts, no
184
+ * execution records — nothing. This is the design, not an omission,
185
+ * and it is what makes the method safe to call while a run sits
186
+ * paused: it cannot interleave with that run's own event sequence
187
+ * because it produces no events to interleave. The ONLY durable
188
+ * consequence a side query can have is what the human does with the
189
+ * answer, and that lands through an existing channel — for the
190
+ * safer-options flow, the amend channel, exactly as a typed denial
191
+ * would. Nothing here touches the durable approval contract.
192
+ *
193
+ * IT IS VISIBLE. It rides the same traceGuard every run request
194
+ * rides, so it lands one request line in the trace ledger, carrying
195
+ * a FRESH runId (it is not the paused run's work) and the `purpose`
196
+ * marker (schemaVersion 4) that lets a rent audit separate on-demand
197
+ * requests from a run's own without heuristics. A request the ledger
198
+ * cannot see is rent nobody can audit, and the entire argument for
199
+ * an on-demand feature is that its rent is countable.
200
+ *
201
+ * IT PAYS ITS OWN, SMALLER RENT. It sends its own short system
202
+ * prompt and ONE user message, and NO TOOLS AT ALL — it cannot call
203
+ * anything, it can only answer. Its declared prediction arm is
204
+ * predictSideQueryRentLedger (scripts/request-surface.mjs), gated
205
+ * against a real call in rent-ledger-gate.test.ts.
206
+ *
207
+ * The returned string is the model's text, concatenated. The caller
208
+ * parses it and must treat a failure to parse as a failure — see the
209
+ * CLI's honest degradation.
210
+ */
211
+ async sideQuery(options) {
212
+ this.ensureHealthy();
213
+ // a fresh id: this request is not the paused run's work, and giving
214
+ // it the run's id would put on-demand rent inside a run's total.
215
+ const runId = crypto.randomUUID();
216
+ const tracer = new RequestTracer({
217
+ root: this.#store.root,
218
+ sessionId: this.id,
219
+ runId,
220
+ provider: this.#config.provider ?? "adapter",
221
+ model: this.#config.model,
222
+ adapterVersion: runtimeVersion(),
223
+ purpose: options.purpose,
224
+ // the manifest's seqRange pointers derive from the log, and a side
225
+ // query's messages come from NO events — an empty log is the
226
+ // honest input, and it keeps the manifest from pointing at events
227
+ // this request never sent.
228
+ log: [],
229
+ // the declared rent arm: its own prompt, no appends, no tools
230
+ rentParts: { base: options.systemPrompt, appends: [] },
231
+ });
232
+ tracer.init();
233
+ const guarded = traceGuard(tracer, this.#adapter);
234
+ let text = "";
235
+ try {
236
+ for await (const ev of guarded.stream({
237
+ model: this.#config.model,
238
+ messages: [{ role: "user", content: options.prompt }],
239
+ systemPrompt: options.systemPrompt,
240
+ ...(options.maxTokens !== undefined ? { maxTokens: options.maxTokens } : {}),
241
+ ...(options.signal !== undefined ? { signal: options.signal } : {}),
242
+ })) {
243
+ if (ev.type === "text_delta")
244
+ text += ev.text;
245
+ }
246
+ }
247
+ finally {
248
+ tracer.finishRun();
249
+ }
250
+ return text;
251
+ }
171
252
  /** Run one user turn. Iterate to consume; `run.abort()` cancels. */
172
253
  run(input, options) {
173
254
  this.ensureHealthy();
@@ -34,6 +34,12 @@ export interface RequestTracerDeps {
34
34
  adapterVersion?: string | null;
35
35
  /** The session log — the manifest's seqRange pointers derive from it. */
36
36
  log: readonly Event[];
37
+ /** TUI2-R3v2 ③ — what these requests are FOR, when they are not a
38
+ * run's own work. Omitted by every run (the tracer Run builds); set to
39
+ * "safer-options" by session.sideQuery. It lands verbatim on each
40
+ * record this tracer settles, which is what lets a rent audit tell an
41
+ * on-demand request from an ambient one without guessing. */
42
+ purpose?: string;
37
43
  /** E3 — the rent ledger's inputs: the base prompt as configured and
38
44
  * the per-extension appends in load order (the adapter's composed
39
45
  * systemPrompt is their RESULT — the parts are what the ledger
@@ -36,6 +36,7 @@ export class RequestTracer {
36
36
  #runId;
37
37
  #adapterVersion;
38
38
  #rentParts;
39
+ #purpose;
39
40
  #requestIndex = 0;
40
41
  #contextHashCounts = new Map();
41
42
  constructor(deps) {
@@ -45,6 +46,7 @@ export class RequestTracer {
45
46
  this.#runId = deps.runId;
46
47
  this.#adapterVersion = deps.adapterVersion ?? null;
47
48
  this.#rentParts = deps.rentParts;
49
+ this.#purpose = deps.purpose;
48
50
  }
49
51
  init() {
50
52
  this.#writer.init();
@@ -139,6 +141,10 @@ export class RequestTracer {
139
141
  kind: "request",
140
142
  requestId: randomUUID(),
141
143
  runId: this.#runId,
144
+ // exactOptionalPropertyTypes: the key is ABSENT on a run request,
145
+ // never `purpose: undefined` — a present-but-undefined key would
146
+ // fail the closed-set gate and would read as a claim in the JSON.
147
+ ...(this.#purpose !== undefined ? { purpose: this.#purpose } : {}),
142
148
  requestIndex: this.#requestIndex++,
143
149
  retryAttempt,
144
150
  provider: this.#provider,
@@ -25,12 +25,22 @@
25
25
  * writers record it; v1/v2 sidecars keep reading as defaults (R1d-1,
26
26
  * R2-1): no rent block = no rent lines = the zero-rent reading, never a
27
27
  * crash.
28
+ *
29
+ * TUI2-R3v2 (0.12.0) — schemaVersion 4: the record gains the OPTIONAL
30
+ * `purpose` marker (the safer-options seam, adjudicated 2026-08-18). A
31
+ * session can now make a request that belongs to no run — a side query
32
+ * (session.sideQuery) — and a ledger that could not tell one from a run
33
+ * request would make every rent audit guesswork. `purpose` is absent on
34
+ * run requests and present on side queries, naming what the request was
35
+ * FOR ("safer-options"). Optional by construction: adding a required
36
+ * field would have invalidated every v3 record ever written, and the
37
+ * whole generation-compat discipline exists to prevent exactly that.
28
38
  */
29
- /** schemaVersion: 3 for 0.2.1 (the rent block). Version 1 = the 1.2.0
30
- * shape, version 2 = the 1.3.0 shape; both kept for generation-compat
31
- * reads (R1d-1, R2-1). Algorithm and shape changes bump it (ADR-0051 §6
32
- * OUT-side versioning). */
33
- export declare const TRACE_SCHEMA_VERSION = 3;
39
+ /** schemaVersion: 4 for 0.12.0 (the optional `purpose` marker). Version
40
+ * 1 = the 1.2.0 shape, 2 = the 1.3.0 shape, 3 = the 0.2.1 shape; all
41
+ * kept for generation-compat reads (R1d-1, R2-1). Algorithm and shape
42
+ * changes bump it (ADR-0051 §6 OUT-side versioning). */
43
+ export declare const TRACE_SCHEMA_VERSION = 4;
34
44
  /** The versions a reader may meet in a ledger. v1 and v2 records are
35
45
  * accepted (generation-compat) and read as defaults — no canonical
36
46
  * block (v1), no rent block (v1, v2). */
@@ -52,7 +62,7 @@ export interface TraceSegment {
52
62
  }
53
63
  /** That is the complete set for 1.2.0. */
54
64
  export interface TraceRecord {
55
- schemaVersion: 3;
65
+ schemaVersion: 4;
56
66
  kind: "request";
57
67
  requestId: string;
58
68
  runId: string;
@@ -101,25 +111,31 @@ export interface TraceRecord {
101
111
  parentInvocationSeq: number;
102
112
  role: string;
103
113
  };
114
+ /** TUI2-R3v2 ③ (v4) — what this request was FOR, when it was not a
115
+ * run's own work. ABSENT on every run request (the overwhelming
116
+ * majority) and present on a side query (session.sideQuery), naming
117
+ * its purpose: "safer-options". A consumer separating on-demand rent
118
+ * from run rent reads this field and needs no heuristic. */
119
+ purpose?: string;
104
120
  ts: number;
105
121
  }
106
122
  /** That is the complete set for 1.2.0. */
107
123
  export interface HeaderLine {
108
- schemaVersion: 3;
124
+ schemaVersion: 4;
109
125
  kind: "header";
110
126
  sessionId: string;
111
127
  kisoVersion: string;
112
128
  createdAt: number;
113
129
  }
114
130
  export interface RunEndLine {
115
- schemaVersion: 3;
131
+ schemaVersion: 4;
116
132
  kind: "run_end";
117
133
  runId: string;
118
134
  ts: number;
119
135
  lastRequestIndex: number;
120
136
  }
121
137
  export interface CrashLine {
122
- schemaVersion: 3;
138
+ schemaVersion: 4;
123
139
  kind: "crash";
124
140
  ts: number;
125
141
  note: string;
@@ -138,7 +154,14 @@ export declare const TRACE_RECORD_FIELDS_V1: readonly ["schemaVersion", "kind",
138
154
  /** The 1.3.0 field set (schemaVersion 2) = the v1 set + `canonical`. */
139
155
  export declare const TRACE_RECORD_FIELDS_V2: readonly ["schemaVersion", "kind", "requestId", "runId", "requestIndex", "retryAttempt", "provider", "model", "adapterVersion", "systemPromptHash", "toolSchemaHash", "contextHash", "contextManifest", "segmentHashes", "stablePrefixFingerprint", "freshInput", "cacheRead", "cacheWrite", "output", "latencyMs", "ttftMs", "toolCalls", "outcome", "lineageLink", "ts", "canonical"];
140
156
  /** The 0.2.1 field set (schemaVersion 3) = the v2 set + `rent`. */
141
- export declare const TRACE_RECORD_FIELDS: readonly ["schemaVersion", "kind", "requestId", "runId", "requestIndex", "retryAttempt", "provider", "model", "adapterVersion", "systemPromptHash", "toolSchemaHash", "contextHash", "contextManifest", "segmentHashes", "stablePrefixFingerprint", "freshInput", "cacheRead", "cacheWrite", "output", "latencyMs", "ttftMs", "toolCalls", "outcome", "lineageLink", "ts", "canonical", "rent"];
157
+ export declare const TRACE_RECORD_FIELDS_V3: readonly ["schemaVersion", "kind", "requestId", "runId", "requestIndex", "retryAttempt", "provider", "model", "adapterVersion", "systemPromptHash", "toolSchemaHash", "contextHash", "contextManifest", "segmentHashes", "stablePrefixFingerprint", "freshInput", "cacheRead", "cacheWrite", "output", "latencyMs", "ttftMs", "toolCalls", "outcome", "lineageLink", "ts", "canonical", "rent"];
158
+ /** The 0.12.0 field set (schemaVersion 4) = the v3 set + `purpose`.
159
+ * TUI2-R3v2 ③: `purpose` is OPTIONAL — a run request omits it entirely,
160
+ * a side query names what it was for. It is listed in TRACE_RECORD_OPTIONAL
161
+ * below so the closed-set gate accepts a record without it. */
162
+ export declare const TRACE_RECORD_FIELDS: readonly ["schemaVersion", "kind", "requestId", "runId", "requestIndex", "retryAttempt", "provider", "model", "adapterVersion", "systemPromptHash", "toolSchemaHash", "contextHash", "contextManifest", "segmentHashes", "stablePrefixFingerprint", "freshInput", "cacheRead", "cacheWrite", "output", "latencyMs", "ttftMs", "toolCalls", "outcome", "lineageLink", "ts", "canonical", "rent", "purpose"];
163
+ /** The fields the closed-set check does not require to be present. */
164
+ export declare const TRACE_RECORD_OPTIONAL: readonly ["purpose"];
142
165
  export declare const TRACE_SEGMENT_FIELDS: readonly ["role", "seqRange", "estTokens", "freshness"];
143
166
  export declare function validateTraceSegment(v: unknown): v is TraceSegment;
144
167
  export declare function validateTraceRecord(v: unknown): v is TraceRecord;
@@ -25,22 +25,33 @@
25
25
  * writers record it; v1/v2 sidecars keep reading as defaults (R1d-1,
26
26
  * R2-1): no rent block = no rent lines = the zero-rent reading, never a
27
27
  * crash.
28
+ *
29
+ * TUI2-R3v2 (0.12.0) — schemaVersion 4: the record gains the OPTIONAL
30
+ * `purpose` marker (the safer-options seam, adjudicated 2026-08-18). A
31
+ * session can now make a request that belongs to no run — a side query
32
+ * (session.sideQuery) — and a ledger that could not tell one from a run
33
+ * request would make every rent audit guesswork. `purpose` is absent on
34
+ * run requests and present on side queries, naming what the request was
35
+ * FOR ("safer-options"). Optional by construction: adding a required
36
+ * field would have invalidated every v3 record ever written, and the
37
+ * whole generation-compat discipline exists to prevent exactly that.
28
38
  */
29
- /** schemaVersion: 3 for 0.2.1 (the rent block). Version 1 = the 1.2.0
30
- * shape, version 2 = the 1.3.0 shape; both kept for generation-compat
31
- * reads (R1d-1, R2-1). Algorithm and shape changes bump it (ADR-0051 §6
32
- * OUT-side versioning). */
33
- export const TRACE_SCHEMA_VERSION = 3;
39
+ /** schemaVersion: 4 for 0.12.0 (the optional `purpose` marker). Version
40
+ * 1 = the 1.2.0 shape, 2 = the 1.3.0 shape, 3 = the 0.2.1 shape; all
41
+ * kept for generation-compat reads (R1d-1, R2-1). Algorithm and shape
42
+ * changes bump it (ADR-0051 §6 OUT-side versioning). */
43
+ export const TRACE_SCHEMA_VERSION = 4;
34
44
  /** The versions a reader may meet in a ledger. v1 and v2 records are
35
45
  * accepted (generation-compat) and read as defaults — no canonical
36
46
  * block (v1), no rent block (v1, v2). */
37
- export const TRACE_SCHEMA_VERSIONS = new Set([1, 2, TRACE_SCHEMA_VERSION]);
47
+ export const TRACE_SCHEMA_VERSIONS = new Set([1, 2, 3, TRACE_SCHEMA_VERSION]);
38
48
  import { PRICING_TABLE_V1, priceFor, pricingTableFor, validateCanonicalUsage } from "../usage/canonical.js";
39
49
  import { validateRentLine } from "./rent.js";
40
50
  export const HASH_SPEC_BY_VERSION = {
41
51
  1: { algorithm: "sha-256", output: "full-hex" },
42
52
  2: { algorithm: "sha-256", output: "full-hex" }, // E2 — the algorithms do not change
43
53
  3: { algorithm: "sha-256", output: "full-hex" }, // E3 — same algorithms, re-pinned (the E2 ritual)
54
+ 4: { algorithm: "sha-256", output: "full-hex" }, // TUI2-R3v2 — `purpose` is a marker, not an input to any hash; re-pinned by the same ritual
44
55
  };
45
56
  export function hashSpecFor(version) {
46
57
  const spec = HASH_SPEC_BY_VERSION[version];
@@ -84,7 +95,14 @@ export const TRACE_RECORD_FIELDS_V1 = [
84
95
  /** The 1.3.0 field set (schemaVersion 2) = the v1 set + `canonical`. */
85
96
  export const TRACE_RECORD_FIELDS_V2 = [...TRACE_RECORD_FIELDS_V1, "canonical"];
86
97
  /** The 0.2.1 field set (schemaVersion 3) = the v2 set + `rent`. */
87
- export const TRACE_RECORD_FIELDS = [...TRACE_RECORD_FIELDS_V2, "rent"];
98
+ export const TRACE_RECORD_FIELDS_V3 = [...TRACE_RECORD_FIELDS_V2, "rent"];
99
+ /** The 0.12.0 field set (schemaVersion 4) = the v3 set + `purpose`.
100
+ * TUI2-R3v2 ③: `purpose` is OPTIONAL — a run request omits it entirely,
101
+ * a side query names what it was for. It is listed in TRACE_RECORD_OPTIONAL
102
+ * below so the closed-set gate accepts a record without it. */
103
+ export const TRACE_RECORD_FIELDS = [...TRACE_RECORD_FIELDS_V3, "purpose"];
104
+ /** The fields the closed-set check does not require to be present. */
105
+ export const TRACE_RECORD_OPTIONAL = ["purpose"];
88
106
  export const TRACE_SEGMENT_FIELDS = ["role", "seqRange", "estTokens", "freshness"];
89
107
  // ── Validators ────────────────────────────────────────────────────────────
90
108
  // Strict by design: extra keys are rejected (the closed set), so a
@@ -135,10 +153,16 @@ export function validateTraceRecord(v) {
135
153
  // block, a v2 sidecar has no rent block — both read as defaults,
136
154
  // accepted, never a crash; the current version is fully checked
137
155
  // (shape + the canonical block + the rent ledger).
138
- if (version !== 1 && version !== 2 && version !== TRACE_SCHEMA_VERSION)
156
+ // TUI2-R3v2 ③: v3 joins the generation-compat set a v3 sidecar has
157
+ // no `purpose` on any record, which reads as "every request was a run
158
+ // request", the true statement about a ledger written before side
159
+ // queries existed.
160
+ if (version !== 1 && version !== 2 && version !== 3 && version !== TRACE_SCHEMA_VERSION)
161
+ return false;
162
+ const fields = version === 1 ? TRACE_RECORD_FIELDS_V1 : version === 2 ? TRACE_RECORD_FIELDS_V2 : version === 3 ? TRACE_RECORD_FIELDS_V3 : TRACE_RECORD_FIELDS;
163
+ if (!hasClosedKeys(v, fields, ["lineageLink", ...TRACE_RECORD_OPTIONAL]))
139
164
  return false;
140
- const fields = version === 1 ? TRACE_RECORD_FIELDS_V1 : version === 2 ? TRACE_RECORD_FIELDS_V2 : TRACE_RECORD_FIELDS;
141
- if (!hasClosedKeys(v, fields, ["lineageLink"]))
165
+ if (v.purpose !== undefined && (typeof v.purpose !== "string" || v.purpose === ""))
142
166
  return false;
143
167
  if (v.kind !== "request")
144
168
  return false;
@@ -1,27 +1,50 @@
1
1
  /**
2
2
  * 0.1.40 (R-C item 3) — the truncation guard: a runtime adapter wrapper.
3
3
  *
4
- * The reference implementation's protection: a truncated stream (stopReason
5
- * max_tokens/length) can
6
- * yield tool args that parse and validate but are silently incomplete —
7
- * executing them is the destructive-bug class. The provider adapters already
8
- * see the stop reason; the RUNTIME vetoes execution of the whole batch:
4
+ * WHY: a truncated stream (stop reason max_tokens/length) can yield tool
5
+ * args that PARSE and VALIDATE while being silently incomplete — a delete
6
+ * whose `filter` never arrived, an edit missing its second hunk. Executing
7
+ * those is the destructive-bug class. The adapters already know the stop
8
+ * reason, so the veto belongs at that boundary.
9
9
  *
10
- * - tool_call_end events are held per turn (the deltas pass through live —
11
- * the UI still shows the calls building, only the COMPLETION is gated);
12
- * - a compatible stop flushes the held calls in order, then the stop — the
13
- * kernel launches them exactly as before (the parallel window survives;
14
- * only the 0.1.26 mid-stream launch timing is gone — the cost of the
15
- * guarantee: the turn's truncated intent is never half-executed);
16
- * - a truncation stop flushes the held calls with input: null — the
17
- * kernel's EXISTING invalid-input denial fails the whole batch without
18
- * executing anything (the same honest null the adapters already emit
19
- * for unparseable partials — zero new protocol surface), and the turn
20
- * still ends with the max_tokens terminal (the loop's voided settle).
10
+ * THE CONTRACT this wrapper establishes:
21
11
  *
22
- * The kernel machinery (the streaming launch, the window, the voided
23
- * settle) is untouchedthe gate lives at the adapter boundary, where the
24
- * stop reason is already known.
12
+ * 1. HOLD. A `tool_call_end` is withheld until the turn's stop reason is
13
+ * known. No call completesso none launches before the provider
14
+ * has said why it stopped. The deltas still pass through live: a
15
+ * surface shows the calls building; only the COMPLETION is gated.
16
+ * 2. RELEASE ON A VALID STOP. A compatible stop flushes the held calls
17
+ * unchanged and in CALL order, then the stop itself. Downstream sees
18
+ * exactly what the provider sent.
19
+ * 3. max_tokens VOIDS THE WHOLE BATCH. Every held call is flushed with
20
+ * `input: null`, and the kernel's EXISTING invalid-input denial fails
21
+ * all of them without executing anything (the same honest null the
22
+ * adapters already emit for unparseable partials — zero new protocol
23
+ * surface). The turn still ends on the max_tokens terminal (the
24
+ * loop's voided settle). All-or-nothing per turn: a truncated intent
25
+ * is never HALF executed.
26
+ * 4. NO STOP AT ALL drops the held calls entirely — the kernel already
27
+ * voids that malformed turn (invalid_request).
28
+ *
29
+ * THE CONSERVATISM SPLIT — core and runtime differ here BY DESIGN:
30
+ *
31
+ * - the RAW KERNEL STREAMS. `loop()` over a bare adapter launches each
32
+ * validated, policy-allowed call the moment its `tool_call_end` lands
33
+ * (ADR-0024 Amendment 1, streaming execution): maximum overlap with
34
+ * the model stream, trusting calls as they arrive.
35
+ * - the FLAGSHIP RUNTIME STOP-GATES. `run.ts` composes this wrapper into
36
+ * EVERY run unconditionally, so the product pays a real latency cost —
37
+ * the 0.1.26 mid-stream launch timing is gone (the parallel window is
38
+ * NOT: the released batch still runs concurrently) — to buy the
39
+ * guarantee.
40
+ *
41
+ * So the kernel's default is speed and the flagship's default is safety;
42
+ * an embedder choosing the kernel alone chooses the streaming launch, and
43
+ * applying this wrapper is how they opt into the guarantee. The kernel
44
+ * machinery (the launch, the window, the voided settle) is untouched
45
+ * either way — the gate lives at the adapter boundary.
46
+ *
47
+ * Pinned by `packages/runtime/tests/truncation-guard.test.ts`.
25
48
  */
26
49
  import type { Adapter } from "@vincemakes/kiso-core";
27
50
  /** Wrap the adapter so a truncated turn's tool batch can never execute. */
@@ -1,27 +1,50 @@
1
1
  /**
2
2
  * 0.1.40 (R-C item 3) — the truncation guard: a runtime adapter wrapper.
3
3
  *
4
- * The reference implementation's protection: a truncated stream (stopReason
5
- * max_tokens/length) can
6
- * yield tool args that parse and validate but are silently incomplete —
7
- * executing them is the destructive-bug class. The provider adapters already
8
- * see the stop reason; the RUNTIME vetoes execution of the whole batch:
4
+ * WHY: a truncated stream (stop reason max_tokens/length) can yield tool
5
+ * args that PARSE and VALIDATE while being silently incomplete — a delete
6
+ * whose `filter` never arrived, an edit missing its second hunk. Executing
7
+ * those is the destructive-bug class. The adapters already know the stop
8
+ * reason, so the veto belongs at that boundary.
9
9
  *
10
- * - tool_call_end events are held per turn (the deltas pass through live —
11
- * the UI still shows the calls building, only the COMPLETION is gated);
12
- * - a compatible stop flushes the held calls in order, then the stop — the
13
- * kernel launches them exactly as before (the parallel window survives;
14
- * only the 0.1.26 mid-stream launch timing is gone — the cost of the
15
- * guarantee: the turn's truncated intent is never half-executed);
16
- * - a truncation stop flushes the held calls with input: null — the
17
- * kernel's EXISTING invalid-input denial fails the whole batch without
18
- * executing anything (the same honest null the adapters already emit
19
- * for unparseable partials — zero new protocol surface), and the turn
20
- * still ends with the max_tokens terminal (the loop's voided settle).
10
+ * THE CONTRACT this wrapper establishes:
21
11
  *
22
- * The kernel machinery (the streaming launch, the window, the voided
23
- * settle) is untouchedthe gate lives at the adapter boundary, where the
24
- * stop reason is already known.
12
+ * 1. HOLD. A `tool_call_end` is withheld until the turn's stop reason is
13
+ * known. No call completesso none launches before the provider
14
+ * has said why it stopped. The deltas still pass through live: a
15
+ * surface shows the calls building; only the COMPLETION is gated.
16
+ * 2. RELEASE ON A VALID STOP. A compatible stop flushes the held calls
17
+ * unchanged and in CALL order, then the stop itself. Downstream sees
18
+ * exactly what the provider sent.
19
+ * 3. max_tokens VOIDS THE WHOLE BATCH. Every held call is flushed with
20
+ * `input: null`, and the kernel's EXISTING invalid-input denial fails
21
+ * all of them without executing anything (the same honest null the
22
+ * adapters already emit for unparseable partials — zero new protocol
23
+ * surface). The turn still ends on the max_tokens terminal (the
24
+ * loop's voided settle). All-or-nothing per turn: a truncated intent
25
+ * is never HALF executed.
26
+ * 4. NO STOP AT ALL drops the held calls entirely — the kernel already
27
+ * voids that malformed turn (invalid_request).
28
+ *
29
+ * THE CONSERVATISM SPLIT — core and runtime differ here BY DESIGN:
30
+ *
31
+ * - the RAW KERNEL STREAMS. `loop()` over a bare adapter launches each
32
+ * validated, policy-allowed call the moment its `tool_call_end` lands
33
+ * (ADR-0024 Amendment 1, streaming execution): maximum overlap with
34
+ * the model stream, trusting calls as they arrive.
35
+ * - the FLAGSHIP RUNTIME STOP-GATES. `run.ts` composes this wrapper into
36
+ * EVERY run unconditionally, so the product pays a real latency cost —
37
+ * the 0.1.26 mid-stream launch timing is gone (the parallel window is
38
+ * NOT: the released batch still runs concurrently) — to buy the
39
+ * guarantee.
40
+ *
41
+ * So the kernel's default is speed and the flagship's default is safety;
42
+ * an embedder choosing the kernel alone chooses the streaming launch, and
43
+ * applying this wrapper is how they opt into the guarantee. The kernel
44
+ * machinery (the launch, the window, the voided settle) is untouched
45
+ * either way — the gate lives at the adapter boundary.
46
+ *
47
+ * Pinned by `packages/runtime/tests/truncation-guard.test.ts`.
25
48
  */
26
49
  /** Wrap the adapter so a truncated turn's tool batch can never execute. */
27
50
  export function truncationGuard(adapter) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-runtime",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "kiso runtime — durable multi-turn agent sessions: AgentDefinition, AgentRuntime, AgentSession, Run, append-only JSONL store.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,11 +25,11 @@
25
25
  "test": "vitest run"
26
26
  },
27
27
  "dependencies": {
28
- "@vincemakes/kiso-core": "0.11.0"
28
+ "@vincemakes/kiso-core": "0.12.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "@vincemakes/kiso-provider-anthropic": "0.11.0",
32
- "@vincemakes/kiso-provider-openai": "0.11.0"
31
+ "@vincemakes/kiso-provider-anthropic": "0.12.0",
32
+ "@vincemakes/kiso-provider-openai": "0.12.0"
33
33
  },
34
34
  "peerDependenciesMeta": {
35
35
  "@vincemakes/kiso-provider-anthropic": {
@@ -40,7 +40,7 @@
40
40
  }
41
41
  },
42
42
  "devDependencies": {
43
- "@vincemakes/kiso-evals": "0.11.0",
43
+ "@vincemakes/kiso-evals": "0.12.0",
44
44
  "@types/node": "^26.1.2",
45
45
  "typescript": "^5.7.2",
46
46
  "vitest": "^3.0.0"