@vincemakes/kiso-runtime 0.15.12 → 0.16.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.d.ts CHANGED
@@ -80,6 +80,7 @@ export declare class AgentRuntime {
80
80
  /** Load an existing session from disk, or create a fresh one. */
81
81
  session(options: {
82
82
  id: string;
83
+ acceptDrift?: boolean;
83
84
  }): Promise<AgentSession>;
84
85
  }
85
86
  /** The one-liner the README promises. */
package/dist/agent.js CHANGED
@@ -11,6 +11,8 @@
11
11
  * package (optional peers — an unused provider costs nothing). The kernel
12
12
  * itself stays dependency-free; the SDKs live in the provider packages.
13
13
  */
14
+ import { resolveContinuationScope } from "./provider/manifest.js";
15
+ import { assessProfileDrift, buildProfile, profilePath, readProfile, writeProfile } from "./profile.js";
14
16
  import { EventLog, ToolRegistry } from "@vincemakes/kiso-core";
15
17
  import { AgentSession } from "./session.js";
16
18
  /** @deprecated the canonical name is `Agent` (root export, 1.1.0); this alias is removed in the next major. */
@@ -53,9 +55,73 @@ export class AgentRuntime {
53
55
  const records = store.load(options.id);
54
56
  const log = new EventLog(records.map((r) => r.event));
55
57
  const adapter = await this.#adapterPromise;
58
+ const startupScope = resolveContinuationScope(this.#definition.provider, this.#definition.model, this.#definition.baseUrl);
59
+ // ── XP-1: the durable execution profile, FAIL-CLOSED ─────────────
60
+ const meta = readProfile(store.root, options.id);
61
+ if (meta.kind === "corrupt") {
62
+ throw new Error(`the session profile ${profilePath(store.root, options.id)} is unreadable (${meta.error}) — BLOCKED: restore the file, or re-create the session; a corrupt profile is never silently rebuilt under today's defaults`);
63
+ }
64
+ const hasEnvelope = log.all.some((e) => e.type === "stop" && e.continuation !== undefined);
65
+ if (meta.kind === "absent" && hasEnvelope) {
66
+ throw new Error(`the session log carries scoped continuation envelopes but ${profilePath(store.root, options.id)} is missing — BLOCKED: an XP-era session without its profile is an integrity failure, never a legacy session`);
67
+ }
68
+ // The CURRENT candidate — what THIS process would run.
69
+ const candidate = buildProfile({
70
+ revision: 0,
71
+ modelId: this.#definition.model,
72
+ provider: startupScope ?? null,
73
+ ...(this.#definition.systemPrompt !== undefined ? { systemPrompt: this.#definition.systemPrompt } : {}),
74
+ registry: this.#registry,
75
+ });
76
+ let restored = null;
77
+ let profilePending = false;
78
+ if (meta.kind === "ok") {
79
+ const drift = assessProfileDrift(meta.profile, {
80
+ provider: startupScope ?? null,
81
+ systemPromptDigest: candidate.systemPromptDigest,
82
+ tools: candidate.tools,
83
+ });
84
+ if (drift.kind === "material" && options.acceptDrift !== true) {
85
+ const listed = drift.reasons.map((r) => `- ${r}`).join("\n");
86
+ throw new Error(`the recorded execution profile no longer matches this process:\n${listed}\nre-open with acceptDrift (the CLI's --accept-drift flag) to proceed under the CURRENT configuration — the acknowledgement is recorded as a new revision; silently rebuilding is forbidden`);
87
+ }
88
+ if (drift.kind === "material") {
89
+ // acknowledged: the current configuration wins, DURABLY.
90
+ writeProfile(store.root, options.id, {
91
+ ...buildProfile({
92
+ revision: meta.profile.revision + 1,
93
+ modelId: this.#definition.model,
94
+ provider: startupScope ?? null,
95
+ ...(this.#definition.systemPrompt !== undefined ? { systemPrompt: this.#definition.systemPrompt } : {}),
96
+ registry: this.#registry,
97
+ }),
98
+ });
99
+ }
100
+ else {
101
+ // RESTORE — the recorded profile wins over the process default
102
+ // (the truthfulness core: the row and the request agree).
103
+ const scope = meta.profile.provider === null ? undefined : meta.profile.provider;
104
+ restored = { model: meta.profile.modelId, reasoning: meta.profile.reasoning, scope };
105
+ }
106
+ }
107
+ else if (log.all.length === 0) {
108
+ // a NEW session: revision 1 lands BEFORE any durable event.
109
+ writeProfile(store.root, options.id, { ...candidate, revision: 1 });
110
+ }
111
+ else {
112
+ // legacy (pre-XP log, no sidecar): generation absence is not
113
+ // drift — restore under current configuration; revision 1 lands
114
+ // at the next explicit selection or first request.
115
+ profilePending = true;
116
+ }
56
117
  const config = {
57
- model: this.#definition.model,
118
+ model: restored?.model ?? this.#definition.model,
58
119
  ...(this.#definition.provider !== undefined ? { provider: this.#definition.provider } : {}),
120
+ ...((restored !== null ? restored.scope : startupScope) !== undefined
121
+ ? { continuationScope: (restored !== null ? restored.scope : startupScope) }
122
+ : {}),
123
+ ...(restored !== null ? { reasoning: restored.reasoning } : {}),
124
+ ...(profilePending ? { profilePending: true } : {}),
59
125
  ...(this.#definition.systemPrompt !== undefined ? { systemPrompt: this.#definition.systemPrompt } : {}),
60
126
  registry: this.#registry,
61
127
  ...(this.#definition.permissionPolicy !== undefined || this.#definition.hooks !== undefined
@@ -137,9 +203,15 @@ async function resolveAdapter(definition) {
137
203
  }
138
204
  case "openai-compat": {
139
205
  const { createOpenAICompatProvider } = await import("@vincemakes/kiso-provider-openai");
206
+ // MG-1 (A5): the adapter's replay identity — the SAME resolution
207
+ // the run's stamping scope uses, so emit and replay agree.
208
+ const scope = resolveContinuationScope("openai-compat", "", definition.baseUrl);
140
209
  return createOpenAICompatProvider({
141
210
  ...(definition.apiKey !== undefined ? { apiKey: definition.apiKey } : {}),
142
211
  ...(definition.baseUrl !== undefined ? { baseUrl: definition.baseUrl } : {}),
212
+ ...(scope !== undefined
213
+ ? { scope: { providerId: scope.providerId, ...(scope.endpoint !== undefined ? { endpoint: scope.endpoint } : {}) } }
214
+ : {}),
143
215
  });
144
216
  }
145
217
  default:
@@ -25,3 +25,4 @@ export * from "./extensions.js";
25
25
  export * from "./trust.js";
26
26
  export * from "./provider/metadata.js";
27
27
  export { canonicalizeUsageForModel } from "./usage/canonical.js";
28
+ export { BUILTIN_MANIFESTS, resolveContinuationScope, type ModelRef, type ProviderManifest } from "./provider/manifest.js";
package/dist/internal.js CHANGED
@@ -30,3 +30,5 @@ export * from "./provider/metadata.js";
30
30
  // PH-1c: the model-keyed cost derivation rides the same door (the root
31
31
  // surface keeps only the frozen canonicalizeUsage).
32
32
  export { canonicalizeUsageForModel } from "./usage/canonical.js";
33
+ // MG-1 (A5): the identity layer — manifests and the scope resolver.
34
+ export { BUILTIN_MANIFESTS, resolveContinuationScope } from "./provider/manifest.js";
@@ -0,0 +1,114 @@
1
+ /**
2
+ * XP-1 — the durable execution profile (the ratified spec §3).
3
+ *
4
+ * ONE product contract: a session must know what will answer its next
5
+ * request after a restart. The profile is a durable session fact OUTSIDE
6
+ * the event log — ADR-0051 §6's OUT class ("session metadata that is not
7
+ * an event"), so no contract amendment is spent on persistence; the §6
8
+ * purity gate extends instead: the correctness derivation never reads it.
9
+ *
10
+ * The sidecar is `<id>.meta.json` (the adjudicated namespaced file — the
11
+ * profile is its first tenant, SX-1's naming joins later), written
12
+ * FAIL-CLOSED: temp → fsync(file) → rename → fsync(parent directory),
13
+ * full replacement per revision. A new session writes revision 1 BEFORE
14
+ * its first durable event; an unreadable sidecar is BLOCKED, never
15
+ * silently treated as absent (the "corrupt = legacy = today's defaults"
16
+ * misclassification is the exact silent rebuild the spec forbids).
17
+ *
18
+ * Never a secret: the profile carries the profile NAME and env-var-shaped
19
+ * references at most — no key, token, or credential material.
20
+ */
21
+ import type { ToolRegistry } from "@vincemakes/kiso-core";
22
+ import type { ReasoningSetting } from "./provider/metadata.js";
23
+ export interface ProfileModelRef {
24
+ readonly providerId: string;
25
+ readonly apiId: string;
26
+ readonly modelId: string;
27
+ readonly endpoint?: string;
28
+ }
29
+ /** One tool of the recorded surface — the INVENTORY itself, not only a
30
+ * digest: a single hash can say "changed" but never WHAT changed, and
31
+ * the drift protocol must tell compatible additions from removals and
32
+ * schema changes. */
33
+ export interface ProfileToolRecord {
34
+ readonly name: string;
35
+ readonly schemaHash: string;
36
+ readonly descriptionHash: string;
37
+ }
38
+ export interface ExecutionProfile {
39
+ /** monotone per session, from 1. */
40
+ readonly revision: number;
41
+ /** ISO time of this revision. */
42
+ readonly at: string;
43
+ /** the RESOLVED model id — recorded even for unscoped bindings: the
44
+ * session must know what answers its next request either way. */
45
+ readonly modelId: string;
46
+ /** null = an unscoped binding (SDK-injected adapter). */
47
+ readonly provider: ProfileModelRef | null;
48
+ /** the config profile NAME — the credential reference is at most the
49
+ * env-var name the config carries; never the secret. */
50
+ readonly profileName: string | null;
51
+ readonly reasoning: ReasoningSetting;
52
+ readonly systemPromptDigest: string;
53
+ /** sorted by name; the digest below is DERIVED from this inventory. */
54
+ readonly tools: readonly ProfileToolRecord[];
55
+ readonly toolManifestDigest: string;
56
+ }
57
+ export declare function toolInventory(registry: ToolRegistry): readonly ProfileToolRecord[];
58
+ export declare function buildProfile(input: {
59
+ readonly revision: number;
60
+ readonly modelId: string;
61
+ readonly provider: ProfileModelRef | null;
62
+ readonly profileName?: string;
63
+ readonly reasoning?: ReasoningSetting;
64
+ readonly systemPrompt?: string;
65
+ readonly registry: ToolRegistry;
66
+ }): ExecutionProfile;
67
+ export declare function profilePath(root: string, sessionId: string): string;
68
+ /** Atomic, fail-closed write: a reader sees the previous revision or the
69
+ * new one, never a torn file — and the RENAME itself is made durable by
70
+ * the parent-directory fsync. */
71
+ export declare function writeProfile(root: string, sessionId: string, profile: ExecutionProfile): void;
72
+ export type ProfileReadResult = {
73
+ readonly kind: "ok";
74
+ readonly profile: ExecutionProfile;
75
+ } | {
76
+ readonly kind: "absent";
77
+ } | {
78
+ readonly kind: "corrupt";
79
+ readonly error: string;
80
+ };
81
+ export declare function readProfile(root: string, sessionId: string): ProfileReadResult;
82
+ export type ProfileDrift = {
83
+ readonly kind: "clean";
84
+ }
85
+ /** the tool surface or the composed prompt moved — NAMED and surfaced
86
+ * (never presented as restored), but composition is per-process BY
87
+ * ARCHITECTURE here (extensions, modes, the E5-ratified task flip,
88
+ * subagent roles), so it never refuses an open. */
89
+ | {
90
+ readonly kind: "surface-changed";
91
+ readonly notes: readonly string[];
92
+ }
93
+ /** new tools only — every recorded tool present and identical. */
94
+ | {
95
+ readonly kind: "compatible-additions";
96
+ readonly added: readonly string[];
97
+ }
98
+ /** WHO ANSWERS changed — the one class that blocks without an
99
+ * explicit acknowledgement. */
100
+ | {
101
+ readonly kind: "material";
102
+ readonly reasons: readonly string[];
103
+ };
104
+ /** The drift protocol's classifier — computed from the INVENTORY diff,
105
+ * never from digest inequality alone: every recorded tool present with
106
+ * identical hashes plus new names = compatible additions (a one-line
107
+ * notice); a missing name, a changed hash, a provider/model divergence,
108
+ * or a system-prompt divergence = MATERIAL (explicit resolution; a
109
+ * digest mismatch is never presented as restoration). */
110
+ export declare function assessProfileDrift(recorded: ExecutionProfile, current: {
111
+ readonly provider: ProfileModelRef | null;
112
+ readonly systemPromptDigest: string;
113
+ readonly tools: readonly ProfileToolRecord[];
114
+ }): ProfileDrift;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * XP-1 — the durable execution profile (the ratified spec §3).
3
+ *
4
+ * ONE product contract: a session must know what will answer its next
5
+ * request after a restart. The profile is a durable session fact OUTSIDE
6
+ * the event log — ADR-0051 §6's OUT class ("session metadata that is not
7
+ * an event"), so no contract amendment is spent on persistence; the §6
8
+ * purity gate extends instead: the correctness derivation never reads it.
9
+ *
10
+ * The sidecar is `<id>.meta.json` (the adjudicated namespaced file — the
11
+ * profile is its first tenant, SX-1's naming joins later), written
12
+ * FAIL-CLOSED: temp → fsync(file) → rename → fsync(parent directory),
13
+ * full replacement per revision. A new session writes revision 1 BEFORE
14
+ * its first durable event; an unreadable sidecar is BLOCKED, never
15
+ * silently treated as absent (the "corrupt = legacy = today's defaults"
16
+ * misclassification is the exact silent rebuild the spec forbids).
17
+ *
18
+ * Never a secret: the profile carries the profile NAME and env-var-shaped
19
+ * references at most — no key, token, or credential material.
20
+ */
21
+ import { closeSync, fsyncSync, mkdtempSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
22
+ import { createHash } from "node:crypto";
23
+ import { dirname, join } from "node:path";
24
+ const sha = (text) => createHash("sha256").update(text).digest("hex");
25
+ export function toolInventory(registry) {
26
+ return registry
27
+ .toSpecs()
28
+ .map((spec) => ({
29
+ name: spec.name,
30
+ schemaHash: sha(JSON.stringify(spec.inputSchema ?? null)),
31
+ descriptionHash: sha(spec.description ?? ""),
32
+ }))
33
+ .sort((a, b) => (a.name < b.name ? -1 : 1));
34
+ }
35
+ export function buildProfile(input) {
36
+ const tools = toolInventory(input.registry);
37
+ return {
38
+ revision: input.revision,
39
+ at: new Date().toISOString(),
40
+ modelId: input.modelId,
41
+ provider: input.provider,
42
+ profileName: input.profileName ?? null,
43
+ reasoning: input.reasoning ?? { thinking: "default", effort: "default" },
44
+ systemPromptDigest: sha(input.systemPrompt ?? ""),
45
+ tools,
46
+ toolManifestDigest: sha(JSON.stringify(tools)),
47
+ };
48
+ }
49
+ export function profilePath(root, sessionId) {
50
+ return join(root, `${sessionId}.meta.json`);
51
+ }
52
+ /** Atomic, fail-closed write: a reader sees the previous revision or the
53
+ * new one, never a torn file — and the RENAME itself is made durable by
54
+ * the parent-directory fsync. */
55
+ export function writeProfile(root, sessionId, profile) {
56
+ const path = profilePath(root, sessionId);
57
+ const tmpDir = mkdtempSync(join(root, ".meta-"));
58
+ const tmp = join(tmpDir, "meta.json");
59
+ try {
60
+ writeFileSync(tmp, `${JSON.stringify({ profile }, null, "\t")}\n`);
61
+ const fd = openSync(tmp, "r");
62
+ try {
63
+ fsyncSync(fd);
64
+ }
65
+ finally {
66
+ closeSync(fd);
67
+ }
68
+ renameSync(tmp, path);
69
+ const dirFd = openSync(dirname(path), "r");
70
+ try {
71
+ fsyncSync(dirFd);
72
+ }
73
+ finally {
74
+ closeSync(dirFd);
75
+ }
76
+ }
77
+ finally {
78
+ rmSync(tmpDir, { recursive: true, force: true });
79
+ }
80
+ }
81
+ export function readProfile(root, sessionId) {
82
+ const path = profilePath(root, sessionId);
83
+ let raw;
84
+ try {
85
+ raw = readFileSync(path, "utf8");
86
+ }
87
+ catch (err) {
88
+ if (err.code === "ENOENT")
89
+ return { kind: "absent" };
90
+ return { kind: "corrupt", error: String(err.message ?? err) };
91
+ }
92
+ try {
93
+ const parsed = JSON.parse(raw);
94
+ const p = parsed.profile;
95
+ if (p === undefined ||
96
+ typeof p.revision !== "number" ||
97
+ typeof p.at !== "string" ||
98
+ typeof p.modelId !== "string" ||
99
+ typeof p.systemPromptDigest !== "string" ||
100
+ typeof p.toolManifestDigest !== "string" ||
101
+ !Array.isArray(p.tools)) {
102
+ return { kind: "corrupt", error: "the profile tenant is missing or malformed" };
103
+ }
104
+ return { kind: "ok", profile: p };
105
+ }
106
+ catch (err) {
107
+ return { kind: "corrupt", error: String(err.message ?? err) };
108
+ }
109
+ }
110
+ /** The drift protocol's classifier — computed from the INVENTORY diff,
111
+ * never from digest inequality alone: every recorded tool present with
112
+ * identical hashes plus new names = compatible additions (a one-line
113
+ * notice); a missing name, a changed hash, a provider/model divergence,
114
+ * or a system-prompt divergence = MATERIAL (explicit resolution; a
115
+ * digest mismatch is never presented as restoration). */
116
+ export function assessProfileDrift(recorded, current) {
117
+ const reasons = [];
118
+ const notes = [];
119
+ const r = recorded.provider;
120
+ const c = current.provider;
121
+ if ((r === null) !== (c === null)) {
122
+ reasons.push(`the recorded binding is ${r === null ? "unscoped" : `${r.providerId}/${r.modelId}`} but the current process serves ${c === null ? "an unscoped adapter" : `${c.providerId}/${c.modelId}`}`);
123
+ }
124
+ else if (r !== null && c !== null && r.providerId !== c.providerId) {
125
+ reasons.push(`the recorded provider is ${r.providerId} but the current process serves ${c.providerId}`);
126
+ }
127
+ if (recorded.systemPromptDigest !== current.systemPromptDigest) {
128
+ notes.push(`the composed system prompt differs from the recorded one (${recorded.systemPromptDigest.slice(0, 12)}… → ${current.systemPromptDigest.slice(0, 12)}…)`);
129
+ }
130
+ const currentByName = new Map(current.tools.map((t) => [t.name, t]));
131
+ const added = [];
132
+ for (const t of recorded.tools) {
133
+ const now = currentByName.get(t.name);
134
+ if (now === undefined) {
135
+ notes.push(`the recorded tool "${t.name}" is not loaded in this process`);
136
+ }
137
+ else if (now.schemaHash !== t.schemaHash) {
138
+ notes.push(`the tool "${t.name}" changed its schema since it was recorded`);
139
+ }
140
+ currentByName.delete(t.name);
141
+ }
142
+ for (const name of currentByName.keys())
143
+ added.push(name);
144
+ if (reasons.length > 0)
145
+ return { kind: "material", reasons };
146
+ if (notes.length > 0)
147
+ return { kind: "surface-changed", notes };
148
+ if (added.length > 0)
149
+ return { kind: "compatible-additions", added };
150
+ return { kind: "clean" };
151
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * MG-1 — the identity layer above the adapter port (ADR-0051 Amendment 5
3
+ * carries the wire half; this file carries WHO things are).
4
+ *
5
+ * Four concepts `openai-compat` used to conflate, separated: the PROVIDER
6
+ * (a vendor identity), the API DIALECT it is driven through, the MODEL,
7
+ * and (for custom endpoints) the ORIGIN. A provider chooses a dialect; it
8
+ * is not itself a dialect. Manifests here are IDENTITY ONLY — no
9
+ * capability data (capabilities are dated claims and live in the metadata
10
+ * registry), no credential material ever.
11
+ *
12
+ * Lives under runtime/internal — the curated root surface does not move.
13
+ */
14
+ import type { ContinuationScope } from "@vincemakes/kiso-core";
15
+ export interface ModelRef {
16
+ readonly providerId: string;
17
+ readonly apiId: string;
18
+ readonly modelId: string;
19
+ }
20
+ export interface ProviderManifest {
21
+ readonly id: string;
22
+ /** A dated claim snapshot, not an API promise (date-stamped ordinal). */
23
+ readonly revision: string;
24
+ readonly authMethods: readonly string[];
25
+ readonly apiIds: readonly string[];
26
+ readonly defaultEndpoint?: string;
27
+ }
28
+ /** The five built-in identities. `custom` is the honest bucket for any
29
+ * OpenAI-compatible endpoint the origin table does not recognize —
30
+ * capabilities default to UNKNOWN there, never borrowed from OpenAI. */
31
+ export declare const BUILTIN_MANIFESTS: readonly ProviderManifest[];
32
+ /** The run's continuation scope, resolved from the live binding. An
33
+ * undefined provider (a directly injected SDK/faux adapter) is an
34
+ * UNSCOPED run: the kernel strips adapter-emitted continuation. */
35
+ export declare function resolveContinuationScope(provider: "anthropic" | "openai-compat" | undefined, model: string, baseUrl?: string): ContinuationScope | undefined;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * MG-1 — the identity layer above the adapter port (ADR-0051 Amendment 5
3
+ * carries the wire half; this file carries WHO things are).
4
+ *
5
+ * Four concepts `openai-compat` used to conflate, separated: the PROVIDER
6
+ * (a vendor identity), the API DIALECT it is driven through, the MODEL,
7
+ * and (for custom endpoints) the ORIGIN. A provider chooses a dialect; it
8
+ * is not itself a dialect. Manifests here are IDENTITY ONLY — no
9
+ * capability data (capabilities are dated claims and live in the metadata
10
+ * registry), no credential material ever.
11
+ *
12
+ * Lives under runtime/internal — the curated root surface does not move.
13
+ */
14
+ /** The five built-in identities. `custom` is the honest bucket for any
15
+ * OpenAI-compatible endpoint the origin table does not recognize —
16
+ * capabilities default to UNKNOWN there, never borrowed from OpenAI. */
17
+ export const BUILTIN_MANIFESTS = [
18
+ { id: "anthropic", revision: "2026-08-26.1", authMethods: ["api-key", "none"], apiIds: ["anthropic-messages"], defaultEndpoint: "https://api.anthropic.com" },
19
+ { id: "openai", revision: "2026-08-26.1", authMethods: ["api-key", "none"], apiIds: ["openai-chat", "openai-responses"], defaultEndpoint: "https://api.openai.com" },
20
+ { id: "deepseek", revision: "2026-08-26.1", authMethods: ["api-key", "none"], apiIds: ["openai-chat"], defaultEndpoint: "https://api.deepseek.com" },
21
+ { id: "zai", revision: "2026-08-26.1", authMethods: ["api-key", "none"], apiIds: ["openai-chat"], defaultEndpoint: "https://api.z.ai" },
22
+ { id: "custom", revision: "2026-08-26.1", authMethods: ["api-key", "none"], apiIds: ["openai-chat"] },
23
+ ];
24
+ /** Known-origin recognition (the ratified Q3 answer): a compat profile
25
+ * whose baseUrl origin equals a built-in manifest's endpoint resolves to
26
+ * that provider identity — deterministic and manifest-driven, so a
27
+ * pre-preset DeepSeek envelope is not foreign the day presets ship. */
28
+ const KNOWN_ORIGINS = {
29
+ "https://api.deepseek.com": "deepseek",
30
+ "https://api.z.ai": "zai",
31
+ "https://open.bigmodel.cn": "zai",
32
+ "https://api.openai.com": "openai",
33
+ };
34
+ /** The run's continuation scope, resolved from the live binding. An
35
+ * undefined provider (a directly injected SDK/faux adapter) is an
36
+ * UNSCOPED run: the kernel strips adapter-emitted continuation. */
37
+ export function resolveContinuationScope(provider, model, baseUrl) {
38
+ if (provider === undefined)
39
+ return undefined;
40
+ if (provider === "anthropic") {
41
+ return { providerId: "anthropic", apiId: "anthropic-messages", modelId: model };
42
+ }
43
+ if (baseUrl === undefined) {
44
+ return { providerId: "openai", apiId: "openai-chat", modelId: model };
45
+ }
46
+ let origin;
47
+ try {
48
+ origin = new URL(baseUrl).origin;
49
+ }
50
+ catch {
51
+ return { providerId: "custom", apiId: "openai-chat", modelId: model, endpoint: baseUrl };
52
+ }
53
+ const known = KNOWN_ORIGINS[origin];
54
+ if (known !== undefined)
55
+ return { providerId: known, apiId: "openai-chat", modelId: model };
56
+ return { providerId: "custom", apiId: "openai-chat", modelId: model, endpoint: origin };
57
+ }
@@ -26,8 +26,57 @@ export interface ModelCapabilities {
26
26
  * OpenAI); "explicit" — the request must place cache_control
27
27
  * breakpoints (Anthropic); "none" — no caching; null = unknown. */
28
28
  readonly promptCaching: "none" | "automatic" | "explicit" | null;
29
- /** the model emits a reasoning stream (thinking); null = unknown. */
30
- readonly reasoning: boolean | null;
29
+ /** XP-1: the reasoning capability matrix supersedes the pre-XP-1
30
+ * boolean IN PLACE (zero consumers existed, verified). null = unknown:
31
+ * no mode list, no effort levels, nothing downstream may guess. */
32
+ readonly reasoning: ReasoningCapabilities | null;
33
+ /** MG-1: the input parts the model accepts (e.g. ["text","image"]);
34
+ * null = unknown — the CLI treats unknown as text-only with an honest
35
+ * notice, never a guess. Evidenced by 0.15.7's image attachments:
36
+ * the gateway must know before the request is built. */
37
+ readonly inputModalities: readonly string[] | null;
38
+ }
39
+ /** XP-1 (the ratified spec §4.1): the two ORTHOGONAL axes. "default" on
40
+ * each axis means the provider's own default, displayed honestly as
41
+ * such. The union nesting effort under enabled stays rejected: Anthropic
42
+ * effort affects whole responses with or without explicit thinking, and
43
+ * some model/effort combinations forbid thinking-disabled. */
44
+ export type ThinkingMode = "default" | "adaptive" | "enabled" | "disabled";
45
+ export type ReasoningEffort = "default" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
46
+ export interface ReasoningSetting {
47
+ readonly thinking: ThinkingMode;
48
+ readonly effort: ReasoningEffort;
49
+ }
50
+ /** XP-1 §4.2: the per-model matrix. A provider's toggle semantics are a
51
+ * dated claim about someone else's API — the same class as a price, so
52
+ * the block carries asOf + source exactly as pricing does. */
53
+ export interface ReasoningCapabilities {
54
+ /** the old boolean's meaning, preserved under its own name. */
55
+ readonly emitsThinkingStream: boolean | null;
56
+ /** null = no request-time toggle is known for this model. */
57
+ readonly thinking: {
58
+ readonly modes: readonly Exclude<ThinkingMode, "default">[];
59
+ readonly default: Exclude<ThinkingMode, "default"> | null;
60
+ } | null;
61
+ /** null = no effort control is known. `levels` are NATIVE only —
62
+ * compatibility mappings are displayed as their resolution, never as
63
+ * distinct native levels. `wire` names the dialect parameter. */
64
+ readonly effort: {
65
+ readonly levels: readonly Exclude<ReasoningEffort, "default">[];
66
+ readonly default: Exclude<ReasoningEffort, "default"> | null;
67
+ readonly wire: string;
68
+ } | null;
69
+ /** invalid combinations the provider documents (e.g. thinking-disabled
70
+ * at certain efforts). */
71
+ readonly forbidden?: readonly ReasoningSetting[];
72
+ readonly asOf: string | null;
73
+ readonly source: string | null;
74
+ }
75
+ /** XP-1: the resolved wire values — what an adapter actually serializes.
76
+ * Empty = provider defaults, byte-identical to a pre-XP-1 request. */
77
+ export interface WireReasoning {
78
+ readonly thinking?: "adaptive" | "enabled" | "disabled";
79
+ readonly effort?: string;
31
80
  }
32
81
  export interface ModelPricing {
33
82
  readonly inputPerM: number;
@@ -45,7 +94,19 @@ export interface ModelMetadataEntry {
45
94
  /** origin qualifier (e.g. "https://api.deepseek.com"): when present,
46
95
  * the entry matches only requests aimed at that endpoint. */
47
96
  readonly endpoint?: string;
97
+ /** MG-1: the provider identity (manifest id) — retires the string
98
+ * inference from the model id's hyphen prefix. */
99
+ readonly providerId?: string;
48
100
  readonly capabilities: ModelCapabilities;
101
+ /** XP-1: a provider-announced retirement, dated and sourced. */
102
+ readonly deprecated?: {
103
+ readonly asOf: string;
104
+ readonly source: string;
105
+ };
106
+ /** MG-1: capability values are dated claims exactly as prices are —
107
+ * null marks an undated legacy claim (the pre-MG-1 table). */
108
+ readonly capabilitiesAsOf?: string | null;
109
+ readonly capabilitiesSource?: string | null;
49
110
  readonly pricing: ModelPricing | null;
50
111
  }
51
112
  /**
@@ -54,3 +115,19 @@ export interface ModelMetadataEntry {
54
115
  * one matches any endpoint. Unknown model → null, never a default.
55
116
  */
56
117
  export declare function lookupModelMetadata(model: string, endpoint?: string): ModelMetadataEntry | null;
118
+ /**
119
+ * XP-1 §4.2 — native-only resolution. The rules, in order:
120
+ * default/default → NO wire fields (the byte-identity anchor — a
121
+ * default-profile session's requests are byte-identical to pre-XP-1);
122
+ * an unknown model refuses any non-default selection (unknown stays
123
+ * unknown — nothing downstream guesses a level into existence);
124
+ * a value outside the matrix's NATIVE list is refused with the native
125
+ * list named — never silently downgraded, never silently mapped.
126
+ */
127
+ export declare function resolveReasoning(model: string, setting: ReasoningSetting, endpoint?: string): {
128
+ readonly ok: true;
129
+ readonly wire: WireReasoning;
130
+ } | {
131
+ readonly ok: false;
132
+ readonly reason: string;
133
+ };
@@ -34,26 +34,82 @@ const DEEPSEEK_PRICING = {
34
34
  const ENTRIES = [
35
35
  {
36
36
  model: "deepseek-chat",
37
+ providerId: "deepseek",
38
+ // XP-1: the changelog entry dated 2026-04-24 discontinues the two
39
+ // legacy names on 2026-07-24 (they pointed at v4-flash during the
40
+ // transition). v6's "unsourced" rider cited the wrong page.
41
+ deprecated: { asOf: "2026-07-24", source: "https://api-docs.deepseek.com/updates/" },
37
42
  endpoint: "https://api.deepseek.com",
38
- capabilities: { contextWindow: null, maxOutputTokens: null, promptCaching: "automatic", reasoning: false },
43
+ capabilities: { contextWindow: null, maxOutputTokens: null, promptCaching: "automatic", reasoning: { emitsThinkingStream: false, thinking: null, effort: null, asOf: null, source: null }, inputModalities: null },
39
44
  pricing: DEEPSEEK_PRICING,
40
45
  },
41
46
  {
42
47
  model: "deepseek-reasoner",
48
+ providerId: "deepseek",
49
+ // XP-1: the changelog entry dated 2026-04-24 discontinues the two
50
+ // legacy names on 2026-07-24 (they pointed at v4-flash during the
51
+ // transition). v6's "unsourced" rider cited the wrong page.
52
+ deprecated: { asOf: "2026-07-24", source: "https://api-docs.deepseek.com/updates/" },
43
53
  endpoint: "https://api.deepseek.com",
44
- capabilities: { contextWindow: null, maxOutputTokens: null, promptCaching: "automatic", reasoning: true },
54
+ capabilities: { contextWindow: null, maxOutputTokens: null, promptCaching: "automatic", reasoning: { emitsThinkingStream: true, thinking: null, effort: null, asOf: null, source: null }, inputModalities: null },
45
55
  pricing: DEEPSEEK_PRICING,
46
56
  },
47
57
  {
48
58
  model: "claude-sonnet-5",
49
- capabilities: { contextWindow: 200_000, maxOutputTokens: null, promptCaching: "explicit", reasoning: true },
59
+ providerId: "anthropic",
60
+ capabilities: { contextWindow: 200_000, maxOutputTokens: null, promptCaching: "explicit",
61
+ reasoning: {
62
+ emitsThinkingStream: true,
63
+ thinking: null,
64
+ effort: { levels: ["low", "medium", "high", "xhigh", "max"], default: "high", wire: "output_config.effort" },
65
+ asOf: "2026-08-26",
66
+ source: "https://platform.claude.com/docs/en/build-with-claude/effort",
67
+ },
68
+ inputModalities: null },
50
69
  // Priced only when the rates are read from the live billing page
51
70
  // and dated — never copied from memory (the review's boundary ②).
52
71
  pricing: null,
53
72
  },
73
+ {
74
+ // XP-1: the current DeepSeek line — the model every RD-1 benchmark
75
+ // artifact was produced with, previously ABSENT (null pricing and
76
+ // capabilities everywhere). Thinking is a request-time toggle,
77
+ // default-ENABLED; effort is native low/high/max (foreign levels
78
+ // are the provider's own mapping, never shown as native). Pricing
79
+ // stays null until read from the live billing page and dated.
80
+ model: "deepseek-v4-flash",
81
+ providerId: "deepseek",
82
+ endpoint: "https://api.deepseek.com",
83
+ capabilities: { contextWindow: null, maxOutputTokens: null, promptCaching: "automatic", reasoning: {
84
+ emitsThinkingStream: true,
85
+ thinking: { modes: ["enabled", "disabled"], default: "enabled" },
86
+ effort: { levels: ["low", "high", "max"], default: "high", wire: "reasoning_effort" },
87
+ asOf: "2026-08-26",
88
+ source: "https://api-docs.deepseek.com/guides/thinking_mode",
89
+ }, inputModalities: null },
90
+ capabilitiesAsOf: "2026-08-26",
91
+ capabilitiesSource: "https://api-docs.deepseek.com/guides/thinking_mode",
92
+ pricing: null,
93
+ },
94
+ {
95
+ model: "deepseek-v4-pro",
96
+ providerId: "deepseek",
97
+ endpoint: "https://api.deepseek.com",
98
+ capabilities: { contextWindow: null, maxOutputTokens: null, promptCaching: "automatic", reasoning: {
99
+ emitsThinkingStream: true,
100
+ thinking: { modes: ["enabled", "disabled"], default: "enabled" },
101
+ effort: { levels: ["low", "high", "max"], default: "high", wire: "reasoning_effort" },
102
+ asOf: "2026-08-26",
103
+ source: "https://api-docs.deepseek.com/guides/thinking_mode",
104
+ }, inputModalities: null },
105
+ capabilitiesAsOf: "2026-08-26",
106
+ capabilitiesSource: "https://api-docs.deepseek.com/guides/thinking_mode",
107
+ pricing: null,
108
+ },
54
109
  {
55
110
  model: "gpt-4o",
56
- capabilities: { contextWindow: 128_000, maxOutputTokens: null, promptCaching: "automatic", reasoning: false },
111
+ providerId: "openai",
112
+ capabilities: { contextWindow: 128_000, maxOutputTokens: null, promptCaching: "automatic", reasoning: { emitsThinkingStream: false, thinking: null, effort: null, asOf: null, source: null }, inputModalities: null },
57
113
  pricing: null,
58
114
  },
59
115
  ];
@@ -80,3 +136,41 @@ function originOf(endpoint) {
80
136
  return endpoint;
81
137
  }
82
138
  }
139
+ /**
140
+ * XP-1 §4.2 — native-only resolution. The rules, in order:
141
+ * default/default → NO wire fields (the byte-identity anchor — a
142
+ * default-profile session's requests are byte-identical to pre-XP-1);
143
+ * an unknown model refuses any non-default selection (unknown stays
144
+ * unknown — nothing downstream guesses a level into existence);
145
+ * a value outside the matrix's NATIVE list is refused with the native
146
+ * list named — never silently downgraded, never silently mapped.
147
+ */
148
+ export function resolveReasoning(model, setting, endpoint) {
149
+ if (setting.thinking === "default" && setting.effort === "default")
150
+ return { ok: true, wire: {} };
151
+ const r = lookupModelMetadata(model, endpoint)?.capabilities.reasoning ?? null;
152
+ if (r === null) {
153
+ return { ok: false, reason: `no reasoning capabilities are known for ${model} — unknown stays unknown; only default/default resolves` };
154
+ }
155
+ const wire = {};
156
+ if (setting.thinking !== "default") {
157
+ if (r.thinking === null || !r.thinking.modes.includes(setting.thinking)) {
158
+ const modes = r.thinking === null ? "none known" : r.thinking.modes.join("/");
159
+ return { ok: false, reason: `${model} does not support thinking mode "${setting.thinking}" (native: ${modes})` };
160
+ }
161
+ wire.thinking = setting.thinking;
162
+ }
163
+ if (setting.effort !== "default") {
164
+ if (r.effort === null || !r.effort.levels.includes(setting.effort)) {
165
+ const levels = r.effort === null ? "none known" : r.effort.levels.join("/");
166
+ return { ok: false, reason: `${model} does not support effort "${setting.effort}" (native: ${levels})` };
167
+ }
168
+ wire.effort = setting.effort;
169
+ }
170
+ for (const f of r.forbidden ?? []) {
171
+ if (f.thinking === setting.thinking && f.effort === setting.effort) {
172
+ return { ok: false, reason: `${model} forbids thinking=${setting.thinking} with effort=${setting.effort}` };
173
+ }
174
+ }
175
+ return { ok: true, wire };
176
+ }
package/dist/run.js CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { denialResult, loop } from "@vincemakes/kiso-core";
7
7
  import { ABORTED, MergedSignal, abortable, openRunId } from "./recovery.js";
8
+ import { resolveReasoning } from "./provider/metadata.js";
8
9
  import { deriveRecoveryPlan, invocationSeqOf } from "./recovery-plan.js";
9
10
  import { composeApprovalChain, composeSystemPrompt, composeToolTable, microcompactFor } from "./compose.js";
10
11
  import { truncationGuard } from "./truncation-guard.js";
@@ -131,6 +132,12 @@ export class Run {
131
132
  ...(this.#config.compaction !== undefined ? { compaction: this.#config.compaction } : {}),
132
133
  ...(microcompact !== undefined ? { microcompact } : {}),
133
134
  ...(this.#config.maxRetries !== undefined ? { maxRetries: this.#config.maxRetries } : {}),
135
+ // MG-1 (A5): the kernel stamps committed envelopes with this.
136
+ ...(this.#config.continuationScope !== undefined ? { continuationScope: this.#config.continuationScope } : {}),
137
+ // XP-1: native-only resolution — an unsupported recorded
138
+ // setting REFUSES the run with the reason (no silent
139
+ // downgrade); default/default adds nothing (byte anchor).
140
+ ...(this.#config.reasoning !== undefined ? resolveReasoningOrThrow(this.#config.model, this.#config.reasoning) : {}),
134
141
  // E1: the composed approval chain — the extensions'
135
142
  // policies composed into ONE gate (deny > allow > ask).
136
143
  ...(approvalChain !== undefined ? { approvalPolicy: approvalChain } : {}),
@@ -788,3 +795,11 @@ export class Run {
788
795
  });
789
796
  }
790
797
  }
798
+ /** XP-1: the run-side half of "no silent downgrade" — a setting the
799
+ * matrix refuses becomes an actionable failure, never a quiet default. */
800
+ function resolveReasoningOrThrow(model, setting) {
801
+ const r = resolveReasoning(model, setting);
802
+ if (!r.ok)
803
+ throw new Error(`the session's recorded reasoning setting cannot run here: ${r.reason}`);
804
+ return Object.keys(r.wire).length > 0 ? { reasoning: r.wire } : {};
805
+ }
package/dist/session.d.ts CHANGED
@@ -29,6 +29,8 @@
29
29
  */
30
30
  import { EventLog, type AbortSignalLike, type Adapter, type Event, type KisoExtension, type Message, type PermissionDecision, type Tool } from "@vincemakes/kiso-core";
31
31
  import { type TaskAssessment } from "./task-assessment.js";
32
+ import { type ContinuationScope } from "@vincemakes/kiso-core";
33
+ import type { ReasoningSetting } from "./provider/metadata.js";
32
34
  import { type SessionStore } from "./store.js";
33
35
  import { Run } from "./run.js";
34
36
  /** TUI2-R3v2 ③ — one off-trajectory model request (session.sideQuery).
@@ -129,12 +131,24 @@ export declare class AgentSession {
129
131
  readonly adapter: Adapter;
130
132
  readonly model: string;
131
133
  readonly provider?: "anthropic" | "openai-compat";
134
+ /** MG-1 (A5): the run's continuation scope — moves atomically with
135
+ * the adapter (absent = unscoped: the kernel strips envelopes). */
136
+ readonly scope?: ContinuationScope;
137
+ /** XP-1: the reasoning axes travel with the binding too; absent =
138
+ * fresh defaults (a new binding never inherits stale effort). */
139
+ readonly reasoning?: ReasoningSetting;
132
140
  }): void;
133
141
  /** E2: the adapter identity ("anthropic" | "openai-compat") — the route
134
142
  * key the canonical consumer (CLI usage, the trace block) keys on. The
135
143
  * per-run tracer reads the SAME live binding; one source, one
136
144
  * route — the CLI and the trace can never disagree. */
137
145
  get provider(): "anthropic" | "openai-compat" | undefined;
146
+ /** XP-1: the model that will answer the NEXT request — the live
147
+ * binding, restored from the durable profile on open. The status row
148
+ * reads THIS, so the row and the request can never disagree. */
149
+ get model(): string;
150
+ /** XP-1: the selected reasoning axes (resolution happens per request). */
151
+ get reasoning(): ReasoningSetting;
138
152
  /**
139
153
  * TUI2-R3v2 ③ — ONE model request that belongs to no run (the
140
154
  * safer-options seam, adjudicated 2026-08-18).
@@ -358,6 +372,13 @@ export interface SessionConfig {
358
372
  /** E1: the adapter identity ("anthropic" | "openai-compat") — trace
359
373
  * provenance, additive (S1 surface untouched: type-only, optional). */
360
374
  readonly provider?: "anthropic" | "openai-compat";
375
+ /** MG-1 (A5): the run's continuation scope — the kernel stamps it on
376
+ * committed envelopes; absent = unscoped (envelopes stripped). */
377
+ readonly continuationScope?: import("@vincemakes/kiso-core").ContinuationScope;
378
+ /** XP-1: the selected reasoning axes (native-only resolution per request). */
379
+ readonly reasoning?: import("./provider/metadata.js").ReasoningSetting;
380
+ /** XP-1 internal: a legacy session's deferred revision-1 write. */
381
+ readonly profilePending?: true;
361
382
  readonly systemPrompt?: string;
362
383
  readonly tools?: readonly Tool<any>[];
363
384
  readonly registry: import("@vincemakes/kiso-core").ToolRegistry;
package/dist/session.js CHANGED
@@ -35,6 +35,7 @@ import { assessTasks } from "./task-assessment.js";
35
35
  * verification surface. Override per call for custom evidence tools. */
36
36
  const DEFAULT_EVIDENCE_TOOLS = new Set(["shell"]);
37
37
  import { denialResult } from "@vincemakes/kiso-core";
38
+ import { buildProfile, readProfile, writeProfile } from "./profile.js";
38
39
  import { DROP_PLACEHOLDER, estimateSummarySavings, KEEP_RECENT_ROUNDS, KEEP_TOKENS_DEFAULT, lastSummaryPoint, MAX_SUMMARY_FAILURES, policyTriggerFromWindow, serializeCovered, SUMMARY_MAX_OUTPUT, summarizeConversation, summaryBoundarySeq, } from "./summarize.js";
39
40
  import { canonicalizeUsageForModel } from "./usage/canonical.js";
40
41
  import { appendFileSync, mkdirSync } from "node:fs";
@@ -84,6 +85,13 @@ export class AgentSession {
84
85
  // setAdapter always had).
85
86
  #model;
86
87
  #provider;
88
+ // MG-1 (A5): travels WITH the adapter, same next-turn semantics.
89
+ #continuationScope;
90
+ // XP-1: the selected axes; resolved per request (next-turn semantics).
91
+ #reasoning;
92
+ // XP-1: a legacy session records revision 1 at the next explicit
93
+ // selection or first request — never eagerly at open.
94
+ #profilePending;
87
95
  #pendingResolvers = new Map();
88
96
  #uncertaintyResolvers = new Map();
89
97
  #answered = new Set();
@@ -137,14 +145,23 @@ export class AgentSession {
137
145
  this.#config = composedHooks === undefined ? config : { ...config, hooks: composedHooks };
138
146
  this.#model = config.model;
139
147
  this.#provider = config.provider;
148
+ this.#continuationScope = config.continuationScope;
149
+ this.#reasoning = config.reasoning ?? { thinking: "default", effort: "default" };
150
+ this.#profilePending = config.profilePending === true;
140
151
  }
141
152
  /** The config a NEW run/resume/summary sees: the frozen startup config
142
153
  * with the LIVE binding fields (model, provider) substituted. Built
143
154
  * fresh per call so an in-flight run keeps the config it started with
144
155
  * — the same boundary setAdapter has always drawn. */
145
156
  #effectiveConfig() {
146
- const { provider: _startup, ...rest } = this.#config;
147
- return { ...rest, model: this.#model, ...(this.#provider !== undefined ? { provider: this.#provider } : {}) };
157
+ const { provider: _startup, continuationScope: _startupScope, ...rest } = this.#config;
158
+ return {
159
+ ...rest,
160
+ model: this.#model,
161
+ ...(this.#provider !== undefined ? { provider: this.#provider } : {}),
162
+ ...(this.#continuationScope !== undefined ? { continuationScope: this.#continuationScope } : {}),
163
+ reasoning: this.#reasoning,
164
+ };
148
165
  }
149
166
  /** Write-ahead through the store; a rejected write POISONS the session
150
167
  * (round 1/round 4): the in-memory log no longer matches the disk — whatever
@@ -204,6 +221,11 @@ export class AgentSession {
204
221
  this.#adapter = binding.adapter;
205
222
  this.#model = binding.model;
206
223
  this.#provider = binding.provider;
224
+ this.#continuationScope = binding.scope;
225
+ this.#reasoning = binding.reasoning ?? { thinking: "default", effort: "default" };
226
+ // XP-1: an explicit selection is DURABLE — the setting survives
227
+ // /resume because a revision records it now, not at some later flush.
228
+ this.#recordProfile();
207
229
  }
208
230
  /** E2: the adapter identity ("anthropic" | "openai-compat") — the route
209
231
  * key the canonical consumer (CLI usage, the trace block) keys on. The
@@ -212,6 +234,31 @@ export class AgentSession {
212
234
  get provider() {
213
235
  return this.#provider;
214
236
  }
237
+ /** XP-1: the model that will answer the NEXT request — the live
238
+ * binding, restored from the durable profile on open. The status row
239
+ * reads THIS, so the row and the request can never disagree. */
240
+ get model() {
241
+ return this.#model;
242
+ }
243
+ /** XP-1: the selected reasoning axes (resolution happens per request). */
244
+ get reasoning() {
245
+ return this.#reasoning;
246
+ }
247
+ /** XP-1: record the live binding as the next durable profile revision
248
+ * (read-modify-write under the session's single-writer ownership). */
249
+ #recordProfile() {
250
+ const prior = readProfile(this.#store.root, this.id);
251
+ const revision = prior.kind === "ok" ? prior.profile.revision + 1 : 1;
252
+ writeProfile(this.#store.root, this.id, buildProfile({
253
+ revision,
254
+ modelId: this.#model,
255
+ provider: this.#continuationScope ?? null,
256
+ reasoning: this.#reasoning,
257
+ ...(this.#config.systemPrompt !== undefined ? { systemPrompt: this.#config.systemPrompt } : {}),
258
+ registry: this.#config.registry,
259
+ }));
260
+ this.#profilePending = false;
261
+ }
215
262
  /**
216
263
  * TUI2-R3v2 ③ — ONE model request that belongs to no run (the
217
264
  * safer-options seam, adjudicated 2026-08-18).
@@ -299,6 +346,8 @@ export class AgentSession {
299
346
  * send one. */
300
347
  run(input, options) {
301
348
  this.ensureHealthy();
349
+ if (this.#profilePending)
350
+ this.#recordProfile(); // XP-1: legacy revision 1, before the first request
302
351
  return new Run(this.#store, this.#adapter, this.#effectiveConfig(), this, input, options?.signal, false, options?.source);
303
352
  }
304
353
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-runtime",
3
- "version": "0.15.12",
3
+ "version": "0.16.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,11 +25,11 @@
25
25
  "test": "vitest run"
26
26
  },
27
27
  "dependencies": {
28
- "@vincemakes/kiso-core": "0.15.12"
28
+ "@vincemakes/kiso-core": "0.16.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "@vincemakes/kiso-provider-anthropic": "0.15.12",
32
- "@vincemakes/kiso-provider-openai": "0.15.12"
31
+ "@vincemakes/kiso-provider-anthropic": "0.16.0",
32
+ "@vincemakes/kiso-provider-openai": "0.16.0"
33
33
  },
34
34
  "peerDependenciesMeta": {
35
35
  "@vincemakes/kiso-provider-anthropic": {
@@ -40,7 +40,7 @@
40
40
  }
41
41
  },
42
42
  "devDependencies": {
43
- "@vincemakes/kiso-evals": "0.15.12",
43
+ "@vincemakes/kiso-evals": "0.16.0",
44
44
  "@types/node": "^26.1.2",
45
45
  "typescript": "^5.7.2",
46
46
  "vitest": "^3.0.0"