@vincemakes/kiso-runtime 0.1.30 → 0.1.32

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 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 { 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).
package/dist/compose.js CHANGED
@@ -3,6 +3,38 @@
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 CC content in kiso's voice, each line bound to the tool that makes
9
+ * it true) filtered to the ACTIVE tool set + each active tool's ONE-line
10
+ * snippet + its guideline bullets. The full descriptions NEVER enter the
11
+ * system prompt — the provider transmits them in the JSON schema anyway
12
+ * (never pay twice). Deterministic: same registry → same table.
13
+ */
14
+ const TOOL_RULES = [
15
+ { tool: "read_file", line: "read files with read_file, never shell cat/head/tail" },
16
+ { tool: "search_text", line: "search with search_text, never shell grep/rg" },
17
+ { tool: "list_dir", line: "list with list_dir, never ls" },
18
+ { tool: "shell", line: "reserve shell for real system commands" },
19
+ ];
20
+ /** The table, or "" when the registry is empty (no vocabulary, no tools). */
21
+ export function composeToolTable(registry) {
22
+ const tools = registry.list();
23
+ if (tools.length === 0)
24
+ return "";
25
+ const active = new Set(tools.map((t) => t.name));
26
+ const lines = [
27
+ "Tool use:",
28
+ ...TOOL_RULES.filter((r) => active.has(r.tool)).map((r) => `- ${r.line}`),
29
+ // the parallel directive: the window applies to every active turn.
30
+ "- batch independent tool calls into one reply — they run in parallel",
31
+ ...tools.flatMap((t) => (t.promptSnippet === undefined ? [] : [`- ${t.promptSnippet}`])),
32
+ ];
33
+ const guidelines = tools.flatMap((t) => (t.promptGuidelines ?? []).map((g) => `- ${t.name}: ${g}`));
34
+ if (guidelines.length > 0)
35
+ lines.push("Active tool guidelines:", ...guidelines);
36
+ return lines.join("\n");
37
+ }
6
38
  /**
7
39
  * E2: the session's systemPrompt plus every extension's append, in LOAD
8
40
  * order, \n\n-joined — deterministic (same extension list → same prompt).
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 { 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,23 @@ 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(this.#config.systemPrompt, this.#config.extensions ?? []);
72
+ const systemPrompt = composeSystemPrompt(basePrompt, this.#config.extensions ?? []);
63
73
  const loopConfig = () => ({
64
- adapter: this.#adapter,
74
+ // 0.1.40 (R-C item 3): the truncation guard gates the model
75
+ // stream — a truncated turn's tool batch never executes.
76
+ adapter: truncationGuard(this.#adapter),
65
77
  model: this.#config.model,
66
78
  sessionId: this.#session.id, // P3: tools see their session (ToolContext.sessionId)
67
79
  ...(systemPrompt !== undefined ? { systemPrompt } : {}),
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).
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
@@ -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
- * ⑥ (todo round): a tool result tagged do-not-compact is DURABLE work
55
- * memory (the todo_set echo) — the summary must never cover its round,
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
- * ⑥ (todo round): a tool result tagged do-not-compact is DURABLE work
89
- * memory (the todo_set echo) — the summary must never cover its round,
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,27 @@
1
+ /**
2
+ * 0.1.40 (R-C item 3) — the truncation guard: a runtime adapter wrapper.
3
+ *
4
+ * The pi protection: a truncated stream (stopReason max_tokens/length) can
5
+ * yield tool args that parse and validate but are silently incomplete —
6
+ * executing them is the destructive-bug class. The provider adapters already
7
+ * see the stop reason; the RUNTIME vetoes execution of the whole batch:
8
+ *
9
+ * - tool_call_end events are held per turn (the deltas pass through live —
10
+ * the UI still shows the calls building, only the COMPLETION is gated);
11
+ * - a compatible stop flushes the held calls in order, then the stop — the
12
+ * kernel launches them exactly as before (the parallel window survives;
13
+ * only the 0.1.26 mid-stream launch timing is gone — the cost of the
14
+ * guarantee: the turn's truncated intent is never half-executed);
15
+ * - a truncation stop flushes the held calls with input: null — the
16
+ * kernel's EXISTING invalid-input denial fails the whole batch without
17
+ * executing anything (the same honest null the adapters already emit
18
+ * for unparseable partials — zero new protocol surface), and the turn
19
+ * still ends with the max_tokens terminal (the loop's voided settle).
20
+ *
21
+ * The kernel machinery (the streaming launch, the window, the voided
22
+ * settle) is untouched — the gate lives at the adapter boundary, where the
23
+ * stop reason is already known.
24
+ */
25
+ import type { Adapter } from "@vincemakes/kiso-core";
26
+ /** Wrap the adapter so a truncated turn's tool batch can never execute. */
27
+ export declare function truncationGuard(adapter: Adapter): Adapter;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * 0.1.40 (R-C item 3) — the truncation guard: a runtime adapter wrapper.
3
+ *
4
+ * The pi protection: a truncated stream (stopReason max_tokens/length) can
5
+ * yield tool args that parse and validate but are silently incomplete —
6
+ * executing them is the destructive-bug class. The provider adapters already
7
+ * see the stop reason; the RUNTIME vetoes execution of the whole batch:
8
+ *
9
+ * - tool_call_end events are held per turn (the deltas pass through live —
10
+ * the UI still shows the calls building, only the COMPLETION is gated);
11
+ * - a compatible stop flushes the held calls in order, then the stop — the
12
+ * kernel launches them exactly as before (the parallel window survives;
13
+ * only the 0.1.26 mid-stream launch timing is gone — the cost of the
14
+ * guarantee: the turn's truncated intent is never half-executed);
15
+ * - a truncation stop flushes the held calls with input: null — the
16
+ * kernel's EXISTING invalid-input denial fails the whole batch without
17
+ * executing anything (the same honest null the adapters already emit
18
+ * for unparseable partials — zero new protocol surface), and the turn
19
+ * still ends with the max_tokens terminal (the loop's voided settle).
20
+ *
21
+ * The kernel machinery (the streaming launch, the window, the voided
22
+ * settle) is untouched — the gate lives at the adapter boundary, where the
23
+ * stop reason is already known.
24
+ */
25
+ /** Wrap the adapter so a truncated turn's tool batch can never execute. */
26
+ export function truncationGuard(adapter) {
27
+ return {
28
+ stream(options) {
29
+ return guardStream(adapter.stream(options));
30
+ },
31
+ };
32
+ }
33
+ async function* guardStream(stream) {
34
+ const held = [];
35
+ for await (const ev of stream) {
36
+ if (ev.type === "tool_call_end") {
37
+ held.push(ev);
38
+ continue; // held — emitted at the stop, complete or nulled
39
+ }
40
+ if (ev.type === "stop") {
41
+ const truncated = ev.reason === "max_tokens";
42
+ for (const call of held) {
43
+ // truncation: null input — the kernel's denial path fails the
44
+ // call without executing it; otherwise: untouched, in order.
45
+ yield truncated ? { ...call, input: null } : call;
46
+ }
47
+ held.length = 0;
48
+ }
49
+ yield ev;
50
+ }
51
+ // A stream that ends WITHOUT a stop drops the held ends — the kernel
52
+ // already voids the malformed turn (invalid_request); the dangling
53
+ // deltas are the pre-existing malformed-stream shape.
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-runtime",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
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.29"
24
+ "@vincemakes/kiso-core": "0.1.31"
25
25
  },
26
26
  "peerDependencies": {
27
- "@vincemakes/kiso-provider-anthropic": "0.1.29",
28
- "@vincemakes/kiso-provider-openai": "0.1.29"
27
+ "@vincemakes/kiso-provider-anthropic": "0.1.32",
28
+ "@vincemakes/kiso-provider-openai": "0.1.32"
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.29",
39
+ "@vincemakes/kiso-evals": "0.1.32",
40
40
  "@types/node": "^26.1.2",
41
41
  "typescript": "^5.7.2",
42
42
  "vitest": "^3.0.0"