@vincemakes/kiso-runtime 0.13.0 → 0.15.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 +5 -0
- package/dist/agent.js +1 -0
- package/dist/compose.js +6 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/internal.d.ts +2 -0
- package/dist/internal.js +7 -0
- package/dist/provider/metadata.d.ts +56 -0
- package/dist/provider/metadata.js +82 -0
- package/dist/run.d.ts +1 -1
- package/dist/run.js +15 -5
- package/dist/session.d.ts +33 -1
- package/dist/session.js +73 -11
- package/dist/task-assessment.d.ts +93 -0
- package/dist/task-assessment.js +165 -0
- package/dist/trace/guard.js +9 -6
- package/dist/usage/canonical.d.ts +15 -0
- package/dist/usage/canonical.js +40 -9
- package/package.json +5 -5
package/dist/agent.d.ts
CHANGED
|
@@ -39,6 +39,10 @@ export interface AgentDefinition {
|
|
|
39
39
|
readonly provider?: "anthropic" | "openai-compat";
|
|
40
40
|
readonly apiKey?: string;
|
|
41
41
|
readonly baseUrl?: string;
|
|
42
|
+
/** PH-1c.1: opt-in Anthropic prompt caching (cache_control
|
|
43
|
+
* breakpoints) — OFF by default; the openai-compat path ignores it
|
|
44
|
+
* (that dialect's caching is server-automatic). Type-only additive. */
|
|
45
|
+
readonly promptCaching?: boolean;
|
|
42
46
|
readonly maxTurns?: number;
|
|
43
47
|
readonly maxTokens?: number;
|
|
44
48
|
readonly temperature?: number;
|
|
@@ -90,4 +94,5 @@ export declare function createAgent(definition: AgentDefinition): AgentRuntime;
|
|
|
90
94
|
export declare function buildAdapter(provider: "anthropic" | "openai-compat", opts?: {
|
|
91
95
|
readonly apiKey?: string;
|
|
92
96
|
readonly baseUrl?: string;
|
|
97
|
+
readonly promptCaching?: boolean;
|
|
93
98
|
}): Promise<Adapter>;
|
package/dist/agent.js
CHANGED
|
@@ -132,6 +132,7 @@ async function resolveAdapter(definition) {
|
|
|
132
132
|
return createAnthropicProvider({
|
|
133
133
|
...(definition.apiKey !== undefined ? { apiKey: definition.apiKey } : {}),
|
|
134
134
|
...(definition.baseUrl !== undefined ? { baseUrl: definition.baseUrl } : {}),
|
|
135
|
+
...(definition.promptCaching !== undefined ? { promptCaching: definition.promptCaching } : {}),
|
|
135
136
|
});
|
|
136
137
|
}
|
|
137
138
|
case "openai-compat": {
|
package/dist/compose.js
CHANGED
|
@@ -201,8 +201,13 @@ export function composeApprovalChain(extensions) {
|
|
|
201
201
|
try {
|
|
202
202
|
v = await Promise.resolve(policy.decide(payload, ctx));
|
|
203
203
|
}
|
|
204
|
-
catch {
|
|
204
|
+
catch (err) {
|
|
205
205
|
v = { action: "ask" }; // a throwing policy counts as ask — it speaks, never silently
|
|
206
|
+
// PH-1a (finding PH-F17): the degradation itself must speak
|
|
207
|
+
// too — a permanently-throwing policy used to manifest only
|
|
208
|
+
// as "why is it asking me about everything?". One stderr
|
|
209
|
+
// line, naming the extension; the verdict is unchanged.
|
|
210
|
+
console.error(`[kiso] approval policy from extension "${extension}" threw (${err instanceof Error ? err.message : String(err)}) — degraded to ask`);
|
|
206
211
|
}
|
|
207
212
|
if (v.action === "abstain")
|
|
208
213
|
continue; // no opinion — not a verdict
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ export { disposeExtensions, loadExtensions, loadProjectExtensions } from "./exte
|
|
|
31
31
|
export type { KisoExtension } from "./extensions.js";
|
|
32
32
|
export { executionForCallId, executionLedger } from "./ledger.js";
|
|
33
33
|
export type { ExecutionRecord, ExecutionStatus } from "./ledger.js";
|
|
34
|
+
export { assessTasks } from "./task-assessment.js";
|
|
35
|
+
export type { EvidenceVerdict, TaskAssessment, TaskClaim } from "./task-assessment.js";
|
|
34
36
|
export { kisoHome, projectArtifacts, recordTrust, trustFor } from "./trust.js";
|
|
35
37
|
export type { ProjectArtifact, ProjectArtifacts, TrustDecision, TrustRecord } from "./trust.js";
|
|
36
38
|
export { canonicalizeUsage } from "./usage/canonical.js";
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,9 @@ export { SessionStore, StaleWriterError, StoreCorruptionError } from "./store.js
|
|
|
31
31
|
export { disposeExtensions, loadExtensions, loadProjectExtensions } from "./extensions.js";
|
|
32
32
|
// ledger
|
|
33
33
|
export { executionForCallId, executionLedger } from "./ledger.js";
|
|
34
|
+
// task assessment (TV-1A) — the pure projection separating the model's
|
|
35
|
+
// CLAIM from VERIFIED under Verified ⟹ evidenceSeq > lastMutationSeq
|
|
36
|
+
export { assessTasks } from "./task-assessment.js";
|
|
34
37
|
// trust
|
|
35
38
|
export { kisoHome, projectArtifacts, recordTrust, trustFor } from "./trust.js";
|
|
36
39
|
// usage — the canonical accounting schema (E2/1.3.0, R4b-1 ruling:
|
package/dist/internal.d.ts
CHANGED
package/dist/internal.js
CHANGED
|
@@ -23,3 +23,10 @@ export * from "./lock-adapter.js";
|
|
|
23
23
|
export * from "./ledger.js";
|
|
24
24
|
export * from "./extensions.js";
|
|
25
25
|
export * from "./trust.js";
|
|
26
|
+
// PH-1c: the model metadata registry (capabilities + dated pricing) —
|
|
27
|
+
// first-party consumers (the CLI window derivation) read it here; the
|
|
28
|
+
// curated root surface does not move.
|
|
29
|
+
export * from "./provider/metadata.js";
|
|
30
|
+
// PH-1c: the model-keyed cost derivation rides the same door (the root
|
|
31
|
+
// surface keeps only the frozen canonicalizeUsage).
|
|
32
|
+
export { canonicalizeUsageForModel } from "./usage/canonical.js";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PH-1c — the model metadata table (findings PH-F14/PH-F15/PH-F16).
|
|
3
|
+
*
|
|
4
|
+
* Capabilities and pricing keyed by MODEL (+ optional endpoint), never
|
|
5
|
+
* by route: `anthropic`/`openai-compat` are protocol conventions — the
|
|
6
|
+
* same route serves models whose windows and rates have nothing in
|
|
7
|
+
* common, which is how the old route-keyed table priced an Anthropic
|
|
8
|
+
* run at DeepSeek's rates. The registry's one discipline: **unknown is
|
|
9
|
+
* null, everywhere** — an absent entry, an absent field, an unverified
|
|
10
|
+
* price all surface as null and no layer downstream may guess.
|
|
11
|
+
*
|
|
12
|
+
* Pricing is SEPARATE from capabilities (the review boundary): a price
|
|
13
|
+
* is a dated claim about someone else's billing page, so every entry
|
|
14
|
+
* carries `asOf` (the freeze date) and `source` (the page). A model we
|
|
15
|
+
* can name but not price stays `pricing: null` — the honest stamp the
|
|
16
|
+
* E2 nullable convention already defined.
|
|
17
|
+
*
|
|
18
|
+
* Lives under runtime/internal — the curated root surface (44 names)
|
|
19
|
+
* does not move; core is untouched.
|
|
20
|
+
*/
|
|
21
|
+
export interface ModelCapabilities {
|
|
22
|
+
/** tokens of context the model accepts; null = unknown. */
|
|
23
|
+
readonly contextWindow: number | null;
|
|
24
|
+
readonly maxOutputTokens: number | null;
|
|
25
|
+
/** "automatic" — the provider caches without request markup (DeepSeek,
|
|
26
|
+
* OpenAI); "explicit" — the request must place cache_control
|
|
27
|
+
* breakpoints (Anthropic); "none" — no caching; null = unknown. */
|
|
28
|
+
readonly promptCaching: "none" | "automatic" | "explicit" | null;
|
|
29
|
+
/** the model emits a reasoning stream (thinking); null = unknown. */
|
|
30
|
+
readonly reasoning: boolean | null;
|
|
31
|
+
}
|
|
32
|
+
export interface ModelPricing {
|
|
33
|
+
readonly inputPerM: number;
|
|
34
|
+
readonly outputPerM: number;
|
|
35
|
+
readonly cacheReadPerM: number;
|
|
36
|
+
readonly cacheWritePerM: number;
|
|
37
|
+
/** the date the rates were read — a price is a dated claim. */
|
|
38
|
+
readonly asOf: string;
|
|
39
|
+
/** the billing page the rates came from. */
|
|
40
|
+
readonly source: string;
|
|
41
|
+
}
|
|
42
|
+
export interface ModelMetadataEntry {
|
|
43
|
+
/** EXACT model id — v1 does no pattern matching. */
|
|
44
|
+
readonly model: string;
|
|
45
|
+
/** origin qualifier (e.g. "https://api.deepseek.com"): when present,
|
|
46
|
+
* the entry matches only requests aimed at that endpoint. */
|
|
47
|
+
readonly endpoint?: string;
|
|
48
|
+
readonly capabilities: ModelCapabilities;
|
|
49
|
+
readonly pricing: ModelPricing | null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Look a model up. `endpoint` narrows: an entry WITH an endpoint only
|
|
53
|
+
* matches when the caller's endpoint origin equals it; an entry without
|
|
54
|
+
* one matches any endpoint. Unknown model → null, never a default.
|
|
55
|
+
*/
|
|
56
|
+
export declare function lookupModelMetadata(model: string, endpoint?: string): ModelMetadataEntry | null;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PH-1c — the model metadata table (findings PH-F14/PH-F15/PH-F16).
|
|
3
|
+
*
|
|
4
|
+
* Capabilities and pricing keyed by MODEL (+ optional endpoint), never
|
|
5
|
+
* by route: `anthropic`/`openai-compat` are protocol conventions — the
|
|
6
|
+
* same route serves models whose windows and rates have nothing in
|
|
7
|
+
* common, which is how the old route-keyed table priced an Anthropic
|
|
8
|
+
* run at DeepSeek's rates. The registry's one discipline: **unknown is
|
|
9
|
+
* null, everywhere** — an absent entry, an absent field, an unverified
|
|
10
|
+
* price all surface as null and no layer downstream may guess.
|
|
11
|
+
*
|
|
12
|
+
* Pricing is SEPARATE from capabilities (the review boundary): a price
|
|
13
|
+
* is a dated claim about someone else's billing page, so every entry
|
|
14
|
+
* carries `asOf` (the freeze date) and `source` (the page). A model we
|
|
15
|
+
* can name but not price stays `pricing: null` — the honest stamp the
|
|
16
|
+
* E2 nullable convention already defined.
|
|
17
|
+
*
|
|
18
|
+
* Lives under runtime/internal — the curated root surface (44 names)
|
|
19
|
+
* does not move; core is untouched.
|
|
20
|
+
*/
|
|
21
|
+
const DEEPSEEK_PRICING = {
|
|
22
|
+
// The E2-frozen rates (pricing table v1, freeze date 2026-08-13),
|
|
23
|
+
// re-homed here with their provenance made explicit. The caveat
|
|
24
|
+
// carries forward verbatim: an approximation, not a bill.
|
|
25
|
+
inputPerM: 0.27,
|
|
26
|
+
outputPerM: 1.1,
|
|
27
|
+
cacheReadPerM: 0.027,
|
|
28
|
+
cacheWritePerM: 0,
|
|
29
|
+
asOf: "2026-08-13",
|
|
30
|
+
source: "https://api-docs.deepseek.com/quick_start/pricing",
|
|
31
|
+
};
|
|
32
|
+
/** The v1 table. Nulls outnumber numbers ON PURPOSE: only values with a
|
|
33
|
+
* named source enter; everything else waits for one. */
|
|
34
|
+
const ENTRIES = [
|
|
35
|
+
{
|
|
36
|
+
model: "deepseek-chat",
|
|
37
|
+
endpoint: "https://api.deepseek.com",
|
|
38
|
+
capabilities: { contextWindow: null, maxOutputTokens: null, promptCaching: "automatic", reasoning: false },
|
|
39
|
+
pricing: DEEPSEEK_PRICING,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
model: "deepseek-reasoner",
|
|
43
|
+
endpoint: "https://api.deepseek.com",
|
|
44
|
+
capabilities: { contextWindow: null, maxOutputTokens: null, promptCaching: "automatic", reasoning: true },
|
|
45
|
+
pricing: DEEPSEEK_PRICING,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
model: "claude-sonnet-5",
|
|
49
|
+
capabilities: { contextWindow: 200_000, maxOutputTokens: null, promptCaching: "explicit", reasoning: true },
|
|
50
|
+
// Priced only when the rates are read from the live billing page
|
|
51
|
+
// and dated — never copied from memory (the review's boundary ②).
|
|
52
|
+
pricing: null,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
model: "gpt-4o",
|
|
56
|
+
capabilities: { contextWindow: 128_000, maxOutputTokens: null, promptCaching: "automatic", reasoning: false },
|
|
57
|
+
pricing: null,
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
/**
|
|
61
|
+
* Look a model up. `endpoint` narrows: an entry WITH an endpoint only
|
|
62
|
+
* matches when the caller's endpoint origin equals it; an entry without
|
|
63
|
+
* one matches any endpoint. Unknown model → null, never a default.
|
|
64
|
+
*/
|
|
65
|
+
export function lookupModelMetadata(model, endpoint) {
|
|
66
|
+
for (const entry of ENTRIES) {
|
|
67
|
+
if (entry.model !== model)
|
|
68
|
+
continue;
|
|
69
|
+
if (entry.endpoint !== undefined && endpoint !== undefined && entry.endpoint !== originOf(endpoint))
|
|
70
|
+
continue;
|
|
71
|
+
return entry;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function originOf(endpoint) {
|
|
76
|
+
try {
|
|
77
|
+
return new URL(endpoint).origin;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return endpoint;
|
|
81
|
+
}
|
|
82
|
+
}
|
package/dist/run.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { type AgentSession, type SessionConfig } from "./session.js";
|
|
|
13
13
|
export declare class Run implements AsyncIterable<Event> {
|
|
14
14
|
#private;
|
|
15
15
|
runId: string;
|
|
16
|
-
constructor(store: SessionStore, adapter: Adapter, config: SessionConfig, session: AgentSession, input: string | undefined, externalSignal: AbortSignalLike | undefined, resume: boolean);
|
|
16
|
+
constructor(store: SessionStore, adapter: Adapter, config: SessionConfig, session: AgentSession, input: string | undefined, externalSignal: AbortSignalLike | undefined, resume: boolean, source?: import("@vincemakes/kiso-core").MessageSource);
|
|
17
17
|
/** Cancel the run: propagates to the adapter (SDK) and future executions. */
|
|
18
18
|
abort(): void;
|
|
19
19
|
[Symbol.asyncIterator](): AsyncIterator<Event>;
|
package/dist/run.js
CHANGED
|
@@ -23,12 +23,17 @@ export class Run {
|
|
|
23
23
|
#session;
|
|
24
24
|
#input;
|
|
25
25
|
#resume;
|
|
26
|
+
#source;
|
|
26
27
|
#abort = new AbortController();
|
|
27
28
|
#externalSignal;
|
|
28
29
|
#decisionIds = [];
|
|
29
30
|
#uncertaintyIds = [];
|
|
30
31
|
#started = false;
|
|
31
|
-
constructor(store, adapter, config, session, input, externalSignal, resume
|
|
32
|
+
constructor(store, adapter, config, session, input, externalSignal, resume,
|
|
33
|
+
// TV-1B: durable PROVENANCE for the input (e.g. the verification
|
|
34
|
+
// seed's source:"system") — who produced the line, never a
|
|
35
|
+
// provider-role escalation. Absent = plain user input.
|
|
36
|
+
source) {
|
|
32
37
|
this.#store = store;
|
|
33
38
|
this.#adapter = adapter;
|
|
34
39
|
this.#config = config;
|
|
@@ -36,6 +41,7 @@ export class Run {
|
|
|
36
41
|
this.#input = input;
|
|
37
42
|
this.#externalSignal = externalSignal;
|
|
38
43
|
this.#resume = resume;
|
|
44
|
+
this.#source = source;
|
|
39
45
|
this.runId = crypto.randomUUID();
|
|
40
46
|
}
|
|
41
47
|
/** Cancel the run: propagates to the adapter (SDK) and future executions. */
|
|
@@ -244,7 +250,7 @@ export class Run {
|
|
|
244
250
|
// session. The prompt is also the first event the consumer
|
|
245
251
|
// sees, so what was asked and what happened live in the same
|
|
246
252
|
// stream.
|
|
247
|
-
const inputEvent = log.append({ type: "user_input", content: this.#input });
|
|
253
|
+
const inputEvent = log.append({ type: "user_input", content: this.#input, ...(this.#source !== undefined ? { source: this.#source } : {}) });
|
|
248
254
|
await this.#session.persist(this.runId, inputEvent);
|
|
249
255
|
yield inputEvent;
|
|
250
256
|
// 2. The loop projects from the session log — multi-turn context
|
|
@@ -430,8 +436,12 @@ export class Run {
|
|
|
430
436
|
verdict = chainVerdict;
|
|
431
437
|
}
|
|
432
438
|
}
|
|
433
|
-
catch {
|
|
439
|
+
catch (err) {
|
|
434
440
|
verdict = { action: "ask" };
|
|
441
|
+
// PH-1a (finding PH-F17): same speaking degradation as the chain's
|
|
442
|
+
// per-policy catch — the recovery path has no extension name, so
|
|
443
|
+
// the chain is named as a whole.
|
|
444
|
+
console.error(`[kiso] approval chain threw during recovery (${err instanceof Error ? err.message : String(err)}) — degraded to ask`);
|
|
435
445
|
}
|
|
436
446
|
if (verdict === undefined && hooks?.onPreTool !== undefined) {
|
|
437
447
|
const decision = await abortable(Promise.resolve(hooks.onPreTool(payload, { sessionId: this.#session.id })), signal);
|
|
@@ -714,8 +724,8 @@ export class Run {
|
|
|
714
724
|
}
|
|
715
725
|
// ruling #12 correction one: the honest note rides the recovered failure too —
|
|
716
726
|
// the receipt and the repaired tool_result reproduce the live path
|
|
717
|
-
// losslessly.
|
|
718
|
-
if (result.isError && tool?.idempotent !== true) {
|
|
727
|
+
// losslessly. WR-1-F1: precondition refusals excluded — nothing ran.
|
|
728
|
+
if (result.isError && result.errorKind !== "precondition" && tool?.idempotent !== true) {
|
|
719
729
|
result = {
|
|
720
730
|
...result,
|
|
721
731
|
content: `${result.content}\n[non-idempotent tool failed — its side effects may have partially applied; verify before retrying]`,
|
package/dist/session.d.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* same package, same exports (index.ts re-exports all four).
|
|
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
|
+
import { type TaskAssessment } from "./task-assessment.js";
|
|
31
32
|
import { type SessionStore } from "./store.js";
|
|
32
33
|
import { Run } from "./run.js";
|
|
33
34
|
/** TUI2-R3v2 ③ — one off-trajectory model request (session.sideQuery).
|
|
@@ -113,9 +114,25 @@ export declare class AgentSession {
|
|
|
113
114
|
* turns (dispatch's /model), never mid-run.
|
|
114
115
|
*/
|
|
115
116
|
setAdapter(adapter: Adapter): void;
|
|
117
|
+
/**
|
|
118
|
+
* PH-1a (finding PH-F8, P0): the ATOMIC model switch — adapter, model
|
|
119
|
+
* id, and provider route replace together, effective at the next run.
|
|
120
|
+
* setAdapter alone is for a same-binding adapter swap (the faux
|
|
121
|
+
* re-arm); a switch that changes WHAT model answers must come through
|
|
122
|
+
* here, or the UI claims one model while requests carry another.
|
|
123
|
+
* Omitting `provider` clears the route — an unknown binding is
|
|
124
|
+
* canonicalized under the honest "adapter" route (null-priced), never
|
|
125
|
+
* the stale one. (The context window joins the binding when per-model
|
|
126
|
+
* metadata exists — the PH-1c registry.)
|
|
127
|
+
*/
|
|
128
|
+
setModelBinding(binding: {
|
|
129
|
+
readonly adapter: Adapter;
|
|
130
|
+
readonly model: string;
|
|
131
|
+
readonly provider?: "anthropic" | "openai-compat";
|
|
132
|
+
}): void;
|
|
116
133
|
/** E2: the adapter identity ("anthropic" | "openai-compat") — the route
|
|
117
134
|
* key the canonical consumer (CLI usage, the trace block) keys on. The
|
|
118
|
-
* per-run tracer reads the SAME
|
|
135
|
+
* per-run tracer reads the SAME live binding; one source, one
|
|
119
136
|
* route — the CLI and the trace can never disagree. */
|
|
120
137
|
get provider(): "anthropic" | "openai-compat" | undefined;
|
|
121
138
|
/**
|
|
@@ -158,6 +175,7 @@ export declare class AgentSession {
|
|
|
158
175
|
/** Run one user turn. Iterate to consume; `run.abort()` cancels. */
|
|
159
176
|
run(input: string, options?: {
|
|
160
177
|
signal?: AbortSignalLike;
|
|
178
|
+
source?: import("@vincemakes/kiso-core").MessageSource;
|
|
161
179
|
}): Run;
|
|
162
180
|
/**
|
|
163
181
|
* Continue the interrupted run (Area 2): apply durable decisions,
|
|
@@ -220,6 +238,20 @@ export declare class AgentSession {
|
|
|
220
238
|
approve(decisionId: string, allow: boolean, reason?: string): Promise<void>;
|
|
221
239
|
/** Executions that started but never reported a result (crash window). */
|
|
222
240
|
uncertainExecutions(): import("./ledger.js").ExecutionRecord[];
|
|
241
|
+
/**
|
|
242
|
+
* TV-1A — assess the task claims and their evidence freshness over THIS
|
|
243
|
+
* session's durable log. The non-mutating set comes from the live tools'
|
|
244
|
+
* own `effects.precommitSafe` certificates (one direction of truth,
|
|
245
|
+
* never a second declaration) — the read-only+free+local contract, the
|
|
246
|
+
* only certificate that proves the world untouched. `concurrency:
|
|
247
|
+
* "shared"` is a SCHEDULING promise and never feeds this set (TV-1C —
|
|
248
|
+
* slow_touch is shared and writes). The evidence policy defaults to
|
|
249
|
+
* {"shell"} — the convention the task extension's own "make the LAST
|
|
250
|
+
* item a verification step" guidance produces.
|
|
251
|
+
*/
|
|
252
|
+
assessTasks(opts?: {
|
|
253
|
+
readonly evidenceTools?: ReadonlySet<string>;
|
|
254
|
+
}): TaskAssessment;
|
|
223
255
|
/**
|
|
224
256
|
* The human's verdict on an interrupted execution, keyed by EXECUTION ID
|
|
225
257
|
* (B group): "rerun" (the human says the side effect did NOT happen — the
|
package/dist/session.js
CHANGED
|
@@ -29,9 +29,14 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import { EventLog, projectMessages, } from "@vincemakes/kiso-core";
|
|
31
31
|
import { executionLedger } from "./ledger.js";
|
|
32
|
+
import { assessTasks } from "./task-assessment.js";
|
|
33
|
+
/** TV-1A — the session-level evidence policy: the PURE projection defaults
|
|
34
|
+
* to ∅ (never inventing evidence); the session names the one built-in
|
|
35
|
+
* verification surface. Override per call for custom evidence tools. */
|
|
36
|
+
const DEFAULT_EVIDENCE_TOOLS = new Set(["shell"]);
|
|
32
37
|
import { denialResult } from "@vincemakes/kiso-core";
|
|
33
38
|
import { DROP_PLACEHOLDER, estimateSummarySavings, KEEP_RECENT_ROUNDS, KEEP_TOKENS_DEFAULT, lastSummaryPoint, MAX_SUMMARY_FAILURES, policyTriggerFromWindow, serializeCovered, SUMMARY_MAX_OUTPUT, summarizeConversation, summaryBoundarySeq, } from "./summarize.js";
|
|
34
|
-
import {
|
|
39
|
+
import { canonicalizeUsageForModel } from "./usage/canonical.js";
|
|
35
40
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
36
41
|
import { join } from "node:path";
|
|
37
42
|
import { estimateTokens } from "@vincemakes/kiso-core";
|
|
@@ -67,9 +72,18 @@ export class AgentSession {
|
|
|
67
72
|
log;
|
|
68
73
|
#store;
|
|
69
74
|
// NOT readonly since 0.1.23: /model replaces it between runs (the
|
|
70
|
-
// constructor and
|
|
75
|
+
// constructor, setAdapter, and setModelBinding are the only writers).
|
|
71
76
|
#adapter;
|
|
72
77
|
#config;
|
|
78
|
+
// PH-1a (finding PH-F8, P0): the LIVE model binding. #config froze the
|
|
79
|
+
// startup model/provider, so a /model switch replaced the adapter while
|
|
80
|
+
// every later run kept sending the OLD model id and canonicalizing
|
|
81
|
+
// usage under the OLD route. These two travel WITH the adapter now —
|
|
82
|
+
// setModelBinding writes all three in one call; runs read them through
|
|
83
|
+
// #effectiveConfig at run construction (next-turn semantics, same as
|
|
84
|
+
// setAdapter always had).
|
|
85
|
+
#model;
|
|
86
|
+
#provider;
|
|
73
87
|
#pendingResolvers = new Map();
|
|
74
88
|
#uncertaintyResolvers = new Map();
|
|
75
89
|
#answered = new Set();
|
|
@@ -121,6 +135,16 @@ export class AgentSession {
|
|
|
121
135
|
}
|
|
122
136
|
const composedHooks = composeHooks(config.hooks, config.extensions ?? []);
|
|
123
137
|
this.#config = composedHooks === undefined ? config : { ...config, hooks: composedHooks };
|
|
138
|
+
this.#model = config.model;
|
|
139
|
+
this.#provider = config.provider;
|
|
140
|
+
}
|
|
141
|
+
/** The config a NEW run/resume/summary sees: the frozen startup config
|
|
142
|
+
* with the LIVE binding fields (model, provider) substituted. Built
|
|
143
|
+
* fresh per call so an in-flight run keeps the config it started with
|
|
144
|
+
* — the same boundary setAdapter has always drawn. */
|
|
145
|
+
#effectiveConfig() {
|
|
146
|
+
const { provider: _startup, ...rest } = this.#config;
|
|
147
|
+
return { ...rest, model: this.#model, ...(this.#provider !== undefined ? { provider: this.#provider } : {}) };
|
|
124
148
|
}
|
|
125
149
|
/** Write-ahead through the store; a rejected write POISONS the session
|
|
126
150
|
* (round 1/round 4): the in-memory log no longer matches the disk — whatever
|
|
@@ -165,12 +189,28 @@ export class AgentSession {
|
|
|
165
189
|
setAdapter(adapter) {
|
|
166
190
|
this.#adapter = adapter;
|
|
167
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* PH-1a (finding PH-F8, P0): the ATOMIC model switch — adapter, model
|
|
194
|
+
* id, and provider route replace together, effective at the next run.
|
|
195
|
+
* setAdapter alone is for a same-binding adapter swap (the faux
|
|
196
|
+
* re-arm); a switch that changes WHAT model answers must come through
|
|
197
|
+
* here, or the UI claims one model while requests carry another.
|
|
198
|
+
* Omitting `provider` clears the route — an unknown binding is
|
|
199
|
+
* canonicalized under the honest "adapter" route (null-priced), never
|
|
200
|
+
* the stale one. (The context window joins the binding when per-model
|
|
201
|
+
* metadata exists — the PH-1c registry.)
|
|
202
|
+
*/
|
|
203
|
+
setModelBinding(binding) {
|
|
204
|
+
this.#adapter = binding.adapter;
|
|
205
|
+
this.#model = binding.model;
|
|
206
|
+
this.#provider = binding.provider;
|
|
207
|
+
}
|
|
168
208
|
/** E2: the adapter identity ("anthropic" | "openai-compat") — the route
|
|
169
209
|
* key the canonical consumer (CLI usage, the trace block) keys on. The
|
|
170
|
-
* per-run tracer reads the SAME
|
|
210
|
+
* per-run tracer reads the SAME live binding; one source, one
|
|
171
211
|
* route — the CLI and the trace can never disagree. */
|
|
172
212
|
get provider() {
|
|
173
|
-
return this.#
|
|
213
|
+
return this.#provider;
|
|
174
214
|
}
|
|
175
215
|
/**
|
|
176
216
|
* TUI2-R3v2 ③ — ONE model request that belongs to no run (the
|
|
@@ -217,8 +257,8 @@ export class AgentSession {
|
|
|
217
257
|
root: this.#store.root,
|
|
218
258
|
sessionId: this.id,
|
|
219
259
|
runId,
|
|
220
|
-
provider: this.#
|
|
221
|
-
model: this.#
|
|
260
|
+
provider: this.#provider ?? "adapter",
|
|
261
|
+
model: this.#model,
|
|
222
262
|
adapterVersion: runtimeVersion(),
|
|
223
263
|
purpose: options.purpose,
|
|
224
264
|
// the manifest's seqRange pointers derive from the log, and a side
|
|
@@ -234,7 +274,7 @@ export class AgentSession {
|
|
|
234
274
|
let text = "";
|
|
235
275
|
try {
|
|
236
276
|
for await (const ev of guarded.stream({
|
|
237
|
-
model: this.#
|
|
277
|
+
model: this.#model,
|
|
238
278
|
messages: [{ role: "user", content: options.prompt }],
|
|
239
279
|
systemPrompt: options.systemPrompt,
|
|
240
280
|
...(options.maxTokens !== undefined ? { maxTokens: options.maxTokens } : {}),
|
|
@@ -252,7 +292,7 @@ export class AgentSession {
|
|
|
252
292
|
/** Run one user turn. Iterate to consume; `run.abort()` cancels. */
|
|
253
293
|
run(input, options) {
|
|
254
294
|
this.ensureHealthy();
|
|
255
|
-
return new Run(this.#store, this.#adapter, this.#
|
|
295
|
+
return new Run(this.#store, this.#adapter, this.#effectiveConfig(), this, input, options?.signal, false, options?.source);
|
|
256
296
|
}
|
|
257
297
|
/**
|
|
258
298
|
* Continue the interrupted run (Area 2): apply durable decisions,
|
|
@@ -262,7 +302,7 @@ export class AgentSession {
|
|
|
262
302
|
*/
|
|
263
303
|
resume() {
|
|
264
304
|
this.ensureHealthy();
|
|
265
|
-
return new Run(this.#store, this.#adapter, this.#
|
|
305
|
+
return new Run(this.#store, this.#adapter, this.#effectiveConfig(), this, undefined, undefined, true);
|
|
266
306
|
}
|
|
267
307
|
/**
|
|
268
308
|
* /compact (ADR-0044): compress the older conversation with a model
|
|
@@ -317,7 +357,7 @@ export class AgentSession {
|
|
|
317
357
|
else {
|
|
318
358
|
const call = await summarizeConversation({
|
|
319
359
|
adapter: this.#adapter,
|
|
320
|
-
model: this.#
|
|
360
|
+
model: this.#model,
|
|
321
361
|
// E6 (a): ONE serialized user message — the DSML bug's
|
|
322
362
|
// raw-message array is structurally dead on this path.
|
|
323
363
|
messages: [{ role: "user", content: serializedInput }],
|
|
@@ -351,7 +391,7 @@ export class AgentSession {
|
|
|
351
391
|
// a degraded ledger costs one stderr line, never the summary.
|
|
352
392
|
if (usage !== null) {
|
|
353
393
|
try {
|
|
354
|
-
const canonical =
|
|
394
|
+
const canonical = canonicalizeUsageForModel(this.#model, undefined, this.#provider ?? "adapter", usage);
|
|
355
395
|
const line = JSON.stringify({ kind: "summary", canonical }) + "\n";
|
|
356
396
|
mkdirSync(join(this.#store.root, "traces"), { recursive: true });
|
|
357
397
|
appendFileSync(join(this.#store.root, "traces", `${this.id}.jsonl`), line);
|
|
@@ -528,6 +568,28 @@ export class AgentSession {
|
|
|
528
568
|
uncertainExecutions() {
|
|
529
569
|
return [...executionLedger(this.log.all).values()].filter((r) => r.status === "uncertain");
|
|
530
570
|
}
|
|
571
|
+
/**
|
|
572
|
+
* TV-1A — assess the task claims and their evidence freshness over THIS
|
|
573
|
+
* session's durable log. The non-mutating set comes from the live tools'
|
|
574
|
+
* own `effects.precommitSafe` certificates (one direction of truth,
|
|
575
|
+
* never a second declaration) — the read-only+free+local contract, the
|
|
576
|
+
* only certificate that proves the world untouched. `concurrency:
|
|
577
|
+
* "shared"` is a SCHEDULING promise and never feeds this set (TV-1C —
|
|
578
|
+
* slow_touch is shared and writes). The evidence policy defaults to
|
|
579
|
+
* {"shell"} — the convention the task extension's own "make the LAST
|
|
580
|
+
* item a verification step" guidance produces.
|
|
581
|
+
*/
|
|
582
|
+
assessTasks(opts) {
|
|
583
|
+
const nonMutatingTools = new Set();
|
|
584
|
+
for (const tool of this.#config.registry.list()) {
|
|
585
|
+
if (tool.effects?.precommitSafe === true)
|
|
586
|
+
nonMutatingTools.add(tool.name);
|
|
587
|
+
}
|
|
588
|
+
return assessTasks(this.log.all, {
|
|
589
|
+
nonMutatingTools,
|
|
590
|
+
evidenceTools: opts?.evidenceTools ?? DEFAULT_EVIDENCE_TOOLS,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
531
593
|
/**
|
|
532
594
|
* The human's verdict on an interrupted execution, keyed by EXECUTION ID
|
|
533
595
|
* (B group): "rerun" (the human says the side effect did NOT happen — the
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TV-1A — TaskAssessment as a PURE PROJECTION + Evidence Freshness.
|
|
3
|
+
*
|
|
4
|
+
* The task checklist the model maintains through task_set is a SELF-REPORT:
|
|
5
|
+
* an item is "done" because the model said so. This projection mechanically
|
|
6
|
+
* separates that CLAIM from VERIFIED, under one frozen sentence:
|
|
7
|
+
*
|
|
8
|
+
* Verified ⟹ evidenceSeq > lastRelevantMutationSeq.
|
|
9
|
+
*
|
|
10
|
+
* No new event kind, no core diff — the projection reads the existing
|
|
11
|
+
* durable vocabulary (`deriveRecoveryPlan` is the shape precedent). The
|
|
12
|
+
* classifiers are maximally conservative; the exemptions are exactly
|
|
13
|
+
* three (TV-1C — no wider taxonomy exists or is wanted):
|
|
14
|
+
*
|
|
15
|
+
* - MUTATION marker: `tool_execution_started` (intent-to-effect) of any
|
|
16
|
+
* name NOT in `nonMutatingTools`. Started — not the receipt — so failed
|
|
17
|
+
* and crash-window executions invalidate too, and pre-EC-1 overlap eras
|
|
18
|
+
* are covered by the same rule. Absence of a certificate is a mutation.
|
|
19
|
+
* The exemption set is fed by `effects.precommitSafe` — the frozen
|
|
20
|
+
* read-only+free+local contract, the only certificate that PROVES the
|
|
21
|
+
* world untouched. `concurrency: "shared"` proves overlap-safety and
|
|
22
|
+
* nothing else (the TV-1C finding: slow_touch is shared AND writes) —
|
|
23
|
+
* it never feeds this set.
|
|
24
|
+
* ONE proven exception (TV-1B, grounded in WR-1A): an execution whose
|
|
25
|
+
* terminal receipt is failed(errorKind:"precondition") never counts —
|
|
26
|
+
* that kind's frozen contract is "work refused BEFORE it starts".
|
|
27
|
+
* - EVIDENCE: only receipts (`tool_execution_succeeded`) of names in
|
|
28
|
+
* `evidenceTools`. Default ∅ — the projection never invents evidence.
|
|
29
|
+
* What TV-1A's verdict asserts is POSITIONAL: the arc's last
|
|
30
|
+
* intent-to-effect was a successful evidence-class run — an "evidence",
|
|
31
|
+
* never a "proof" (semantic knowledge is the TV-1B driver's).
|
|
32
|
+
* - task_set itself never invalidates: recording the claim after the check
|
|
33
|
+
* is the natural arc (tests → mark done), and the claim-recording act
|
|
34
|
+
* mutates nothing the evidence observed.
|
|
35
|
+
* - VOIDED RANGES (TV-1C): a `model_output_abandoned` marker voids
|
|
36
|
+
* (voidFromSeq, seq] — the durable vocabulary's own "never happened",
|
|
37
|
+
* the same ranges the kernel projection skips. Every consumer here
|
|
38
|
+
* skips them uniformly: a voided echo is not a claim, a voided receipt
|
|
39
|
+
* is not evidence, a voided start is not a mutation. task_set waits
|
|
40
|
+
* for turn commit today (TT-1), so the scheduler already keeps claims
|
|
41
|
+
* out of voided turns — the projection encodes the admissibility rule
|
|
42
|
+
* itself instead of relying on that forever.
|
|
43
|
+
*
|
|
44
|
+
* `verified` means FRESH EVIDENCE UNDER THE CONFIGURED POLICY — never
|
|
45
|
+
* "objectively proven correct". Goal Truth is external (PE-1's
|
|
46
|
+
* evaluator); this projection only ever ranks the model's own claim
|
|
47
|
+
* against the trajectory's own receipts.
|
|
48
|
+
*/
|
|
49
|
+
import type { Event } from "@vincemakes/kiso-core";
|
|
50
|
+
/** One item of the model's plan, exactly as last claimed. */
|
|
51
|
+
export interface TaskClaim {
|
|
52
|
+
readonly text: string;
|
|
53
|
+
readonly status: "pending" | "active" | "done";
|
|
54
|
+
}
|
|
55
|
+
/** The freshness verdict. `stale` and `unreadable` NAME their cause. */
|
|
56
|
+
export type EvidenceVerdict = {
|
|
57
|
+
readonly kind: "verified";
|
|
58
|
+
readonly evidenceSeq: number;
|
|
59
|
+
} | {
|
|
60
|
+
readonly kind: "stale";
|
|
61
|
+
readonly evidenceSeq: number;
|
|
62
|
+
readonly invalidatedBySeq: number;
|
|
63
|
+
} | {
|
|
64
|
+
readonly kind: "none";
|
|
65
|
+
} | {
|
|
66
|
+
readonly kind: "unreadable";
|
|
67
|
+
readonly atSeq: number;
|
|
68
|
+
readonly reason: string;
|
|
69
|
+
};
|
|
70
|
+
export interface TaskAssessment {
|
|
71
|
+
/** The LAST successful task_set echo, parsed — the model's claims. */
|
|
72
|
+
readonly claims: readonly TaskClaim[];
|
|
73
|
+
/** True only for a non-empty plan whose every item is claimed done. */
|
|
74
|
+
readonly allClaimedDone: boolean;
|
|
75
|
+
readonly lastTaskSetSeq: number | null;
|
|
76
|
+
/** Seq of the last mutation-class `tool_execution_started`, if any. */
|
|
77
|
+
readonly lastMutationSeq: number | null;
|
|
78
|
+
readonly evidence: EvidenceVerdict;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Assess the task claims and their evidence freshness over a durable
|
|
82
|
+
* trajectory. Pure: same events, same options, same assessment.
|
|
83
|
+
*/
|
|
84
|
+
export declare function assessTasks(events: readonly Event[], opts?: {
|
|
85
|
+
/** Names whose executions PROVABLY never mutate the observable
|
|
86
|
+
* world (from `effects.precommitSafe` certificates — read-only +
|
|
87
|
+
* free + local). `concurrency: "shared"` is NOT such a proof
|
|
88
|
+
* (TV-1C). Default ∅ — everything mutates. */
|
|
89
|
+
readonly nonMutatingTools?: ReadonlySet<string>;
|
|
90
|
+
/** Names whose successful receipts count as evidence. Default ∅ —
|
|
91
|
+
* nothing does. */
|
|
92
|
+
readonly evidenceTools?: ReadonlySet<string>;
|
|
93
|
+
}): TaskAssessment;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TV-1A — TaskAssessment as a PURE PROJECTION + Evidence Freshness.
|
|
3
|
+
*
|
|
4
|
+
* The task checklist the model maintains through task_set is a SELF-REPORT:
|
|
5
|
+
* an item is "done" because the model said so. This projection mechanically
|
|
6
|
+
* separates that CLAIM from VERIFIED, under one frozen sentence:
|
|
7
|
+
*
|
|
8
|
+
* Verified ⟹ evidenceSeq > lastRelevantMutationSeq.
|
|
9
|
+
*
|
|
10
|
+
* No new event kind, no core diff — the projection reads the existing
|
|
11
|
+
* durable vocabulary (`deriveRecoveryPlan` is the shape precedent). The
|
|
12
|
+
* classifiers are maximally conservative; the exemptions are exactly
|
|
13
|
+
* three (TV-1C — no wider taxonomy exists or is wanted):
|
|
14
|
+
*
|
|
15
|
+
* - MUTATION marker: `tool_execution_started` (intent-to-effect) of any
|
|
16
|
+
* name NOT in `nonMutatingTools`. Started — not the receipt — so failed
|
|
17
|
+
* and crash-window executions invalidate too, and pre-EC-1 overlap eras
|
|
18
|
+
* are covered by the same rule. Absence of a certificate is a mutation.
|
|
19
|
+
* The exemption set is fed by `effects.precommitSafe` — the frozen
|
|
20
|
+
* read-only+free+local contract, the only certificate that PROVES the
|
|
21
|
+
* world untouched. `concurrency: "shared"` proves overlap-safety and
|
|
22
|
+
* nothing else (the TV-1C finding: slow_touch is shared AND writes) —
|
|
23
|
+
* it never feeds this set.
|
|
24
|
+
* ONE proven exception (TV-1B, grounded in WR-1A): an execution whose
|
|
25
|
+
* terminal receipt is failed(errorKind:"precondition") never counts —
|
|
26
|
+
* that kind's frozen contract is "work refused BEFORE it starts".
|
|
27
|
+
* - EVIDENCE: only receipts (`tool_execution_succeeded`) of names in
|
|
28
|
+
* `evidenceTools`. Default ∅ — the projection never invents evidence.
|
|
29
|
+
* What TV-1A's verdict asserts is POSITIONAL: the arc's last
|
|
30
|
+
* intent-to-effect was a successful evidence-class run — an "evidence",
|
|
31
|
+
* never a "proof" (semantic knowledge is the TV-1B driver's).
|
|
32
|
+
* - task_set itself never invalidates: recording the claim after the check
|
|
33
|
+
* is the natural arc (tests → mark done), and the claim-recording act
|
|
34
|
+
* mutates nothing the evidence observed.
|
|
35
|
+
* - VOIDED RANGES (TV-1C): a `model_output_abandoned` marker voids
|
|
36
|
+
* (voidFromSeq, seq] — the durable vocabulary's own "never happened",
|
|
37
|
+
* the same ranges the kernel projection skips. Every consumer here
|
|
38
|
+
* skips them uniformly: a voided echo is not a claim, a voided receipt
|
|
39
|
+
* is not evidence, a voided start is not a mutation. task_set waits
|
|
40
|
+
* for turn commit today (TT-1), so the scheduler already keeps claims
|
|
41
|
+
* out of voided turns — the projection encodes the admissibility rule
|
|
42
|
+
* itself instead of relying on that forever.
|
|
43
|
+
*
|
|
44
|
+
* `verified` means FRESH EVIDENCE UNDER THE CONFIGURED POLICY — never
|
|
45
|
+
* "objectively proven correct". Goal Truth is external (PE-1's
|
|
46
|
+
* evaluator); this projection only ever ranks the model's own claim
|
|
47
|
+
* against the trajectory's own receipts.
|
|
48
|
+
*/
|
|
49
|
+
const TASK_TOOL = "task_set";
|
|
50
|
+
const STATUSES = new Set(["pending", "active", "done"]);
|
|
51
|
+
const COUNT_LINE = /^\[task\] (\d+) items? — (\d+) pending, (\d+) active, (\d+) done$/;
|
|
52
|
+
const ITEM_LINE = /^\[(\w+)\] (.*)$/;
|
|
53
|
+
/** Parse the canonical task_set echo (the frozen content contract the
|
|
54
|
+
* checklist cell also reads). Returns the claims, or the reason it
|
|
55
|
+
* cannot — never a guess. */
|
|
56
|
+
function parseEcho(content) {
|
|
57
|
+
const lines = content.split("\n");
|
|
58
|
+
const head = COUNT_LINE.exec(lines[0] ?? "");
|
|
59
|
+
if (head === null)
|
|
60
|
+
return { reason: "line 1 is not the [task] count line" };
|
|
61
|
+
const total = Number(head[1]);
|
|
62
|
+
const declared = { pending: Number(head[2]), active: Number(head[3]), done: Number(head[4]) };
|
|
63
|
+
const claims = [];
|
|
64
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
65
|
+
const m = ITEM_LINE.exec(lines[i]);
|
|
66
|
+
if (m === null || !STATUSES.has(m[1]))
|
|
67
|
+
return { reason: `line ${i + 1} is not a [pending|active|done] item line` };
|
|
68
|
+
claims.push({ text: m[2], status: m[1] });
|
|
69
|
+
}
|
|
70
|
+
const counts = { pending: 0, active: 0, done: 0 };
|
|
71
|
+
for (const c of claims)
|
|
72
|
+
counts[c.status] += 1;
|
|
73
|
+
if (claims.length !== total || counts.pending !== declared.pending || counts.active !== declared.active || counts.done !== declared.done) {
|
|
74
|
+
return { reason: "the count line disagrees with the item lines" };
|
|
75
|
+
}
|
|
76
|
+
return { claims };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Assess the task claims and their evidence freshness over a durable
|
|
80
|
+
* trajectory. Pure: same events, same options, same assessment.
|
|
81
|
+
*/
|
|
82
|
+
export function assessTasks(events, opts) {
|
|
83
|
+
const nonMutating = opts?.nonMutatingTools ?? new Set();
|
|
84
|
+
const evidenceNames = opts?.evidenceTools ?? new Set();
|
|
85
|
+
// TV-1C: PASS 0 — the voided ranges. (voidFromSeq, seq] of every
|
|
86
|
+
// `model_output_abandoned` marker is "never happened" for this whole
|
|
87
|
+
// projection, exactly as the kernel's own context projection treats
|
|
88
|
+
// it. Markers are rare; a linear range check is honest and enough.
|
|
89
|
+
const voidRanges = [];
|
|
90
|
+
for (const ev of events) {
|
|
91
|
+
if (ev.type === "model_output_abandoned")
|
|
92
|
+
voidRanges.push({ from: ev.voidFromSeq, to: ev.seq });
|
|
93
|
+
}
|
|
94
|
+
const voided = (seq) => voidRanges.some((r) => seq > r.from && seq <= r.to);
|
|
95
|
+
// TV-1B: PASS 1 — executions whose terminal receipt is a
|
|
96
|
+
// PRECONDITION failure are PROVEN no-mutation (WR-1A froze that
|
|
97
|
+
// contract: work refused before it starts). Everything else stays
|
|
98
|
+
// conservative: no receipt (crash window), fatal/transient/
|
|
99
|
+
// invalid_input, success, and legacy receipts with no errorKind.
|
|
100
|
+
// Two passes, no temporal rollback state — same events, same verdict.
|
|
101
|
+
const provenNoMutation = new Set();
|
|
102
|
+
for (const ev of events) {
|
|
103
|
+
if (voided(ev.seq))
|
|
104
|
+
continue;
|
|
105
|
+
if (ev.type === "tool_execution_failed" && ev.errorKind === "precondition") {
|
|
106
|
+
provenNoMutation.add(ev.executionId);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const nameByExecution = new Map();
|
|
110
|
+
let claims = [];
|
|
111
|
+
let lastTaskSetSeq = null;
|
|
112
|
+
let unreadable = null;
|
|
113
|
+
let lastMutationSeq = null;
|
|
114
|
+
let lastEvidenceSeq = null;
|
|
115
|
+
let staleBySeq = null;
|
|
116
|
+
for (const ev of events) {
|
|
117
|
+
if (voided(ev.seq))
|
|
118
|
+
continue;
|
|
119
|
+
if (ev.type === "tool_execution_started") {
|
|
120
|
+
nameByExecution.set(ev.executionId, ev.name);
|
|
121
|
+
if (ev.name !== TASK_TOOL && !nonMutating.has(ev.name) && !provenNoMutation.has(ev.executionId)) {
|
|
122
|
+
lastMutationSeq = ev.seq;
|
|
123
|
+
if (lastEvidenceSeq !== null && staleBySeq === null)
|
|
124
|
+
staleBySeq = ev.seq;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else if (ev.type === "tool_execution_succeeded") {
|
|
128
|
+
const name = nameByExecution.get(ev.executionId);
|
|
129
|
+
if (name === TASK_TOOL) {
|
|
130
|
+
const parsed = parseEcho(ev.result.content);
|
|
131
|
+
if ("reason" in parsed) {
|
|
132
|
+
unreadable = { atSeq: ev.seq, reason: parsed.reason };
|
|
133
|
+
claims = [];
|
|
134
|
+
lastTaskSetSeq = ev.seq;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
unreadable = null;
|
|
138
|
+
claims = parsed.claims;
|
|
139
|
+
lastTaskSetSeq = ev.seq;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else if (name !== undefined && evidenceNames.has(name)) {
|
|
143
|
+
// a fresh evidence receipt supersedes both the older evidence
|
|
144
|
+
// AND any staleness its own start incurred.
|
|
145
|
+
lastEvidenceSeq = ev.seq;
|
|
146
|
+
staleBySeq = null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const allClaimedDone = claims.length > 0 && claims.every((c) => c.status === "done");
|
|
151
|
+
let evidence;
|
|
152
|
+
if (unreadable !== null) {
|
|
153
|
+
evidence = { kind: "unreadable", atSeq: unreadable.atSeq, reason: unreadable.reason };
|
|
154
|
+
}
|
|
155
|
+
else if (lastEvidenceSeq === null) {
|
|
156
|
+
evidence = { kind: "none" };
|
|
157
|
+
}
|
|
158
|
+
else if (staleBySeq !== null) {
|
|
159
|
+
evidence = { kind: "stale", evidenceSeq: lastEvidenceSeq, invalidatedBySeq: staleBySeq };
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
evidence = { kind: "verified", evidenceSeq: lastEvidenceSeq };
|
|
163
|
+
}
|
|
164
|
+
return { claims, allClaimedDone, lastTaskSetSeq, lastMutationSeq, evidence };
|
|
165
|
+
}
|
package/dist/trace/guard.js
CHANGED
|
@@ -25,7 +25,7 @@ import { randomUUID } from "node:crypto";
|
|
|
25
25
|
import { buildContextManifest, segmentHashes } from "./manifest.js";
|
|
26
26
|
import { cacheableHashes } from "./analyze.js";
|
|
27
27
|
import { hashContext, hashSystemPrompt, hashToolSpecs, stablePrefixFingerprint } from "./hash.js";
|
|
28
|
-
import { PRICING_TABLE_V1,
|
|
28
|
+
import { PRICING_TABLE_V1, canonicalizeUsageForModel } from "../usage/canonical.js";
|
|
29
29
|
import { buildRentLedger } from "./rent.js";
|
|
30
30
|
import { TRACE_SCHEMA_VERSION } from "./record.js";
|
|
31
31
|
import { TraceWriter } from "./writer.js";
|
|
@@ -201,11 +201,14 @@ export class RequestTracer {
|
|
|
201
201
|
record.cacheWrite = p.cacheWrite ?? null;
|
|
202
202
|
record.output = p.outputTokens ?? 0;
|
|
203
203
|
}
|
|
204
|
-
// E2 — the canonical block formalizes the same raw (the
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
204
|
+
// E2 — the canonical block formalizes the same raw (the validator
|
|
205
|
+
// pins block == quartet, so the two can never drift apart). Unknown
|
|
206
|
+
// usage canonicalizes to the "0 = unknown" convention, consistent
|
|
207
|
+
// with the quartet above. PH-1c: the PRICE keys on the record's own
|
|
208
|
+
// MODEL via the metadata registry (route-keyed pricing priced an
|
|
209
|
+
// Anthropic run at DeepSeek's rates); the convention still keys on
|
|
210
|
+
// the route. An unpriced model costs null — never a fallback rate.
|
|
211
|
+
record.canonical = canonicalizeUsageForModel(record.model, undefined, this.#provider, {
|
|
209
212
|
inputTokens: p.inputTokens,
|
|
210
213
|
outputTokens: p.outputTokens,
|
|
211
214
|
cacheRead: p.cacheRead,
|
|
@@ -111,6 +111,21 @@ export declare function priceFor(route: string, u: {
|
|
|
111
111
|
* builtin v1 table so the export's default behavior is the pinned one
|
|
112
112
|
* and a future injection never widens this frozen signature. */
|
|
113
113
|
export declare function canonicalizeUsage(route: string, raw: RawUsage, table?: PricingTable): CanonicalUsage;
|
|
114
|
+
/**
|
|
115
|
+
* PH-1c — the MODEL-KEYED cost path (findings PH-F14/PH-F16). The
|
|
116
|
+
* convention still keys on the ROUTE (a protocol property: fresh vs
|
|
117
|
+
* total input); the PRICE keys on the model+endpoint metadata entry —
|
|
118
|
+
* the route-keyed table is how an Anthropic run got priced at
|
|
119
|
+
* DeepSeek's rates. No metadata entry, or an entry with `pricing:
|
|
120
|
+
* null`, costs null — the honest stamp, never a fallback rate.
|
|
121
|
+
*
|
|
122
|
+
* A NEW function beside canonicalizeUsage, not a widened signature —
|
|
123
|
+
* the E2 ruling froze that one ("a future injection never widens this
|
|
124
|
+
* frozen signature"), and this is the future it reserved room for.
|
|
125
|
+
* Metadata-priced records stamp pricingTableId "metadata" (version 1);
|
|
126
|
+
* the (id, version) tuple ritual carries forward unchanged.
|
|
127
|
+
*/
|
|
128
|
+
export declare function canonicalizeUsageForModel(model: string, endpoint: string | undefined, route: string, raw: RawUsage): CanonicalUsage;
|
|
114
129
|
/** The canonical schema's invariants, machine-checked: closed key set;
|
|
115
130
|
* non-negative numbers; cacheWrite/reasoning null-or-number; the pinned
|
|
116
131
|
* sentence — cacheRead can never exceed total (= input + cacheRead +
|
package/dist/usage/canonical.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* accounting boundary (R4 Case B, pure derivation; the union does not
|
|
28
28
|
* move). Raw is provider observation; canonical is the accounting truth.
|
|
29
29
|
*/
|
|
30
|
+
import { lookupModelMetadata } from "../provider/metadata.js";
|
|
30
31
|
/** The route → input-convention table (R2a). `input` semantics differ
|
|
31
32
|
* between the two routes; the convention is a property of the ROUTE, not
|
|
32
33
|
* the provider. Unknown routes fall back to "total" — exactly the
|
|
@@ -61,15 +62,14 @@ export function pricingTableFor(version) {
|
|
|
61
62
|
return table;
|
|
62
63
|
}
|
|
63
64
|
export function priceFor(route, u, table) {
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
const entry = table.entries[route]
|
|
72
|
-
(route in INPUT_CONVENTIONS ? undefined : table.entries["openai-compat"]);
|
|
65
|
+
// PH-1a (supersedes R5b-④c's mirror fallback): a route missing from
|
|
66
|
+
// the table is a HOLE → null, the explicit-absent stamp — REAL routes
|
|
67
|
+
// and unknown routes alike. The retired mirror fallback priced the
|
|
68
|
+
// "adapter" route (a directly-injected adapter, finding PH-F7) at the
|
|
69
|
+
// openai-compat entry: a fabricated cost on exactly the runs whose
|
|
70
|
+
// rates are unknown. null is the honest answer; the builtin table
|
|
71
|
+
// never backfills an injected table's hole either.
|
|
72
|
+
const entry = table.entries[route];
|
|
73
73
|
if (entry === undefined)
|
|
74
74
|
return null;
|
|
75
75
|
return ((u.input * entry.inputPerM +
|
|
@@ -107,6 +107,37 @@ export function canonicalizeUsage(route, raw, table = PRICING_TABLE_V1) {
|
|
|
107
107
|
pricingTableVersion: table.version,
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* PH-1c — the MODEL-KEYED cost path (findings PH-F14/PH-F16). The
|
|
112
|
+
* convention still keys on the ROUTE (a protocol property: fresh vs
|
|
113
|
+
* total input); the PRICE keys on the model+endpoint metadata entry —
|
|
114
|
+
* the route-keyed table is how an Anthropic run got priced at
|
|
115
|
+
* DeepSeek's rates. No metadata entry, or an entry with `pricing:
|
|
116
|
+
* null`, costs null — the honest stamp, never a fallback rate.
|
|
117
|
+
*
|
|
118
|
+
* A NEW function beside canonicalizeUsage, not a widened signature —
|
|
119
|
+
* the E2 ruling froze that one ("a future injection never widens this
|
|
120
|
+
* frozen signature"), and this is the future it reserved room for.
|
|
121
|
+
* Metadata-priced records stamp pricingTableId "metadata" (version 1);
|
|
122
|
+
* the (id, version) tuple ritual carries forward unchanged.
|
|
123
|
+
*/
|
|
124
|
+
export function canonicalizeUsageForModel(model, endpoint, route, raw) {
|
|
125
|
+
const base = canonicalizeUsage(route, raw, EMPTY_TABLE); // convention math only — EMPTY_TABLE prices nothing
|
|
126
|
+
const entry = lookupModelMetadata(model, endpoint);
|
|
127
|
+
const pricing = entry?.pricing ?? null;
|
|
128
|
+
if (pricing === null) {
|
|
129
|
+
return { ...base, costUsd: null, pricingTableId: "metadata", pricingTableVersion: 1 };
|
|
130
|
+
}
|
|
131
|
+
const costUsd = (base.input * pricing.inputPerM +
|
|
132
|
+
base.output * pricing.outputPerM +
|
|
133
|
+
base.cacheRead * pricing.cacheReadPerM +
|
|
134
|
+
(base.cacheWrite ?? 0) * pricing.cacheWritePerM) /
|
|
135
|
+
1e6;
|
|
136
|
+
return { ...base, costUsd, pricingTableId: "metadata", pricingTableVersion: 1 };
|
|
137
|
+
}
|
|
138
|
+
/** No entries at all — canonicalizeUsageForModel's convention-only pass
|
|
139
|
+
* through canonicalizeUsage; every price it could produce is null. */
|
|
140
|
+
const EMPTY_TABLE = { id: "metadata", version: 1, entries: {} };
|
|
110
141
|
// ── The validator ──────────────────────────────────────────────────────────
|
|
111
142
|
// Strict by design: closed keys (a misspelled field can never enter a
|
|
112
143
|
// ledger), non-negative numbers, the pinned sentence machine-checked, and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "kiso runtime — 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.15.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@vincemakes/kiso-provider-anthropic": "0.
|
|
32
|
-
"@vincemakes/kiso-provider-openai": "0.
|
|
31
|
+
"@vincemakes/kiso-provider-anthropic": "0.15.0",
|
|
32
|
+
"@vincemakes/kiso-provider-openai": "0.15.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.15.0",
|
|
44
44
|
"@types/node": "^26.1.2",
|
|
45
45
|
"typescript": "^5.7.2",
|
|
46
46
|
"vitest": "^3.0.0"
|