@vincemakes/kiso-runtime 0.50.1 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agent.js CHANGED
@@ -15,13 +15,18 @@ import { resolveContinuationScope } from "./provider/manifest.js";
15
15
  import { assessProfileDrift, buildProfile, profilePath, readProfile, writeProfile } from "./profile.js";
16
16
  import { EventLog, ToolRegistry } from "@vincemakes/kiso-core";
17
17
  import { AgentSession } from "./session.js";
18
+ import { snapshotCheckpointSections } from "./summarize.js";
18
19
  /** @deprecated the canonical name is `Agent` (root export, 1.1.0); this alias is removed in the next major. */
19
20
  export class AgentRuntime {
20
21
  #definition;
22
+ /** 0.51.0: the context policy sessions receive — `checkpointSections` validated and copied at construction (a host that mutates its array afterwards changes nothing); every other field stays the host's own reference, as before. */
23
+ #contextPolicy;
21
24
  #registry;
22
25
  #adapterPromise;
23
26
  constructor(definition) {
24
27
  this.#definition = definition;
28
+ const sections = snapshotCheckpointSections(definition.contextPolicy?.checkpointSections);
29
+ this.#contextPolicy = sections === undefined || definition.contextPolicy === undefined ? definition.contextPolicy : { ...definition.contextPolicy, checkpointSections: sections };
25
30
  this.#registry = new ToolRegistry();
26
31
  for (const tool of definition.tools)
27
32
  this.#registry.register(tool);
@@ -178,7 +183,7 @@ export class AgentRuntime {
178
183
  ...(this.#definition.maxTokens !== undefined ? { maxTokens: this.#definition.maxTokens } : {}),
179
184
  ...(this.#definition.temperature !== undefined ? { temperature: this.#definition.temperature } : {}),
180
185
  ...(this.#definition.microcompact !== undefined ? { microcompact: this.#definition.microcompact } : {}),
181
- ...(this.#definition.contextPolicy !== undefined ? { contextPolicy: this.#definition.contextPolicy } : {}),
186
+ ...(this.#contextPolicy !== undefined ? { contextPolicy: this.#contextPolicy } : {}),
182
187
  ...(this.#definition.maxRetries !== undefined ? { maxRetries: this.#definition.maxRetries } : {}),
183
188
  ...(this.#definition.streamIdleMs !== undefined ? { streamIdleMs: this.#definition.streamIdleMs } : {}),
184
189
  ...(this.#definition.toolRules !== undefined ? { toolRules: this.#definition.toolRules } : {}),
package/dist/index.d.ts CHANGED
@@ -24,6 +24,8 @@ 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 { DEFAULT_CHECKPOINT_SECTIONS } from "./summarize.js";
28
+ export type { CheckpointSection } from "./summarize.js";
27
29
  export { Run, RunClosedError } from "./run.js";
28
30
  export { SessionStore, StaleWriterError, StoreCorruptionError } from "./store.js";
29
31
  export type { Event, SessionMeta, StoreRecord } from "./store.js";
package/dist/index.js CHANGED
@@ -23,6 +23,8 @@ export { AgentRuntime, AgentRuntime as Agent, createAgent } from "./agent.js";
23
23
  // session
24
24
  export { AgentSession, AgentSession as Session } from "./session.js";
25
25
  export { PoisonedSessionError, ResumeBlockedError } from "./session.js";
26
+ // 0.51.0: host-defined checkpoint sections (the type and the coding outline as data; the prompt machinery stays behind ./internal)
27
+ export { DEFAULT_CHECKPOINT_SECTIONS } from "./summarize.js";
26
28
  // run
27
29
  export { Run, RunClosedError } from "./run.js";
28
30
  // store
@@ -76,6 +76,15 @@ export interface ReasoningCapabilities {
76
76
  readonly levels: readonly Exclude<ReasoningEffort, "default">[];
77
77
  readonly default: Exclude<ReasoningEffort, "default"> | null;
78
78
  readonly wire: string;
79
+ /** a route that SPELLS a level differently on the wire (Command Code
80
+ * takes `off` for no reasoning, and refuses `none`): the level a
81
+ * person picks stays the native name, the wire carries the route's
82
+ * word. Absent = every level goes as named. */
83
+ readonly values?: Readonly<Partial<Record<Exclude<ReasoningEffort, "default">, string>>>;
84
+ /** the levels came from the USER's profile, not from a dated source:
85
+ * a claim of theirs, shown as such (the profile's `contextWindow` is
86
+ * the same kind of claim). Absent on every registry row. */
87
+ readonly declared?: true;
79
88
  } | null;
80
89
  /** invalid combinations the provider documents (e.g. thinking-disabled
81
90
  * at certain efforts). */
@@ -120,12 +129,20 @@ export interface ModelMetadataEntry {
120
129
  readonly capabilitiesSource?: string | null;
121
130
  readonly pricing: ModelPricing | null;
122
131
  }
132
+ /** Every row of the table, for the gate that holds the whole table to its
133
+ * contract (shape, dates, sources, unique keys). Internal; read-only. */
134
+ export declare function modelMetadataRows(): readonly ModelMetadataEntry[];
123
135
  /**
124
136
  * Look a model up. `endpoint` narrows: an entry WITH an endpoint only
125
137
  * matches when the caller's endpoint origin equals it; an entry without
126
138
  * one matches any endpoint. Unknown model → null, never a default.
127
139
  */
128
140
  export declare function lookupModelMetadata(model: string, endpoint?: string): ModelMetadataEntry | null;
141
+ export declare function declareEffort(model: string, endpoint: string | undefined, effort: Omit<NonNullable<ReasoningCapabilities["effort"]>, "declared" | "wire"> & {
142
+ readonly wire?: string;
143
+ }): void;
144
+ /** For tests and a config reload: forget every declaration. */
145
+ export declare function clearDeclaredEfforts(): void;
129
146
  /** CW-1: the aliases, for the gate that holds each one to a dated source
130
147
  * and a registered target. */
131
148
  export declare function modelAliases(): Readonly<Record<string, {
@@ -18,6 +18,7 @@
18
18
  * Lives under runtime/internal — the curated root surface (44 names)
19
19
  * does not move; core is untouched.
20
20
  */
21
+ import { OPENROUTER_ROWS } from "./openrouter-rows.js";
21
22
  /**
22
23
  * REG-1 — the OFF-PEAK rates, read from the vendor's pricing table on
23
24
  * 2026-09-17. The table carries TWO sets: off-peak and peak, the peak
@@ -63,12 +64,16 @@ const ANTHROPIC_PRICING_SOURCE = "https://platform.claude.com/docs/en/about-clau
63
64
  const ANTHROPIC_MODELS_SOURCE = "https://platform.claude.com/docs/en/models/overview";
64
65
  const ANTHROPIC_EFFORT_SOURCE = "https://platform.claude.com/docs/en/build-with-claude/effort";
65
66
  const ANTHROPIC_ASOF = "2026-09-07";
66
- const anthropicPricing = (inputPerM, outputPerM, cacheReadPerM, cacheWritePerM) => ({
67
+ /** The 5.5 line (Opus 5.5, Sonnet 5.5, Haiku 5.5): the models overview, the
68
+ * effort page, the thinking table and the pricing page, all read 2026-10-17. */
69
+ const ANTHROPIC_ASOF_55 = "2026-10-17";
70
+ const ANTHROPIC_THINKING_SOURCE = "https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting";
71
+ const anthropicPricing = (inputPerM, outputPerM, cacheReadPerM, cacheWritePerM, asOf = ANTHROPIC_ASOF) => ({
67
72
  inputPerM,
68
73
  outputPerM,
69
74
  cacheReadPerM,
70
75
  cacheWritePerM,
71
- asOf: ANTHROPIC_ASOF,
76
+ asOf,
72
77
  source: ANTHROPIC_PRICING_SOURCE,
73
78
  });
74
79
  /** The 5-line's effort control: five native levels, default high, wire
@@ -77,24 +82,24 @@ const anthropicPricing = (inputPerM, outputPerM, cacheReadPerM, cacheWritePerM)
77
82
  * gives the modes per model. */
78
83
  const FIVE_LEVELS = { levels: ["low", "medium", "high", "xhigh", "max"], default: "high", wire: "output_config.effort" };
79
84
  function anthropicLine() {
80
- const row = (model, capabilities, pricing) => ({
85
+ const row = (model, capabilities, pricing, asOf = ANTHROPIC_ASOF) => ({
81
86
  model,
82
87
  providerId: "anthropic",
83
88
  capabilities,
84
- capabilitiesAsOf: ANTHROPIC_ASOF,
89
+ capabilitiesAsOf: asOf,
85
90
  capabilitiesSource: ANTHROPIC_MODELS_SOURCE,
86
91
  pricing,
87
92
  });
88
- const fiveLine = (thinkingModes, forbidden) => ({
93
+ const fiveLine = (thinkingModes, forbidden, opts = {}) => ({
89
94
  contextWindow: 1_000_000,
90
95
  maxOutputTokens: 128_000,
91
96
  promptCaching: "explicit",
92
97
  reasoning: {
93
98
  emitsThinkingStream: true,
94
99
  thinking: { modes: thinkingModes, default: "adaptive" },
95
- effort: FIVE_LEVELS,
100
+ effort: opts.effortDefault === undefined ? FIVE_LEVELS : { ...FIVE_LEVELS, default: opts.effortDefault },
96
101
  ...(forbidden !== undefined ? { forbidden } : {}),
97
- asOf: ANTHROPIC_ASOF,
102
+ asOf: opts.asOf ?? ANTHROPIC_ASOF,
98
103
  source: ANTHROPIC_EFFORT_SOURCE,
99
104
  },
100
105
  inputModalities: ["text", "image"],
@@ -121,6 +126,30 @@ function anthropicLine() {
121
126
  // Haiku 4.5: the pinned id and its alias (the registry matches exact ids).
122
127
  row("claude-haiku-4-5-20251001", haiku, haikuPricing),
123
128
  row("claude-haiku-4-5", haiku, haikuPricing),
129
+ // The 5.5 line (read 2026-10-17; every model id is its own pinned
130
+ // snapshot, so there is no dated alias). All three: 1M window, 128K
131
+ // output, five native effort levels, wire output_config.effort.
132
+ //
133
+ // Opus 5.5: adaptive ALWAYS on ("disabled" is a 400 at every level);
134
+ // the default effort is MEDIUM, one below Opus 5's. Cache hits cost 5%
135
+ // of input.
136
+ row("claude-opus-5-5", fiveLine(["adaptive"], undefined, { effortDefault: "medium", asOf: ANTHROPIC_ASOF_55 }), anthropicPricing(4, 20, 0.2, 5, ANTHROPIC_ASOF_55), ANTHROPIC_ASOF_55),
137
+ // Sonnet 5.5: adaptive, on by default. "disabled" is a 400 at every
138
+ // level; the vendor's own off switch is the new thinking type
139
+ // `between_tools` (accepted only here, only at effort high or below),
140
+ // which the registry's thinking vocabulary does not have, so it is
141
+ // not offered. Default effort high. Cache hits cost 5% of input.
142
+ row("claude-sonnet-5-5", fiveLine(["adaptive"], undefined, { asOf: ANTHROPIC_ASOF_55 }), anthropicPricing(2, 10, 0.1, 2.5, ANTHROPIC_ASOF_55), ANTHROPIC_ASOF_55),
143
+ // Haiku 5.5: adaptive, on by default; "disabled" accepted at effort
144
+ // high or below, a 400 with xhigh or max (the forbidden pair). Default
145
+ // effort medium. PRICE: the vendor prices by PROMPT LENGTH. The row
146
+ // carries the rates for prompts up to 100,000 tokens; a prompt over
147
+ // that pays $0.50 in / $2.50 out / $0.05 cache read / $0.625 cache
148
+ // write, five times the row. ModelPricing has one set of rates, so a
149
+ // bill computed from this row for a long prompt is a fifth of the
150
+ // truth: the one-rate-against-two-rates shape the DeepSeek peak
151
+ // rates already have.
152
+ row("claude-haiku-5-5", fiveLine(["adaptive", "disabled"], [{ thinking: "disabled", effort: "xhigh" }, { thinking: "disabled", effort: "max" }], { effortDefault: "medium", asOf: ANTHROPIC_ASOF_55 }), anthropicPricing(0.1, 0.5, 0.01, 0.125, ANTHROPIC_ASOF_55), ANTHROPIC_ASOF_55),
124
153
  ];
125
154
  }
126
155
  const OPENAI_MODELS_ASOF = "2026-09-08";
@@ -141,7 +170,7 @@ const openaiRow = (model, effortDefault, rate, page, opts = {}) => ({
141
170
  }, inputModalities: null },
142
171
  capabilitiesAsOf: opts.asOf ?? OPENAI_MODELS_ASOF,
143
172
  capabilitiesSource: page,
144
- pricing: { ...rate, cacheWritePerM: 0, asOf: opts.asOf ?? OPENAI_MODELS_ASOF, source: page },
173
+ pricing: { ...rate, cacheWritePerM: rate.cacheWritePerM ?? 0, asOf: opts.asOf ?? OPENAI_MODELS_ASOF, source: page },
145
174
  });
146
175
  /** The subscription backend's row for the same id: the presets' four
147
176
  * levels (no `none`), the presets' context window, no price, and a cache
@@ -171,12 +200,24 @@ const ULTRA_LADDER = ["low", "medium", "high", "xhigh", "max", "ultra"];
171
200
  * The owner's `sol` profile moved to `gpt-6.1-sol` on 2026-10-08, and with
172
201
  * no row kiso sent it no effort: the remembered `high` never reached the
173
202
  * wire and no reasoning summary came back. The id answered on the
174
- * subscription backend that night (the 0.47.3 live sessions). No
175
- * first-party row: no model page was read for it. */
203
+ * subscription backend that night (the 0.47.3 live sessions). The
204
+ * first-party row was added 2026-10-17 (OPENAI_MODELS_ASOF_4). */
176
205
  const OPENAI_MODELS_ASOF_3 = "2026-10-09";
177
206
  const CHATGPT_PRESETS_SOURCE_3 = "https://github.com/openai/codex/blob/d63a9b8344cfe58bc78bbe319b560378fc8756ef/codex-rs/models-manager/models.json";
207
+ /** The first-party pages for the GPT-6.1 / GPT-6 sol and luna rows, read
208
+ * 2026-10-17 (the model page is the source for the levels, the window, the
209
+ * output cap and the price alike, as for every first-party row). */
210
+ const OPENAI_MODELS_ASOF_4 = "2026-10-17";
178
211
  /** The v1 table. Nulls outnumber numbers ON PURPOSE: only values with a
179
212
  * named source enter; everything else waits for one. */
213
+ /** Command Code: what the gateway ACCEPTS (probe, 2026-10-17) intersected with
214
+ * each model's native levels; `none` is spelled `off` on this route. */
215
+ const COMMAND_CODE_EFFORT_SOURCE = "probe 2026-10-17: POST https://api.commandcode.ai/provider/v1/chat/completions with a sentinel reasoning_effort names the legal set; native levels per model: the first-party deepseek-flash row and https://openrouter.ai/api/v1/models";
216
+ const COMMAND_CODE_EFFORT = {
217
+ "deepseek/deepseek-v4.1-flash": { levels: ["none", "low", "high", "max"], default: "high", wire: "reasoning_effort", values: { none: "off" } },
218
+ "z-ai/glm-5.3-flash": { levels: ["low", "high", "max"], default: null, wire: "reasoning_effort" },
219
+ "xai/grok-4.7": { levels: ["low", "medium", "high", "xhigh"], default: null, wire: "reasoning_effort" },
220
+ };
180
221
  const ENTRIES = [
181
222
  {
182
223
  model: "deepseek-chat",
@@ -418,43 +459,98 @@ const ENTRIES = [
418
459
  chatgptRow("gpt-6-astra", { levels: ULTRA_LADDER, default: "low", source: CHATGPT_PRESETS_SOURCE_2, asOf: OPENAI_MODELS_ASOF_2 }),
419
460
  chatgptRow("gpt-5.6-sol", { levels: ULTRA_LADDER, default: "low", source: CHATGPT_PRESETS_SOURCE_2, asOf: OPENAI_MODELS_ASOF_2 }),
420
461
  chatgptRow("gpt-6.1-sol", { levels: ULTRA_LADDER, default: "low", source: CHATGPT_PRESETS_SOURCE_3, asOf: OPENAI_MODELS_ASOF_3 }),
421
- // The compat table's SECOND row (roadmap R3.2 §1a step 1): GLM 5.3 Flash
422
- // through OpenRouter, read from OpenRouter's models API on 2026-09-09. The
423
- // id is OpenRouter's, so the row is keyed to that origin; the
424
- // continuation scope stamps such a profile `custom` at openrouter.ai
425
- // (manifest.ts's KNOWN_ORIGINS is deliberately not widened — the scope
426
- // identity is a durable fact and this row needs only the lookup). The
427
- // window is the TOP provider's (1,048,576), not the listing's 1,310,720:
428
- // what a request actually gets. No default effort — the page states
429
- // none. `reasoning_effort` is in the model's supported parameters; the
430
- // stream's think arrives as `reasoning` (GLM-F1, the compat adapter).
431
- {
432
- model: "z-ai/glm-5.3-flash",
433
- endpoint: "https://openrouter.ai",
462
+ // 2026-10-17: the first-party rows the 0.48.0 note said were missing
463
+ // ("no model page was read for it"). gpt-6.1-sol: five levels, and the
464
+ // page says `none` and `minimal` are not supported; default medium.
465
+ // gpt-6-sol and gpt-6-luna: `none` through `max`, default medium.
466
+ // PRICE: each page also lists a LONG-PROMPT tier: a prompt over 272,000
467
+ // input tokens pays 2x the input and cache rates and 1.5x the output
468
+ // rate for the whole request. The rows carry the standard rates; a bill
469
+ // computed from them for a long prompt is low (the one-rate-against-
470
+ // two-rates shape the DeepSeek peak rates already have). Cache writes
471
+ // are priced on these pages and carried (sol $2.50, luna $0.125).
472
+ openaiRow("gpt-6.1-sol", "medium", { inputPerM: 2, outputPerM: 10, cacheReadPerM: 0.1, cacheWritePerM: 2.5 }, "https://developers.openai.com/api/docs/models/gpt-6.1-sol", { levels: ["low", "medium", "high", "xhigh", "max"], asOf: OPENAI_MODELS_ASOF_4 }),
473
+ openaiRow("gpt-6-sol", "medium", { inputPerM: 2, outputPerM: 10, cacheReadPerM: 0.2, cacheWritePerM: 2.5 }, "https://developers.openai.com/api/docs/models/gpt-6-sol", { levels: ["none", "low", "medium", "high", "xhigh", "max"], asOf: OPENAI_MODELS_ASOF_4 }),
474
+ openaiRow("gpt-6-luna", "medium", { inputPerM: 0.1, outputPerM: 0.5, cacheReadPerM: 0.01, cacheWritePerM: 0.125 }, "https://developers.openai.com/api/docs/models/gpt-6-luna", { levels: ["none", "low", "medium", "high", "xhigh", "max"], asOf: OPENAI_MODELS_ASOF_4 }),
475
+ // The OpenRouter route (origin https://openrouter.ai): generated from
476
+ // OpenRouter's own model list by scripts/registry-or-rows.mjs and kept in
477
+ // openrouter-rows.ts, dated 2026-10-17. It replaces the hand-written
478
+ // z-ai/glm-5.3-flash row of 2026-09-09 (roadmap R3.2 §1a step 1), which
479
+ // the list has since moved past: the efforts are now max / high / low.
480
+ ...OPENROUTER_ROWS,
481
+ // The Command Code gateway (origin https://api.commandcode.ai), read by a
482
+ // PROBE on 2026-10-17: a chat completion with a nonsense reasoning_effort
483
+ // and max_tokens 1 is refused before generation and the 400 names the
484
+ // legal set, `off | low | medium | high | xhigh | max`. That enum belongs
485
+ // to the gateway, not to a model: the same list came back for every model
486
+ // probed. So a row here offers the INTERSECTION of that set with the
487
+ // model's own native levels. `none` goes on the wire as `off`: the gateway
488
+ // refuses `none` (400, probed the same day) and accepts `off` (200). Each
489
+ // model's native levels: deepseek from the first-party deepseek-flash row
490
+ // (live-probed 2026-09-14); glm and grok from OpenRouter's list, which is
491
+ // a router's statement and is named as such in `source`. Mimo has no
492
+ // stated efforts anywhere, so it has no row: unknown stays unknown. No
493
+ // default, window, output cap or price is claimed here: the window falls
494
+ // to the model's identity row, and the gateway's plan has no per-token
495
+ // price.
496
+ ...["deepseek/deepseek-v4.1-flash", "z-ai/glm-5.3-flash", "xai/grok-4.7"].map((model) => ({
497
+ model,
498
+ endpoint: "https://api.commandcode.ai",
434
499
  capabilities: {
435
- contextWindow: 1_048_576,
436
- maxOutputTokens: 131_072,
437
- promptCaching: "automatic",
500
+ contextWindow: null,
501
+ maxOutputTokens: null,
502
+ promptCaching: null,
438
503
  reasoning: {
439
- emitsThinkingStream: true,
504
+ emitsThinkingStream: null,
440
505
  thinking: null,
441
- effort: { levels: ["low", "medium", "high"], default: null, wire: "reasoning_effort" },
442
- asOf: "2026-09-09",
443
- source: "https://openrouter.ai/docs/use-cases/reasoning-tokens",
506
+ effort: COMMAND_CODE_EFFORT[model],
507
+ asOf: "2026-10-17",
508
+ source: COMMAND_CODE_EFFORT_SOURCE,
444
509
  },
445
510
  inputModalities: null,
446
511
  },
447
- capabilitiesAsOf: "2026-09-09",
448
- capabilitiesSource: "https://openrouter.ai/api/v1/models",
449
- pricing: { inputPerM: 0.075, outputPerM: 0.25, cacheReadPerM: 0.015, cacheWritePerM: 0, asOf: "2026-09-09", source: "https://openrouter.ai/api/v1/models" },
450
- },
512
+ capabilitiesAsOf: "2026-10-17",
513
+ capabilitiesSource: COMMAND_CODE_EFFORT_SOURCE,
514
+ pricing: null,
515
+ })),
451
516
  ];
517
+ /** Every row of the table, for the gate that holds the whole table to its
518
+ * contract (shape, dates, sources, unique keys). Internal; read-only. */
519
+ export function modelMetadataRows() {
520
+ return ENTRIES;
521
+ }
452
522
  /**
453
523
  * Look a model up. `endpoint` narrows: an entry WITH an endpoint only
454
524
  * matches when the caller's endpoint origin equals it; an entry without
455
525
  * one matches any endpoint. Unknown model → null, never a default.
456
526
  */
457
527
  export function lookupModelMetadata(model, endpoint) {
528
+ const base = lookupRegistryRow(model, endpoint);
529
+ const declared = DECLARED_EFFORT.get(declarationKey(model, endpoint));
530
+ if (declared === undefined)
531
+ return base;
532
+ // a profile's declaration wins over the registry's effort for that
533
+ // (model, endpoint); the rest of the row (window, price, thinking modes)
534
+ // stays the registry's, and a model the registry does not know becomes a
535
+ // row that claims nothing but the declared levels
536
+ const reasoning = {
537
+ emitsThinkingStream: base?.capabilities.reasoning?.emitsThinkingStream ?? null,
538
+ thinking: base?.capabilities.reasoning?.thinking ?? null,
539
+ effort: declared,
540
+ ...(base?.capabilities.reasoning?.forbidden !== undefined ? { forbidden: base.capabilities.reasoning.forbidden } : {}),
541
+ asOf: null,
542
+ source: "declared in the profile",
543
+ };
544
+ if (base !== null)
545
+ return { ...base, capabilities: { ...base.capabilities, reasoning } };
546
+ return {
547
+ model,
548
+ ...(endpoint !== undefined ? { endpoint: originOf(endpoint) } : {}),
549
+ capabilities: { contextWindow: null, maxOutputTokens: null, promptCaching: null, reasoning, inputModalities: null },
550
+ pricing: null,
551
+ };
552
+ }
553
+ function lookupRegistryRow(model, endpoint) {
458
554
  for (const entry of ENTRIES) {
459
555
  if (entry.model !== model)
460
556
  continue;
@@ -464,6 +560,18 @@ export function lookupModelMetadata(model, endpoint) {
464
560
  }
465
561
  return null;
466
562
  }
563
+ /** A profile's own effort levels (the CLI reads them from the user's config
564
+ * at startup). Keyed by (model, endpoint origin); a later declaration for
565
+ * the same key replaces the earlier. Process-wide, like the table. */
566
+ const DECLARED_EFFORT = new Map();
567
+ const declarationKey = (model, endpoint) => `${model}@${endpoint === undefined ? "" : originOf(endpoint)}`;
568
+ export function declareEffort(model, endpoint, effort) {
569
+ DECLARED_EFFORT.set(declarationKey(model, endpoint), { ...effort, wire: effort.wire ?? "reasoning_effort", declared: true });
570
+ }
571
+ /** For tests and a config reload: forget every declaration. */
572
+ export function clearDeclaredEfforts() {
573
+ DECLARED_EFFORT.clear();
574
+ }
467
575
  /**
468
576
  * CW-1 — names one set of weights answers to across routes, each a dated
469
577
  * vendor statement. The key is the FOLDED name a route serves (vendor
@@ -473,6 +581,11 @@ const MODEL_ALIASES = {
473
581
  // The pricing table: `deepseek-flash` IS DeepSeek-V4.1-Flash. Gateways
474
582
  // serve it by the version name (`deepseek-v4.1-flash`).
475
583
  "deepseek-v4.1-flash": { is: "deepseek-flash", asOf: "2026-09-17", source: "https://api-docs.deepseek.com/quick_start/pricing/" },
584
+ // OpenRouter spells the 5.5 line with a dot; the vendor's ids carry dashes
585
+ // (the models overview, read 2026-10-17). Same weights.
586
+ "claude-opus-5.5": { is: "claude-opus-5-5", asOf: "2026-10-17", source: "https://platform.claude.com/docs/en/models/overview" },
587
+ "claude-sonnet-5.5": { is: "claude-sonnet-5-5", asOf: "2026-10-17", source: "https://platform.claude.com/docs/en/models/overview" },
588
+ "claude-haiku-5.5": { is: "claude-haiku-5-5", asOf: "2026-10-17", source: "https://platform.claude.com/docs/en/models/overview" },
476
589
  };
477
590
  /** CW-1: the aliases, for the gate that holds each one to a dated source
478
591
  * and a registered target. */
@@ -565,7 +678,7 @@ export function resolveReasoning(model, setting, endpoint) {
565
678
  const levels = r.effort === null ? "none known" : r.effort.levels.join("/");
566
679
  return { ok: false, reason: `${model} does not support effort "${setting.effort}" (native: ${levels})` };
567
680
  }
568
- wire.effort = setting.effort;
681
+ wire.effort = r.effort.values?.[setting.effort] ?? setting.effort;
569
682
  }
570
683
  for (const f of r.forbidden ?? []) {
571
684
  if (f.thinking === setting.thinking && f.effort === setting.effort) {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * GENERATED by scripts/registry-or-rows.mjs from https://openrouter.ai/api/v1/models on 2026-10-17.
3
+ * Do not edit by hand: run the script and commit the diff. The rules for what
4
+ * a row says (and does not) are in the script's header.
5
+ */
6
+ import type { ModelMetadataEntry } from "./metadata.js";
7
+ export declare const OPENROUTER_ROWS: readonly ModelMetadataEntry[];
@@ -0,0 +1,14 @@
1
+ export const OPENROUTER_ROWS = [
2
+ { "model": "anthropic/claude-opus-5.5", "endpoint": "https://openrouter.ai", "capabilities": { "contextWindow": 1000000, "maxOutputTokens": 128000, "promptCaching": null, "reasoning": { "emitsThinkingStream": null, "thinking": null, "effort": { "levels": ["low", "medium", "high", "xhigh", "max"], "default": null, "wire": "reasoning_effort" }, "asOf": "2026-10-17", "source": "https://openrouter.ai/api/v1/models" }, "inputModalities": ["text", "image", "file"] }, "capabilitiesAsOf": "2026-10-17", "capabilitiesSource": "https://openrouter.ai/api/v1/models", "pricing": null },
3
+ { "model": "anthropic/claude-sonnet-5.5", "endpoint": "https://openrouter.ai", "capabilities": { "contextWindow": 1000000, "maxOutputTokens": 128000, "promptCaching": null, "reasoning": { "emitsThinkingStream": null, "thinking": null, "effort": { "levels": ["low", "medium", "high", "xhigh", "max"], "default": null, "wire": "reasoning_effort" }, "asOf": "2026-10-17", "source": "https://openrouter.ai/api/v1/models" }, "inputModalities": ["text", "image", "file"] }, "capabilitiesAsOf": "2026-10-17", "capabilitiesSource": "https://openrouter.ai/api/v1/models", "pricing": null },
4
+ { "model": "anthropic/claude-haiku-5.5", "endpoint": "https://openrouter.ai", "capabilities": { "contextWindow": 1000000, "maxOutputTokens": 128000, "promptCaching": null, "reasoning": { "emitsThinkingStream": null, "thinking": null, "effort": { "levels": ["none", "low", "medium", "high", "xhigh", "max"], "default": null, "wire": "reasoning_effort" }, "asOf": "2026-10-17", "source": "https://openrouter.ai/api/v1/models" }, "inputModalities": ["text", "image", "file"] }, "capabilitiesAsOf": "2026-10-17", "capabilitiesSource": "https://openrouter.ai/api/v1/models", "pricing": null },
5
+ { "model": "openai/gpt-6.1-sol", "endpoint": "https://openrouter.ai", "capabilities": { "contextWindow": 1050000, "maxOutputTokens": 128000, "promptCaching": null, "reasoning": { "emitsThinkingStream": null, "thinking": null, "effort": { "levels": ["low", "medium", "high", "xhigh", "max"], "default": null, "wire": "reasoning_effort" }, "asOf": "2026-10-17", "source": "https://openrouter.ai/api/v1/models" }, "inputModalities": ["file", "image", "text"] }, "capabilitiesAsOf": "2026-10-17", "capabilitiesSource": "https://openrouter.ai/api/v1/models", "pricing": null },
6
+ { "model": "openai/gpt-6-sol", "endpoint": "https://openrouter.ai", "capabilities": { "contextWindow": 1050000, "maxOutputTokens": 128000, "promptCaching": null, "reasoning": { "emitsThinkingStream": null, "thinking": null, "effort": { "levels": ["none", "low", "medium", "high", "xhigh", "max"], "default": null, "wire": "reasoning_effort" }, "asOf": "2026-10-17", "source": "https://openrouter.ai/api/v1/models" }, "inputModalities": ["file", "image", "text"] }, "capabilitiesAsOf": "2026-10-17", "capabilitiesSource": "https://openrouter.ai/api/v1/models", "pricing": null },
7
+ { "model": "openai/gpt-6-luna", "endpoint": "https://openrouter.ai", "capabilities": { "contextWindow": 1050000, "maxOutputTokens": 128000, "promptCaching": null, "reasoning": { "emitsThinkingStream": null, "thinking": null, "effort": { "levels": ["none", "low", "medium", "high", "xhigh", "max"], "default": null, "wire": "reasoning_effort" }, "asOf": "2026-10-17", "source": "https://openrouter.ai/api/v1/models" }, "inputModalities": ["file", "image", "text"] }, "capabilitiesAsOf": "2026-10-17", "capabilitiesSource": "https://openrouter.ai/api/v1/models", "pricing": null },
8
+ { "model": "deepseek/deepseek-v4.1-flash", "endpoint": "https://openrouter.ai", "capabilities": { "contextWindow": 1048576, "maxOutputTokens": null, "promptCaching": null, "reasoning": { "emitsThinkingStream": null, "thinking": null, "effort": { "levels": ["none", "low", "high", "max"], "default": null, "wire": "reasoning_effort" }, "asOf": "2026-10-17", "source": "https://openrouter.ai/api/v1/models" }, "inputModalities": ["text", "image"] }, "capabilitiesAsOf": "2026-10-17", "capabilitiesSource": "https://openrouter.ai/api/v1/models", "pricing": null },
9
+ { "model": "x-ai/grok-4.7", "endpoint": "https://openrouter.ai", "capabilities": { "contextWindow": 500000, "maxOutputTokens": null, "promptCaching": null, "reasoning": { "emitsThinkingStream": null, "thinking": null, "effort": { "levels": ["low", "medium", "high", "xhigh"], "default": null, "wire": "reasoning_effort" }, "asOf": "2026-10-17", "source": "https://openrouter.ai/api/v1/models" }, "inputModalities": ["text", "image", "file"] }, "capabilitiesAsOf": "2026-10-17", "capabilitiesSource": "https://openrouter.ai/api/v1/models", "pricing": null },
10
+ { "model": "z-ai/glm-5.3-flash", "endpoint": "https://openrouter.ai", "capabilities": { "contextWindow": 1048576, "maxOutputTokens": null, "promptCaching": "automatic", "reasoning": { "emitsThinkingStream": true, "thinking": null, "effort": { "levels": ["low", "high", "max"], "default": null, "wire": "reasoning_effort" }, "asOf": "2026-10-17", "source": "https://openrouter.ai/api/v1/models" }, "inputModalities": ["text", "image", "video"] }, "capabilitiesAsOf": "2026-10-17", "capabilitiesSource": "https://openrouter.ai/api/v1/models", "pricing": null },
11
+ { "model": "moonshotai/kimi-k3", "endpoint": "https://openrouter.ai", "capabilities": { "contextWindow": 1048576, "maxOutputTokens": null, "promptCaching": null, "reasoning": { "emitsThinkingStream": null, "thinking": null, "effort": { "levels": ["none", "low", "high", "max"], "default": null, "wire": "reasoning_effort" }, "asOf": "2026-10-17", "source": "https://openrouter.ai/api/v1/models" }, "inputModalities": ["text", "image", "video"] }, "capabilitiesAsOf": "2026-10-17", "capabilitiesSource": "https://openrouter.ai/api/v1/models", "pricing": null },
12
+ { "model": "xiaomi/mimo-v2.6-flash", "endpoint": "https://openrouter.ai", "capabilities": { "contextWindow": 1050000, "maxOutputTokens": 131072, "promptCaching": null, "reasoning": null, "inputModalities": ["text", "image", "video", "audio"] }, "capabilitiesAsOf": "2026-10-17", "capabilitiesSource": "https://openrouter.ai/api/v1/models", "pricing": null },
13
+ { "model": "google/gemini-4-argon", "endpoint": "https://openrouter.ai", "capabilities": { "contextWindow": 1048576, "maxOutputTokens": 65536, "promptCaching": null, "reasoning": { "emitsThinkingStream": null, "thinking": null, "effort": { "levels": ["low", "medium", "high"], "default": null, "wire": "reasoning_effort" }, "asOf": "2026-10-17", "source": "https://openrouter.ai/api/v1/models" }, "inputModalities": ["audio", "file", "image", "text", "video"] }, "capabilitiesAsOf": "2026-10-17", "capabilitiesSource": "https://openrouter.ai/api/v1/models", "pricing": null },
14
+ ];
package/dist/session.d.ts CHANGED
@@ -31,6 +31,7 @@ import { EventLog, type AbortSignalLike, type Adapter, type Event, type KisoExte
31
31
  import { type OverflowMeasure } from "./overflow-belt.js";
32
32
  import { type ContinuationScope } from "@vincemakes/kiso-core";
33
33
  import { type ReasoningSetting } from "./provider/metadata.js";
34
+ import type { CheckpointSection } from "./summarize.js";
34
35
  import { type SessionStore, type StoreRecord } from "./store.js";
35
36
  import { Run } from "./run.js";
36
37
  import { type TaskDeliveryOptions } from "./tasks/delivery.js";
@@ -432,6 +433,17 @@ export declare class AgentSession {
432
433
  * /compact affordance point.
433
434
  */
434
435
  export interface ContextPolicy {
436
+ /**
437
+ * 0.51.0 — the sections of every checkpoint this session writes (the run-start
438
+ * auto-summary, the in-run tiers, the manual compact). Static data: a list of
439
+ * `{ heading, instruction }`, rendered in place of the coding outline; EVERY
440
+ * listed heading must appear as its own `## <heading>` line or the checkpoint
441
+ * is rejected (the usual route: serialized retry, then the breaker). Absent:
442
+ * the coding outline of 0.50.0, byte for byte. Validated and copied when the
443
+ * agent or session is constructed; extend the default with
444
+ * `[...DEFAULT_CHECKPOINT_SECTIONS, { heading, instruction }]`.
445
+ */
446
+ readonly checkpointSections?: readonly CheckpointSection[];
435
447
  /**
436
448
  * A — the auto-summary: at run start, when the projected context
437
449
  * crosses the trigger AND enough uncovered rounds exist, the
package/dist/session.js CHANGED
@@ -35,7 +35,7 @@ import { denialResult } from "@vincemakes/kiso-core";
35
35
  import { buildProfile, readProfile, writeProfile, writeSummary } from "./profile.js";
36
36
  import { summarizeEvents } from "./session-summary.js";
37
37
  import { lookupModelMetadata, resolveReasoning } from "./provider/metadata.js";
38
- import { DROP_PLACEHOLDER, estimateSummarySavings, KEEP_RECENT_ROUNDS, KEEP_TOKENS_DEFAULT, lastSummaryPoint, MAX_SUMMARY_FAILURES, policyTriggerFromWindow, serializeCovered, MANUAL_SUMMARY_BUDGET, SUMMARY_MAX_OUTPUT, summarizeConversation, SummaryBudgetExhausted, summaryBoundarySeq, } from "./summarize.js";
38
+ import { DROP_PLACEHOLDER, estimateSummarySavings, KEEP_RECENT_ROUNDS, KEEP_TOKENS_DEFAULT, lastSummaryPoint, MAX_SUMMARY_FAILURES, policyTriggerFromWindow, serializeCovered, MANUAL_SUMMARY_BUDGET, SUMMARY_MAX_OUTPUT, resolveCheckpoint, snapshotCheckpointSections, summarizeConversation, SummaryBudgetExhausted, summaryBoundarySeq, } from "./summarize.js";
39
39
  import { canonicalizeUsageForModel } from "./usage/canonical.js";
40
40
  import { contextAnchor } from "./context-anchor.js";
41
41
  import { appendFileSync, mkdirSync } from "node:fs";
@@ -102,6 +102,8 @@ export class AgentSession {
102
102
  // constructor, setAdapter, and setModelBinding are the only writers).
103
103
  #adapter;
104
104
  #config;
105
+ /** 0.51.0: the checkpoint every summary call is built from — resolved ONCE at construction from a snapshot of `contextPolicy.checkpointSections`; the raw host array is never read again. */
106
+ #checkpoint;
105
107
  // PH-1a (finding PH-F8, P0): the LIVE model binding. #config froze the
106
108
  // startup model/provider, so a /model switch replaced the adapter while
107
109
  // every later run kept sending the OLD model id and canonicalizing
@@ -193,6 +195,10 @@ export class AgentSession {
193
195
  }
194
196
  const composedHooks = composeHooks(config.hooks, config.extensions ?? []);
195
197
  this.#config = composedHooks === undefined ? config : { ...config, hooks: composedHooks };
198
+ // 0.51.0: validate and snapshot the host's checkpoint sections here, at
199
+ // construction (a direct `new AgentSession` included); an invalid list throws.
200
+ const sections = snapshotCheckpointSections(config.contextPolicy?.checkpointSections);
201
+ this.#checkpoint = sections === undefined ? undefined : resolveCheckpoint(sections);
196
202
  this.#model = config.model;
197
203
  this.#provider = config.provider;
198
204
  this.#baseUrl = config.baseUrl;
@@ -415,6 +421,7 @@ export class AgentSession {
415
421
  await this.#config.hooks?.onRetry?.(info, {});
416
422
  },
417
423
  ...(args.onProgress !== undefined ? { onProgress: args.onProgress } : {}),
424
+ ...(this.#checkpoint !== undefined ? { checkpoint: this.#checkpoint } : {}),
418
425
  };
419
426
  try {
420
427
  // The in-band call carries the run's whole context, so the overflow
@@ -159,8 +159,57 @@ export declare const DSML_MARKERS: readonly ["<tool_call", "<tool_use", "<invoke
159
159
  * guards the trust-critical tail). */
160
160
  export declare const REQUIRED_SECTIONS: readonly ["## Current work", "## Next steps"];
161
161
  /** null = pass; an error string = reject. Empty text is the existing
162
- * no-text rule's domain, reported here too (defense in depth). */
163
- export declare function validateSummary(text: string): string | null;
162
+ * no-text rule's domain, reported here too (defense in depth).
163
+ * `headings` (a host's `checkpointSections`, canonical form): when given,
164
+ * EVERY listed heading must appear as its own line (`## <heading>`,
165
+ * trimmed) instead of the two default tail sections matched by substring —
166
+ * `## Story` is a substring of `## Storyboard`, so the custom path cannot
167
+ * match that way. */
168
+ export declare function validateSummary(text: string, headings?: readonly string[]): string | null;
169
+ /**
170
+ * 0.51.0 — host-defined checkpoint sections (plan-checkpoint-sections rev 2.1;
171
+ * ADR-0055 Amendment). Kiso owns the mechanics of a checkpoint — when it is
172
+ * written, its boundary, its validation, its retries; the host owns what the
173
+ * checkpoint records. The list is static data: a callback could read time,
174
+ * state or spend, and two requests from one configuration would differ.
175
+ */
176
+ export interface CheckpointSection {
177
+ /** One line, rendered as `## <heading>`. */
178
+ readonly heading: string;
179
+ /** What the section must hold; rendered under the heading. */
180
+ readonly instruction: string;
181
+ }
182
+ /** The seven sections of the coding outline, as data. Deep-frozen: a host
183
+ * extends it with `[...DEFAULT_CHECKPOINT_SECTIONS, { heading, instruction }]`
184
+ * and cannot rewrite the shared default. */
185
+ export declare const DEFAULT_CHECKPOINT_SECTIONS: readonly CheckpointSection[];
186
+ /** The kiso-owned rule the CUSTOM path adds: strict every-section validation
187
+ * must not push the model to invent content for a section with nothing in it.
188
+ * The default path does not carry it — its bytes stay 0.50.0's. */
189
+ export declare const CHECKPOINT_NONE_RECORDED_RULE = "Every listed section must appear. If the covered conversation holds nothing for a section, write that none is recorded; never invent content to fill it.";
190
+ /** The custom-path checkpoint prompt for `sections` (canonical form). */
191
+ export declare function renderCheckpointPrompt(sections: readonly CheckpointSection[]): string;
192
+ /** The rendered sections block's cap, UTF-8 bytes. */
193
+ export declare const MAX_CHECKPOINT_SECTIONS_BYTES = 8192;
194
+ /**
195
+ * Validate and snapshot a host's list: a deep copy, trimmed, frozen at both
196
+ * levels, or `undefined` for an absent list. Throws
197
+ * `Invalid contextPolicy.checkpointSections: …` — fail fast, never truncate.
198
+ * The snapshot is taken at construction (the runtime's, and a direct
199
+ * session's); the raw host array is never read again.
200
+ */
201
+ export declare function snapshotCheckpointSections(input: readonly CheckpointSection[] | undefined): readonly CheckpointSection[] | undefined;
202
+ /** What one summary call is built from: the system prompt of the serialized
203
+ * form, the instruction of the in-band form, and the headings validation
204
+ * requires (`null` = the default two-section substring rule). */
205
+ export interface ResolvedCheckpoint {
206
+ readonly prompt: string;
207
+ readonly inBand: string;
208
+ readonly headings: readonly string[] | null;
209
+ }
210
+ /** Absent sections return the EXISTING constants (the path stays byte-identical
211
+ * to 0.50.0), not a rebuild of them. `sections` must already be a snapshot. */
212
+ export declare function resolveCheckpoint(sections?: readonly CheckpointSection[]): ResolvedCheckpoint;
164
213
  export interface SummarizeConversationOptions {
165
214
  readonly adapter: Adapter;
166
215
  readonly model: string;
@@ -196,6 +245,9 @@ export interface SummarizeConversationOptions {
196
245
  * the final usage arrive. Observation only: a throw is swallowed, and
197
246
  * the request is identical with or without it. */
198
247
  readonly onProgress?: (progress: SummaryProgress) => void;
248
+ /** 0.51.0: the checkpoint the call is built from (`resolveCheckpoint`).
249
+ * Absent, the coding outline of 0.50.0, byte for byte. */
250
+ readonly checkpoint?: ResolvedCheckpoint;
199
251
  }
200
252
  /**
201
253
  * 0.40.0 — the summary call's progress against its output budget.
package/dist/summarize.js CHANGED
@@ -243,8 +243,13 @@ export const DSML_MARKERS = ["<tool_call", "<tool_use", "<invoke", "tool_calls",
243
243
  * guards the trust-critical tail). */
244
244
  export const REQUIRED_SECTIONS = ["## Current work", "## Next steps"];
245
245
  /** null = pass; an error string = reject. Empty text is the existing
246
- * no-text rule's domain, reported here too (defense in depth). */
247
- export function validateSummary(text) {
246
+ * no-text rule's domain, reported here too (defense in depth).
247
+ * `headings` (a host's `checkpointSections`, canonical form): when given,
248
+ * EVERY listed heading must appear as its own line (`## <heading>`,
249
+ * trimmed) instead of the two default tail sections matched by substring —
250
+ * `## Story` is a substring of `## Storyboard`, so the custom path cannot
251
+ * match that way. */
252
+ export function validateSummary(text, headings) {
248
253
  const trimmed = text.trim();
249
254
  if (trimmed === "")
250
255
  return "the summary is empty";
@@ -253,12 +258,117 @@ export function validateSummary(text) {
253
258
  if (lower.includes(marker))
254
259
  return `the summary carries a tool-call marker (${marker}) — reject`;
255
260
  }
261
+ if (headings !== undefined) {
262
+ const lines = new Set(trimmed.split(/\r?\n/).map((l) => l.trim()));
263
+ for (const h of headings) {
264
+ if (!lines.has(`## ${h}`))
265
+ return `the summary is missing the required section ## ${h} — a truncated or incomplete checkpoint`;
266
+ }
267
+ return null;
268
+ }
256
269
  for (const section of REQUIRED_SECTIONS) {
257
270
  if (!trimmed.includes(section))
258
271
  return `the summary is missing the required section ${section} — a truncated or incomplete checkpoint`;
259
272
  }
260
273
  return null;
261
274
  }
275
+ /** The seven sections of the coding outline, as data. Deep-frozen: a host
276
+ * extends it with `[...DEFAULT_CHECKPOINT_SECTIONS, { heading, instruction }]`
277
+ * and cannot rewrite the shared default. */
278
+ export const DEFAULT_CHECKPOINT_SECTIONS = Object.freeze([
279
+ { heading: "Goal", instruction: "The user's goal and the acceptance criterion, in one or two sentences." },
280
+ { heading: "Constraints", instruction: "The constraints, requirements, and rulings the work must honor." },
281
+ {
282
+ heading: "User requests",
283
+ instruction: `Every user request of the task — those an earlier checkpoint lists and
284
+ those in the covered range — enumerated one by one, each with what it
285
+ asked for and what was done about it.`,
286
+ },
287
+ {
288
+ heading: "Files and changes",
289
+ instruction: `Every file touched — exact paths, what changed, and why. Include the
290
+ precise code-level changes later turns may need to continue.`,
291
+ },
292
+ {
293
+ heading: "Errors and fixes",
294
+ instruction: `Every error encountered and its resolution; commands run and their
295
+ outcomes.`,
296
+ },
297
+ {
298
+ heading: "Current work",
299
+ instruction: `The current state of the work — what is done, what is not. Quote the
300
+ current task's criterion VERBATIM if one exists.`,
301
+ },
302
+ { heading: "Next steps", instruction: "The concrete next steps, in order." },
303
+ ].map((x) => Object.freeze(x)));
304
+ /** The kiso-owned rule the CUSTOM path adds: strict every-section validation
305
+ * must not push the model to invent content for a section with nothing in it.
306
+ * The default path does not carry it — its bytes stay 0.50.0's. */
307
+ export const CHECKPOINT_NONE_RECORDED_RULE = "Every listed section must appear. If the covered conversation holds nothing for a section, write that none is recorded; never invent content to fill it.";
308
+ const SECTIONS_LEAD = "Produce the checkpoint with exactly these sections, in this order:\n\n";
309
+ // The frame is cut from the constant itself — the host's sections replace only
310
+ // the block between these two marks, never a copy of the frame.
311
+ const FRAME_HEAD = SUMMARY_PROMPT.slice(0, SUMMARY_PROMPT.indexOf(SECTIONS_LEAD) + SECTIONS_LEAD.length);
312
+ const FRAME_TAIL = SUMMARY_PROMPT.slice(SUMMARY_PROMPT.indexOf("Preserve concrete identifiers VERBATIM"));
313
+ const renderSectionBlocks = (sections) => sections.map((x) => `## ${x.heading}\n${x.instruction}`).join("\n\n");
314
+ /** The custom-path checkpoint prompt for `sections` (canonical form). */
315
+ export function renderCheckpointPrompt(sections) {
316
+ return `${FRAME_HEAD}${renderSectionBlocks(sections)}\n\n${CHECKPOINT_NONE_RECORDED_RULE}\n\n${FRAME_TAIL}`;
317
+ }
318
+ const MAX_SECTIONS = 16;
319
+ const MAX_HEADING_CHARS = 80;
320
+ /** The rendered sections block's cap, UTF-8 bytes. */
321
+ export const MAX_CHECKPOINT_SECTIONS_BYTES = 8192;
322
+ /**
323
+ * Validate and snapshot a host's list: a deep copy, trimmed, frozen at both
324
+ * levels, or `undefined` for an absent list. Throws
325
+ * `Invalid contextPolicy.checkpointSections: …` — fail fast, never truncate.
326
+ * The snapshot is taken at construction (the runtime's, and a direct
327
+ * session's); the raw host array is never read again.
328
+ */
329
+ export function snapshotCheckpointSections(input) {
330
+ if (input === undefined)
331
+ return undefined;
332
+ const fail = (why) => {
333
+ throw new Error(`Invalid contextPolicy.checkpointSections: ${why}`);
334
+ };
335
+ if (!Array.isArray(input))
336
+ return fail("it must be an array");
337
+ if (input.length < 1 || input.length > MAX_SECTIONS)
338
+ return fail(`it must hold 1 to ${MAX_SECTIONS} sections, got ${input.length}`);
339
+ const seen = new Set();
340
+ const out = input.map((x, i) => {
341
+ const heading = typeof x?.heading === "string" ? x.heading.trim() : "";
342
+ const instruction = typeof x?.instruction === "string" ? x.instruction.trim() : "";
343
+ if (heading === "")
344
+ return fail(`section ${i + 1} has an empty heading`);
345
+ if (/[\r\n]/.test(heading))
346
+ return fail(`section ${i + 1}: the heading must be one line`);
347
+ if (heading.length > MAX_HEADING_CHARS)
348
+ return fail(`section ${i + 1}: the heading is ${heading.length} characters, the cap is ${MAX_HEADING_CHARS}`);
349
+ if (heading.startsWith("#"))
350
+ return fail(`section ${i + 1}: the heading must not start with "#" (kiso adds "## ")`);
351
+ const key = heading.toLowerCase();
352
+ if (seen.has(key))
353
+ return fail(`section ${i + 1}: the heading "${heading}" repeats an earlier one`);
354
+ seen.add(key);
355
+ if (instruction === "")
356
+ return fail(`section ${i + 1} ("${heading}") has an empty instruction`);
357
+ return Object.freeze({ heading, instruction });
358
+ });
359
+ const bytes = Buffer.byteLength(renderSectionBlocks(out), "utf8");
360
+ if (bytes > MAX_CHECKPOINT_SECTIONS_BYTES)
361
+ fail(`the rendered sections are ${bytes} bytes, the cap is ${MAX_CHECKPOINT_SECTIONS_BYTES}`);
362
+ return Object.freeze(out);
363
+ }
364
+ /** Absent sections return the EXISTING constants (the path stays byte-identical
365
+ * to 0.50.0), not a rebuild of them. `sections` must already be a snapshot. */
366
+ export function resolveCheckpoint(sections) {
367
+ if (sections === undefined)
368
+ return { prompt: SUMMARY_PROMPT, inBand: SUMMARY_IN_BAND, headings: null };
369
+ const prompt = renderCheckpointPrompt(sections);
370
+ return { prompt, inBand: `${SUMMARY_IN_BAND.slice(0, SUMMARY_IN_BAND.length - SUMMARY_PROMPT.length)}${prompt}`, headings: sections.map((x) => x.heading) };
371
+ }
262
372
  /** A thrown value the adapter CLASSIFIED — `{ code, retryable, message }`
263
373
  * (ADR-0005 rule 1). The core's own guard is private, and this asks a
264
374
  * narrower question than that one: is this a failure someone already
@@ -385,12 +495,13 @@ async function summaryAttempt(options) {
385
495
  const budget = options.maxOutputTokens ?? null;
386
496
  report({ produced: 0, budget, reasoningUnseen: false, reported: false });
387
497
  const inBand = options.inBand;
498
+ const cp = options.checkpoint;
388
499
  for await (const ev of adapter.stream({
389
500
  model,
390
501
  ...(inBand === undefined
391
- ? { messages, systemPrompt: SUMMARY_PROMPT }
502
+ ? { messages, systemPrompt: cp?.prompt ?? SUMMARY_PROMPT }
392
503
  : {
393
- messages: [...messages, { role: "user", content: inBand.focus === undefined ? SUMMARY_IN_BAND : `${SUMMARY_IN_BAND}\n\nFocus the summary on: ${inBand.focus}` }],
504
+ messages: [...messages, { role: "user", content: inBand.focus === undefined ? (cp?.inBand ?? SUMMARY_IN_BAND) : `${cp?.inBand ?? SUMMARY_IN_BAND}\n\nFocus the summary on: ${inBand.focus}` }],
394
505
  ...(inBand.systemPrompt !== undefined ? { systemPrompt: inBand.systemPrompt } : {}),
395
506
  tools: inBand.tools,
396
507
  }),
@@ -453,7 +564,7 @@ async function summaryAttempt(options) {
453
564
  }
454
565
  // E6 (b): a non-checkpoint summary is an honest failure — throw, the
455
566
  // caller reports it, nothing is persisted (the auto-T5-1 regression).
456
- const invalid = validateSummary(trimmed);
567
+ const invalid = validateSummary(trimmed, cp?.headings ?? undefined);
457
568
  if (invalid !== null) {
458
569
  throw new Error(`the summary call produced an invalid summary: ${invalid}`);
459
570
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-runtime",
3
- "version": "0.50.1",
3
+ "version": "0.51.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.50.1"
28
+ "@vincemakes/kiso-core": "0.51.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "@vincemakes/kiso-provider-anthropic": "0.50.1",
32
- "@vincemakes/kiso-provider-openai": "0.50.1",
33
- "@vincemakes/kiso-provider-openai-responses": "0.50.1"
31
+ "@vincemakes/kiso-provider-anthropic": "0.51.0",
32
+ "@vincemakes/kiso-provider-openai": "0.51.0",
33
+ "@vincemakes/kiso-provider-openai-responses": "0.51.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.50.1",
47
+ "@vincemakes/kiso-evals": "0.51.0",
48
48
  "@types/node": "^26.1.2",
49
49
  "typescript": "^5.7.2",
50
50
  "vitest": "^3.0.0"