@vincemakes/kiso-runtime 0.11.0 → 0.13.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 +2 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +5 -2
- package/dist/ledger.d.ts +10 -1
- package/dist/ledger.js +10 -1
- package/dist/recovery-plan.js +43 -8
- package/dist/session.d.ts +52 -0
- package/dist/session.js +81 -0
- package/dist/trace/guard.d.ts +6 -0
- package/dist/trace/guard.js +6 -0
- package/dist/trace/record.d.ts +33 -10
- package/dist/trace/record.js +34 -10
- package/dist/truncation-guard.d.ts +71 -21
- package/dist/truncation-guard.js +71 -21
- package/package.json +5 -5
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
|
|
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
|
|
15
|
+
* names are deprecated aliases, removed in the next major (additive). The
|
|
16
|
+
* cadence is ADR-0051 Amendment 4 — the 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
|
|
15
|
+
* names are deprecated aliases, removed in the next major (additive). The
|
|
16
|
+
* cadence is ADR-0051 Amendment 4 — the 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" (
|
|
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" (
|
|
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/recovery-plan.js
CHANGED
|
@@ -79,20 +79,45 @@ 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 };
|
|
85
|
-
// 4. Gap B: a
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
89
|
+
// 4. Gap B: a no-stop suffix of MODEL OUTPUT is an abandoned draft — void
|
|
90
|
+
// it. The boundary/draft scans run over the OPEN RUN's events
|
|
91
|
+
// INCLUDING the driver's own appends — the driver re-derives after
|
|
92
|
+
// every append, and the marker IT appended must already be the last
|
|
93
|
+
// boundary (the old one-pass Gap B never needed this: it ran before
|
|
94
|
+
// any append).
|
|
95
|
+
//
|
|
96
|
+
// EC-1, finding EC1-F1: the scan counts `tool_call_end` too. It used
|
|
97
|
+
// to be text-only, on the 0.1.44 reasoning that "a bare tool-call
|
|
98
|
+
// suffix is the legal approval-panel pause, never a draft" — but that
|
|
99
|
+
// reasoning only ever held for a suffix carrying a REQUEST (the
|
|
100
|
+
// liveAsk clause below, which still decides that case). A bare call
|
|
101
|
+
// with no request is not a pause; it is an uncommitted draft, and
|
|
102
|
+
// leaving it un-voided projects an assistant `tool_use` block with no
|
|
103
|
+
// result — a provider 400. Pre-EC-1 the window was microseconds wide
|
|
104
|
+
// (the stop was persisted the moment it arrived); ① holds the stop
|
|
105
|
+
// until the stream is exhausted, so crash-pair A lands in this shape
|
|
106
|
+
// by construction.
|
|
92
107
|
const openEvents = openRunEvents(events, scope);
|
|
93
108
|
const boundary = [...openEvents].reverse().find(isBoundary);
|
|
94
109
|
if (boundary !== undefined) {
|
|
95
|
-
|
|
110
|
+
// A call only makes a DRAFT while it is still pure intent. Once it
|
|
111
|
+
// has a durable `tool_execution_started` the world may have moved,
|
|
112
|
+
// and the existing repair passes own it: voiding such a call would
|
|
113
|
+
// strand a real receipt behind a voided declaration (pair atomicity
|
|
114
|
+
// would then drop its result and the model would never learn the
|
|
115
|
+
// outcome of work that actually happened). Pre-EC-1 logs contain
|
|
116
|
+
// exactly that shape — a call that launched mid-stream and finished
|
|
117
|
+
// before its stop was persisted — and they must keep recovering the
|
|
118
|
+
// way they always did.
|
|
119
|
+
const unexecuted = (e) => e.type === "tool_call_end" && !openEvents.some((x) => x.type === "tool_execution_started" && x.callId === e.callId);
|
|
120
|
+
const afterBoundary = openEvents.some((e) => (e.type === "text_delta" || e.type === "thinking" || unexecuted(e)) && e.seq > boundary.seq);
|
|
96
121
|
// The approval-panel pause: a suffix that carries a pending ask of the
|
|
97
122
|
// LIVE turn — the last boundary is the user_input, no stop since — is
|
|
98
123
|
// the human's pause, never a draft: the call was extracted and asked,
|
|
@@ -101,6 +126,16 @@ export function deriveRecoveryPlan(events, scope) {
|
|
|
101
126
|
// crash mid-pause leaves exactly this shape.) A request AFTER a STOP
|
|
102
127
|
// is different: it is the draft's own ask (0143's shape) — the marker
|
|
103
128
|
// voids it and the request expires with the draft.
|
|
129
|
+
//
|
|
130
|
+
// EC-1 ERA NOTE — this clause is now GENERATION COMPAT, and is kept
|
|
131
|
+
// for that reason alone. A kiso at 0.13.0 or later cannot produce the
|
|
132
|
+
// shape: asks moved AFTER Turn Commit (③), so a durable
|
|
133
|
+
// `permission_requested` always has a durable stop before it. Logs
|
|
134
|
+
// written by EARLIER bins do carry it, they are on disk, and they
|
|
135
|
+
// must keep re-presenting their ask instead of being voided as
|
|
136
|
+
// drafts. Deleting this clause would silently change how pre-EC-1
|
|
137
|
+
// sessions recover; it stays until the generation corpus no longer
|
|
138
|
+
// contains a pre-EC-1 era.
|
|
104
139
|
const liveAsk = afterBoundary &&
|
|
105
140
|
boundary.type === "user_input" &&
|
|
106
141
|
openEvents.some((e) => e.type === "permission_requested" && e.seq > boundary.seq);
|
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();
|
package/dist/trace/guard.d.ts
CHANGED
|
@@ -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
|
package/dist/trace/guard.js
CHANGED
|
@@ -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,
|
package/dist/trace/record.d.ts
CHANGED
|
@@ -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:
|
|
30
|
-
* shape,
|
|
31
|
-
* reads (R1d-1, R2-1). Algorithm and shape
|
|
32
|
-
* OUT-side versioning). */
|
|
33
|
-
export declare const TRACE_SCHEMA_VERSION =
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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;
|
package/dist/trace/record.js
CHANGED
|
@@ -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:
|
|
30
|
-
* shape,
|
|
31
|
-
* reads (R1d-1, R2-1). Algorithm and shape
|
|
32
|
-
* OUT-side versioning). */
|
|
33
|
-
export const TRACE_SCHEMA_VERSION =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,77 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 0.1.40 (R-C item 3) — the truncation guard: a runtime adapter wrapper.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
+
*
|
|
10
|
+
* THE CONTRACT this wrapper establishes:
|
|
11
|
+
*
|
|
12
|
+
* 1. HOLD. A `tool_call_end` is withheld until the turn's stop reason is
|
|
13
|
+
* known. No call completes — so 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
|
+
* EC-1 ④ — THE CONTRACT AMENDMENT. The kernel closed the destructive half
|
|
30
|
+
* of this by itself: `max_tokens` cannot carry a tool call, so a truncated
|
|
31
|
+
* turn never reaches Turn Commit, and a commit-required handler never starts
|
|
32
|
+
* before that commit. What max_tokens means now, in full — the four clauses
|
|
33
|
+
* that replace the old "zero tools executed on truncation" line:
|
|
34
|
+
*
|
|
35
|
+
* 1. COMMIT-REQUIRED CALLS NEVER EXECUTE. On either path, guarded or bare.
|
|
36
|
+
* This is the kernel's guarantee, not the wrapper's, and it holds for
|
|
37
|
+
* every tool that declares nothing — which is every write, edit and
|
|
38
|
+
* shell tool kiso ships.
|
|
39
|
+
* 2. PRECOMMIT-SAFE CALLS MAY ALREADY HAVE EXECUTED, and that execution is
|
|
40
|
+
* DECLARED HARMLESS. A tool carrying `effects.precommitSafe` says
|
|
41
|
+
* running it before the turn commits is harmless for EVERY invocation —
|
|
42
|
+
* read-only, free, local. Bare, such a call launches during the stream
|
|
43
|
+
* and a truncated turn may find its receipt already durable. That is
|
|
44
|
+
* the certificate being spent, not a leak.
|
|
45
|
+
* 3. THE TURN IS NOT COMMITTED. No durable stop is written. The calls are
|
|
46
|
+
* an uncommitted draft, which is what the resume sees.
|
|
47
|
+
* 4. PRECOMMIT RESULTS NEVER LEGITIMIZE IT (invariant 7). A durable
|
|
48
|
+
* receipt from clause 2 is an execution fact and nothing more: it does
|
|
49
|
+
* not commit the invocation, and it does not make the model turn valid.
|
|
50
|
+
*
|
|
51
|
+
* WHAT THE WRAPPER STILL BUYS, given all that:
|
|
52
|
+
*
|
|
53
|
+
* - REPORTING. It releases the held batch with `input: null`, so every
|
|
54
|
+
* call is ANSWERED with an honest invalid_input result. Bare, the same
|
|
55
|
+
* turn leaves its calls with no results at all — an uncommitted draft
|
|
56
|
+
* the resume must void.
|
|
57
|
+
* - THE PRECOMMIT CASE. The hold sits UPSTREAM of the kernel: a held call
|
|
58
|
+
* never reaches the loop until the stop is known, so clause 2's "may
|
|
59
|
+
* already have executed" is exactly what the guard removes. Guarded,
|
|
60
|
+
* nothing runs at all — not even a declared read.
|
|
61
|
+
*
|
|
62
|
+
* So the conservatism split did not disappear, it MOVED. It used to be the
|
|
63
|
+
* difference between a destructive edit running and not running; it is now
|
|
64
|
+
* the difference between a harmless read running and not running, plus the
|
|
65
|
+
* reporting. The kernel's default is speed for CERTIFIED calls and safety
|
|
66
|
+
* for everything else; the flagship runtime composes this wrapper into every
|
|
67
|
+
* run and pays the latency to have neither.
|
|
68
|
+
*
|
|
69
|
+
* Pinned by `packages/runtime/tests/truncation-guard.test.ts` (clauses 1-3
|
|
70
|
+
* of the wrapper contract, byte-unchanged across EC-1 — the amendment did
|
|
71
|
+
* not weaken the guard) and by
|
|
72
|
+
* `packages/runtime/tests/sc1-truncation-contract-pins.test.ts` (clause 4 of
|
|
73
|
+
* the wrapper contract, and the four amended max_tokens clauses above — the
|
|
74
|
+
* declared TRUNCATION CLASS).
|
|
25
75
|
*/
|
|
26
76
|
import type { Adapter } from "@vincemakes/kiso-core";
|
|
27
77
|
/** Wrap the adapter so a truncated turn's tool batch can never execute. */
|
package/dist/truncation-guard.js
CHANGED
|
@@ -1,27 +1,77 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 0.1.40 (R-C item 3) — the truncation guard: a runtime adapter wrapper.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
+
*
|
|
10
|
+
* THE CONTRACT this wrapper establishes:
|
|
11
|
+
*
|
|
12
|
+
* 1. HOLD. A `tool_call_end` is withheld until the turn's stop reason is
|
|
13
|
+
* known. No call completes — so 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
|
+
* EC-1 ④ — THE CONTRACT AMENDMENT. The kernel closed the destructive half
|
|
30
|
+
* of this by itself: `max_tokens` cannot carry a tool call, so a truncated
|
|
31
|
+
* turn never reaches Turn Commit, and a commit-required handler never starts
|
|
32
|
+
* before that commit. What max_tokens means now, in full — the four clauses
|
|
33
|
+
* that replace the old "zero tools executed on truncation" line:
|
|
34
|
+
*
|
|
35
|
+
* 1. COMMIT-REQUIRED CALLS NEVER EXECUTE. On either path, guarded or bare.
|
|
36
|
+
* This is the kernel's guarantee, not the wrapper's, and it holds for
|
|
37
|
+
* every tool that declares nothing — which is every write, edit and
|
|
38
|
+
* shell tool kiso ships.
|
|
39
|
+
* 2. PRECOMMIT-SAFE CALLS MAY ALREADY HAVE EXECUTED, and that execution is
|
|
40
|
+
* DECLARED HARMLESS. A tool carrying `effects.precommitSafe` says
|
|
41
|
+
* running it before the turn commits is harmless for EVERY invocation —
|
|
42
|
+
* read-only, free, local. Bare, such a call launches during the stream
|
|
43
|
+
* and a truncated turn may find its receipt already durable. That is
|
|
44
|
+
* the certificate being spent, not a leak.
|
|
45
|
+
* 3. THE TURN IS NOT COMMITTED. No durable stop is written. The calls are
|
|
46
|
+
* an uncommitted draft, which is what the resume sees.
|
|
47
|
+
* 4. PRECOMMIT RESULTS NEVER LEGITIMIZE IT (invariant 7). A durable
|
|
48
|
+
* receipt from clause 2 is an execution fact and nothing more: it does
|
|
49
|
+
* not commit the invocation, and it does not make the model turn valid.
|
|
50
|
+
*
|
|
51
|
+
* WHAT THE WRAPPER STILL BUYS, given all that:
|
|
52
|
+
*
|
|
53
|
+
* - REPORTING. It releases the held batch with `input: null`, so every
|
|
54
|
+
* call is ANSWERED with an honest invalid_input result. Bare, the same
|
|
55
|
+
* turn leaves its calls with no results at all — an uncommitted draft
|
|
56
|
+
* the resume must void.
|
|
57
|
+
* - THE PRECOMMIT CASE. The hold sits UPSTREAM of the kernel: a held call
|
|
58
|
+
* never reaches the loop until the stop is known, so clause 2's "may
|
|
59
|
+
* already have executed" is exactly what the guard removes. Guarded,
|
|
60
|
+
* nothing runs at all — not even a declared read.
|
|
61
|
+
*
|
|
62
|
+
* So the conservatism split did not disappear, it MOVED. It used to be the
|
|
63
|
+
* difference between a destructive edit running and not running; it is now
|
|
64
|
+
* the difference between a harmless read running and not running, plus the
|
|
65
|
+
* reporting. The kernel's default is speed for CERTIFIED calls and safety
|
|
66
|
+
* for everything else; the flagship runtime composes this wrapper into every
|
|
67
|
+
* run and pays the latency to have neither.
|
|
68
|
+
*
|
|
69
|
+
* Pinned by `packages/runtime/tests/truncation-guard.test.ts` (clauses 1-3
|
|
70
|
+
* of the wrapper contract, byte-unchanged across EC-1 — the amendment did
|
|
71
|
+
* not weaken the guard) and by
|
|
72
|
+
* `packages/runtime/tests/sc1-truncation-contract-pins.test.ts` (clause 4 of
|
|
73
|
+
* the wrapper contract, and the four amended max_tokens clauses above — the
|
|
74
|
+
* declared TRUNCATION CLASS).
|
|
25
75
|
*/
|
|
26
76
|
/** Wrap the adapter so a truncated turn's tool batch can never execute. */
|
|
27
77
|
export function truncationGuard(adapter) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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.
|
|
28
|
+
"@vincemakes/kiso-core": "0.13.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@vincemakes/kiso-provider-anthropic": "0.
|
|
32
|
-
"@vincemakes/kiso-provider-openai": "0.
|
|
31
|
+
"@vincemakes/kiso-provider-anthropic": "0.13.0",
|
|
32
|
+
"@vincemakes/kiso-provider-openai": "0.13.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.
|
|
43
|
+
"@vincemakes/kiso-evals": "0.13.0",
|
|
44
44
|
"@types/node": "^26.1.2",
|
|
45
45
|
"typescript": "^5.7.2",
|
|
46
46
|
"vitest": "^3.0.0"
|