@vincemakes/kiso-runtime 0.43.0 → 0.45.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.
@@ -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,15 +24,14 @@ 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";
31
31
  export type { KisoExtension } from "./extensions.js";
32
32
  export { executionForCallId, executionLedger } from "./ledger.js";
33
33
  export type { ExecutionRecord, ExecutionStatus } from "./ledger.js";
34
- export { assessTasks } from "./task-assessment.js";
35
- export type { EvidenceVerdict, TaskAssessment, TaskClaim } from "./task-assessment.js";
36
34
  export { kisoHome, projectArtifacts, recordTrust, trustFor } from "./trust.js";
37
35
  export type { ProjectArtifact, ProjectArtifacts, TrustDecision, TrustRecord } from "./trust.js";
38
36
  export { canonicalizeUsage } from "./usage/canonical.js";
37
+ export { estimateTokens } from "./estimate-tokens.js";
package/dist/index.js CHANGED
@@ -24,16 +24,13 @@ 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
31
31
  export { disposeExtensions, loadExtensions, loadProjectExtensions } from "./extensions.js";
32
32
  // ledger
33
33
  export { executionForCallId, executionLedger } from "./ledger.js";
34
- // task assessment (TV-1A) — the pure projection separating the model's
35
- // CLAIM from VERIFIED under Verified ⟹ evidenceSeq > lastMutationSeq
36
- export { assessTasks } from "./task-assessment.js";
37
34
  // trust
38
35
  export { kisoHome, projectArtifacts, recordTrust, trustFor } from "./trust.js";
39
36
  // usage — the canonical accounting schema (E2/1.3.0, R4b-1 ruling:
@@ -45,3 +42,7 @@ export { kisoHome, projectArtifacts, recordTrust, trustFor } from "./trust.js";
45
42
  // provider observation; canonicalizeUsage derives at the accounting
46
43
  // boundary (R4 Case B — the frozen usage union does not move).
47
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,9 +51,15 @@ 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
- // TV-1B: durable PROVENANCE for the input (e.g. the verification
41
- // seed's source:"system") — who produced the line, never a
61
+ // TV-1B: durable PROVENANCE for the input (e.g. source:"system"
62
+ // for a line the product wrote) — who produced the line, never a
42
63
  // provider-role escalation. Absent = plain user input.
43
64
  source,
44
65
  // 0.40.0: how a person composed the turn (a skill + the typed line) —
@@ -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
@@ -505,7 +567,10 @@ export class Run {
505
567
  yield log.append({ type: "tool_result", callId: call.callId, invocationSeq: call.seq, content: refusal, isError: true, errorKind: "invalid_input" });
506
568
  return;
507
569
  }
508
- const payload = { callId: call.callId, name: call.name, input: call.input ?? {} };
570
+ // #20: the same lexical evidence the fresh path hands onPreTool — THIS
571
+ // invocation's deltas, anchored by its seq; never given to the chain.
572
+ const rawInput = rawInputOf(log.all, call.callId, call.seq);
573
+ const payload = { callId: call.callId, name: call.name, input: call.input ?? {}, ...(rawInput !== undefined ? { rawInput } : {}) };
509
574
  const policyCall = { name: payload.name, input: payload.input };
510
575
  let verdict;
511
576
  try {
@@ -825,7 +890,7 @@ export class Run {
825
890
  };
826
891
  }
827
892
  if (this.#config.hooks?.onPostTool) {
828
- result = await this.#config.hooks.onPostTool({ callId, name, input }, result, { sessionId: this.#session.id });
893
+ result = await this.#config.hooks.onPostTool({ callId, name, input, ...(rawInput !== undefined ? { rawInput } : {}) }, result, { sessionId: this.#session.id });
829
894
  }
830
895
  }
831
896
  // ruling #12 correction one: the honest note rides the recovered failure too —
package/dist/session.d.ts CHANGED
@@ -29,7 +29,6 @@
29
29
  */
30
30
  import { EventLog, type AbortSignalLike, type Adapter, type Event, type KisoExtension, type Message, type PermissionDecision, type Tool, type ToolSpec, type EventInput } from "@vincemakes/kiso-core";
31
31
  import { type OverflowMeasure } from "./overflow-belt.js";
32
- import { type TaskAssessment } from "./task-assessment.js";
33
32
  import { type ContinuationScope } from "@vincemakes/kiso-core";
34
33
  import { type ReasoningSetting } from "./provider/metadata.js";
35
34
  import { type SessionStore, type StoreRecord } from "./store.js";
@@ -375,20 +374,6 @@ export declare class AgentSession {
375
374
  approve(decisionId: string, allow: boolean, reason?: string): Promise<void>;
376
375
  /** Executions that started but never reported a result (crash window). */
377
376
  uncertainExecutions(): import("./ledger.js").ExecutionRecord[];
378
- /**
379
- * TV-1A — assess the task claims and their evidence freshness over THIS
380
- * session's durable log. The non-mutating set comes from the live tools'
381
- * own `effects.precommitSafe` certificates (one direction of truth,
382
- * never a second declaration) — the read-only+free+local contract, the
383
- * only certificate that proves the world untouched. `concurrency:
384
- * "shared"` is a SCHEDULING promise and never feeds this set (TV-1C —
385
- * slow_touch is shared and writes). The evidence policy defaults to
386
- * {"shell"} — the convention the task extension's own "make the LAST
387
- * item a verification step" guidance produces.
388
- */
389
- assessTasks(opts?: {
390
- readonly evidenceTools?: ReadonlySet<string>;
391
- }): TaskAssessment;
392
377
  /**
393
378
  * The human's verdict on an interrupted execution, keyed by EXECUTION ID
394
379
  * (B group): "rerun" (the human says the side effect did NOT happen — the
package/dist/session.js CHANGED
@@ -31,11 +31,6 @@ import { EventLog, projectMessages, SUMMARY_FRAMING, } from "@vincemakes/kiso-co
31
31
  import { executionLedger } from "./ledger.js";
32
32
  import { overflowBelt } from "./overflow-belt.js";
33
33
  import { windowLearner } from "./window-learner.js";
34
- import { assessTasks } from "./task-assessment.js";
35
- /** TV-1A — the session-level evidence policy: the PURE projection defaults
36
- * to ∅ (never inventing evidence); the session names the one built-in
37
- * verification surface. Override per call for custom evidence tools. */
38
- const DEFAULT_EVIDENCE_TOOLS = new Set(["shell"]);
39
34
  import { denialResult } from "@vincemakes/kiso-core";
40
35
  import { buildProfile, readProfile, writeProfile, writeSummary } from "./profile.js";
41
36
  import { summarizeEvents } from "./session-summary.js";
@@ -45,7 +40,7 @@ import { canonicalizeUsageForModel } from "./usage/canonical.js";
45
40
  import { contextAnchor } from "./context-anchor.js";
46
41
  import { appendFileSync, mkdirSync } from "node:fs";
47
42
  import { join } from "node:path";
48
- import { estimateTokens } from "@vincemakes/kiso-core";
43
+ import { estimateTokens } from "./estimate-tokens.js";
49
44
  import { StaleWriterError } from "./store.js";
50
45
  import { composeHooks, composeSystemPrompt, microcompactFor, runBasePrompt } from "./compose.js";
51
46
  import { checkpointBoundarySeq } from "./checkpoint.js";
@@ -1064,28 +1059,6 @@ export class AgentSession {
1064
1059
  uncertainExecutions() {
1065
1060
  return [...executionLedger(this.log.all).values()].filter((r) => r.status === "uncertain");
1066
1061
  }
1067
- /**
1068
- * TV-1A — assess the task claims and their evidence freshness over THIS
1069
- * session's durable log. The non-mutating set comes from the live tools'
1070
- * own `effects.precommitSafe` certificates (one direction of truth,
1071
- * never a second declaration) — the read-only+free+local contract, the
1072
- * only certificate that proves the world untouched. `concurrency:
1073
- * "shared"` is a SCHEDULING promise and never feeds this set (TV-1C —
1074
- * slow_touch is shared and writes). The evidence policy defaults to
1075
- * {"shell"} — the convention the task extension's own "make the LAST
1076
- * item a verification step" guidance produces.
1077
- */
1078
- assessTasks(opts) {
1079
- const nonMutatingTools = new Set();
1080
- for (const tool of this.#config.registry.list()) {
1081
- if (tool.effects?.precommitSafe === true)
1082
- nonMutatingTools.add(tool.name);
1083
- }
1084
- return assessTasks(this.log.all, {
1085
- nonMutatingTools,
1086
- evidenceTools: opts?.evidenceTools ?? DEFAULT_EVIDENCE_TOOLS,
1087
- });
1088
- }
1089
1062
  /**
1090
1063
  * The human's verdict on an interrupted execution, keyed by EXECUTION ID
1091
1064
  * (B group): "rerun" (the human says the side effect did NOT happen — the
@@ -270,8 +270,9 @@ export declare function estimateEventTokens(ev: Event): number;
270
270
  * rounds exist (nothing worth covering yet).
271
271
  *
272
272
  * ⑥ (task round): a tool result tagged do-not-compact is DURABLE work
273
- * memory (the task_set echo) — the summary must never cover its round,
274
- * or the model loses the current list. When the base boundary would
273
+ * memory (a tool declares it so; the retired task extension's list was
274
+ * the first) — the summary must never cover its round, or the model
275
+ * loses it. When the base boundary would
275
276
  * cover such a result, the boundary pulls back to just before the round
276
277
  * containing the LATEST one (still a turn boundary). A protected round
277
278
  * as the FIRST uncovered round leaves nothing before it to cover →
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. */
@@ -501,8 +502,9 @@ export function estimateEventTokens(ev) {
501
502
  * rounds exist (nothing worth covering yet).
502
503
  *
503
504
  * ⑥ (task round): a tool result tagged do-not-compact is DURABLE work
504
- * memory (the task_set echo) — the summary must never cover its round,
505
- * or the model loses the current list. When the base boundary would
505
+ * memory (a tool declares it so; the retired task extension's list was
506
+ * the first) — the summary must never cover its round, or the model
507
+ * loses it. When the base boundary would
506
508
  * cover such a result, the boundary pulls back to just before the round
507
509
  * containing the LATEST one (still a turn boundary). A protected round
508
510
  * as the FIRST uncovered round leaves nothing before it to cover →
@@ -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.43.0",
3
+ "version": "0.45.0",
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.43.0"
28
+ "@vincemakes/kiso-core": "0.45.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "@vincemakes/kiso-provider-anthropic": "0.43.0",
32
- "@vincemakes/kiso-provider-openai": "0.43.0",
33
- "@vincemakes/kiso-provider-openai-responses": "0.43.0"
31
+ "@vincemakes/kiso-provider-anthropic": "0.45.0",
32
+ "@vincemakes/kiso-provider-openai": "0.45.0",
33
+ "@vincemakes/kiso-provider-openai-responses": "0.45.0"
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.43.0",
47
+ "@vincemakes/kiso-evals": "0.45.0",
48
48
  "@types/node": "^26.1.2",
49
49
  "typescript": "^5.7.2",
50
50
  "vitest": "^3.0.0"
@@ -1,93 +0,0 @@
1
- /**
2
- * TV-1A — TaskAssessment as a PURE PROJECTION + Evidence Freshness.
3
- *
4
- * The task checklist the model maintains through task_set is a SELF-REPORT:
5
- * an item is "done" because the model said so. This projection mechanically
6
- * separates that CLAIM from VERIFIED, under one frozen sentence:
7
- *
8
- * Verified ⟹ evidenceSeq > lastRelevantMutationSeq.
9
- *
10
- * No new event kind, no core diff — the projection reads the existing
11
- * durable vocabulary (`deriveRecoveryPlan` is the shape precedent). The
12
- * classifiers are maximally conservative; the exemptions are exactly
13
- * three (TV-1C — no wider taxonomy exists or is wanted):
14
- *
15
- * - MUTATION marker: `tool_execution_started` (intent-to-effect) of any
16
- * name NOT in `nonMutatingTools`. Started — not the receipt — so failed
17
- * and crash-window executions invalidate too, and pre-EC-1 overlap eras
18
- * are covered by the same rule. Absence of a certificate is a mutation.
19
- * The exemption set is fed by `effects.precommitSafe` — the frozen
20
- * read-only+free+local contract, the only certificate that PROVES the
21
- * world untouched. `concurrency: "shared"` proves overlap-safety and
22
- * nothing else (the TV-1C finding: slow_touch is shared AND writes) —
23
- * it never feeds this set.
24
- * ONE proven exception (TV-1B, grounded in WR-1A): an execution whose
25
- * terminal receipt is failed(errorKind:"precondition") never counts —
26
- * that kind's frozen contract is "work refused BEFORE it starts".
27
- * - EVIDENCE: only receipts (`tool_execution_succeeded`) of names in
28
- * `evidenceTools`. Default ∅ — the projection never invents evidence.
29
- * What TV-1A's verdict asserts is POSITIONAL: the arc's last
30
- * intent-to-effect was a successful evidence-class run — an "evidence",
31
- * never a "proof" (semantic knowledge is the TV-1B driver's).
32
- * - task_set itself never invalidates: recording the claim after the check
33
- * is the natural arc (tests → mark done), and the claim-recording act
34
- * mutates nothing the evidence observed.
35
- * - VOIDED RANGES (TV-1C): a `model_output_abandoned` marker voids
36
- * (voidFromSeq, seq] — the durable vocabulary's own "never happened",
37
- * the same ranges the kernel projection skips. Every consumer here
38
- * skips them uniformly: a voided echo is not a claim, a voided receipt
39
- * is not evidence, a voided start is not a mutation. task_set waits
40
- * for turn commit today (TT-1), so the scheduler already keeps claims
41
- * out of voided turns — the projection encodes the admissibility rule
42
- * itself instead of relying on that forever.
43
- *
44
- * `verified` means FRESH EVIDENCE UNDER THE CONFIGURED POLICY — never
45
- * "objectively proven correct". Goal Truth is external (PE-1's
46
- * evaluator); this projection only ever ranks the model's own claim
47
- * against the trajectory's own receipts.
48
- */
49
- import type { Event } from "@vincemakes/kiso-core";
50
- /** One item of the model's plan, exactly as last claimed. */
51
- export interface TaskClaim {
52
- readonly text: string;
53
- readonly status: "pending" | "active" | "done";
54
- }
55
- /** The freshness verdict. `stale` and `unreadable` NAME their cause. */
56
- export type EvidenceVerdict = {
57
- readonly kind: "verified";
58
- readonly evidenceSeq: number;
59
- } | {
60
- readonly kind: "stale";
61
- readonly evidenceSeq: number;
62
- readonly invalidatedBySeq: number;
63
- } | {
64
- readonly kind: "none";
65
- } | {
66
- readonly kind: "unreadable";
67
- readonly atSeq: number;
68
- readonly reason: string;
69
- };
70
- export interface TaskAssessment {
71
- /** The LAST successful task_set echo, parsed — the model's claims. */
72
- readonly claims: readonly TaskClaim[];
73
- /** True only for a non-empty plan whose every item is claimed done. */
74
- readonly allClaimedDone: boolean;
75
- readonly lastTaskSetSeq: number | null;
76
- /** Seq of the last mutation-class `tool_execution_started`, if any. */
77
- readonly lastMutationSeq: number | null;
78
- readonly evidence: EvidenceVerdict;
79
- }
80
- /**
81
- * Assess the task claims and their evidence freshness over a durable
82
- * trajectory. Pure: same events, same options, same assessment.
83
- */
84
- export declare function assessTasks(events: readonly Event[], opts?: {
85
- /** Names whose executions PROVABLY never mutate the observable
86
- * world (from `effects.precommitSafe` certificates — read-only +
87
- * free + local). `concurrency: "shared"` is NOT such a proof
88
- * (TV-1C). Default ∅ — everything mutates. */
89
- readonly nonMutatingTools?: ReadonlySet<string>;
90
- /** Names whose successful receipts count as evidence. Default ∅ —
91
- * nothing does. */
92
- readonly evidenceTools?: ReadonlySet<string>;
93
- }): TaskAssessment;
@@ -1,165 +0,0 @@
1
- /**
2
- * TV-1A — TaskAssessment as a PURE PROJECTION + Evidence Freshness.
3
- *
4
- * The task checklist the model maintains through task_set is a SELF-REPORT:
5
- * an item is "done" because the model said so. This projection mechanically
6
- * separates that CLAIM from VERIFIED, under one frozen sentence:
7
- *
8
- * Verified ⟹ evidenceSeq > lastRelevantMutationSeq.
9
- *
10
- * No new event kind, no core diff — the projection reads the existing
11
- * durable vocabulary (`deriveRecoveryPlan` is the shape precedent). The
12
- * classifiers are maximally conservative; the exemptions are exactly
13
- * three (TV-1C — no wider taxonomy exists or is wanted):
14
- *
15
- * - MUTATION marker: `tool_execution_started` (intent-to-effect) of any
16
- * name NOT in `nonMutatingTools`. Started — not the receipt — so failed
17
- * and crash-window executions invalidate too, and pre-EC-1 overlap eras
18
- * are covered by the same rule. Absence of a certificate is a mutation.
19
- * The exemption set is fed by `effects.precommitSafe` — the frozen
20
- * read-only+free+local contract, the only certificate that PROVES the
21
- * world untouched. `concurrency: "shared"` proves overlap-safety and
22
- * nothing else (the TV-1C finding: slow_touch is shared AND writes) —
23
- * it never feeds this set.
24
- * ONE proven exception (TV-1B, grounded in WR-1A): an execution whose
25
- * terminal receipt is failed(errorKind:"precondition") never counts —
26
- * that kind's frozen contract is "work refused BEFORE it starts".
27
- * - EVIDENCE: only receipts (`tool_execution_succeeded`) of names in
28
- * `evidenceTools`. Default ∅ — the projection never invents evidence.
29
- * What TV-1A's verdict asserts is POSITIONAL: the arc's last
30
- * intent-to-effect was a successful evidence-class run — an "evidence",
31
- * never a "proof" (semantic knowledge is the TV-1B driver's).
32
- * - task_set itself never invalidates: recording the claim after the check
33
- * is the natural arc (tests → mark done), and the claim-recording act
34
- * mutates nothing the evidence observed.
35
- * - VOIDED RANGES (TV-1C): a `model_output_abandoned` marker voids
36
- * (voidFromSeq, seq] — the durable vocabulary's own "never happened",
37
- * the same ranges the kernel projection skips. Every consumer here
38
- * skips them uniformly: a voided echo is not a claim, a voided receipt
39
- * is not evidence, a voided start is not a mutation. task_set waits
40
- * for turn commit today (TT-1), so the scheduler already keeps claims
41
- * out of voided turns — the projection encodes the admissibility rule
42
- * itself instead of relying on that forever.
43
- *
44
- * `verified` means FRESH EVIDENCE UNDER THE CONFIGURED POLICY — never
45
- * "objectively proven correct". Goal Truth is external (PE-1's
46
- * evaluator); this projection only ever ranks the model's own claim
47
- * against the trajectory's own receipts.
48
- */
49
- const TASK_TOOL = "task_set";
50
- const STATUSES = new Set(["pending", "active", "done"]);
51
- const COUNT_LINE = /^\[task\] (\d+) items? — (\d+) pending, (\d+) active, (\d+) done$/;
52
- const ITEM_LINE = /^\[(\w+)\] (.*)$/;
53
- /** Parse the canonical task_set echo (the frozen content contract the
54
- * checklist cell also reads). Returns the claims, or the reason it
55
- * cannot — never a guess. */
56
- function parseEcho(content) {
57
- const lines = content.split("\n");
58
- const head = COUNT_LINE.exec(lines[0] ?? "");
59
- if (head === null)
60
- return { reason: "line 1 is not the [task] count line" };
61
- const total = Number(head[1]);
62
- const declared = { pending: Number(head[2]), active: Number(head[3]), done: Number(head[4]) };
63
- const claims = [];
64
- for (let i = 1; i < lines.length; i += 1) {
65
- const m = ITEM_LINE.exec(lines[i]);
66
- if (m === null || !STATUSES.has(m[1]))
67
- return { reason: `line ${i + 1} is not a [pending|active|done] item line` };
68
- claims.push({ text: m[2], status: m[1] });
69
- }
70
- const counts = { pending: 0, active: 0, done: 0 };
71
- for (const c of claims)
72
- counts[c.status] += 1;
73
- if (claims.length !== total || counts.pending !== declared.pending || counts.active !== declared.active || counts.done !== declared.done) {
74
- return { reason: "the count line disagrees with the item lines" };
75
- }
76
- return { claims };
77
- }
78
- /**
79
- * Assess the task claims and their evidence freshness over a durable
80
- * trajectory. Pure: same events, same options, same assessment.
81
- */
82
- export function assessTasks(events, opts) {
83
- const nonMutating = opts?.nonMutatingTools ?? new Set();
84
- const evidenceNames = opts?.evidenceTools ?? new Set();
85
- // TV-1C: PASS 0 — the voided ranges. (voidFromSeq, seq] of every
86
- // `model_output_abandoned` marker is "never happened" for this whole
87
- // projection, exactly as the kernel's own context projection treats
88
- // it. Markers are rare; a linear range check is honest and enough.
89
- const voidRanges = [];
90
- for (const ev of events) {
91
- if (ev.type === "model_output_abandoned")
92
- voidRanges.push({ from: ev.voidFromSeq, to: ev.seq });
93
- }
94
- const voided = (seq) => voidRanges.some((r) => seq > r.from && seq <= r.to);
95
- // TV-1B: PASS 1 — executions whose terminal receipt is a
96
- // PRECONDITION failure are PROVEN no-mutation (WR-1A froze that
97
- // contract: work refused before it starts). Everything else stays
98
- // conservative: no receipt (crash window), fatal/transient/
99
- // invalid_input, success, and legacy receipts with no errorKind.
100
- // Two passes, no temporal rollback state — same events, same verdict.
101
- const provenNoMutation = new Set();
102
- for (const ev of events) {
103
- if (voided(ev.seq))
104
- continue;
105
- if (ev.type === "tool_execution_failed" && ev.errorKind === "precondition") {
106
- provenNoMutation.add(ev.executionId);
107
- }
108
- }
109
- const nameByExecution = new Map();
110
- let claims = [];
111
- let lastTaskSetSeq = null;
112
- let unreadable = null;
113
- let lastMutationSeq = null;
114
- let lastEvidenceSeq = null;
115
- let staleBySeq = null;
116
- for (const ev of events) {
117
- if (voided(ev.seq))
118
- continue;
119
- if (ev.type === "tool_execution_started") {
120
- nameByExecution.set(ev.executionId, ev.name);
121
- if (ev.name !== TASK_TOOL && !nonMutating.has(ev.name) && !provenNoMutation.has(ev.executionId)) {
122
- lastMutationSeq = ev.seq;
123
- if (lastEvidenceSeq !== null && staleBySeq === null)
124
- staleBySeq = ev.seq;
125
- }
126
- }
127
- else if (ev.type === "tool_execution_succeeded") {
128
- const name = nameByExecution.get(ev.executionId);
129
- if (name === TASK_TOOL) {
130
- const parsed = parseEcho(ev.result.content);
131
- if ("reason" in parsed) {
132
- unreadable = { atSeq: ev.seq, reason: parsed.reason };
133
- claims = [];
134
- lastTaskSetSeq = ev.seq;
135
- }
136
- else {
137
- unreadable = null;
138
- claims = parsed.claims;
139
- lastTaskSetSeq = ev.seq;
140
- }
141
- }
142
- else if (name !== undefined && evidenceNames.has(name)) {
143
- // a fresh evidence receipt supersedes both the older evidence
144
- // AND any staleness its own start incurred.
145
- lastEvidenceSeq = ev.seq;
146
- staleBySeq = null;
147
- }
148
- }
149
- }
150
- const allClaimedDone = claims.length > 0 && claims.every((c) => c.status === "done");
151
- let evidence;
152
- if (unreadable !== null) {
153
- evidence = { kind: "unreadable", atSeq: unreadable.atSeq, reason: unreadable.reason };
154
- }
155
- else if (lastEvidenceSeq === null) {
156
- evidence = { kind: "none" };
157
- }
158
- else if (staleBySeq !== null) {
159
- evidence = { kind: "stale", evidenceSeq: lastEvidenceSeq, invalidatedBySeq: staleBySeq };
160
- }
161
- else {
162
- evidence = { kind: "verified", evidenceSeq: lastEvidenceSeq };
163
- }
164
- return { claims, allClaimedDone, lastTaskSetSeq, lastMutationSeq, evidence };
165
- }