@vincemakes/kiso-runtime 0.44.0 → 0.45.1

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.
@@ -15,7 +15,7 @@
15
15
  * differently), or one of the STALE_AFTER events after it, each of which
16
16
  * changes what the next request carries.
17
17
  */
18
- import { type Event, type Usage } from "@vincemakes/kiso-core";
18
+ import type { Event, Usage } from "@vincemakes/kiso-core";
19
19
  /**
20
20
  * `usageTotal` turns one usage event into the tokens that request put in
21
21
  * the context: its canonical prompt (fresh + cache read + cache write — the
@@ -1,21 +1,4 @@
1
- /**
2
- * 0.40.0 (the owner's session) — what the context holds, anchored on the
3
- * last BILLED request.
4
- *
5
- * The estimate alone read a Chinese-heavy context as ~470k while the
6
- * provider billed 732,448 input tokens on a 1M window: the ctx row said
7
- * "~53% left" and the microcompact trigger (window/2) never fired. The
8
- * provider's own count of the last request is the truth; only what the log
9
- * appended after it is estimated.
10
- *
11
- * The anchor is refused — the caller falls back to the estimate — when no
12
- * bill describes the context any more: no usage yet, a usage the provider
13
- * did not report (`known: false`), a usage at or before `floorSeq` (the
14
- * session switched models since, and a different tokenizer counts
15
- * differently), or one of the STALE_AFTER events after it, each of which
16
- * changes what the next request carries.
17
- */
18
- import { estimateTokens } from "@vincemakes/kiso-core";
1
+ import { estimateTokens } from "./estimate-tokens.js";
19
2
  const STALE_AFTER = new Set([
20
3
  "compacted",
21
4
  "microcompacted",
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The token estimator: runtime context-accounting policy.
3
+ *
4
+ * A stable, monotone chars/4 proxy for the size of a message list. The
5
+ * runtime's context policy, request budget, summary layer, context anchor
6
+ * and request trace read it; the kernel never did.
7
+ *
8
+ * ADR-0043 Amendment 13: moved here unchanged from the core package
9
+ * (`kernel/compaction.ts`). Since ADR-0044 moved compaction into the
10
+ * microcompact boundary, that file held only this estimator, and nothing
11
+ * in core called it — so it left the counted kernel instead of the cap
12
+ * moving.
13
+ */
14
+ import type { Message } from "@vincemakes/kiso-core";
15
+ /**
16
+ * Rough token estimate (chars/4 + structural overhead). Calibration-free on
17
+ * purpose: context economy only needs a stable MONOTONE proxy, not an exact
18
+ * count — the threshold absorbs the error (the predecessor's ADR-0007). The proxy's
19
+ * three-word contract, and the pins that hold it, are
20
+ * packages/core/tests/sc1b-estimator.test.ts (it stays with the
21
+ * kernel-loop cases it also pins).
22
+ */
23
+ export declare function estimateTokens(messages: readonly Message[]): number;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The token estimator: runtime context-accounting policy.
3
+ *
4
+ * A stable, monotone chars/4 proxy for the size of a message list. The
5
+ * runtime's context policy, request budget, summary layer, context anchor
6
+ * and request trace read it; the kernel never did.
7
+ *
8
+ * ADR-0043 Amendment 13: moved here unchanged from the core package
9
+ * (`kernel/compaction.ts`). Since ADR-0044 moved compaction into the
10
+ * microcompact boundary, that file held only this estimator, and nothing
11
+ * in core called it — so it left the counted kernel instead of the cap
12
+ * moving.
13
+ */
14
+ /**
15
+ * What a non-text block (an image) contributes. There is no character count
16
+ * to proxy, and this module refuses to guess a provider's image
17
+ * tokenization; the figure exists so a block is never worth ZERO, which is
18
+ * what MONOTONE requires. Deliberately small: under-stating an image is a
19
+ * threshold that fires slightly late, over-stating it is one that fires on
20
+ * a conversation that was never large.
21
+ */
22
+ const NON_TEXT_BLOCK_TOKENS = 8;
23
+ /**
24
+ * chars/4 over a `string | ContentBlock[]` content field.
25
+ *
26
+ * SC-1b ①: the array arm must be summed BLOCK BY BLOCK. Reading `.length`
27
+ * off it yields the block COUNT — a five-block 50 KB message scored ~2
28
+ * tokens, and the live microcompact threshold believed it.
29
+ */
30
+ function contentTokens(content) {
31
+ if (typeof content === "string")
32
+ return textTokens(content);
33
+ let tokens = 0;
34
+ for (const block of content) {
35
+ tokens += block.type === "text" ? textTokens(block.text) : NON_TEXT_BLOCK_TOKENS;
36
+ }
37
+ return tokens;
38
+ }
39
+ /**
40
+ * 0.40.0 (the owner's session): chars/4, except that a CJK character counts
41
+ * as one token — which is about what a tokenizer spends on one. Plain
42
+ * chars/4 read a Chinese-heavy 730k context as ~470k, so the thresholds
43
+ * never fired and the ctx row lied. Text without CJK scores exactly what
44
+ * it scored before. A counting loop, not a regex match: the text can be
45
+ * megabytes, and a match array would hold one entry per character.
46
+ */
47
+ function textTokens(text) {
48
+ let cjk = 0;
49
+ for (let i = 0; i < text.length; i++) {
50
+ const c = text.charCodeAt(i);
51
+ // kana, CJK ext A, unified ideographs, Hangul syllables,
52
+ // compatibility ideographs, full-width forms
53
+ if ((c >= 0x3040 && c <= 0x30ff) || (c >= 0x3400 && c <= 0x4dbf) || (c >= 0x4e00 && c <= 0x9fff) || (c >= 0xac00 && c <= 0xd7af) || (c >= 0xf900 && c <= 0xfaff) || (c >= 0xff00 && c <= 0xffef))
54
+ cjk++;
55
+ }
56
+ return cjk + Math.ceil((text.length - cjk) / 4);
57
+ }
58
+ /**
59
+ * Rough token estimate (chars/4 + structural overhead). Calibration-free on
60
+ * purpose: context economy only needs a stable MONOTONE proxy, not an exact
61
+ * count — the threshold absorbs the error (the predecessor's ADR-0007). The proxy's
62
+ * three-word contract, and the pins that hold it, are
63
+ * packages/core/tests/sc1b-estimator.test.ts (it stays with the
64
+ * kernel-loop cases it also pins).
65
+ */
66
+ export function estimateTokens(messages) {
67
+ let total = 0;
68
+ for (const msg of messages) {
69
+ if (msg.role === "user") {
70
+ total += contentTokens(msg.content);
71
+ }
72
+ else if (msg.role === "assistant") {
73
+ for (const block of msg.blocks) {
74
+ total +=
75
+ block.type === "text"
76
+ ? textTokens(block.text)
77
+ : textTokens(JSON.stringify(block.input)) + 20;
78
+ }
79
+ }
80
+ else {
81
+ total += contentTokens(msg.content) + 10;
82
+ }
83
+ }
84
+ return total;
85
+ }
package/dist/index.d.ts CHANGED
@@ -24,7 +24,7 @@ export { AgentSession, AgentSession as Session } from "./session.js";
24
24
  export type { SideQueryOptions } from "./session.js";
25
25
  export { PoisonedSessionError, ResumeBlockedError } from "./session.js";
26
26
  export type { ApprovalRequest, CompactInfo, ContextPolicy, SessionConfig, SummarizeResult } from "./session.js";
27
- export { Run } from "./run.js";
27
+ export { Run, RunClosedError } from "./run.js";
28
28
  export { SessionStore, StaleWriterError, StoreCorruptionError } from "./store.js";
29
29
  export type { Event, SessionMeta, StoreRecord } from "./store.js";
30
30
  export { disposeExtensions, loadExtensions, loadProjectExtensions } from "./extensions.js";
@@ -34,3 +34,4 @@ export type { ExecutionRecord, ExecutionStatus } from "./ledger.js";
34
34
  export { kisoHome, projectArtifacts, recordTrust, trustFor } from "./trust.js";
35
35
  export type { ProjectArtifact, ProjectArtifacts, TrustDecision, TrustRecord } from "./trust.js";
36
36
  export { canonicalizeUsage } from "./usage/canonical.js";
37
+ export { estimateTokens } from "./estimate-tokens.js";
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ export { AgentRuntime, AgentRuntime as Agent, createAgent } from "./agent.js";
24
24
  export { AgentSession, AgentSession as Session } from "./session.js";
25
25
  export { PoisonedSessionError, ResumeBlockedError } from "./session.js";
26
26
  // run
27
- export { Run } from "./run.js";
27
+ export { Run, RunClosedError } from "./run.js";
28
28
  // store
29
29
  export { SessionStore, StaleWriterError, StoreCorruptionError } from "./store.js";
30
30
  // extensions
@@ -42,3 +42,7 @@ export { kisoHome, projectArtifacts, recordTrust, trustFor } from "./trust.js";
42
42
  // provider observation; canonicalizeUsage derives at the accounting
43
43
  // boundary (R4 Case B — the frozen usage union does not move).
44
44
  export { canonicalizeUsage } from "./usage/canonical.js";
45
+ // ADR-0043 Amendment 13: the token estimator is runtime context-accounting
46
+ // policy, not a kernel primitive — it moved here unchanged from the core
47
+ // package, which never called it.
48
+ export { estimateTokens } from "./estimate-tokens.js";
@@ -17,7 +17,7 @@
17
17
  * This is a display and design entry. The auto-compact policy keeps its
18
18
  * own number (`estimateCtxRatio` in the CLI) — moving it is A1b's.
19
19
  */
20
- import { estimateTokens } from "@vincemakes/kiso-core";
20
+ import { estimateTokens } from "./estimate-tokens.js";
21
21
  const chars = (s) => Math.ceil(s.length / 4);
22
22
  export function requestBudget(parts, window) {
23
23
  const system = parts.systemPrompt === undefined ? 0 : chars(parts.systemPrompt);
package/dist/run.d.ts CHANGED
@@ -7,8 +7,19 @@ import { type AbortSignalLike, type Adapter, type ContentBlock, type Event } fro
7
7
  import type { SessionStore } from "./store.js";
8
8
  import { type AgentSession, type SessionConfig } from "./session.js";
9
9
  /**
10
- * A single turn. Async-iterable, so `for await (const ev of session.run(x))`
11
- * is the natural shape; the handle also carries the runId and the abort.
10
+ * ADR-0057: `run.steer()` after the run decided to end — its ingress is
11
+ * sealed. The host starts a new run with the input instead.
12
+ */
13
+ export declare class RunClosedError extends Error {
14
+ readonly runId: string;
15
+ constructor(runId: string);
16
+ }
17
+ /**
18
+ * A single foreground execution, from its input to one terminal (ADR-0057:
19
+ * it may admit further input at Safe Admission boundaries).
20
+ * Async-iterable, so `for await (const ev of session.run(x))` is the
21
+ * natural shape; the handle also carries the runId, the abort and the
22
+ * steer.
12
23
  */
13
24
  export declare class Run implements AsyncIterable<Event> {
14
25
  #private;
@@ -16,5 +27,16 @@ export declare class Run implements AsyncIterable<Event> {
16
27
  constructor(store: SessionStore, adapter: Adapter, config: SessionConfig, session: AgentSession, input: string | readonly ContentBlock[] | undefined, externalSignal: AbortSignalLike | undefined, resume: boolean, source?: import("@vincemakes/kiso-core").MessageSource, via?: import("@vincemakes/kiso-core").UserInputVia);
17
28
  /** Cancel the run: propagates to the adapter (SDK) and future executions. */
18
29
  abort(): void;
30
+ /**
31
+ * ADR-0057 — hand this run a person's input. It is admitted at the next
32
+ * Safe Admission boundary (every started effect settled, no approval
33
+ * open) as a user_input the model sees on its next request. Throws
34
+ * {@link RunClosedError} once the run has decided to end.
35
+ */
36
+ steer(content: string | readonly ContentBlock[]): void;
37
+ /** ADR-0057 — take back the input that has not been admitted yet. */
38
+ retract(): readonly (string | readonly ContentBlock[])[];
39
+ /** ADR-0057 — input that arrived but was not admitted when the run ended. */
40
+ unadmitted(): readonly (string | readonly ContentBlock[])[];
19
41
  [Symbol.asyncIterator](): AsyncIterator<Event>;
20
42
  }
package/dist/run.js CHANGED
@@ -15,8 +15,23 @@ import { RequestTracer, traceGuard } from "./trace/guard.js";
15
15
  import { runtimeVersion } from "./trace/writer.js";
16
16
  import { ResumeBlockedError } from "./session.js";
17
17
  /**
18
- * A single turn. Async-iterable, so `for await (const ev of session.run(x))`
19
- * is the natural shape; the handle also carries the runId and the abort.
18
+ * ADR-0057: `run.steer()` after the run decided to end — its ingress is
19
+ * sealed. The host starts a new run with the input instead.
20
+ */
21
+ export class RunClosedError extends Error {
22
+ runId;
23
+ constructor(runId) {
24
+ super(`run ${runId} has ended — start a new run with this input`);
25
+ this.name = "RunClosedError";
26
+ this.runId = runId;
27
+ }
28
+ }
29
+ /**
30
+ * A single foreground execution, from its input to one terminal (ADR-0057:
31
+ * it may admit further input at Safe Admission boundaries).
32
+ * Async-iterable, so `for await (const ev of session.run(x))` is the
33
+ * natural shape; the handle also carries the runId, the abort and the
34
+ * steer.
20
35
  */
21
36
  export class Run {
22
37
  runId;
@@ -36,6 +51,12 @@ export class Run {
36
51
  #externalSignal;
37
52
  #decisionIds = [];
38
53
  #started = false;
54
+ // ADR-0057 — the ephemeral ingress: a person's input in arrival order,
55
+ // held in memory until an admission site takes it. The log owns what was
56
+ // admitted; nothing here is durable (a crash before admission loses it).
57
+ #pending = [];
58
+ #unadmitted = [];
59
+ #sealed = false;
39
60
  constructor(store, adapter, config, session, input, externalSignal, resume,
40
61
  // TV-1B: durable PROVENANCE for the input (e.g. source:"system"
41
62
  // for a line the product wrote) — who produced the line, never a
@@ -59,6 +80,41 @@ export class Run {
59
80
  abort() {
60
81
  this.#abort.abort();
61
82
  }
83
+ /**
84
+ * ADR-0057 — hand this run a person's input. It is admitted at the next
85
+ * Safe Admission boundary (every started effect settled, no approval
86
+ * open) as a user_input the model sees on its next request. Throws
87
+ * {@link RunClosedError} once the run has decided to end.
88
+ */
89
+ steer(content) {
90
+ if (this.#sealed)
91
+ throw new RunClosedError(this.runId);
92
+ this.#pending.push(content);
93
+ }
94
+ /** ADR-0057 — take back the input that has not been admitted yet. */
95
+ retract() {
96
+ return this.#pending.splice(0);
97
+ }
98
+ /** ADR-0057 — input that arrived but was not admitted when the run ended. */
99
+ unadmitted() {
100
+ return [...this.#unadmitted];
101
+ }
102
+ /** The kernel's `admit`: what is pending becomes ONE human input — a
103
+ * single line as itself, several as text blocks in arrival order. */
104
+ async #admit(mode) {
105
+ if (mode !== "seal" && this.#pending.length > 0) {
106
+ const lines = this.#pending.splice(0);
107
+ const content = lines.length === 1 ? lines[0] : lines.flatMap((l) => (typeof l === "string" ? [{ type: "text", text: l }] : l));
108
+ return [{ kind: "human", content, source: "user" }];
109
+ }
110
+ if (mode !== "take")
111
+ this.#seal();
112
+ return [];
113
+ }
114
+ #seal() {
115
+ this.#sealed = true;
116
+ this.#unadmitted.push(...this.#pending.splice(0));
117
+ }
62
118
  async *[Symbol.asyncIterator]() {
63
119
  if (this.#started)
64
120
  throw new Error("a run may only be consumed once");
@@ -204,6 +260,9 @@ export class Run {
204
260
  // the human gave in the same instant as the abort is
205
261
  // recorded, exactly once.
206
262
  approvalVerdict: (decisionId) => this.#session.approvalVerdict(decisionId),
263
+ // ADR-0057: the ingress — the kernel admits at its three sites
264
+ // and seals at every terminal.
265
+ admit: (mode) => this.#admit(mode),
207
266
  });
208
267
  const self = this;
209
268
  const runLoop = async function* () {
@@ -328,6 +387,9 @@ export class Run {
328
387
  yield ev;
329
388
  }
330
389
  finally {
390
+ // ADR-0057: however the run ended — terminal, throw or abandoned —
391
+ // its ingress is closed; pending input is handed back.
392
+ this.#seal();
331
393
  // round 5(P1-5): flush verdicts the consumer submitted before the
332
394
  // generator was abandoned — an approve()/resolveUncertain() whose
333
395
  // durable event the loop never got to persist must STILL land on
package/dist/session.js CHANGED
@@ -40,7 +40,7 @@ import { canonicalizeUsageForModel } from "./usage/canonical.js";
40
40
  import { contextAnchor } from "./context-anchor.js";
41
41
  import { appendFileSync, mkdirSync } from "node:fs";
42
42
  import { join } from "node:path";
43
- import { estimateTokens } from "@vincemakes/kiso-core";
43
+ import { estimateTokens } from "./estimate-tokens.js";
44
44
  import { StaleWriterError } from "./store.js";
45
45
  import { composeHooks, composeSystemPrompt, microcompactFor, runBasePrompt } from "./compose.js";
46
46
  import { checkpointBoundarySeq } from "./checkpoint.js";
package/dist/summarize.js CHANGED
@@ -13,7 +13,8 @@
13
13
  * unchanged ("nothing happened"). Only the generated `summarized` event
14
14
  * lands on disk; the original events stay there forever.
15
15
  */
16
- import { estimateTokens, DO_NOT_COMPACT, DEFAULT_MAX_RETRIES, RETRY_AFTER_MAX_MS, retryDelayMs } from "@vincemakes/kiso-core";
16
+ import { DO_NOT_COMPACT, DEFAULT_MAX_RETRIES, RETRY_AFTER_MAX_MS, retryDelayMs } from "@vincemakes/kiso-core";
17
+ import { estimateTokens } from "./estimate-tokens.js";
17
18
  /** K (ADR-0044): the recent ROUNDS kept intact by /compact — a constant,
18
19
  * not a knob. The covered range ends just before the K-th most recent
19
20
  * round, so the model still reasons over the recent conversation. */
@@ -20,7 +20,7 @@
20
20
  * surprise), every turn range degrades to null — honest thin pointers
21
21
  * rather than wrong ones.
22
22
  */
23
- import { estimateTokens } from "@vincemakes/kiso-core";
23
+ import { estimateTokens } from "../estimate-tokens.js";
24
24
  import { canonicalJson, hashSystemPrompt, hashToolSpecs, sha256Hex } from "./hash.js";
25
25
  /** The log's visible user boundaries, in log order: every user_input
26
26
  * whose replacement (if any) is not a veto. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-runtime",
3
- "version": "0.44.0",
3
+ "version": "0.45.1",
4
4
  "description": "kiso runtime \u2014 durable multi-turn agent sessions: AgentDefinition, AgentRuntime, AgentSession, Run, append-only JSONL store.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,12 +25,12 @@
25
25
  "test": "vitest run"
26
26
  },
27
27
  "dependencies": {
28
- "@vincemakes/kiso-core": "0.44.0"
28
+ "@vincemakes/kiso-core": "0.45.1"
29
29
  },
30
30
  "peerDependencies": {
31
- "@vincemakes/kiso-provider-anthropic": "0.44.0",
32
- "@vincemakes/kiso-provider-openai": "0.44.0",
33
- "@vincemakes/kiso-provider-openai-responses": "0.44.0"
31
+ "@vincemakes/kiso-provider-anthropic": "0.45.1",
32
+ "@vincemakes/kiso-provider-openai": "0.45.1",
33
+ "@vincemakes/kiso-provider-openai-responses": "0.45.1"
34
34
  },
35
35
  "peerDependenciesMeta": {
36
36
  "@vincemakes/kiso-provider-anthropic": {
@@ -44,7 +44,7 @@
44
44
  }
45
45
  },
46
46
  "devDependencies": {
47
- "@vincemakes/kiso-evals": "0.44.0",
47
+ "@vincemakes/kiso-evals": "0.45.1",
48
48
  "@types/node": "^26.1.2",
49
49
  "typescript": "^5.7.2",
50
50
  "vitest": "^3.0.0"