@vincemakes/kiso-runtime 0.15.11 → 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 +1 -0
- package/dist/agent.js +73 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +2 -0
- package/dist/profile.d.ts +114 -0
- package/dist/profile.js +151 -0
- package/dist/provider/manifest.d.ts +35 -0
- package/dist/provider/manifest.js +57 -0
- package/dist/provider/metadata.d.ts +79 -2
- package/dist/provider/metadata.js +98 -4
- package/dist/run.js +15 -0
- package/dist/session.d.ts +21 -0
- package/dist/session.js +51 -2
- package/package.json +5 -5
package/dist/agent.d.ts
CHANGED
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:
|
package/dist/internal.d.ts
CHANGED
|
@@ -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;
|
package/dist/profile.js
ADDED
|
@@ -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
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
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.
|
|
28
|
+
"@vincemakes/kiso-core": "0.16.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@vincemakes/kiso-provider-anthropic": "0.
|
|
32
|
-
"@vincemakes/kiso-provider-openai": "0.
|
|
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.
|
|
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"
|