@vincemakes/kiso-runtime 0.40.2 → 0.40.3

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
@@ -87,6 +87,7 @@ export class AgentRuntime {
87
87
  });
88
88
  let restored = null;
89
89
  let profilePending = false;
90
+ let newSession = null;
90
91
  let driftAcknowledgement = null;
91
92
  if (meta.kind === "ok") {
92
93
  const drift = assessProfileDrift(meta.profile, {
@@ -134,8 +135,10 @@ export class AgentRuntime {
134
135
  }
135
136
  }
136
137
  else if (log.all.length === 0) {
137
- // a NEW session: revision 1 lands BEFORE any durable event.
138
- writeProfile(store.root, options.id, { ...candidate, revision: 1 });
138
+ // a NEW session. DC-60: nothing of it reaches the disk before its
139
+ // first durable event — revision 1 (with the workspace it opened in)
140
+ // lands WITH that event, still before it (session.ts persist).
141
+ newSession = { workspace: candidate.workspace };
139
142
  }
140
143
  else {
141
144
  // legacy (pre-XP log, no sidecar): generation absence is not
@@ -154,6 +157,7 @@ export class AgentRuntime {
154
157
  : {}),
155
158
  ...(restored !== null ? { reasoning: restored.reasoning } : {}),
156
159
  ...(profilePending ? { profilePending: true } : {}),
160
+ ...(newSession !== null ? { newSession } : {}),
157
161
  ...(this.#definition.profileName !== undefined ? { profileName: this.#definition.profileName } : {}),
158
162
  ...(driftAcknowledgement !== null ? { driftAcknowledgement } : {}),
159
163
  ...(this.#definition.systemPrompt !== undefined ? { systemPrompt: this.#definition.systemPrompt } : {}),
@@ -126,6 +126,48 @@ export interface ModelMetadataEntry {
126
126
  * one matches any endpoint. Unknown model → null, never a default.
127
127
  */
128
128
  export declare function lookupModelMetadata(model: string, endpoint?: string): ModelMetadataEntry | null;
129
+ /** CW-1: the aliases, for the gate that holds each one to a dated source
130
+ * and a registered target. */
131
+ export declare function modelAliases(): Readonly<Record<string, {
132
+ readonly is: string;
133
+ readonly asOf: string;
134
+ readonly source: string;
135
+ }>>;
136
+ /** CW-1: a model id reduced to the weights it names — `deepseek/DeepSeek-V4.1-Flash`
137
+ * and `deepseek-v4.1-flash` are both `deepseek-flash`. A variant suffix
138
+ * (`:free`) is NOT folded: a variant may serve a smaller window. */
139
+ export declare function modelIdentity(model: string): string;
140
+ /** Where a resolved window came from — the source a display names. */
141
+ export type ContextWindowSource = "route" | "upstream" | "model";
142
+ export interface ResolvedContextWindow {
143
+ readonly tokens: number;
144
+ readonly source: ContextWindowSource;
145
+ /** the registry row's model id the figure was read from */
146
+ readonly from: string;
147
+ }
148
+ /**
149
+ * CW-1 (owner, 2026-09-23) — the context window for a route the registry
150
+ * has no row for.
151
+ *
152
+ * Rows are keyed by (model, endpoint) because a route can answer
153
+ * differently from the vendor — gpt-5.5 is 1,050,000 at the first-party
154
+ * API and 272,000 at the subscription backend. The same key made every
155
+ * forwarder, relay and gateway a miss: `deepseek-v4.1-flash` behind a local
156
+ * forwarder to a model gateway read `ctx ?`, and the tiers compacted a 1M model
157
+ * against the CLI's 200K fallback. A window belongs to the weights; a
158
+ * route can only cap it lower. In order:
159
+ *
160
+ * route — this model's row at this endpoint (lookupModelMetadata);
161
+ * upstream — its row at the endpoint a forwarder names as its upstream;
162
+ * model — every row of the same model identity, at any endpoint;
163
+ * where rows disagree, the SMALLEST — the one figure no row
164
+ * contradicts.
165
+ *
166
+ * A null window falls through; after the last step, null. Only the window
167
+ * resolves this way: price, reasoning wire fields and max output are
168
+ * facts about a route and stay on lookupModelMetadata.
169
+ */
170
+ export declare function lookupContextWindow(model: string, endpoint?: string, upstream?: string): ResolvedContextWindow | null;
129
171
  /**
130
172
  * XP-1 §4.2 — native-only resolution. The rules, in order:
131
173
  * default/default → NO wire fields (the byte-identity anchor — a
@@ -455,6 +455,70 @@ export function lookupModelMetadata(model, endpoint) {
455
455
  }
456
456
  return null;
457
457
  }
458
+ /**
459
+ * CW-1 — names one set of weights answers to across routes, each a dated
460
+ * vendor statement. The key is the FOLDED name a route serves (vendor
461
+ * prefix off, lower case); the value names the registry row.
462
+ */
463
+ const MODEL_ALIASES = {
464
+ // The pricing table: `deepseek-flash` IS DeepSeek-V4.1-Flash. Gateways
465
+ // serve it by the version name (`deepseek-v4.1-flash`).
466
+ "deepseek-v4.1-flash": { is: "deepseek-flash", asOf: "2026-09-17", source: "https://api-docs.deepseek.com/quick_start/pricing/" },
467
+ };
468
+ /** CW-1: the aliases, for the gate that holds each one to a dated source
469
+ * and a registered target. */
470
+ export function modelAliases() {
471
+ return MODEL_ALIASES;
472
+ }
473
+ /** CW-1: a model id reduced to the weights it names — `deepseek/DeepSeek-V4.1-Flash`
474
+ * and `deepseek-v4.1-flash` are both `deepseek-flash`. A variant suffix
475
+ * (`:free`) is NOT folded: a variant may serve a smaller window. */
476
+ export function modelIdentity(model) {
477
+ const folded = model.slice(model.lastIndexOf("/") + 1).toLowerCase();
478
+ return MODEL_ALIASES[folded]?.is ?? folded;
479
+ }
480
+ /**
481
+ * CW-1 (owner, 2026-09-23) — the context window for a route the registry
482
+ * has no row for.
483
+ *
484
+ * Rows are keyed by (model, endpoint) because a route can answer
485
+ * differently from the vendor — gpt-5.5 is 1,050,000 at the first-party
486
+ * API and 272,000 at the subscription backend. The same key made every
487
+ * forwarder, relay and gateway a miss: `deepseek-v4.1-flash` behind a local
488
+ * forwarder to a model gateway read `ctx ?`, and the tiers compacted a 1M model
489
+ * against the CLI's 200K fallback. A window belongs to the weights; a
490
+ * route can only cap it lower. In order:
491
+ *
492
+ * route — this model's row at this endpoint (lookupModelMetadata);
493
+ * upstream — its row at the endpoint a forwarder names as its upstream;
494
+ * model — every row of the same model identity, at any endpoint;
495
+ * where rows disagree, the SMALLEST — the one figure no row
496
+ * contradicts.
497
+ *
498
+ * A null window falls through; after the last step, null. Only the window
499
+ * resolves this way: price, reasoning wire fields and max output are
500
+ * facts about a route and stay on lookupModelMetadata.
501
+ */
502
+ export function lookupContextWindow(model, endpoint, upstream) {
503
+ const route = lookupModelMetadata(model, endpoint);
504
+ if (route !== null && route.capabilities.contextWindow !== null)
505
+ return { tokens: route.capabilities.contextWindow, source: "route", from: route.model };
506
+ if (upstream !== undefined) {
507
+ const up = lookupModelMetadata(model, upstream);
508
+ if (up !== null && up.capabilities.contextWindow !== null)
509
+ return { tokens: up.capabilities.contextWindow, source: "upstream", from: up.model };
510
+ }
511
+ const identity = modelIdentity(model);
512
+ let found = null;
513
+ for (const entry of ENTRIES) {
514
+ const tokens = entry.capabilities.contextWindow;
515
+ if (tokens === null || modelIdentity(entry.model) !== identity)
516
+ continue;
517
+ if (found === null || tokens < found.tokens)
518
+ found = { tokens, source: "model", from: entry.model };
519
+ }
520
+ return found;
521
+ }
458
522
  function originOf(endpoint) {
459
523
  try {
460
524
  return new URL(endpoint).origin;
package/dist/run.js CHANGED
@@ -73,6 +73,11 @@ export class Run {
73
73
  // here, before any log or disk mutation.
74
74
  this.#session.ensureHealthy();
75
75
  this.#session.beginRun(this);
76
+ // DC-60: a resume of a session with no durable event has nothing to
77
+ // recover — and writes nothing (no trace header, no summary row) for
78
+ // a session that never began. The CLI runs one at every start.
79
+ if (this.#resume && this.#session.log.all.length === 0)
80
+ return;
76
81
  const log = this.#session.log;
77
82
  // The static prompt parts — computed ONCE, before the tracer, and
78
83
  // reused for both the composed string (below) and the rent ledger
package/dist/session.d.ts CHANGED
@@ -501,6 +501,14 @@ export interface ContextPolicy {
501
501
  * caller's own windowTokens counts as stated. Only a stated window
502
502
  * arms the overflow belt. */
503
503
  readonly statedWindow?: () => number | null;
504
+ /** CW-1 batch 2: told when an endpoint's refusal states its cap — the
505
+ * binding it was learned for rides along. The tiers have already
506
+ * taken it (only ever down); the caller keeps it for later sessions. */
507
+ readonly onWindowLearned?: (learned: {
508
+ readonly tokens: number;
509
+ readonly model: string;
510
+ readonly baseUrl?: string;
511
+ }) => void;
504
512
  };
505
513
  }
506
514
  export interface SessionConfig {
@@ -521,6 +529,11 @@ export interface SessionConfig {
521
529
  readonly reasoning?: import("./provider/metadata.js").ReasoningSetting;
522
530
  /** XP-1 internal: a legacy session's deferred revision-1 write. */
523
531
  readonly profilePending?: true;
532
+ /** DC-60 internal: a NEW session — nothing is on disk yet; its revision 1
533
+ * (with the workspace it opened in) lands with its first durable event. */
534
+ readonly newSession?: {
535
+ readonly workspace: string | null;
536
+ };
524
537
  /** 0.40.0: the config profile name in force — recorded per revision. */
525
538
  readonly profileName?: string;
526
539
  /** 0.40.0: set when this open acknowledged material drift. */
package/dist/session.js CHANGED
@@ -30,6 +30,7 @@
30
30
  import { EventLog, projectMessages, SUMMARY_FRAMING, } from "@vincemakes/kiso-core";
31
31
  import { executionLedger } from "./ledger.js";
32
32
  import { overflowBelt } from "./overflow-belt.js";
33
+ import { windowLearner } from "./window-learner.js";
33
34
  import { assessTasks } from "./task-assessment.js";
34
35
  /** TV-1A — the session-level evidence policy: the PURE projection defaults
35
36
  * to ∅ (never inventing evidence); the session names the one built-in
@@ -126,6 +127,8 @@ export class AgentSession {
126
127
  // XP-1: a legacy session records revision 1 at the next explicit
127
128
  // selection or first request — never eagerly at open.
128
129
  #profilePending;
130
+ /** DC-60: set until a NEW session's first durable event lands. */
131
+ #newSession;
129
132
  /** 0.40.0: the config profile name of the live binding (configuration:
130
133
  * each revision records the name in force when it is written). */
131
134
  #profileName;
@@ -199,6 +202,7 @@ export class AgentSession {
199
202
  // CTX-1: starts as the startup policy, then follows the binding.
200
203
  this.#microcompact = config.microcompact;
201
204
  this.#profilePending = config.profilePending === true;
205
+ this.#newSession = config.newSession;
202
206
  this.#profileName = config.profileName ?? null;
203
207
  this.driftAcknowledgement = config.driftAcknowledgement ?? null;
204
208
  }
@@ -241,6 +245,13 @@ export class AgentSession {
241
245
  async persist(runId, event) {
242
246
  this.ensureHealthy();
243
247
  try {
248
+ // DC-60: a NEW session's revision 1 lands with its first durable
249
+ // event and BEFORE it — a log line never exists without its profile.
250
+ if (this.#newSession !== undefined) {
251
+ const { workspace } = this.#newSession;
252
+ this.#newSession = undefined;
253
+ this.#writeProfileRevision(workspace);
254
+ }
244
255
  await this.#store.append(this.id, runId, event);
245
256
  if (event.type === "usage" && event.known)
246
257
  this.#lastUsageAt = Date.now();
@@ -342,8 +353,9 @@ export class AgentSession {
342
353
  * abort is never answered with the second call. Throws when both fail.
343
354
  */
344
355
  async #checkpointCall(args) {
356
+ const learned = this.#learning(this.#adapter);
345
357
  const common = {
346
- adapter: this.#adapter,
358
+ adapter: learned,
347
359
  model: this.#model,
348
360
  maxOutputTokens: args.budget,
349
361
  ...(args.signal !== undefined ? { signal: args.signal } : {}),
@@ -357,7 +369,7 @@ export class AgentSession {
357
369
  // The in-band call carries the run's whole context, so the overflow
358
370
  // belt applies to it; the serialised fallback below is small, and
359
371
  // its failures keep their own classification.
360
- const inBandAdapter = overflowBelt(this.#adapter, () => this.overflowMeasure());
372
+ const inBandAdapter = overflowBelt(learned, () => this.overflowMeasure());
361
373
  const result = await summarizeConversation({ ...common, adapter: inBandAdapter, messages: args.messages, inBand: args.inBand, ...(args.reasoning !== undefined ? { reasoning: args.reasoning } : {}) });
362
374
  return { result, path: "in-band" };
363
375
  }
@@ -499,6 +511,26 @@ export class AgentSession {
499
511
  setMicrocompactThreshold(thresholdTokens) {
500
512
  this.#microcompact = { thresholdTokens };
501
513
  }
514
+ /** CW-1 batch 2: the adapter as a run or a summary uses it — a refusal
515
+ * that states a cap is learned for the binding in force NOW, the one
516
+ * this adapter belongs to (a /model mid-run must not receive it). */
517
+ #learning(adapter) {
518
+ const at = { model: this.#model, baseUrl: this.#baseUrl };
519
+ return windowLearner(adapter, (tokens) => this.#learnWindow(tokens, at));
520
+ }
521
+ /** CW-1 batch 2: a route stated its cap. The in-run tiers take it at once
522
+ * — the kernel's one overflow compaction runs next and must aim at the
523
+ * real window, not past it — and only ever DOWN; the caller is told, to
524
+ * keep it for the next session and to say so. */
525
+ #learnWindow(tokens, at) {
526
+ const tiers = this.#config.contextPolicy?.tiers;
527
+ if (at.model === this.#model && at.baseUrl === this.#baseUrl) {
528
+ const current = this.#tiersWindow ?? tiers?.windowTokens;
529
+ if (current === undefined || tokens < current)
530
+ this.#tiersWindow = tokens;
531
+ }
532
+ tiers?.onWindowLearned?.({ tokens, model: at.model, ...(at.baseUrl !== undefined ? { baseUrl: at.baseUrl } : {}) });
533
+ }
502
534
  /** ADR-0055 Amendment 2 (decision 3): the overflow belt's measure at send
503
535
  * time — null unless a window is STATED and a bill anchors the context. */
504
536
  overflowMeasure() {
@@ -570,6 +602,10 @@ export class AgentSession {
570
602
  * recovery, projection or a request reads it.
571
603
  */
572
604
  recordSummary(open) {
605
+ // DC-60: a session with no durable event has no row — writing one made
606
+ // a session that never began appear in the list.
607
+ if (this.log.all.length === 0)
608
+ return;
573
609
  try {
574
610
  const prior = readProfile(this.#store.root, this.id);
575
611
  writeSummary(this.#store.root, this.id, summarizeEvents(this.log.all, {
@@ -585,8 +621,18 @@ export class AgentSession {
585
621
  }
586
622
  }
587
623
  /** XP-1: record the live binding as the next durable profile revision
588
- * (read-modify-write under the session's single-writer ownership). */
624
+ * (read-modify-write under the session's single-writer ownership).
625
+ * DC-60: a NEW session keeps the binding in memory until its first
626
+ * durable event, which records it (persist). */
589
627
  #recordProfile() {
628
+ if (this.#newSession !== undefined)
629
+ return;
630
+ this.#writeProfileRevision(null);
631
+ }
632
+ /** The next profile revision from the live binding. `startWorkspace` is
633
+ * the workspace a NEW session opened in; with no prior revision and no
634
+ * start (a legacy session's first revision) the start is unknown: null. */
635
+ #writeProfileRevision(startWorkspace) {
590
636
  const prior = readProfile(this.#store.root, this.id);
591
637
  const revision = prior.kind === "ok" ? prior.profile.revision + 1 : 1;
592
638
  writeProfile(this.#store.root, this.id, buildProfile({
@@ -595,9 +641,9 @@ export class AgentSession {
595
641
  provider: this.#continuationScope ?? null,
596
642
  profileName: this.#profileName,
597
643
  // 0.40.0: history is CARRIED from the prior revision, never
598
- // re-derived from this process. With no prior (a legacy
599
- // session's first revision) the start is unknown: null.
600
- workspace: prior.kind === "ok" ? prior.profile.workspace : null,
644
+ // re-derived from this process. With no prior, a NEW session's
645
+ // start is the workspace it opened in; a legacy one's is unknown.
646
+ workspace: prior.kind === "ok" ? prior.profile.workspace : startWorkspace,
601
647
  reasoning: this.#reasoning,
602
648
  ...(this.#config.systemPrompt !== undefined ? { systemPrompt: this.#config.systemPrompt } : {}),
603
649
  registry: this.#config.registry,
@@ -696,7 +742,7 @@ export class AgentSession {
696
742
  this.ensureHealthy();
697
743
  if (this.#profilePending)
698
744
  this.#recordProfile(); // XP-1: legacy revision 1, before the first request
699
- return new Run(this.#store, this.#adapter, this.#effectiveConfig(), this, input, options?.signal, false, options?.source, options?.via);
745
+ return new Run(this.#store, this.#learning(this.#adapter), this.#effectiveConfig(), this, input, options?.signal, false, options?.source, options?.via);
700
746
  }
701
747
  /**
702
748
  * Continue the interrupted run (Area 2): apply durable decisions,
@@ -706,7 +752,7 @@ export class AgentSession {
706
752
  */
707
753
  resume() {
708
754
  this.ensureHealthy();
709
- return new Run(this.#store, this.#adapter, this.#effectiveConfig(), this, undefined, undefined, true);
755
+ return new Run(this.#store, this.#learning(this.#adapter), this.#effectiveConfig(), this, undefined, undefined, true);
710
756
  }
711
757
  /**
712
758
  * /compact (ADR-0044): compress the older conversation with a model
@@ -0,0 +1,26 @@
1
+ /**
2
+ * CW-1 batch 2 — a route's real window, read from its own refusal.
3
+ *
4
+ * Batch 1 gives a registered model a window at any endpoint, and at an
5
+ * endpoint no row names that window is the model's — a vendor statement
6
+ * about the weights, which a relay may cap lower. The one source that is
7
+ * right for ANY route is the route itself, and a route that refuses an
8
+ * oversize request usually states its cap. d4fc (2026-09-22) died on:
9
+ *
10
+ * This model's maximum context length is 1048576 tokens. However, you
11
+ * requested 131072 output tokens and your prompt contains at least
12
+ * 917505 input tokens ...
13
+ *
14
+ * The kernel already compacts once and retries once on context_overflow;
15
+ * what it lacked is the figure, so the compaction and every request after
16
+ * it aimed at the wrong window. This wrapper reads the figure off the
17
+ * refusal and hands it on, then rethrows the error untouched — it changes
18
+ * no classification and no retry.
19
+ */
20
+ import type { Adapter } from "@vincemakes/kiso-core";
21
+ /** The cap a refusal states, or null — anything that does not say one in
22
+ * the two wordings above is null, never a guess. */
23
+ export declare function overflowCap(message: string): number | null;
24
+ /** Wraps an adapter so a refusal that states a cap is reported once, and
25
+ * then thrown on exactly as it came. A caller's abort passes untouched. */
26
+ export declare function windowLearner(adapter: Adapter, onCap: (tokens: number) => void): Adapter;
@@ -0,0 +1,40 @@
1
+ /** The two wordings the providers kiso speaks to use for the cap:
2
+ * OpenAI-shaped and vLLM-shaped servers (and gateways in front of them)
3
+ * say `maximum context length is N tokens`; Anthropic-shaped ones say
4
+ * `prompt is too long: A tokens > N maximum`. */
5
+ const CAP_PATTERNS = [/maximum context length is\s+(\d[\d,_]*)\s*tokens/i, /prompt is too long:\s*\d[\d,_]*\s*tokens\s*>\s*(\d[\d,_]*)\s*maximum/i];
6
+ /** A window below this, or above the second bound, is not a window a
7
+ * refusal would state — a figure outside them is a misread, never a cap. */
8
+ const MIN_CAP = 1_000;
9
+ const MAX_CAP = 100_000_000;
10
+ /** The cap a refusal states, or null — anything that does not say one in
11
+ * the two wordings above is null, never a guess. */
12
+ export function overflowCap(message) {
13
+ for (const re of CAP_PATTERNS) {
14
+ const m = re.exec(message);
15
+ if (m === null)
16
+ continue;
17
+ const n = Number(m[1].replace(/[,_]/g, ""));
18
+ return Number.isSafeInteger(n) && n >= MIN_CAP && n <= MAX_CAP ? n : null;
19
+ }
20
+ return null;
21
+ }
22
+ /** Wraps an adapter so a refusal that states a cap is reported once, and
23
+ * then thrown on exactly as it came. A caller's abort passes untouched. */
24
+ export function windowLearner(adapter, onCap) {
25
+ return { stream: (options) => learning(adapter, options, onCap) };
26
+ }
27
+ async function* learning(adapter, options, onCap) {
28
+ try {
29
+ yield* adapter.stream(options);
30
+ }
31
+ catch (err) {
32
+ if (options.signal?.aborted !== true) {
33
+ const message = typeof err === "object" && err !== null && typeof err.message === "string" ? err.message : null;
34
+ const cap = message === null ? null : overflowCap(message);
35
+ if (cap !== null)
36
+ onCap(cap);
37
+ }
38
+ throw err;
39
+ }
40
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-runtime",
3
- "version": "0.40.2",
3
+ "version": "0.40.3",
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.40.2"
28
+ "@vincemakes/kiso-core": "0.40.3"
29
29
  },
30
30
  "peerDependencies": {
31
- "@vincemakes/kiso-provider-anthropic": "0.40.2",
32
- "@vincemakes/kiso-provider-openai": "0.40.2",
33
- "@vincemakes/kiso-provider-openai-responses": "0.40.2"
31
+ "@vincemakes/kiso-provider-anthropic": "0.40.3",
32
+ "@vincemakes/kiso-provider-openai": "0.40.3",
33
+ "@vincemakes/kiso-provider-openai-responses": "0.40.3"
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.40.2",
47
+ "@vincemakes/kiso-evals": "0.40.3",
48
48
  "@types/node": "^26.1.2",
49
49
  "typescript": "^5.7.2",
50
50
  "vitest": "^3.0.0"