@vincemakes/kiso-runtime 0.1.31 → 0.1.33
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.d.ts +23 -1
- package/dist/compose.js +114 -0
- package/dist/run.js +19 -4
- package/dist/session.d.ts +15 -1
- package/dist/session.js +31 -3
- package/dist/summarize.d.ts +2 -2
- package/dist/summarize.js +2 -2
- package/dist/truncation-guard.d.ts +28 -0
- package/dist/truncation-guard.js +55 -0
- package/package.json +5 -5
package/dist/compose.d.ts
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
* from session.ts: the extension system-prompt appends, the extension
|
|
4
4
|
* hook composition (the existing come first), and the loop's microcompact config lookup.
|
|
5
5
|
*/
|
|
6
|
-
import type { HookHost, KisoExtension } from "@vincemakes/kiso-core";
|
|
6
|
+
import type { ApprovalChain, HookHost, KisoExtension, ToolRegistry } from "@vincemakes/kiso-core";
|
|
7
7
|
import type { SessionConfig } from "./session.js";
|
|
8
|
+
/** The table, or "" when the registry is empty (no vocabulary, no tools). */
|
|
9
|
+
export declare function composeToolTable(registry: ToolRegistry): string;
|
|
8
10
|
/**
|
|
9
11
|
* E2: the session's systemPrompt plus every extension's append, in LOAD
|
|
10
12
|
* order, \n\n-joined — deterministic (same extension list → same prompt).
|
|
@@ -30,3 +32,23 @@ export declare function microcompactFor(config: SessionConfig): {
|
|
|
30
32
|
readonly thresholdTokens: number;
|
|
31
33
|
readonly keepResults?: number;
|
|
32
34
|
} | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* E1 (W21/R3 — moved out of the kernel by the 2026-08-09 corrective
|
|
37
|
+
* action; the composition is extension wiring, the same category as
|
|
38
|
+
* composeHooks): compose the extensions' approval policies into ONE
|
|
39
|
+
* chain — the kernel's gate. deny > allow > ask: any deny wins (the
|
|
40
|
+
* FIRST denial's reason), then ANY allow (a LATER allow beats an
|
|
41
|
+
* EARLIER ask: the allow-only dont-ask-again extension must override a
|
|
42
|
+
* mode tier's ask — the old ask-wins chain left it structurally dead),
|
|
43
|
+
* and an ask falls into the kernel's human flow (its speaker = the
|
|
44
|
+
* first non-abstain — the panel's why-asked line). Only an ask-free
|
|
45
|
+
* chain auto-approves: decidedBy = the deciding extension (the FIRST
|
|
46
|
+
* allow — the symmetry of the first denial; an every-speaker-allows
|
|
47
|
+
* chain records the first speaker). An all-abstain chain (ADR-0042)
|
|
48
|
+
* ASKS — no opinion is never a silent allow; absent a channel the
|
|
49
|
+
* kernel's honest denial. A policy that throws counts as ask — it
|
|
50
|
+
* speaks, never silently. No policies → undefined (no chain: the
|
|
51
|
+
* kernel's plain flow — the all-abstain ask exists only where a chain
|
|
52
|
+
* does).
|
|
53
|
+
*/
|
|
54
|
+
export declare function composeApprovalChain(extensions: readonly KisoExtension[]): ApprovalChain | undefined;
|
package/dist/compose.js
CHANGED
|
@@ -3,6 +3,42 @@
|
|
|
3
3
|
* from session.ts: the extension system-prompt appends, the extension
|
|
4
4
|
* hook composition (the existing come first), and the loop's microcompact config lookup.
|
|
5
5
|
*/
|
|
6
|
+
/**
|
|
7
|
+
* 0.1.40 (R-C item 1) — the tool substitution table: the fixed vocabulary
|
|
8
|
+
* (the reference implementation's content in kiso's voice, each line bound
|
|
9
|
+
* to the tool that makes
|
|
10
|
+
* it true) filtered to the ACTIVE tool set + each active tool's ONE-line
|
|
11
|
+
* snippet + its guideline bullets. The full descriptions NEVER enter the
|
|
12
|
+
* system prompt — the provider transmits them in the JSON schema anyway
|
|
13
|
+
* (never pay twice). Deterministic: same registry → same table.
|
|
14
|
+
*/
|
|
15
|
+
const TOOL_RULES = [
|
|
16
|
+
{ tool: "read_file", line: "read files with read_file, never shell cat/head/tail" },
|
|
17
|
+
{ tool: "search_text", line: "search with search_text, never shell grep/rg" },
|
|
18
|
+
{ tool: "list_dir", line: "list with list_dir, never ls" },
|
|
19
|
+
{ tool: "shell", line: "reserve shell for real system commands" },
|
|
20
|
+
];
|
|
21
|
+
/** The table, or "" when the registry is empty (no vocabulary, no tools). */
|
|
22
|
+
export function composeToolTable(registry) {
|
|
23
|
+
const tools = registry.list();
|
|
24
|
+
if (tools.length === 0)
|
|
25
|
+
return "";
|
|
26
|
+
const active = new Set(tools.map((t) => t.name));
|
|
27
|
+
const lines = [
|
|
28
|
+
"Tool use:",
|
|
29
|
+
...TOOL_RULES.filter((r) => active.has(r.tool)).map((r) => `- ${r.line}`),
|
|
30
|
+
// the parallel directive: the window applies to every active turn.
|
|
31
|
+
"- batch independent tool calls into one reply — they run in parallel",
|
|
32
|
+
// D1: a tool result is evidence, not an answer — the turn ends with
|
|
33
|
+
// the findings that make the evidence useful to the human.
|
|
34
|
+
"- end your turn with your findings — never end on a bare tool result",
|
|
35
|
+
...tools.flatMap((t) => (t.promptSnippet === undefined ? [] : [`- ${t.promptSnippet}`])),
|
|
36
|
+
];
|
|
37
|
+
const guidelines = tools.flatMap((t) => (t.promptGuidelines ?? []).map((g) => `- ${t.name}: ${g}`));
|
|
38
|
+
if (guidelines.length > 0)
|
|
39
|
+
lines.push("Active tool guidelines:", ...guidelines);
|
|
40
|
+
return lines.join("\n");
|
|
41
|
+
}
|
|
6
42
|
/**
|
|
7
43
|
* E2: the session's systemPrompt plus every extension's append, in LOAD
|
|
8
44
|
* order, \n\n-joined — deterministic (same extension list → same prompt).
|
|
@@ -115,3 +151,81 @@ export function microcompactFor(config) {
|
|
|
115
151
|
}
|
|
116
152
|
return undefined;
|
|
117
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* E1 (W21/R3 — moved out of the kernel by the 2026-08-09 corrective
|
|
156
|
+
* action; the composition is extension wiring, the same category as
|
|
157
|
+
* composeHooks): compose the extensions' approval policies into ONE
|
|
158
|
+
* chain — the kernel's gate. deny > allow > ask: any deny wins (the
|
|
159
|
+
* FIRST denial's reason), then ANY allow (a LATER allow beats an
|
|
160
|
+
* EARLIER ask: the allow-only dont-ask-again extension must override a
|
|
161
|
+
* mode tier's ask — the old ask-wins chain left it structurally dead),
|
|
162
|
+
* and an ask falls into the kernel's human flow (its speaker = the
|
|
163
|
+
* first non-abstain — the panel's why-asked line). Only an ask-free
|
|
164
|
+
* chain auto-approves: decidedBy = the deciding extension (the FIRST
|
|
165
|
+
* allow — the symmetry of the first denial; an every-speaker-allows
|
|
166
|
+
* chain records the first speaker). An all-abstain chain (ADR-0042)
|
|
167
|
+
* ASKS — no opinion is never a silent allow; absent a channel the
|
|
168
|
+
* kernel's honest denial. A policy that throws counts as ask — it
|
|
169
|
+
* speaks, never silently. No policies → undefined (no chain: the
|
|
170
|
+
* kernel's plain flow — the all-abstain ask exists only where a chain
|
|
171
|
+
* does).
|
|
172
|
+
*/
|
|
173
|
+
export function composeApprovalChain(extensions) {
|
|
174
|
+
const policies = extensions.flatMap((e) => (e.approvals ?? []).map((policy) => ({ extension: e.name, policy })));
|
|
175
|
+
if (policies.length === 0)
|
|
176
|
+
return undefined;
|
|
177
|
+
return {
|
|
178
|
+
async decide(payload, ctx) {
|
|
179
|
+
let chainVerdict;
|
|
180
|
+
let deniedReason;
|
|
181
|
+
let deniedBy;
|
|
182
|
+
let allowedBy; // the FIRST allowing extension (the composition's decider for an allow)
|
|
183
|
+
let firstSpeaker; // the first non-abstain verdict's extension
|
|
184
|
+
let anySpoke = false;
|
|
185
|
+
for (const { extension, policy } of policies) {
|
|
186
|
+
let v;
|
|
187
|
+
try {
|
|
188
|
+
v = await Promise.resolve(policy.decide(payload, ctx));
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
v = { action: "ask" }; // a throwing policy counts as ask — it speaks, never silently
|
|
192
|
+
}
|
|
193
|
+
if (v.action === "abstain")
|
|
194
|
+
continue; // no opinion — not a verdict
|
|
195
|
+
anySpoke = true;
|
|
196
|
+
firstSpeaker ??= extension;
|
|
197
|
+
if (v.action === "deny") {
|
|
198
|
+
deniedBy ??= extension;
|
|
199
|
+
deniedReason ??= v.reason; // the FIRST denial's reason
|
|
200
|
+
}
|
|
201
|
+
else if (v.action === "allow") {
|
|
202
|
+
// deny > allow > ask — the allow overrides any EARLIER
|
|
203
|
+
// ask in the chain (the allow-only dont-ask-again
|
|
204
|
+
// extension after a mode tier that asked). The first
|
|
205
|
+
// allow is the deciding one — the symmetry of deniedBy.
|
|
206
|
+
allowedBy ??= extension;
|
|
207
|
+
chainVerdict = { action: "allow" };
|
|
208
|
+
}
|
|
209
|
+
else if (chainVerdict === undefined) {
|
|
210
|
+
chainVerdict = { action: "ask" }; // recorded — a later allow overrides it
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (deniedBy !== undefined) {
|
|
214
|
+
return { action: "deny", reason: deniedReason ?? "denied", decidedBy: deniedBy };
|
|
215
|
+
}
|
|
216
|
+
if (allowedBy !== undefined) {
|
|
217
|
+
return { action: "allow", decidedBy: allowedBy }; // an allow beat any earlier ask
|
|
218
|
+
}
|
|
219
|
+
if (chainVerdict === undefined && anySpoke) {
|
|
220
|
+
return { action: "allow", decidedBy: firstSpeaker }; // every speaker allows
|
|
221
|
+
}
|
|
222
|
+
if (chainVerdict === undefined) {
|
|
223
|
+
// an all-abstain (ADR-0042): NO policy speaks — the call falls
|
|
224
|
+
// to the ask flow, never to a silent auto-approve. The human
|
|
225
|
+
// decides; absent a channel, the kernel's honest denial.
|
|
226
|
+
return { action: "ask" };
|
|
227
|
+
}
|
|
228
|
+
return { action: "ask", ...(firstSpeaker !== undefined ? { speaker: firstSpeaker } : {}) };
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
package/dist/run.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { denialResult, loop } from "@vincemakes/kiso-core";
|
|
7
7
|
import { ABORTED, MergedSignal, abortable, openRunId } from "./recovery.js";
|
|
8
|
-
import { composeSystemPrompt, microcompactFor } from "./compose.js";
|
|
8
|
+
import { composeApprovalChain, composeSystemPrompt, composeToolTable, microcompactFor } from "./compose.js";
|
|
9
|
+
import { truncationGuard } from "./truncation-guard.js";
|
|
9
10
|
import { ResumeBlockedError } from "./session.js";
|
|
10
11
|
/**
|
|
11
12
|
* A single turn. Async-iterable, so `for await (const ev of session.run(x))`
|
|
@@ -56,12 +57,24 @@ export class Run {
|
|
|
56
57
|
// E2: the session's own microcompact wins; otherwise the FIRST
|
|
57
58
|
// extension providing a compaction config supplies it.
|
|
58
59
|
const microcompact = microcompactFor(this.#config);
|
|
60
|
+
// 0.1.40 (R-C item 1): the tool substitution table — the ACTIVE tool
|
|
61
|
+
// set's vocabulary, snippets, and guidelines — sits BETWEEN the
|
|
62
|
+
// session's base prompt and the extension appends: generated
|
|
63
|
+
// machinery never outranks the deliberate extension text (the E2
|
|
64
|
+
// "append lands at the END" contract holds). "" when empty.
|
|
65
|
+
const toolTable = composeToolTable(this.#config.registry);
|
|
66
|
+
const basePrompt = toolTable === "" ? this.#config.systemPrompt
|
|
67
|
+
: this.#config.systemPrompt === undefined ? toolTable
|
|
68
|
+
: `${this.#config.systemPrompt}\n\n${toolTable}`;
|
|
59
69
|
// E2: the session's own systemPrompt first, then every extension
|
|
60
70
|
// append in LOAD order — deterministic (same extensions → same
|
|
61
71
|
// prompt); no appends → byte-identical to the extension-less run.
|
|
62
|
-
const systemPrompt = composeSystemPrompt(
|
|
72
|
+
const systemPrompt = composeSystemPrompt(basePrompt, this.#config.extensions ?? []);
|
|
73
|
+
const approvalChain = composeApprovalChain(this.#config.extensions ?? []);
|
|
63
74
|
const loopConfig = () => ({
|
|
64
|
-
|
|
75
|
+
// 0.1.40 (R-C item 3): the truncation guard gates the model
|
|
76
|
+
// stream — a truncated turn's tool batch never executes.
|
|
77
|
+
adapter: truncationGuard(this.#adapter),
|
|
65
78
|
model: this.#config.model,
|
|
66
79
|
sessionId: this.#session.id, // P3: tools see their session (ToolContext.sessionId)
|
|
67
80
|
...(systemPrompt !== undefined ? { systemPrompt } : {}),
|
|
@@ -73,7 +86,9 @@ export class Run {
|
|
|
73
86
|
...(this.#config.compaction !== undefined ? { compaction: this.#config.compaction } : {}),
|
|
74
87
|
...(microcompact !== undefined ? { microcompact } : {}),
|
|
75
88
|
...(this.#config.maxRetries !== undefined ? { maxRetries: this.#config.maxRetries } : {}),
|
|
76
|
-
|
|
89
|
+
// E1: the composed approval chain — the extensions'
|
|
90
|
+
// policies composed into ONE gate (deny > allow > ask).
|
|
91
|
+
...(approvalChain !== undefined ? { approvalPolicy: approvalChain } : {}),
|
|
77
92
|
log,
|
|
78
93
|
signal,
|
|
79
94
|
resolveApproval: (decisionId) => new Promise((resolve) => {
|
package/dist/session.d.ts
CHANGED
|
@@ -61,6 +61,16 @@ export interface SummarizeResult {
|
|
|
61
61
|
/** The estimated tokens the compression saved (chars/4 proxy). */
|
|
62
62
|
readonly savedTokens: number;
|
|
63
63
|
}
|
|
64
|
+
/** W18: the knowable pre-call data, surfaced through onStart — everything
|
|
65
|
+
* the indicator's indeterminate row shows (rounds, the token estimate)
|
|
66
|
+
* is computed locally BEFORE the one adapter call; no fraction exists. */
|
|
67
|
+
export interface CompactInfo {
|
|
68
|
+
readonly coversToSeq: number;
|
|
69
|
+
/** The covered user rounds — the inputs in (previous summary point, boundary]. */
|
|
70
|
+
readonly rounds: number;
|
|
71
|
+
/** The covered content's estimated tokens (the chars/4 proxy). */
|
|
72
|
+
readonly tokens: number;
|
|
73
|
+
}
|
|
64
74
|
export declare class AgentSession {
|
|
65
75
|
#private;
|
|
66
76
|
readonly id: string;
|
|
@@ -112,6 +122,7 @@ export declare class AgentSession {
|
|
|
112
122
|
summarize(options?: {
|
|
113
123
|
keepRounds?: number;
|
|
114
124
|
signal?: AbortSignalLike;
|
|
125
|
+
onStart?: (info: CompactInfo) => void;
|
|
115
126
|
}): Promise<SummarizeResult | null>;
|
|
116
127
|
/**
|
|
117
128
|
* Pauses that still await a human decision (durable, survives restart).
|
|
@@ -128,8 +139,11 @@ export declare class AgentSession {
|
|
|
128
139
|
* next resume applies it without re-asking. The crash window between a
|
|
129
140
|
* resolve and the run's write is benign: nothing has executed yet, so a
|
|
130
141
|
* lost decision only re-presents the request.
|
|
142
|
+
* W21: an optional reason rides a DENIAL (the panel's feedback — the
|
|
143
|
+
* tool_result carries `[Permission denied] <the words>`); allow reasons
|
|
144
|
+
* are never persisted (the words ride the next user turn instead).
|
|
131
145
|
*/
|
|
132
|
-
approve(decisionId: string, allow: boolean): Promise<void>;
|
|
146
|
+
approve(decisionId: string, allow: boolean, reason?: string): Promise<void>;
|
|
133
147
|
/** Executions that started but never reported a result (crash window). */
|
|
134
148
|
uncertainExecutions(): import("@vincemakes/kiso-core").ExecutionRecord[];
|
|
135
149
|
/**
|
package/dist/session.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import { EventLog, executionLedger, projectMessages, } from "@vincemakes/kiso-core";
|
|
31
31
|
import { denialResult } from "@vincemakes/kiso-core";
|
|
32
32
|
import { estimateSummarySavings, KEEP_RECENT_ROUNDS, lastSummaryPoint, summarizeConversation, summaryBoundarySeq, } from "./summarize.js";
|
|
33
|
+
import { estimateTokens } from "@vincemakes/kiso-core";
|
|
33
34
|
import { StaleWriterError } from "./store.js";
|
|
34
35
|
import { composeHooks } from "./compose.js";
|
|
35
36
|
import { Run } from "./run.js";
|
|
@@ -181,18 +182,39 @@ export class AgentSession {
|
|
|
181
182
|
async summarize(options = {}) {
|
|
182
183
|
this.ensureHealthy();
|
|
183
184
|
const keepRounds = options.keepRounds ?? KEEP_RECENT_ROUNDS;
|
|
185
|
+
// W18: the signal is observed at EVERY phase boundary — the cancel
|
|
186
|
+
// affordance works for the whole call (local work included), never
|
|
187
|
+
// just the adapter's wait. The abort error is the honest "nothing
|
|
188
|
+
// happened" outcome (ADR-0044 crash semantics).
|
|
189
|
+
const cancelled = () => new Error("the compaction was cancelled");
|
|
190
|
+
if (options.signal !== undefined && options.signal.aborted)
|
|
191
|
+
throw cancelled();
|
|
184
192
|
const events = this.log.all;
|
|
185
193
|
const boundary = summaryBoundarySeq(events, keepRounds);
|
|
186
194
|
if (boundary === undefined)
|
|
187
195
|
return null;
|
|
188
196
|
const prevPoint = lastSummaryPoint(events);
|
|
189
197
|
const covered = projectMessages(events.filter((e) => e.seq > prevPoint && e.seq <= boundary && e.type !== "summarized"));
|
|
198
|
+
// W18: the indicator's pre-call data — rounds + the token estimate
|
|
199
|
+
// are knowable BEFORE the adapter call; the summary itself is ONE
|
|
200
|
+
// call with no fraction (kiso never invents a percentage here).
|
|
201
|
+
if (options.signal !== undefined && options.signal.aborted)
|
|
202
|
+
throw cancelled();
|
|
203
|
+
options.onStart?.({
|
|
204
|
+
coversToSeq: boundary,
|
|
205
|
+
rounds: events.filter((e) => e.type === "user_input" && e.seq > prevPoint && e.seq <= boundary).length,
|
|
206
|
+
tokens: estimateTokens(covered),
|
|
207
|
+
});
|
|
190
208
|
const summary = await summarizeConversation({
|
|
191
209
|
adapter: this.#adapter,
|
|
192
210
|
model: this.#config.model,
|
|
193
211
|
messages: covered,
|
|
194
212
|
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
195
213
|
});
|
|
214
|
+
// The post-call boundary check: an abort that landed while the
|
|
215
|
+
// adapter returned must NOT persist — "nothing happened".
|
|
216
|
+
if (options.signal !== undefined && options.signal.aborted)
|
|
217
|
+
throw cancelled();
|
|
196
218
|
const full = this.log.append({ type: "summarized", coversToSeq: boundary, summary });
|
|
197
219
|
// The record rides the LAST recorded run's id — a summarized fact
|
|
198
220
|
// must never open a run of its own: the open-run gate keys on
|
|
@@ -248,8 +270,11 @@ export class AgentSession {
|
|
|
248
270
|
* next resume applies it without re-asking. The crash window between a
|
|
249
271
|
* resolve and the run's write is benign: nothing has executed yet, so a
|
|
250
272
|
* lost decision only re-presents the request.
|
|
273
|
+
* W21: an optional reason rides a DENIAL (the panel's feedback — the
|
|
274
|
+
* tool_result carries `[Permission denied] <the words>`); allow reasons
|
|
275
|
+
* are never persisted (the words ride the next user turn instead).
|
|
251
276
|
*/
|
|
252
|
-
async approve(decisionId, allow) {
|
|
277
|
+
async approve(decisionId, allow, reason) {
|
|
253
278
|
// round 4: a poisoned session may not mutate the log — checked before
|
|
254
279
|
// anything is recorded.
|
|
255
280
|
this.ensureHealthy();
|
|
@@ -284,7 +309,10 @@ export class AgentSession {
|
|
|
284
309
|
// cannot issue while awaiting approve().)
|
|
285
310
|
this.#pendingDurableApprovals.set(decisionId, allow);
|
|
286
311
|
this.#pendingResolvers.delete(decisionId);
|
|
287
|
-
|
|
312
|
+
// W21: the panel's feedback rides the denial — the tool_result
|
|
313
|
+
// carries `[Permission denied] <the words>` (the rejection
|
|
314
|
+
// asymmetry: words keep the run alive).
|
|
315
|
+
resolver(allow ? { action: "allow" } : { action: "deny", reason: reason ?? "denied by user" });
|
|
288
316
|
return;
|
|
289
317
|
}
|
|
290
318
|
const runId = request?.runId ?? "approval";
|
|
@@ -293,7 +321,7 @@ export class AgentSession {
|
|
|
293
321
|
decisionId,
|
|
294
322
|
...(request !== undefined ? { callId: request.event.callId } : {}),
|
|
295
323
|
decision: allow ? "approved" : "denied",
|
|
296
|
-
...(allow ? {} : { reason: "denied by user" }),
|
|
324
|
+
...(allow ? {} : { reason: reason ?? "denied by user" }),
|
|
297
325
|
});
|
|
298
326
|
await this.persist(runId, decided);
|
|
299
327
|
}
|
package/dist/summarize.d.ts
CHANGED
|
@@ -51,8 +51,8 @@ export declare function lastSummaryPoint(events: readonly Event[]): number;
|
|
|
51
51
|
* a message. Returns undefined when fewer than keepRounds+1 uncovered
|
|
52
52
|
* rounds exist (nothing worth covering yet).
|
|
53
53
|
*
|
|
54
|
-
* ⑥ (
|
|
55
|
-
* memory (the
|
|
54
|
+
* ⑥ (task round): a tool result tagged do-not-compact is DURABLE work
|
|
55
|
+
* memory (the task_set echo) — the summary must never cover its round,
|
|
56
56
|
* or the model loses the current list. When the base boundary would
|
|
57
57
|
* cover such a result, the boundary pulls back to just before the round
|
|
58
58
|
* containing the LATEST one (still a turn boundary). A protected round
|
package/dist/summarize.js
CHANGED
|
@@ -85,8 +85,8 @@ export function lastSummaryPoint(events) {
|
|
|
85
85
|
* a message. Returns undefined when fewer than keepRounds+1 uncovered
|
|
86
86
|
* rounds exist (nothing worth covering yet).
|
|
87
87
|
*
|
|
88
|
-
* ⑥ (
|
|
89
|
-
* memory (the
|
|
88
|
+
* ⑥ (task round): a tool result tagged do-not-compact is DURABLE work
|
|
89
|
+
* memory (the task_set echo) — the summary must never cover its round,
|
|
90
90
|
* or the model loses the current list. When the base boundary would
|
|
91
91
|
* cover such a result, the boundary pulls back to just before the round
|
|
92
92
|
* containing the LATEST one (still a turn boundary). A protected round
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.1.40 (R-C item 3) — the truncation guard: a runtime adapter wrapper.
|
|
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:
|
|
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).
|
|
21
|
+
*
|
|
22
|
+
* The kernel machinery (the streaming launch, the window, the voided
|
|
23
|
+
* settle) is untouched — the gate lives at the adapter boundary, where the
|
|
24
|
+
* stop reason is already known.
|
|
25
|
+
*/
|
|
26
|
+
import type { Adapter } from "@vincemakes/kiso-core";
|
|
27
|
+
/** Wrap the adapter so a truncated turn's tool batch can never execute. */
|
|
28
|
+
export declare function truncationGuard(adapter: Adapter): Adapter;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.1.40 (R-C item 3) — the truncation guard: a runtime adapter wrapper.
|
|
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:
|
|
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).
|
|
21
|
+
*
|
|
22
|
+
* The kernel machinery (the streaming launch, the window, the voided
|
|
23
|
+
* settle) is untouched — the gate lives at the adapter boundary, where the
|
|
24
|
+
* stop reason is already known.
|
|
25
|
+
*/
|
|
26
|
+
/** Wrap the adapter so a truncated turn's tool batch can never execute. */
|
|
27
|
+
export function truncationGuard(adapter) {
|
|
28
|
+
return {
|
|
29
|
+
stream(options) {
|
|
30
|
+
return guardStream(adapter.stream(options));
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function* guardStream(stream) {
|
|
35
|
+
const held = [];
|
|
36
|
+
for await (const ev of stream) {
|
|
37
|
+
if (ev.type === "tool_call_end") {
|
|
38
|
+
held.push(ev);
|
|
39
|
+
continue; // held — emitted at the stop, complete or nulled
|
|
40
|
+
}
|
|
41
|
+
if (ev.type === "stop") {
|
|
42
|
+
const truncated = ev.reason === "max_tokens";
|
|
43
|
+
for (const call of held) {
|
|
44
|
+
// truncation: null input — the kernel's denial path fails the
|
|
45
|
+
// call without executing it; otherwise: untouched, in order.
|
|
46
|
+
yield truncated ? { ...call, input: null } : call;
|
|
47
|
+
}
|
|
48
|
+
held.length = 0;
|
|
49
|
+
}
|
|
50
|
+
yield ev;
|
|
51
|
+
}
|
|
52
|
+
// A stream that ends WITHOUT a stop drops the held ends — the kernel
|
|
53
|
+
// already voids the malformed turn (invalid_request); the dangling
|
|
54
|
+
// deltas are the pre-existing malformed-stream shape.
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
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",
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
"test": "vitest run"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@vincemakes/kiso-core": "0.1.
|
|
24
|
+
"@vincemakes/kiso-core": "0.1.32"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
|
-
"@vincemakes/kiso-provider-anthropic": "0.1.
|
|
28
|
-
"@vincemakes/kiso-provider-openai": "0.1.
|
|
27
|
+
"@vincemakes/kiso-provider-anthropic": "0.1.33",
|
|
28
|
+
"@vincemakes/kiso-provider-openai": "0.1.33"
|
|
29
29
|
},
|
|
30
30
|
"peerDependenciesMeta": {
|
|
31
31
|
"@vincemakes/kiso-provider-anthropic": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@vincemakes/kiso-evals": "0.1.
|
|
39
|
+
"@vincemakes/kiso-evals": "0.1.33",
|
|
40
40
|
"@types/node": "^26.1.2",
|
|
41
41
|
"typescript": "^5.7.2",
|
|
42
42
|
"vitest": "^3.0.0"
|