@vincemakes/kiso-runtime 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +2 -0
- package/dist/agent.js +1 -0
- package/dist/compose.d.ts +11 -6
- package/dist/compose.js +21 -7
- package/dist/index.d.ts +1 -1
- package/dist/run.js +38 -12
- package/dist/session.d.ts +81 -0
- package/dist/session.js +116 -8
- package/dist/summarize.d.ts +99 -3
- package/dist/summarize.js +224 -14
- package/dist/trace/guard.d.ts +6 -0
- package/dist/trace/guard.js +16 -0
- package/dist/trace/record.d.ts +27 -11
- package/dist/trace/record.js +33 -13
- package/dist/trace/rent.d.ts +81 -0
- package/dist/trace/rent.js +96 -0
- package/package.json +1 -1
package/dist/summarize.js
CHANGED
|
@@ -18,30 +18,176 @@ import { estimateTokens, DO_NOT_COMPACT } from "@vincemakes/kiso-core";
|
|
|
18
18
|
* not a knob. The covered range ends just before the K-th most recent
|
|
19
19
|
* round, so the model still reasons over the recent conversation. */
|
|
20
20
|
export const KEEP_RECENT_ROUNDS = 4;
|
|
21
|
+
/** E6 (a) — the input-side DSML guard (the finding E6-F4/F5 follow-up):
|
|
22
|
+
* the guard sentence sits at the TOP of the system prompt (the BEFORE
|
|
23
|
+
* copy of the sandwich) AND again after the </conversation> block in the
|
|
24
|
+
* serialized input (the AFTER copy). The summarizer is a side-channel
|
|
25
|
+
* task — it must never continue the work, never touch tools, and only
|
|
26
|
+
* emit the summary text. */
|
|
27
|
+
export const SUMMARY_GUARD = "Only output the summary. Do not continue the conversation. Do not use any tools.";
|
|
28
|
+
/** The tool-result truncation ceiling in the serialized input: a huge
|
|
29
|
+
* result must not dominate the summary input, and the truncation is
|
|
30
|
+
* MARKED with the discarded character count — never silent. */
|
|
31
|
+
export const SUMMARY_RESULT_MAX_CHARS = 2000;
|
|
32
|
+
/**
|
|
33
|
+
* E6 (g) — the reserve arithmetic (the pre-registered numbers): the
|
|
34
|
+
* armed trigger is WINDOW − RESERVE, never a fixed low absolute (the
|
|
35
|
+
* e6probe's fixed 1300 fired 16-19× a session — the pathology the
|
|
36
|
+
* window math kills). The reserve is what ONE fire must buy back:
|
|
37
|
+
* the summary's own output budget (4,000), the kept-suffix token
|
|
38
|
+
* floor (20,000, item (f)), and the current run's in-flight context
|
|
39
|
+
* while the post-fire projection settles (8,000).
|
|
40
|
+
*/
|
|
41
|
+
export const SUMMARY_MAX_OUTPUT = 4000;
|
|
42
|
+
export const KEEP_TOKENS_DEFAULT = 20000;
|
|
43
|
+
export const IN_FLIGHT_HEADROOM = 8000;
|
|
44
|
+
export const POLICY_RESERVE = SUMMARY_MAX_OUTPUT + KEEP_TOKENS_DEFAULT + IN_FLIGHT_HEADROOM;
|
|
45
|
+
/** The reference context-window scale (the flash-family window); the
|
|
46
|
+
* env overrides. The default arming point is 120,000 − 32,000 =
|
|
47
|
+
* 88,000 — a post-fire projection (≥ 24k) can never re-cross it, so
|
|
48
|
+
* the session settles after one fire. */
|
|
49
|
+
export const DEFAULT_CONTEXT_WINDOW = 120000;
|
|
50
|
+
/** The armed trigger for a context window: window − POLICY_RESERVE. A
|
|
51
|
+
* window below the reserve arms a NEGATIVE trigger — the session
|
|
52
|
+
* never fires (the honest inert refusal: the window cannot hold even
|
|
53
|
+
* the post-fire projection, so the policy stays off, never clamped
|
|
54
|
+
* into pretending). */
|
|
55
|
+
export function policyTriggerFromWindow(windowTokens = DEFAULT_CONTEXT_WINDOW) {
|
|
56
|
+
return windowTokens - POLICY_RESERVE;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* E6 (h) — the circuit breaker: MAX_SUMMARY_FAILURES consecutive
|
|
60
|
+
* summary failures per session stand the auto policy down (no further
|
|
61
|
+
* auto-fire attempts; a success resets). Both adapter failures and the
|
|
62
|
+
* (b) validation rejections count — they throw through the policy's
|
|
63
|
+
* safe catch. A persistent summary failure (a broken provider, a
|
|
64
|
+
* hostile model) must never wedge the session into paying the call
|
|
65
|
+
* every run.
|
|
66
|
+
*/
|
|
67
|
+
export const MAX_SUMMARY_FAILURES = 3;
|
|
68
|
+
export function serializeCovered(options) {
|
|
69
|
+
const { events, prevPoint, boundary } = options;
|
|
70
|
+
const lines = ["<conversation>"];
|
|
71
|
+
// E6 (d) (the order's R4): the old summary texts are RETAINED CONTEXT —
|
|
72
|
+
// the durable record of the earlier ranges. They render first, labeled
|
|
73
|
+
// do-not-re-summarize: the summarizer must know what the earlier
|
|
74
|
+
// summaries covered, but never fold them into the new checkpoint.
|
|
75
|
+
const retained = events.filter((e) => e.type === "summarized" && e.coversToSeq <= prevPoint);
|
|
76
|
+
if (retained.length > 0) {
|
|
77
|
+
lines.push("[retained context — do not re-summarize]");
|
|
78
|
+
for (const r of retained)
|
|
79
|
+
lines.push(`[summary covers to seq ${r.coversToSeq}] ${r.summary}`);
|
|
80
|
+
lines.push("[end retained context]");
|
|
81
|
+
}
|
|
82
|
+
for (const ev of events) {
|
|
83
|
+
if (ev.seq <= prevPoint || ev.seq > boundary || ev.type === "summarized")
|
|
84
|
+
continue;
|
|
85
|
+
switch (ev.type) {
|
|
86
|
+
case "user_input":
|
|
87
|
+
lines.push(`[user] ${ev.content}`);
|
|
88
|
+
break;
|
|
89
|
+
case "text_delta":
|
|
90
|
+
lines.push(`[assistant] ${ev.text}`);
|
|
91
|
+
break;
|
|
92
|
+
case "tool_call_end":
|
|
93
|
+
lines.push(`[tool call ${ev.name}] ${JSON.stringify(ev.input ?? null)}`);
|
|
94
|
+
break;
|
|
95
|
+
case "tool_result": {
|
|
96
|
+
const content = String(ev.content ?? "");
|
|
97
|
+
if (content.length > SUMMARY_RESULT_MAX_CHARS) {
|
|
98
|
+
const rest = content.length - SUMMARY_RESULT_MAX_CHARS;
|
|
99
|
+
lines.push(`[tool result] ${content.slice(0, SUMMARY_RESULT_MAX_CHARS)}… (${rest.toLocaleString("en-US")} more chars truncated)`);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
lines.push(`[tool result] ${content}`);
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
default:
|
|
107
|
+
break; // thinking and the rest never enter the transcript surface
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
lines.push("</conversation>", "", SUMMARY_GUARD);
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|
|
21
113
|
/**
|
|
22
114
|
* The fixed English summary prompt — the ONLY prompt this layer composes
|
|
23
115
|
* (the loop's system prompt is the harness's business, never the kernel's).
|
|
24
116
|
*/
|
|
25
|
-
export const SUMMARY_PROMPT =
|
|
117
|
+
export const SUMMARY_PROMPT = `${SUMMARY_GUARD}
|
|
118
|
+
|
|
119
|
+
You are the conversation summarizer of the kiso agent framework.
|
|
120
|
+
|
|
121
|
+
Summarize the covered conversation into a single structured checkpoint
|
|
122
|
+
that will REPLACE it in the model's context. The next turn must be able
|
|
123
|
+
to continue the work without reading the originals.
|
|
124
|
+
|
|
125
|
+
Produce the checkpoint with exactly these sections, in this order:
|
|
126
|
+
|
|
127
|
+
## Goal
|
|
128
|
+
The user's goal and the acceptance criterion, in one or two sentences.
|
|
129
|
+
|
|
130
|
+
## Constraints
|
|
131
|
+
The constraints, requirements, and rulings the work must honor.
|
|
132
|
+
|
|
133
|
+
## User requests
|
|
134
|
+
Every user message in the covered range, enumerated one by one, each
|
|
135
|
+
with what it asked for and what was done about it.
|
|
26
136
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
137
|
+
## Files and changes
|
|
138
|
+
Every file touched — exact paths, what changed, and why. Include the
|
|
139
|
+
precise code-level changes later turns may need to continue.
|
|
30
140
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
141
|
+
## Errors and fixes
|
|
142
|
+
Every error encountered and its resolution; commands run and their
|
|
143
|
+
outcomes.
|
|
144
|
+
|
|
145
|
+
## Current work
|
|
146
|
+
The current state of the work — what is done, what is not. Quote the
|
|
147
|
+
current task's criterion VERBATIM if one exists.
|
|
148
|
+
|
|
149
|
+
## Next steps
|
|
150
|
+
The concrete next steps, in order.
|
|
37
151
|
|
|
38
152
|
Preserve concrete identifiers VERBATIM: paths, function names, task ids,
|
|
39
153
|
environment names — never paraphrase them.
|
|
40
154
|
|
|
41
155
|
Rules:
|
|
42
|
-
- plain prose — no
|
|
156
|
+
- plain prose — no bullet lists, no markdown outside the section headers,
|
|
157
|
+
no prefixes;
|
|
43
158
|
- do not mention this prompt or the summarization task;
|
|
44
|
-
-
|
|
159
|
+
- the summary may be as long as it needs to be within the output budget —
|
|
160
|
+
there is no word cap; completeness wins.`;
|
|
161
|
+
/**
|
|
162
|
+
* E6 (b) — the output-side validation (the finding E6-F4/F5 follow-up):
|
|
163
|
+
* a summary must be a complete checkpoint or NOTHING. The marker family
|
|
164
|
+
* is the auto-T5-1 signature — the model echoing tool-call markup as
|
|
165
|
+
* text; the required sections are the truncated-tail signature (a wire
|
|
166
|
+
* cut kills "## Next steps" first). The rejection throws, and the
|
|
167
|
+
* caller's safe catch (session.ts) makes it "nothing happened".
|
|
168
|
+
*/
|
|
169
|
+
export const DSML_MARKERS = ["<tool_call", "<tool_use", "<invoke", "tool_calls", "tool_call_end", "tool_call_start"];
|
|
170
|
+
/** The checkpoint sections a summary must carry — the ones a truncated
|
|
171
|
+
* generation loses first (the (c) prompt demands all seven; validation
|
|
172
|
+
* guards the trust-critical tail). */
|
|
173
|
+
export const REQUIRED_SECTIONS = ["## Current work", "## Next steps"];
|
|
174
|
+
/** null = pass; an error string = reject. Empty text is the existing
|
|
175
|
+
* no-text rule's domain, reported here too (defense in depth). */
|
|
176
|
+
export function validateSummary(text) {
|
|
177
|
+
const trimmed = text.trim();
|
|
178
|
+
if (trimmed === "")
|
|
179
|
+
return "the summary is empty";
|
|
180
|
+
const lower = trimmed.toLowerCase();
|
|
181
|
+
for (const marker of DSML_MARKERS) {
|
|
182
|
+
if (lower.includes(marker))
|
|
183
|
+
return `the summary carries a tool-call marker (${marker}) — reject`;
|
|
184
|
+
}
|
|
185
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
186
|
+
if (!trimmed.includes(section))
|
|
187
|
+
return `the summary is missing the required section ${section} — a truncated or incomplete checkpoint`;
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
45
191
|
/**
|
|
46
192
|
* The one-shot summary call. Collects the adapter's text deltas into the
|
|
47
193
|
* summary; usage/stop pass through untouched. Throws when the model
|
|
@@ -50,21 +196,38 @@ Rules:
|
|
|
50
196
|
export async function summarizeConversation(options) {
|
|
51
197
|
const { adapter, model, messages } = options;
|
|
52
198
|
let text = "";
|
|
199
|
+
let usage = null;
|
|
53
200
|
for await (const ev of adapter.stream({
|
|
54
201
|
model,
|
|
55
202
|
messages,
|
|
56
203
|
systemPrompt: SUMMARY_PROMPT,
|
|
57
204
|
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
205
|
+
...(options.maxOutputTokens !== undefined ? { maxTokens: options.maxOutputTokens } : {}),
|
|
58
206
|
})) {
|
|
59
207
|
if (ev.type === "text_delta")
|
|
60
208
|
text += ev.text;
|
|
209
|
+
// The LAST usage event is the call's (a turn reports usage once).
|
|
210
|
+
if (ev.type === "usage" && ev.known) {
|
|
211
|
+
usage = { inputTokens: ev.inputTokens, outputTokens: ev.outputTokens, cacheRead: ev.cacheRead, cacheWrite: ev.cacheWrite };
|
|
212
|
+
}
|
|
61
213
|
}
|
|
62
214
|
const trimmed = text.trim();
|
|
63
215
|
if (trimmed === "") {
|
|
64
216
|
throw new Error("the summary call produced no text");
|
|
65
217
|
}
|
|
66
|
-
|
|
218
|
+
// E6 (b): a non-checkpoint summary is an honest failure — throw, the
|
|
219
|
+
// caller reports it, nothing is persisted (the auto-T5-1 regression).
|
|
220
|
+
const invalid = validateSummary(trimmed);
|
|
221
|
+
if (invalid !== null) {
|
|
222
|
+
throw new Error(`the summary call produced an invalid summary: ${invalid}`);
|
|
223
|
+
}
|
|
224
|
+
return { text: trimmed, usage };
|
|
67
225
|
}
|
|
226
|
+
/** E6 — the crux-experiment drop arm: the covered turns are replaced by
|
|
227
|
+
* this fixed placeholder with NO model call. Experiment-only (the
|
|
228
|
+
* contextPolicy drop mode); the adopted shape — if the crux evidence
|
|
229
|
+
* earns it — is a distinct `dropped` event family, not this text. */
|
|
230
|
+
export const DROP_PLACEHOLDER = "[e6-crux: the covered turns were dropped without a summary; continue from the kept turns and this placeholder]";
|
|
68
231
|
/**
|
|
69
232
|
* The last summary point: the previous `summarized` event's coversToSeq,
|
|
70
233
|
* or -1 (the trajectory's start) when none exists. The covered range of
|
|
@@ -78,6 +241,23 @@ export function lastSummaryPoint(events) {
|
|
|
78
241
|
}
|
|
79
242
|
return prev;
|
|
80
243
|
}
|
|
244
|
+
/** The chars/4 token proxy for a single EVENT (the same convention as
|
|
245
|
+
* estimateTokens, event-shaped — the (f) keep-floor walk needs the kept
|
|
246
|
+
* suffix's tokens without projecting it). */
|
|
247
|
+
function estimateEventTokens(ev) {
|
|
248
|
+
switch (ev.type) {
|
|
249
|
+
case "user_input":
|
|
250
|
+
return Math.ceil(ev.content.length / 4);
|
|
251
|
+
case "text_delta":
|
|
252
|
+
return Math.ceil(ev.text.length / 4);
|
|
253
|
+
case "tool_call_end":
|
|
254
|
+
return Math.ceil(JSON.stringify(ev.input ?? null).length / 4) + 20;
|
|
255
|
+
case "tool_result":
|
|
256
|
+
return Math.ceil(String(ev.content ?? "").length / 4);
|
|
257
|
+
default:
|
|
258
|
+
return 0;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
81
261
|
/**
|
|
82
262
|
* The covered range's end: the seq of the event just before the
|
|
83
263
|
* keepRounds-th most recent user_input AFTER the last summary point —
|
|
@@ -103,7 +283,7 @@ export function lastSummaryPoint(events) {
|
|
|
103
283
|
* (the operative list is the LATEST echo — the old ⑥ semantics:
|
|
104
284
|
* superseded echoes stay coverable).
|
|
105
285
|
*/
|
|
106
|
-
export function summaryBoundarySeq(events, keepRounds = KEEP_RECENT_ROUNDS) {
|
|
286
|
+
export function summaryBoundarySeq(events, keepRounds = KEEP_RECENT_ROUNDS, keepTokens) {
|
|
107
287
|
const prevPoint = lastSummaryPoint(events);
|
|
108
288
|
const uncoveredInputs = [];
|
|
109
289
|
for (const ev of events) {
|
|
@@ -114,6 +294,36 @@ export function summaryBoundarySeq(events, keepRounds = KEEP_RECENT_ROUNDS) {
|
|
|
114
294
|
return undefined;
|
|
115
295
|
const firstUncovered = uncoveredInputs[0];
|
|
116
296
|
let boundary = uncoveredInputs[uncoveredInputs.length - keepRounds] - 1;
|
|
297
|
+
// E6 (f): the keep budget is rounds AND tokens. A kept suffix smaller
|
|
298
|
+
// than keepTokens is a break the session cannot amortize (the E5-F1
|
|
299
|
+
// accounting) — walk the boundary back (keep more) until the kept
|
|
300
|
+
// events clear the floor. The walk picks the smallest kept suffix
|
|
301
|
+
// meeting it: per-event cumulative tokens, one pass. A floor the whole
|
|
302
|
+
// uncovered range cannot meet → nothing to compact (the policy is
|
|
303
|
+
// inert on small sessions — the token-shaped restraint).
|
|
304
|
+
if (keepTokens !== undefined && keepTokens > 0) {
|
|
305
|
+
const prefixTokens = [0];
|
|
306
|
+
let total = 0;
|
|
307
|
+
for (const ev of events) {
|
|
308
|
+
total += estimateEventTokens(ev);
|
|
309
|
+
prefixTokens.push(total);
|
|
310
|
+
}
|
|
311
|
+
const keptTokens = (b) => total - prefixTokens[b + 1];
|
|
312
|
+
let floorBoundary;
|
|
313
|
+
for (let i = uncoveredInputs.length - 1; i >= 0; i--) {
|
|
314
|
+
const b = uncoveredInputs[i] - 1;
|
|
315
|
+
if (keptTokens(b) >= keepTokens) {
|
|
316
|
+
floorBoundary = b;
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// b < firstUncovered covers no whole round (or the empty residue) —
|
|
321
|
+
// the honest nothing-to-compact.
|
|
322
|
+
if (floorBoundary === undefined || floorBoundary < firstUncovered)
|
|
323
|
+
return undefined;
|
|
324
|
+
if (floorBoundary < boundary)
|
|
325
|
+
boundary = floorBoundary;
|
|
326
|
+
}
|
|
117
327
|
// The protected pullback applies ONCE on the base range (⑥); the
|
|
118
328
|
// straddle pullback recomputes against the SHRINKING range below it.
|
|
119
329
|
const protectedBoundary = latestProtectedBoundary(events, prevPoint, boundary);
|
package/dist/trace/guard.d.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* the honest nullable quartet is a schema bump, deferred.
|
|
23
23
|
*/
|
|
24
24
|
import type { Adapter, AdapterEvent, Event, StreamOptions } from "@vincemakes/kiso-core";
|
|
25
|
+
import { type RentParts } from "./rent.js";
|
|
25
26
|
export interface RequestTracerDeps {
|
|
26
27
|
root: string;
|
|
27
28
|
sessionId: string;
|
|
@@ -33,6 +34,11 @@ export interface RequestTracerDeps {
|
|
|
33
34
|
adapterVersion?: string | null;
|
|
34
35
|
/** The session log — the manifest's seqRange pointers derive from it. */
|
|
35
36
|
log: readonly Event[];
|
|
37
|
+
/** E3 — the rent ledger's inputs: the base prompt as configured and
|
|
38
|
+
* the per-extension appends in load order (the adapter's composed
|
|
39
|
+
* systemPrompt is their RESULT — the parts are what the ledger
|
|
40
|
+
* counts; the composed string itself is unchanged, I6). */
|
|
41
|
+
rentParts?: RentParts;
|
|
36
42
|
}
|
|
37
43
|
export declare class RequestTracer {
|
|
38
44
|
#private;
|
package/dist/trace/guard.js
CHANGED
|
@@ -26,6 +26,7 @@ import { buildContextManifest, segmentHashes } from "./manifest.js";
|
|
|
26
26
|
import { cacheableHashes } from "./analyze.js";
|
|
27
27
|
import { hashContext, hashSystemPrompt, hashToolSpecs, stablePrefixFingerprint } from "./hash.js";
|
|
28
28
|
import { PRICING_TABLE_V1, canonicalizeUsage } from "../usage/canonical.js";
|
|
29
|
+
import { buildRentLedger } from "./rent.js";
|
|
29
30
|
import { TRACE_SCHEMA_VERSION } from "./record.js";
|
|
30
31
|
import { TraceWriter } from "./writer.js";
|
|
31
32
|
export class RequestTracer {
|
|
@@ -34,6 +35,7 @@ export class RequestTracer {
|
|
|
34
35
|
#provider;
|
|
35
36
|
#runId;
|
|
36
37
|
#adapterVersion;
|
|
38
|
+
#rentParts;
|
|
37
39
|
#requestIndex = 0;
|
|
38
40
|
#contextHashCounts = new Map();
|
|
39
41
|
constructor(deps) {
|
|
@@ -42,6 +44,7 @@ export class RequestTracer {
|
|
|
42
44
|
this.#provider = deps.provider;
|
|
43
45
|
this.#runId = deps.runId;
|
|
44
46
|
this.#adapterVersion = deps.adapterVersion ?? null;
|
|
47
|
+
this.#rentParts = deps.rentParts;
|
|
45
48
|
}
|
|
46
49
|
init() {
|
|
47
50
|
this.#writer.init();
|
|
@@ -119,6 +122,18 @@ export class RequestTracer {
|
|
|
119
122
|
this.#contextHashCounts.set(contextHash, retryAttempt + 1);
|
|
120
123
|
const manifest = buildContextManifest({ log: this.#log, systemPrompt, tools, messages });
|
|
121
124
|
const hashes = segmentHashes(systemPrompt, tools, messages);
|
|
125
|
+
// E3 — the static rent ledger, one line per surface; the envelope
|
|
126
|
+
// derives from the request the guard already has (R5), the base and
|
|
127
|
+
// the appends from the threaded parts (R3/R4 — observation only).
|
|
128
|
+
// exactOptionalPropertyTypes: an absent surface is an absent key —
|
|
129
|
+
// never an explicit undefined (R9).
|
|
130
|
+
const rentParts = this.#rentParts;
|
|
131
|
+
const rent = buildRentLedger({
|
|
132
|
+
model: options.model,
|
|
133
|
+
...(rentParts?.base !== undefined ? { base: rentParts.base } : {}),
|
|
134
|
+
...(rentParts?.appends !== undefined ? { appends: rentParts.appends } : {}),
|
|
135
|
+
...(tools !== undefined ? { tools } : {}),
|
|
136
|
+
});
|
|
122
137
|
return {
|
|
123
138
|
schemaVersion: TRACE_SCHEMA_VERSION,
|
|
124
139
|
kind: "request",
|
|
@@ -156,6 +171,7 @@ export class RequestTracer {
|
|
|
156
171
|
pricingTableId: PRICING_TABLE_V1.id,
|
|
157
172
|
pricingTableVersion: PRICING_TABLE_V1.version,
|
|
158
173
|
},
|
|
174
|
+
rent,
|
|
159
175
|
latencyMs: 0,
|
|
160
176
|
ttftMs: 0,
|
|
161
177
|
toolCalls: [],
|
package/dist/trace/record.d.ts
CHANGED
|
@@ -19,15 +19,24 @@
|
|
|
19
19
|
* quartet stays as provider observation above it). The validators accept
|
|
20
20
|
* BOTH generations (R1d-1): a v1 sidecar has no canonical block and reads
|
|
21
21
|
* as defaults at every consumer — never a crash.
|
|
22
|
+
*
|
|
23
|
+
* E3 (0.2.1) — schemaVersion 3: the record gains the `rent` block — the
|
|
24
|
+
* static rent ledger, one line per surface (trace/rent.ts). The v3
|
|
25
|
+
* writers record it; v1/v2 sidecars keep reading as defaults (R1d-1,
|
|
26
|
+
* R2-1): no rent block = no rent lines = the zero-rent reading, never a
|
|
27
|
+
* crash.
|
|
22
28
|
*/
|
|
23
|
-
/** schemaVersion:
|
|
24
|
-
* shape,
|
|
25
|
-
* changes bump it (ADR-0051 §6
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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;
|
|
34
|
+
/** The versions a reader may meet in a ledger. v1 and v2 records are
|
|
35
|
+
* accepted (generation-compat) and read as defaults — no canonical
|
|
36
|
+
* block (v1), no rent block (v1, v2). */
|
|
29
37
|
export declare const TRACE_SCHEMA_VERSIONS: Readonly<Set<number>>;
|
|
30
38
|
import type { CanonicalUsage } from "../usage/canonical.js";
|
|
39
|
+
import { type RentLine } from "./rent.js";
|
|
31
40
|
export type Freshness = "fresh" | "cache_read" | "cache_write";
|
|
32
41
|
/** That is the complete set for 1.2.0. */
|
|
33
42
|
export type Outcome = "ok" | "provider_error" | "aborted";
|
|
@@ -43,7 +52,7 @@ export interface TraceSegment {
|
|
|
43
52
|
}
|
|
44
53
|
/** That is the complete set for 1.2.0. */
|
|
45
54
|
export interface TraceRecord {
|
|
46
|
-
schemaVersion:
|
|
55
|
+
schemaVersion: 3;
|
|
47
56
|
kind: "request";
|
|
48
57
|
requestId: string;
|
|
49
58
|
runId: string;
|
|
@@ -77,6 +86,11 @@ export interface TraceRecord {
|
|
|
77
86
|
* the validator pins the equality — and carries the cost from the
|
|
78
87
|
* versioned pricing table (every cost records its table version). */
|
|
79
88
|
canonical: CanonicalUsage;
|
|
89
|
+
/** E3 — the static rent ledger: one line per surface (system:base,
|
|
90
|
+
* system:ext:<name>, tool:<name>, envelope), counts never payloads —
|
|
91
|
+
* see trace/rent.ts. v3 requires the block; a v1/v2 sidecar has none
|
|
92
|
+
* and reads as the zero-rent ledger (R2-1). */
|
|
93
|
+
rent: RentLine[];
|
|
80
94
|
latencyMs: number;
|
|
81
95
|
ttftMs: number;
|
|
82
96
|
toolCalls: string[];
|
|
@@ -91,21 +105,21 @@ export interface TraceRecord {
|
|
|
91
105
|
}
|
|
92
106
|
/** That is the complete set for 1.2.0. */
|
|
93
107
|
export interface HeaderLine {
|
|
94
|
-
schemaVersion:
|
|
108
|
+
schemaVersion: 3;
|
|
95
109
|
kind: "header";
|
|
96
110
|
sessionId: string;
|
|
97
111
|
kisoVersion: string;
|
|
98
112
|
createdAt: number;
|
|
99
113
|
}
|
|
100
114
|
export interface RunEndLine {
|
|
101
|
-
schemaVersion:
|
|
115
|
+
schemaVersion: 3;
|
|
102
116
|
kind: "run_end";
|
|
103
117
|
runId: string;
|
|
104
118
|
ts: number;
|
|
105
119
|
lastRequestIndex: number;
|
|
106
120
|
}
|
|
107
121
|
export interface CrashLine {
|
|
108
|
-
schemaVersion:
|
|
122
|
+
schemaVersion: 3;
|
|
109
123
|
kind: "crash";
|
|
110
124
|
ts: number;
|
|
111
125
|
note: string;
|
|
@@ -122,7 +136,9 @@ export declare function hashSpecFor(version: number): HashSpec;
|
|
|
122
136
|
* canonical block and reads as defaults at every consumer. */
|
|
123
137
|
export declare const TRACE_RECORD_FIELDS_V1: 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"];
|
|
124
138
|
/** The 1.3.0 field set (schemaVersion 2) = the v1 set + `canonical`. */
|
|
125
|
-
export declare const
|
|
139
|
+
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
|
+
/** 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"];
|
|
126
142
|
export declare const TRACE_SEGMENT_FIELDS: readonly ["role", "seqRange", "estTokens", "freshness"];
|
|
127
143
|
export declare function validateTraceSegment(v: unknown): v is TraceSegment;
|
|
128
144
|
export declare function validateTraceRecord(v: unknown): v is TraceRecord;
|
package/dist/trace/record.js
CHANGED
|
@@ -19,18 +19,28 @@
|
|
|
19
19
|
* quartet stays as provider observation above it). The validators accept
|
|
20
20
|
* BOTH generations (R1d-1): a v1 sidecar has no canonical block and reads
|
|
21
21
|
* as defaults at every consumer — never a crash.
|
|
22
|
+
*
|
|
23
|
+
* E3 (0.2.1) — schemaVersion 3: the record gains the `rent` block — the
|
|
24
|
+
* static rent ledger, one line per surface (trace/rent.ts). The v3
|
|
25
|
+
* writers record it; v1/v2 sidecars keep reading as defaults (R1d-1,
|
|
26
|
+
* R2-1): no rent block = no rent lines = the zero-rent reading, never a
|
|
27
|
+
* crash.
|
|
22
28
|
*/
|
|
23
|
-
/** schemaVersion:
|
|
24
|
-
* shape,
|
|
25
|
-
* changes bump it (ADR-0051 §6
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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;
|
|
34
|
+
/** The versions a reader may meet in a ledger. v1 and v2 records are
|
|
35
|
+
* accepted (generation-compat) and read as defaults — no canonical
|
|
36
|
+
* block (v1), no rent block (v1, v2). */
|
|
37
|
+
export const TRACE_SCHEMA_VERSIONS = new Set([1, 2, TRACE_SCHEMA_VERSION]);
|
|
30
38
|
import { PRICING_TABLE_V1, priceFor, pricingTableFor, validateCanonicalUsage } from "../usage/canonical.js";
|
|
39
|
+
import { validateRentLine } from "./rent.js";
|
|
31
40
|
export const HASH_SPEC_BY_VERSION = {
|
|
32
41
|
1: { algorithm: "sha-256", output: "full-hex" },
|
|
33
42
|
2: { algorithm: "sha-256", output: "full-hex" }, // E2 — the algorithms do not change
|
|
43
|
+
3: { algorithm: "sha-256", output: "full-hex" }, // E3 — same algorithms, re-pinned (the E2 ritual)
|
|
34
44
|
};
|
|
35
45
|
export function hashSpecFor(version) {
|
|
36
46
|
const spec = HASH_SPEC_BY_VERSION[version];
|
|
@@ -72,7 +82,9 @@ export const TRACE_RECORD_FIELDS_V1 = [
|
|
|
72
82
|
"ts",
|
|
73
83
|
];
|
|
74
84
|
/** The 1.3.0 field set (schemaVersion 2) = the v1 set + `canonical`. */
|
|
75
|
-
export const
|
|
85
|
+
export const TRACE_RECORD_FIELDS_V2 = [...TRACE_RECORD_FIELDS_V1, "canonical"];
|
|
86
|
+
/** The 0.2.1 field set (schemaVersion 3) = the v2 set + `rent`. */
|
|
87
|
+
export const TRACE_RECORD_FIELDS = [...TRACE_RECORD_FIELDS_V2, "rent"];
|
|
76
88
|
export const TRACE_SEGMENT_FIELDS = ["role", "seqRange", "estTokens", "freshness"];
|
|
77
89
|
// ── Validators ────────────────────────────────────────────────────────────
|
|
78
90
|
// Strict by design: extra keys are rejected (the closed set), so a
|
|
@@ -119,12 +131,13 @@ export function validateTraceRecord(v) {
|
|
|
119
131
|
if (!isRecord(v))
|
|
120
132
|
return false;
|
|
121
133
|
const version = v.schemaVersion;
|
|
122
|
-
// generation-compat (R1d-1): a v1 sidecar has no canonical
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
134
|
+
// generation-compat (R1d-1, R2-1): a v1 sidecar has no canonical
|
|
135
|
+
// block, a v2 sidecar has no rent block — both read as defaults,
|
|
136
|
+
// accepted, never a crash; the current version is fully checked
|
|
137
|
+
// (shape + the canonical block + the rent ledger).
|
|
138
|
+
if (version !== 1 && version !== 2 && version !== TRACE_SCHEMA_VERSION)
|
|
126
139
|
return false;
|
|
127
|
-
const fields = version === 1 ? TRACE_RECORD_FIELDS_V1 : TRACE_RECORD_FIELDS;
|
|
140
|
+
const fields = version === 1 ? TRACE_RECORD_FIELDS_V1 : version === 2 ? TRACE_RECORD_FIELDS_V2 : TRACE_RECORD_FIELDS;
|
|
128
141
|
if (!hasClosedKeys(v, fields, ["lineageLink"]))
|
|
129
142
|
return false;
|
|
130
143
|
if (v.kind !== "request")
|
|
@@ -197,6 +210,13 @@ export function validateTraceRecord(v) {
|
|
|
197
210
|
return false;
|
|
198
211
|
}
|
|
199
212
|
}
|
|
213
|
+
if (version === TRACE_SCHEMA_VERSION) {
|
|
214
|
+
// the rent block: every line validates (closed fields, non-empty
|
|
215
|
+
// surface, non-negative integer chars, the estTokens == ceil(chars/4)
|
|
216
|
+
// cross-check — R6). v1/v2 sidecars have no block (R2-1).
|
|
217
|
+
if (!Array.isArray(v.rent) || !v.rent.every(validateRentLine))
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
200
220
|
if (!isNumber(v.ts))
|
|
201
221
|
return false;
|
|
202
222
|
return true;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E3 (0.2.1) — the rent ledger: every request's static model-side rent,
|
|
3
|
+
* one line per surface.
|
|
4
|
+
*
|
|
5
|
+
* The static rent is the surface the model pays REGARDLESS of the turn's
|
|
6
|
+
* content: the base system prompt, each extension's append, every tool's
|
|
7
|
+
* serialized spec, and the per-request envelope. One line per surface,
|
|
8
|
+
* four classes, measured in chars + the rounds' est-token convention
|
|
9
|
+
* (chars/4, ceil — the E1 script's formula, the work order's own words,
|
|
10
|
+
* R6):
|
|
11
|
+
*
|
|
12
|
+
* - system:base the session's own prompt as the CLI handed it
|
|
13
|
+
* (built-in constant + project instructions — the
|
|
14
|
+
* runtime cannot split those, R3). The runtime's
|
|
15
|
+
* generated tool table is machinery BETWEEN base and
|
|
16
|
+
* appends; it stays out of the ledger (counts live
|
|
17
|
+
* in tool:<name> lines and the base's own line).
|
|
18
|
+
* - system:ext:<name> one line per extension with a non-empty
|
|
19
|
+
* systemPrompt.append, measured on the append
|
|
20
|
+
* string, in load order.
|
|
21
|
+
* - tool:<name> one line per tool, measured on the exact
|
|
22
|
+
* serialized ToolSpec projection the adapters
|
|
23
|
+
* receive ({name, description, inputSchema} — the
|
|
24
|
+
* registry.toSpecs() shape, protocol/messages.ts).
|
|
25
|
+
* - envelope the per-request fixed overhead OUTSIDE the
|
|
26
|
+
* conversation payloads: the R5 skeleton
|
|
27
|
+
* JSON.stringify({model, messages: [], tools: []})
|
|
28
|
+
* — a function of the model string only, never of
|
|
29
|
+
* the payloads (the skeleton is the definition).
|
|
30
|
+
*
|
|
31
|
+
* R9: an absent surface is an absent line — an unconfigured mcp, a
|
|
32
|
+
* session with no project instructions: no line. The absence IS the
|
|
33
|
+
* ledger statement (the 0.1.45 diet-A precedent: not paid = no rent).
|
|
34
|
+
*
|
|
35
|
+
* Determinism (the reviewer-facing property): every line is derivable
|
|
36
|
+
* from components the reviewer can recompute — the exported prompt
|
|
37
|
+
* constant, the appends, the ToolSpec array, the request skeleton. The
|
|
38
|
+
* ledger stores COUNTS, never payloads (the seqRange thin-pointer
|
|
39
|
+
* discipline applied to rent).
|
|
40
|
+
*
|
|
41
|
+
* buildRentLedger is the SINGLE source: the R7 star gate drives a real
|
|
42
|
+
* session with the script's predicted composition and asserts the
|
|
43
|
+
* recorded ledger equals the prediction line for line.
|
|
44
|
+
*/
|
|
45
|
+
import type { ToolSpec } from "@vincemakes/kiso-core";
|
|
46
|
+
export interface RentLine {
|
|
47
|
+
/** "system:base" | "system:ext:<name>" | "tool:<name>" | "envelope" */
|
|
48
|
+
surface: string;
|
|
49
|
+
/** length of the serialized surface, measured, never a copy */
|
|
50
|
+
chars: number;
|
|
51
|
+
/** Math.ceil(chars / 4) — the E1 script convention, pinned (R6) */
|
|
52
|
+
estTokens: number;
|
|
53
|
+
}
|
|
54
|
+
/** The closed line set (the same gate discipline as the record fields). */
|
|
55
|
+
export declare const RENT_LINE_FIELDS: readonly ["surface", "chars", "estTokens"];
|
|
56
|
+
/** What the runtime knows about the static surface beyond what the
|
|
57
|
+
* adapter call itself carries: the base prompt as configured, and the
|
|
58
|
+
* per-extension appends in load order (the adapter's composed
|
|
59
|
+
* systemPrompt is the result — the parts are the ledger's inputs). */
|
|
60
|
+
export interface RentParts {
|
|
61
|
+
base?: string;
|
|
62
|
+
appends?: readonly {
|
|
63
|
+
name: string;
|
|
64
|
+
text: string;
|
|
65
|
+
}[];
|
|
66
|
+
}
|
|
67
|
+
export interface RentInput extends RentParts {
|
|
68
|
+
model: string;
|
|
69
|
+
tools?: readonly ToolSpec[];
|
|
70
|
+
}
|
|
71
|
+
/** The one ledger: system:base, then system:ext:* (load order), then
|
|
72
|
+
* tool:* (the array's order — the registry's toSpecs() order), then the
|
|
73
|
+
* envelope. Absent surfaces contribute no lines (R9). */
|
|
74
|
+
export declare function buildRentLedger(input: RentInput): RentLine[];
|
|
75
|
+
/** The line validator — the same strict discipline as the record's other
|
|
76
|
+
* closed sets: no key outside the spec, every spec'd key present,
|
|
77
|
+
* non-empty surface (R9: a line exists only for a surface that exists),
|
|
78
|
+
* non-negative integer chars, and the estTokens cross-check (R6). The
|
|
79
|
+
* schema pins the LINE, not the multiset — duplicate surfaces are the
|
|
80
|
+
* writer's business, never a schema crash. */
|
|
81
|
+
export declare function validateRentLine(v: unknown): v is RentLine;
|