@vincemakes/kiso-runtime 0.12.0 → 0.14.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/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/recovery-plan.js +39 -8
- package/dist/run.d.ts +1 -1
- package/dist/run.js +10 -4
- package/dist/session.d.ts +13 -0
- package/dist/session.js +25 -1
- package/dist/task-assessment.d.ts +73 -0
- package/dist/task-assessment.js +133 -0
- package/dist/truncation-guard.d.ts +46 -19
- package/dist/truncation-guard.js +43 -16
- package/package.json +5 -5
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/recovery-plan.js
CHANGED
|
@@ -86,17 +86,38 @@ export function deriveRecoveryPlan(events, scope) {
|
|
|
86
86
|
const uncertain = [...executionLedger(events).values()].filter((r) => r.status === "uncertain");
|
|
87
87
|
if (uncertain.length > 0)
|
|
88
88
|
return { kind: "RESOLVE_UNCERTAIN", executionId: uncertain[0].executionId };
|
|
89
|
-
// 4. Gap B: a
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
89
|
+
// 4. Gap B: a no-stop suffix of MODEL OUTPUT is an abandoned draft — void
|
|
90
|
+
// it. The boundary/draft scans run over the OPEN RUN's events
|
|
91
|
+
// INCLUDING the driver's own appends — the driver re-derives after
|
|
92
|
+
// every append, and the marker IT appended must already be the last
|
|
93
|
+
// boundary (the old one-pass Gap B never needed this: it ran before
|
|
94
|
+
// any append).
|
|
95
|
+
//
|
|
96
|
+
// EC-1, finding EC1-F1: the scan counts `tool_call_end` too. It used
|
|
97
|
+
// to be text-only, on the 0.1.44 reasoning that "a bare tool-call
|
|
98
|
+
// suffix is the legal approval-panel pause, never a draft" — but that
|
|
99
|
+
// reasoning only ever held for a suffix carrying a REQUEST (the
|
|
100
|
+
// liveAsk clause below, which still decides that case). A bare call
|
|
101
|
+
// with no request is not a pause; it is an uncommitted draft, and
|
|
102
|
+
// leaving it un-voided projects an assistant `tool_use` block with no
|
|
103
|
+
// result — a provider 400. Pre-EC-1 the window was microseconds wide
|
|
104
|
+
// (the stop was persisted the moment it arrived); ① holds the stop
|
|
105
|
+
// until the stream is exhausted, so crash-pair A lands in this shape
|
|
106
|
+
// by construction.
|
|
96
107
|
const openEvents = openRunEvents(events, scope);
|
|
97
108
|
const boundary = [...openEvents].reverse().find(isBoundary);
|
|
98
109
|
if (boundary !== undefined) {
|
|
99
|
-
|
|
110
|
+
// A call only makes a DRAFT while it is still pure intent. Once it
|
|
111
|
+
// has a durable `tool_execution_started` the world may have moved,
|
|
112
|
+
// and the existing repair passes own it: voiding such a call would
|
|
113
|
+
// strand a real receipt behind a voided declaration (pair atomicity
|
|
114
|
+
// would then drop its result and the model would never learn the
|
|
115
|
+
// outcome of work that actually happened). Pre-EC-1 logs contain
|
|
116
|
+
// exactly that shape — a call that launched mid-stream and finished
|
|
117
|
+
// before its stop was persisted — and they must keep recovering the
|
|
118
|
+
// way they always did.
|
|
119
|
+
const unexecuted = (e) => e.type === "tool_call_end" && !openEvents.some((x) => x.type === "tool_execution_started" && x.callId === e.callId);
|
|
120
|
+
const afterBoundary = openEvents.some((e) => (e.type === "text_delta" || e.type === "thinking" || unexecuted(e)) && e.seq > boundary.seq);
|
|
100
121
|
// The approval-panel pause: a suffix that carries a pending ask of the
|
|
101
122
|
// LIVE turn — the last boundary is the user_input, no stop since — is
|
|
102
123
|
// the human's pause, never a draft: the call was extracted and asked,
|
|
@@ -105,6 +126,16 @@ export function deriveRecoveryPlan(events, scope) {
|
|
|
105
126
|
// crash mid-pause leaves exactly this shape.) A request AFTER a STOP
|
|
106
127
|
// is different: it is the draft's own ask (0143's shape) — the marker
|
|
107
128
|
// voids it and the request expires with the draft.
|
|
129
|
+
//
|
|
130
|
+
// EC-1 ERA NOTE — this clause is now GENERATION COMPAT, and is kept
|
|
131
|
+
// for that reason alone. A kiso at 0.13.0 or later cannot produce the
|
|
132
|
+
// shape: asks moved AFTER Turn Commit (③), so a durable
|
|
133
|
+
// `permission_requested` always has a durable stop before it. Logs
|
|
134
|
+
// written by EARLIER bins do carry it, they are on disk, and they
|
|
135
|
+
// must keep re-presenting their ask instead of being voided as
|
|
136
|
+
// drafts. Deleting this clause would silently change how pre-EC-1
|
|
137
|
+
// sessions recover; it stays until the generation corpus no longer
|
|
138
|
+
// contains a pre-EC-1 era.
|
|
108
139
|
const liveAsk = afterBoundary &&
|
|
109
140
|
boundary.type === "user_input" &&
|
|
110
141
|
openEvents.some((e) => e.type === "permission_requested" && e.seq > boundary.seq);
|
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
|
|
@@ -714,8 +720,8 @@ export class Run {
|
|
|
714
720
|
}
|
|
715
721
|
// ruling #12 correction one: the honest note rides the recovered failure too —
|
|
716
722
|
// the receipt and the repaired tool_result reproduce the live path
|
|
717
|
-
// losslessly.
|
|
718
|
-
if (result.isError && tool?.idempotent !== true) {
|
|
723
|
+
// losslessly. WR-1-F1: precondition refusals excluded — nothing ran.
|
|
724
|
+
if (result.isError && result.errorKind !== "precondition" && tool?.idempotent !== true) {
|
|
719
725
|
result = {
|
|
720
726
|
...result,
|
|
721
727
|
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).
|
|
@@ -158,6 +159,7 @@ export declare class AgentSession {
|
|
|
158
159
|
/** Run one user turn. Iterate to consume; `run.abort()` cancels. */
|
|
159
160
|
run(input: string, options?: {
|
|
160
161
|
signal?: AbortSignalLike;
|
|
162
|
+
source?: import("@vincemakes/kiso-core").MessageSource;
|
|
161
163
|
}): Run;
|
|
162
164
|
/**
|
|
163
165
|
* Continue the interrupted run (Area 2): apply durable decisions,
|
|
@@ -220,6 +222,17 @@ export declare class AgentSession {
|
|
|
220
222
|
approve(decisionId: string, allow: boolean, reason?: string): Promise<void>;
|
|
221
223
|
/** Executions that started but never reported a result (crash window). */
|
|
222
224
|
uncertainExecutions(): import("./ledger.js").ExecutionRecord[];
|
|
225
|
+
/**
|
|
226
|
+
* TV-1A — assess the task claims and their evidence freshness over THIS
|
|
227
|
+
* session's durable log. The shared-tool set comes from the live tools'
|
|
228
|
+
* own `effects.concurrency: "shared"` certificates (one direction of
|
|
229
|
+
* truth, never a second declaration); the evidence policy defaults to
|
|
230
|
+
* {"shell"} — the convention the task extension's own "make the LAST
|
|
231
|
+
* item a verification step" guidance produces.
|
|
232
|
+
*/
|
|
233
|
+
assessTasks(opts?: {
|
|
234
|
+
readonly evidenceTools?: ReadonlySet<string>;
|
|
235
|
+
}): TaskAssessment;
|
|
223
236
|
/**
|
|
224
237
|
* The human's verdict on an interrupted execution, keyed by EXECUTION ID
|
|
225
238
|
* (B group): "rerun" (the human says the side effect did NOT happen — the
|
package/dist/session.js
CHANGED
|
@@ -29,6 +29,11 @@
|
|
|
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
39
|
import { canonicalizeUsage } from "./usage/canonical.js";
|
|
@@ -252,7 +257,7 @@ export class AgentSession {
|
|
|
252
257
|
/** Run one user turn. Iterate to consume; `run.abort()` cancels. */
|
|
253
258
|
run(input, options) {
|
|
254
259
|
this.ensureHealthy();
|
|
255
|
-
return new Run(this.#store, this.#adapter, this.#config, this, input, options?.signal, false);
|
|
260
|
+
return new Run(this.#store, this.#adapter, this.#config, this, input, options?.signal, false, options?.source);
|
|
256
261
|
}
|
|
257
262
|
/**
|
|
258
263
|
* Continue the interrupted run (Area 2): apply durable decisions,
|
|
@@ -528,6 +533,25 @@ export class AgentSession {
|
|
|
528
533
|
uncertainExecutions() {
|
|
529
534
|
return [...executionLedger(this.log.all).values()].filter((r) => r.status === "uncertain");
|
|
530
535
|
}
|
|
536
|
+
/**
|
|
537
|
+
* TV-1A — assess the task claims and their evidence freshness over THIS
|
|
538
|
+
* session's durable log. The shared-tool set comes from the live tools'
|
|
539
|
+
* own `effects.concurrency: "shared"` certificates (one direction of
|
|
540
|
+
* truth, never a second declaration); the evidence policy defaults to
|
|
541
|
+
* {"shell"} — the convention the task extension's own "make the LAST
|
|
542
|
+
* item a verification step" guidance produces.
|
|
543
|
+
*/
|
|
544
|
+
assessTasks(opts) {
|
|
545
|
+
const sharedTools = new Set();
|
|
546
|
+
for (const tool of this.#config.registry.list()) {
|
|
547
|
+
if (tool.effects?.concurrency === "shared")
|
|
548
|
+
sharedTools.add(tool.name);
|
|
549
|
+
}
|
|
550
|
+
return assessTasks(this.log.all, {
|
|
551
|
+
sharedTools,
|
|
552
|
+
evidenceTools: opts?.evidenceTools ?? DEFAULT_EVIDENCE_TOOLS,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
531
555
|
/**
|
|
532
556
|
* The human's verdict on an interrupted execution, keyed by EXECUTION ID
|
|
533
557
|
* (B group): "rerun" (the human says the side effect did NOT happen — the
|
|
@@ -0,0 +1,73 @@
|
|
|
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 and reuse EC-1's certificate
|
|
13
|
+
* direction verbatim:
|
|
14
|
+
*
|
|
15
|
+
* - MUTATION marker: `tool_execution_started` (intent-to-effect) of any
|
|
16
|
+
* name NOT in `sharedTools`. Started — not the receipt — so failed and
|
|
17
|
+
* crash-window executions invalidate too, and pre-EC-1 overlap eras are
|
|
18
|
+
* covered by the same rule. Absence of a certificate is a mutation.
|
|
19
|
+
* ONE proven exception (TV-1B, grounded in WR-1A): an execution whose
|
|
20
|
+
* terminal receipt is failed(errorKind:"precondition") never counts —
|
|
21
|
+
* that kind's frozen contract is "work refused BEFORE it starts".
|
|
22
|
+
* - EVIDENCE: only receipts (`tool_execution_succeeded`) of names in
|
|
23
|
+
* `evidenceTools`. Default ∅ — the projection never invents evidence.
|
|
24
|
+
* What TV-1A's verdict asserts is POSITIONAL: the arc's last
|
|
25
|
+
* intent-to-effect was a successful evidence-class run — an "evidence",
|
|
26
|
+
* never a "proof" (semantic knowledge is the TV-1B driver's).
|
|
27
|
+
* - task_set itself never invalidates: recording the claim after the check
|
|
28
|
+
* is the natural arc (tests → mark done), and the claim-recording act
|
|
29
|
+
* mutates nothing the evidence observed.
|
|
30
|
+
*/
|
|
31
|
+
import type { Event } from "@vincemakes/kiso-core";
|
|
32
|
+
/** One item of the model's plan, exactly as last claimed. */
|
|
33
|
+
export interface TaskClaim {
|
|
34
|
+
readonly text: string;
|
|
35
|
+
readonly status: "pending" | "active" | "done";
|
|
36
|
+
}
|
|
37
|
+
/** The freshness verdict. `stale` and `unreadable` NAME their cause. */
|
|
38
|
+
export type EvidenceVerdict = {
|
|
39
|
+
readonly kind: "verified";
|
|
40
|
+
readonly evidenceSeq: number;
|
|
41
|
+
} | {
|
|
42
|
+
readonly kind: "stale";
|
|
43
|
+
readonly evidenceSeq: number;
|
|
44
|
+
readonly invalidatedBySeq: number;
|
|
45
|
+
} | {
|
|
46
|
+
readonly kind: "none";
|
|
47
|
+
} | {
|
|
48
|
+
readonly kind: "unreadable";
|
|
49
|
+
readonly atSeq: number;
|
|
50
|
+
readonly reason: string;
|
|
51
|
+
};
|
|
52
|
+
export interface TaskAssessment {
|
|
53
|
+
/** The LAST successful task_set echo, parsed — the model's claims. */
|
|
54
|
+
readonly claims: readonly TaskClaim[];
|
|
55
|
+
/** True only for a non-empty plan whose every item is claimed done. */
|
|
56
|
+
readonly allClaimedDone: boolean;
|
|
57
|
+
readonly lastTaskSetSeq: number | null;
|
|
58
|
+
/** Seq of the last mutation-class `tool_execution_started`, if any. */
|
|
59
|
+
readonly lastMutationSeq: number | null;
|
|
60
|
+
readonly evidence: EvidenceVerdict;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Assess the task claims and their evidence freshness over a durable
|
|
64
|
+
* trajectory. Pure: same events, same options, same assessment.
|
|
65
|
+
*/
|
|
66
|
+
export declare function assessTasks(events: readonly Event[], opts?: {
|
|
67
|
+
/** Names whose executions never mutate (from `effects.concurrency:
|
|
68
|
+
* "shared"` certificates). Default ∅ — everything mutates. */
|
|
69
|
+
readonly sharedTools?: ReadonlySet<string>;
|
|
70
|
+
/** Names whose successful receipts count as evidence. Default ∅ —
|
|
71
|
+
* nothing does. */
|
|
72
|
+
readonly evidenceTools?: ReadonlySet<string>;
|
|
73
|
+
}): TaskAssessment;
|
|
@@ -0,0 +1,133 @@
|
|
|
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 and reuse EC-1's certificate
|
|
13
|
+
* direction verbatim:
|
|
14
|
+
*
|
|
15
|
+
* - MUTATION marker: `tool_execution_started` (intent-to-effect) of any
|
|
16
|
+
* name NOT in `sharedTools`. Started — not the receipt — so failed and
|
|
17
|
+
* crash-window executions invalidate too, and pre-EC-1 overlap eras are
|
|
18
|
+
* covered by the same rule. Absence of a certificate is a mutation.
|
|
19
|
+
* ONE proven exception (TV-1B, grounded in WR-1A): an execution whose
|
|
20
|
+
* terminal receipt is failed(errorKind:"precondition") never counts —
|
|
21
|
+
* that kind's frozen contract is "work refused BEFORE it starts".
|
|
22
|
+
* - EVIDENCE: only receipts (`tool_execution_succeeded`) of names in
|
|
23
|
+
* `evidenceTools`. Default ∅ — the projection never invents evidence.
|
|
24
|
+
* What TV-1A's verdict asserts is POSITIONAL: the arc's last
|
|
25
|
+
* intent-to-effect was a successful evidence-class run — an "evidence",
|
|
26
|
+
* never a "proof" (semantic knowledge is the TV-1B driver's).
|
|
27
|
+
* - task_set itself never invalidates: recording the claim after the check
|
|
28
|
+
* is the natural arc (tests → mark done), and the claim-recording act
|
|
29
|
+
* mutates nothing the evidence observed.
|
|
30
|
+
*/
|
|
31
|
+
const TASK_TOOL = "task_set";
|
|
32
|
+
const STATUSES = new Set(["pending", "active", "done"]);
|
|
33
|
+
const COUNT_LINE = /^\[task\] (\d+) items? — (\d+) pending, (\d+) active, (\d+) done$/;
|
|
34
|
+
const ITEM_LINE = /^\[(\w+)\] (.*)$/;
|
|
35
|
+
/** Parse the canonical task_set echo (the frozen content contract the
|
|
36
|
+
* checklist cell also reads). Returns the claims, or the reason it
|
|
37
|
+
* cannot — never a guess. */
|
|
38
|
+
function parseEcho(content) {
|
|
39
|
+
const lines = content.split("\n");
|
|
40
|
+
const head = COUNT_LINE.exec(lines[0] ?? "");
|
|
41
|
+
if (head === null)
|
|
42
|
+
return { reason: "line 1 is not the [task] count line" };
|
|
43
|
+
const total = Number(head[1]);
|
|
44
|
+
const declared = { pending: Number(head[2]), active: Number(head[3]), done: Number(head[4]) };
|
|
45
|
+
const claims = [];
|
|
46
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
47
|
+
const m = ITEM_LINE.exec(lines[i]);
|
|
48
|
+
if (m === null || !STATUSES.has(m[1]))
|
|
49
|
+
return { reason: `line ${i + 1} is not a [pending|active|done] item line` };
|
|
50
|
+
claims.push({ text: m[2], status: m[1] });
|
|
51
|
+
}
|
|
52
|
+
const counts = { pending: 0, active: 0, done: 0 };
|
|
53
|
+
for (const c of claims)
|
|
54
|
+
counts[c.status] += 1;
|
|
55
|
+
if (claims.length !== total || counts.pending !== declared.pending || counts.active !== declared.active || counts.done !== declared.done) {
|
|
56
|
+
return { reason: "the count line disagrees with the item lines" };
|
|
57
|
+
}
|
|
58
|
+
return { claims };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Assess the task claims and their evidence freshness over a durable
|
|
62
|
+
* trajectory. Pure: same events, same options, same assessment.
|
|
63
|
+
*/
|
|
64
|
+
export function assessTasks(events, opts) {
|
|
65
|
+
const shared = opts?.sharedTools ?? new Set();
|
|
66
|
+
const evidenceNames = opts?.evidenceTools ?? new Set();
|
|
67
|
+
// TV-1B: PASS 1 — executions whose terminal receipt is a
|
|
68
|
+
// PRECONDITION failure are PROVEN no-mutation (WR-1A froze that
|
|
69
|
+
// contract: work refused before it starts). Everything else stays
|
|
70
|
+
// conservative: no receipt (crash window), fatal/transient/
|
|
71
|
+
// invalid_input, success, and legacy receipts with no errorKind.
|
|
72
|
+
// Two passes, no temporal rollback state — same events, same verdict.
|
|
73
|
+
const provenNoMutation = new Set();
|
|
74
|
+
for (const ev of events) {
|
|
75
|
+
if (ev.type === "tool_execution_failed" && ev.errorKind === "precondition") {
|
|
76
|
+
provenNoMutation.add(ev.executionId);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const nameByExecution = new Map();
|
|
80
|
+
let claims = [];
|
|
81
|
+
let lastTaskSetSeq = null;
|
|
82
|
+
let unreadable = null;
|
|
83
|
+
let lastMutationSeq = null;
|
|
84
|
+
let lastEvidenceSeq = null;
|
|
85
|
+
let staleBySeq = null;
|
|
86
|
+
for (const ev of events) {
|
|
87
|
+
if (ev.type === "tool_execution_started") {
|
|
88
|
+
nameByExecution.set(ev.executionId, ev.name);
|
|
89
|
+
if (ev.name !== TASK_TOOL && !shared.has(ev.name) && !provenNoMutation.has(ev.executionId)) {
|
|
90
|
+
lastMutationSeq = ev.seq;
|
|
91
|
+
if (lastEvidenceSeq !== null && staleBySeq === null)
|
|
92
|
+
staleBySeq = ev.seq;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else if (ev.type === "tool_execution_succeeded") {
|
|
96
|
+
const name = nameByExecution.get(ev.executionId);
|
|
97
|
+
if (name === TASK_TOOL) {
|
|
98
|
+
const parsed = parseEcho(ev.result.content);
|
|
99
|
+
if ("reason" in parsed) {
|
|
100
|
+
unreadable = { atSeq: ev.seq, reason: parsed.reason };
|
|
101
|
+
claims = [];
|
|
102
|
+
lastTaskSetSeq = ev.seq;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
unreadable = null;
|
|
106
|
+
claims = parsed.claims;
|
|
107
|
+
lastTaskSetSeq = ev.seq;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if (name !== undefined && evidenceNames.has(name)) {
|
|
111
|
+
// a fresh evidence receipt supersedes both the older evidence
|
|
112
|
+
// AND any staleness its own start incurred.
|
|
113
|
+
lastEvidenceSeq = ev.seq;
|
|
114
|
+
staleBySeq = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const allClaimedDone = claims.length > 0 && claims.every((c) => c.status === "done");
|
|
119
|
+
let evidence;
|
|
120
|
+
if (unreadable !== null) {
|
|
121
|
+
evidence = { kind: "unreadable", atSeq: unreadable.atSeq, reason: unreadable.reason };
|
|
122
|
+
}
|
|
123
|
+
else if (lastEvidenceSeq === null) {
|
|
124
|
+
evidence = { kind: "none" };
|
|
125
|
+
}
|
|
126
|
+
else if (staleBySeq !== null) {
|
|
127
|
+
evidence = { kind: "stale", evidenceSeq: lastEvidenceSeq, invalidatedBySeq: staleBySeq };
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
evidence = { kind: "verified", evidenceSeq: lastEvidenceSeq };
|
|
131
|
+
}
|
|
132
|
+
return { claims, allClaimedDone, lastTaskSetSeq, lastMutationSeq, evidence };
|
|
133
|
+
}
|
|
@@ -26,25 +26,52 @@
|
|
|
26
26
|
* 4. NO STOP AT ALL drops the held calls entirely — the kernel already
|
|
27
27
|
* voids that malformed turn (invalid_request).
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* -
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
29
|
+
* EC-1 ④ — THE CONTRACT AMENDMENT. The kernel closed the destructive half
|
|
30
|
+
* of this by itself: `max_tokens` cannot carry a tool call, so a truncated
|
|
31
|
+
* turn never reaches Turn Commit, and a commit-required handler never starts
|
|
32
|
+
* before that commit. What max_tokens means now, in full — the four clauses
|
|
33
|
+
* that replace the old "zero tools executed on truncation" line:
|
|
34
|
+
*
|
|
35
|
+
* 1. COMMIT-REQUIRED CALLS NEVER EXECUTE. On either path, guarded or bare.
|
|
36
|
+
* This is the kernel's guarantee, not the wrapper's, and it holds for
|
|
37
|
+
* every tool that declares nothing — which is every write, edit and
|
|
38
|
+
* shell tool kiso ships.
|
|
39
|
+
* 2. PRECOMMIT-SAFE CALLS MAY ALREADY HAVE EXECUTED, and that execution is
|
|
40
|
+
* DECLARED HARMLESS. A tool carrying `effects.precommitSafe` says
|
|
41
|
+
* running it before the turn commits is harmless for EVERY invocation —
|
|
42
|
+
* read-only, free, local. Bare, such a call launches during the stream
|
|
43
|
+
* and a truncated turn may find its receipt already durable. That is
|
|
44
|
+
* the certificate being spent, not a leak.
|
|
45
|
+
* 3. THE TURN IS NOT COMMITTED. No durable stop is written. The calls are
|
|
46
|
+
* an uncommitted draft, which is what the resume sees.
|
|
47
|
+
* 4. PRECOMMIT RESULTS NEVER LEGITIMIZE IT (invariant 7). A durable
|
|
48
|
+
* receipt from clause 2 is an execution fact and nothing more: it does
|
|
49
|
+
* not commit the invocation, and it does not make the model turn valid.
|
|
50
|
+
*
|
|
51
|
+
* WHAT THE WRAPPER STILL BUYS, given all that:
|
|
52
|
+
*
|
|
53
|
+
* - REPORTING. It releases the held batch with `input: null`, so every
|
|
54
|
+
* call is ANSWERED with an honest invalid_input result. Bare, the same
|
|
55
|
+
* turn leaves its calls with no results at all — an uncommitted draft
|
|
56
|
+
* the resume must void.
|
|
57
|
+
* - THE PRECOMMIT CASE. The hold sits UPSTREAM of the kernel: a held call
|
|
58
|
+
* never reaches the loop until the stop is known, so clause 2's "may
|
|
59
|
+
* already have executed" is exactly what the guard removes. Guarded,
|
|
60
|
+
* nothing runs at all — not even a declared read.
|
|
61
|
+
*
|
|
62
|
+
* So the conservatism split did not disappear, it MOVED. It used to be the
|
|
63
|
+
* difference between a destructive edit running and not running; it is now
|
|
64
|
+
* the difference between a harmless read running and not running, plus the
|
|
65
|
+
* reporting. The kernel's default is speed for CERTIFIED calls and safety
|
|
66
|
+
* for everything else; the flagship runtime composes this wrapper into every
|
|
67
|
+
* run and pays the latency to have neither.
|
|
68
|
+
*
|
|
69
|
+
* Pinned by `packages/runtime/tests/truncation-guard.test.ts` (clauses 1-3
|
|
70
|
+
* of the wrapper contract, byte-unchanged across EC-1 — the amendment did
|
|
71
|
+
* not weaken the guard) and by
|
|
72
|
+
* `packages/runtime/tests/sc1-truncation-contract-pins.test.ts` (clause 4 of
|
|
73
|
+
* the wrapper contract, and the four amended max_tokens clauses above — the
|
|
74
|
+
* declared TRUNCATION CLASS).
|
|
48
75
|
*/
|
|
49
76
|
import type { Adapter } from "@vincemakes/kiso-core";
|
|
50
77
|
/** Wrap the adapter so a truncated turn's tool batch can never execute. */
|
package/dist/truncation-guard.js
CHANGED
|
@@ -26,25 +26,52 @@
|
|
|
26
26
|
* 4. NO STOP AT ALL drops the held calls entirely — the kernel already
|
|
27
27
|
* voids that malformed turn (invalid_request).
|
|
28
28
|
*
|
|
29
|
-
*
|
|
29
|
+
* EC-1 ④ — THE CONTRACT AMENDMENT. The kernel closed the destructive half
|
|
30
|
+
* of this by itself: `max_tokens` cannot carry a tool call, so a truncated
|
|
31
|
+
* turn never reaches Turn Commit, and a commit-required handler never starts
|
|
32
|
+
* before that commit. What max_tokens means now, in full — the four clauses
|
|
33
|
+
* that replace the old "zero tools executed on truncation" line:
|
|
30
34
|
*
|
|
31
|
-
* -
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* -
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
35
|
+
* 1. COMMIT-REQUIRED CALLS NEVER EXECUTE. On either path, guarded or bare.
|
|
36
|
+
* This is the kernel's guarantee, not the wrapper's, and it holds for
|
|
37
|
+
* every tool that declares nothing — which is every write, edit and
|
|
38
|
+
* shell tool kiso ships.
|
|
39
|
+
* 2. PRECOMMIT-SAFE CALLS MAY ALREADY HAVE EXECUTED, and that execution is
|
|
40
|
+
* DECLARED HARMLESS. A tool carrying `effects.precommitSafe` says
|
|
41
|
+
* running it before the turn commits is harmless for EVERY invocation —
|
|
42
|
+
* read-only, free, local. Bare, such a call launches during the stream
|
|
43
|
+
* and a truncated turn may find its receipt already durable. That is
|
|
44
|
+
* the certificate being spent, not a leak.
|
|
45
|
+
* 3. THE TURN IS NOT COMMITTED. No durable stop is written. The calls are
|
|
46
|
+
* an uncommitted draft, which is what the resume sees.
|
|
47
|
+
* 4. PRECOMMIT RESULTS NEVER LEGITIMIZE IT (invariant 7). A durable
|
|
48
|
+
* receipt from clause 2 is an execution fact and nothing more: it does
|
|
49
|
+
* not commit the invocation, and it does not make the model turn valid.
|
|
40
50
|
*
|
|
41
|
-
*
|
|
42
|
-
* an embedder choosing the kernel alone chooses the streaming launch, and
|
|
43
|
-
* applying this wrapper is how they opt into the guarantee. The kernel
|
|
44
|
-
* machinery (the launch, the window, the voided settle) is untouched
|
|
45
|
-
* either way — the gate lives at the adapter boundary.
|
|
51
|
+
* WHAT THE WRAPPER STILL BUYS, given all that:
|
|
46
52
|
*
|
|
47
|
-
*
|
|
53
|
+
* - REPORTING. It releases the held batch with `input: null`, so every
|
|
54
|
+
* call is ANSWERED with an honest invalid_input result. Bare, the same
|
|
55
|
+
* turn leaves its calls with no results at all — an uncommitted draft
|
|
56
|
+
* the resume must void.
|
|
57
|
+
* - THE PRECOMMIT CASE. The hold sits UPSTREAM of the kernel: a held call
|
|
58
|
+
* never reaches the loop until the stop is known, so clause 2's "may
|
|
59
|
+
* already have executed" is exactly what the guard removes. Guarded,
|
|
60
|
+
* nothing runs at all — not even a declared read.
|
|
61
|
+
*
|
|
62
|
+
* So the conservatism split did not disappear, it MOVED. It used to be the
|
|
63
|
+
* difference between a destructive edit running and not running; it is now
|
|
64
|
+
* the difference between a harmless read running and not running, plus the
|
|
65
|
+
* reporting. The kernel's default is speed for CERTIFIED calls and safety
|
|
66
|
+
* for everything else; the flagship runtime composes this wrapper into every
|
|
67
|
+
* run and pays the latency to have neither.
|
|
68
|
+
*
|
|
69
|
+
* Pinned by `packages/runtime/tests/truncation-guard.test.ts` (clauses 1-3
|
|
70
|
+
* of the wrapper contract, byte-unchanged across EC-1 — the amendment did
|
|
71
|
+
* not weaken the guard) and by
|
|
72
|
+
* `packages/runtime/tests/sc1-truncation-contract-pins.test.ts` (clause 4 of
|
|
73
|
+
* the wrapper contract, and the four amended max_tokens clauses above — the
|
|
74
|
+
* declared TRUNCATION CLASS).
|
|
48
75
|
*/
|
|
49
76
|
/** Wrap the adapter so a truncated turn's tool batch can never execute. */
|
|
50
77
|
export function truncationGuard(adapter) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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.14.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@vincemakes/kiso-provider-anthropic": "0.
|
|
32
|
-
"@vincemakes/kiso-provider-openai": "0.
|
|
31
|
+
"@vincemakes/kiso-provider-anthropic": "0.14.0",
|
|
32
|
+
"@vincemakes/kiso-provider-openai": "0.14.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.14.0",
|
|
44
44
|
"@types/node": "^26.1.2",
|
|
45
45
|
"typescript": "^5.7.2",
|
|
46
46
|
"vitest": "^3.0.0"
|