@vincemakes/kiso-runtime 0.42.3 → 0.44.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 CHANGED
@@ -31,8 +31,6 @@ 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";
36
34
  export { kisoHome, projectArtifacts, recordTrust, trustFor } from "./trust.js";
37
35
  export type { ProjectArtifact, ProjectArtifacts, TrustDecision, TrustRecord } from "./trust.js";
38
36
  export { canonicalizeUsage } from "./usage/canonical.js";
package/dist/index.js CHANGED
@@ -31,9 +31,6 @@ 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";
37
34
  // trust
38
35
  export { kisoHome, projectArtifacts, recordTrust, trustFor } from "./trust.js";
39
36
  // usage — the canonical accounting schema (E2/1.3.0, R4b-1 ruling:
@@ -147,16 +147,25 @@ export function deriveRecoveryPlan(events, scope) {
147
147
  // undecided → the pipeline must decide; decided-approved without an
148
148
  // execution → execute the persisted call; decided-denied without a
149
149
  // model-facing result → repair it from the denial.
150
- const hasRequest = (callId, after) => events.some((e) => e.type === "permission_requested" && e.callId === callId && e.seq > after);
151
- const hasExecution = (callId, after) => events.some((e) => e.type === "tool_execution_started" && e.callId === callId && e.seq > after);
152
- const hasResult = (callId, after) => events.some((e) => e.type === "tool_result" && e.callId === callId && e.seq > after);
150
+ // 0430-F1: an event binds to an invocation by its OWN invocationSeq
151
+ // when both carry one (0.1.43+) — callId is a correlation key the model
152
+ // may re-use; an old log's event without one binds by callId after the
153
+ // call, as before.
154
+ const bound = (e, callId, after, invocationSeq) => e.invocationSeq !== undefined && invocationSeq !== undefined ? e.invocationSeq === invocationSeq : e.callId === callId && e.seq > after;
155
+ const hasRequest = (callId, after, invocationSeq) => events.some((e) => e.type === "permission_requested" && bound(e, callId, after, invocationSeq));
156
+ const hasExecution = (callId, after, invocationSeq) => events.some((e) => e.type === "tool_execution_started" && bound(e, callId, after, invocationSeq));
157
+ const hasResult = (callId, after, invocationSeq) => events.some((e) => e.type === "tool_result" && bound(e, callId, after, invocationSeq));
153
158
  const decidedForCall = (callId, after) => {
154
159
  for (const e of events) {
155
160
  if (e.type !== "permission_decided")
156
161
  continue;
157
162
  // a durable POLICY verdict binds the call (E1); a human verdict
158
- // binds its request, never the call (the requests pass owns it)
159
- if (e.decidedBy !== undefined && e.callId === callId && e.seq > after)
163
+ // binds its request, never the call (the requests pass owns it).
164
+ // 0430-F1: a decision that names its invocation binds EXACTLY that
165
+ // one (callId repeats); only an old log's decision binds by callId.
166
+ if (e.decidedBy === undefined)
167
+ continue;
168
+ if (e.invocationSeq !== undefined ? e.invocationSeq === after : e.callId === callId && e.seq > after)
160
169
  return e;
161
170
  }
162
171
  return undefined;
@@ -179,18 +188,24 @@ export function deriveRecoveryPlan(events, scope) {
179
188
  // a stored request owns the invocation (the requests pass below) —
180
189
  // "only a durable permission_decided authorizes an effect"; Gap A
181
190
  // must never re-decide over a stored request.
182
- if (hasRequest(call.callId, call.seq))
191
+ if (hasRequest(call.callId, call.seq, call.seq))
183
192
  continue;
184
- if (hasResult(call.callId, call.seq))
193
+ if (hasResult(call.callId, call.seq, call.seq))
185
194
  continue; // closed — nothing to fill
186
195
  const decided = decidedForCall(call.callId, call.seq);
196
+ // 0430-F1: a started execution proves the invocation crossed the
197
+ // permission gate on the fresh path (which records no decision for
198
+ // a default or hook allow) — it is never re-decided; step 6 owns its
199
+ // receipt.
200
+ if (decided === undefined && hasExecution(call.callId, call.seq, call.seq))
201
+ continue;
187
202
  if (decided === undefined)
188
203
  return { kind: "DECIDE_PERMISSION", invocationSeq: call.seq };
189
204
  if (decided.decision === "approved") {
190
- if (!hasExecution(call.callId, call.seq))
205
+ if (!hasExecution(call.callId, call.seq, call.seq))
191
206
  return { kind: "EXECUTE", invocationSeq: call.seq };
192
207
  }
193
- else if (!hasResult(call.callId, call.seq)) {
208
+ else if (!hasResult(call.callId, call.seq, call.seq)) {
194
209
  return { kind: "REPAIR_RESULT", invocationSeq: call.seq };
195
210
  }
196
211
  }
@@ -213,10 +228,10 @@ export function deriveRecoveryPlan(events, scope) {
213
228
  return { kind: "WAIT_PERMISSION", invocationSeq: invocationSeq ?? pending.seq };
214
229
  }
215
230
  if (decided.decision === "approved") {
216
- if (!hasExecution(pending.callId, pending.seq))
231
+ if (!hasExecution(pending.callId, pending.seq, invocationSeq))
217
232
  return { kind: "EXECUTE", invocationSeq: invocationSeq ?? pending.seq };
218
233
  }
219
- else if (!hasResult(pending.callId, pending.seq)) {
234
+ else if (!hasResult(pending.callId, pending.seq, invocationSeq)) {
220
235
  return { kind: "REPAIR_RESULT", invocationSeq: invocationSeq ?? pending.seq };
221
236
  }
222
237
  }
@@ -226,7 +241,7 @@ export function deriveRecoveryPlan(events, scope) {
226
241
  for (const ev of scope) {
227
242
  if (ev.type !== "tool_execution_succeeded" && ev.type !== "tool_execution_failed")
228
243
  continue;
229
- if (!hasResult(ev.callId, ev.seq) && !events.some((e) => e.type === "tool_result" && e.executionId === ev.executionId)) {
244
+ if (!hasResult(ev.callId, ev.seq, ev.invocationSeq) && !events.some((e) => e.type === "tool_result" && e.executionId === ev.executionId)) {
230
245
  return { kind: "REPAIR_RESULT", executionId: ev.executionId };
231
246
  }
232
247
  }
package/dist/run.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * persistence, the loop drive, the durable recovery state machine), moved
4
4
  * verbatim from session.ts.
5
5
  */
6
- import { denialResult, loop } from "@vincemakes/kiso-core";
6
+ import { denialResult, loop, validateArgs } from "@vincemakes/kiso-core";
7
7
  import { ABORTED, MergedSignal, abortable, openRunId } from "./recovery.js";
8
8
  import { resolveReasoning } from "./provider/metadata.js";
9
9
  import { deriveRecoveryPlan, invocationSeqOf } from "./recovery-plan.js";
@@ -37,8 +37,8 @@ export class Run {
37
37
  #decisionIds = [];
38
38
  #started = false;
39
39
  constructor(store, adapter, config, session, input, externalSignal, resume,
40
- // TV-1B: durable PROVENANCE for the input (e.g. the verification
41
- // seed's source:"system") — who produced the line, never a
40
+ // TV-1B: durable PROVENANCE for the input (e.g. source:"system"
41
+ // for a line the product wrote) — who produced the line, never a
42
42
  // provider-role escalation. Absent = plain user input.
43
43
  source,
44
44
  // 0.40.0: how a person composed the turn (a skill + the typed line) —
@@ -494,7 +494,21 @@ export class Run {
494
494
  throw new Error(`the recovery plan derived a call outside the scope (seq ${invocationSeq})`);
495
495
  // The chain's PolicyCall carries name+input only — callId is the
496
496
  // framework's, the hook's is the provider-facing ToolCallPayload.
497
- const payload = { callId: call.callId, name: call.name, input: call.input ?? {} };
497
+ // 0430-F1: the fresh path's preflight, in the same order and with the
498
+ // same words — a call the kernel refuses before any policy (unknown
499
+ // tool, unparsable arguments, a schema failure) is refused here the
500
+ // same way: one invalid_input result, no decision, no execution.
501
+ const tool = this.#config.registry.get(call.name);
502
+ const schemaError = tool !== undefined && call.input !== null ? validateArgs(tool.parameters, call.input) : null;
503
+ const refusal = tool === undefined ? `Unknown tool: ${call.name}` : call.input === null ? "Arguments failed to parse as JSON" : schemaError !== null ? `Arguments failed schema validation:${schemaError}` : undefined;
504
+ if (refusal !== undefined) {
505
+ yield log.append({ type: "tool_result", callId: call.callId, invocationSeq: call.seq, content: refusal, isError: true, errorKind: "invalid_input" });
506
+ return;
507
+ }
508
+ // #20: the same lexical evidence the fresh path hands onPreTool — THIS
509
+ // invocation's deltas, anchored by its seq; never given to the chain.
510
+ const rawInput = rawInputOf(log.all, call.callId, call.seq);
511
+ const payload = { callId: call.callId, name: call.name, input: call.input ?? {}, ...(rawInput !== undefined ? { rawInput } : {}) };
498
512
  const policyCall = { name: payload.name, input: payload.input };
499
513
  let verdict;
500
514
  try {
@@ -525,13 +539,26 @@ export class Run {
525
539
  }
526
540
  if (verdict === undefined)
527
541
  verdict = { action: "allow" }; // no policies — the kernel's default allow
528
- // The recovery's own decision is a POLICY verdict (the plan's E1
529
- // rule: only a decidedBy-carrying decision binds the call — a human
530
- // verdict binds its request, never the call). The driver's write
531
- // must satisfy its own plan: no decidedBy → the re-derive would
532
- // derive DECIDE_PERMISSION again forever (the plan cannot tell the
533
- // driver's write from a human's). Stamped "mode:default" when the
534
- // verdict carries none — the gates' seeded convention.
542
+ // 0430-F1: a verdict WITHOUT a speaker — the default allow, an
543
+ // onPreTool allow or deny — is not a durable fact on the fresh path
544
+ // and is not one here: allow goes straight to the execution (the
545
+ // plan no longer re-decides a started invocation), deny straight to
546
+ // the denial result. Only a speaking chain's verdict is recorded,
547
+ // exactly as the fresh path records it. (Before: a fabricated
548
+ // "mode:default" decision, written so the plan could make progress.)
549
+ if (!("decidedBy" in verdict)) {
550
+ if (verdict.action === "allow") {
551
+ yield* this.#executeInvocation(call.seq, scope, log, signal);
552
+ return;
553
+ }
554
+ if (verdict.action === "deny") {
555
+ yield* this.#denialResult(call.callId, ("reason" in verdict && verdict.reason !== undefined ? verdict.reason : undefined) ?? "denied", call.seq);
556
+ return;
557
+ }
558
+ }
559
+ // A speaking chain's decision binds the call (the plan's E1 rule: a
560
+ // decidedBy-carrying decision binds the call — a human verdict binds
561
+ // its request, never the call).
535
562
  const decisionId = `d-${log.all.length + 1}`;
536
563
  if (verdict.action === "allow") {
537
564
  yield log.append({
@@ -655,6 +682,9 @@ export class Run {
655
682
  ? {
656
683
  type: "tool_result",
657
684
  callId: ev.callId,
685
+ // 0430-F1: the receipt's invocation identity rides the fill, as
686
+ // a fresh result carries call.seq — bound exactly ever after.
687
+ ...(ev.invocationSeq !== undefined ? { invocationSeq: ev.invocationSeq } : {}),
658
688
  content: ev.result.content,
659
689
  isError: false,
660
690
  // round 8: the repaired result reproduces the normal path
@@ -665,6 +695,7 @@ export class Run {
665
695
  : {
666
696
  type: "tool_result",
667
697
  callId: ev.callId,
698
+ ...(ev.invocationSeq !== undefined ? { invocationSeq: ev.invocationSeq } : {}),
668
699
  content: ev.error,
669
700
  isError: true,
670
701
  ...(ev.errorKind !== undefined ? { errorKind: ev.errorKind } : {}),
@@ -687,7 +718,10 @@ export class Run {
687
718
  const decided = log.all.find((e) => e.type === "permission_decided" &&
688
719
  (request !== undefined
689
720
  ? e.decisionId === request.decisionId
690
- : call !== undefined && e.callId === call.callId && e.seq > call.seq && e.decidedBy !== undefined));
721
+ : call !== undefined &&
722
+ e.decidedBy !== undefined &&
723
+ // 0430-F1: exact invocation first; an old log's decision by callId
724
+ (e.invocationSeq !== undefined ? e.invocationSeq === call.seq : e.callId === call.callId && e.seq > call.seq)));
691
725
  if (decided === undefined || (request?.callId ?? call?.callId) === undefined) {
692
726
  throw new Error(`the recovery plan derived a denial without its decision (seq ${invocationSeq})`);
693
727
  }
@@ -755,6 +789,12 @@ export class Run {
755
789
  const log = this.#session.log;
756
790
  const tool = this.#config.registry.get(name);
757
791
  const executionId = `ex-${log.lastSeq + 1}`;
792
+ // 0.43.0 (#13, 0420-F1): ToolContext is a function of the durable
793
+ // invocation and execution, never of fresh vs recovery — the same
794
+ // sessionId, callId, executionId and lexical arguments the fresh path
795
+ // hands the handler, the last derived from THIS invocation's deltas.
796
+ const rawInput = rawInputOf(log.all, callId, invocationSeq);
797
+ const ctx = { signal, sessionId: this.#session.id, callId, executionId, ...(rawInput !== undefined ? { rawInput } : {}) };
758
798
  // An abort that landed while the decision was being applied must
759
799
  // not start the side effect (finding 3).
760
800
  if (signal.aborted)
@@ -777,7 +817,7 @@ export class Run {
777
817
  result = { content: "aborted before execution", isError: true, errorKind: "fatal" };
778
818
  }
779
819
  else {
780
- result = await tool.execute(input, { signal });
820
+ result = await tool.execute(input, ctx);
781
821
  }
782
822
  }
783
823
  catch (err) {
@@ -788,7 +828,7 @@ export class Run {
788
828
  };
789
829
  }
790
830
  if (this.#config.hooks?.onPostTool) {
791
- result = await this.#config.hooks.onPostTool({ callId, name, input }, result, { sessionId: this.#session.id });
831
+ result = await this.#config.hooks.onPostTool({ callId, name, input, ...(rawInput !== undefined ? { rawInput } : {}) }, result, { sessionId: this.#session.id });
792
832
  }
793
833
  }
794
834
  // ruling #12 correction one: the honest note rides the recovered failure too —
@@ -850,6 +890,7 @@ export class Run {
850
890
  content: denial.content,
851
891
  isError: true,
852
892
  errorKind: denial.errorKind,
893
+ tags: denial.tags, // 0430-F1: the fresh path's denial carries them
853
894
  });
854
895
  }
855
896
  }
@@ -861,3 +902,27 @@ function resolveReasoningOrThrow(model, setting, endpoint) {
861
902
  throw new Error(`the session's recorded reasoning setting cannot run here: ${r.reason}`);
862
903
  return Object.keys(r.wire).length > 0 ? { reasoning: r.wire } : {};
863
904
  }
905
+ /** 0.43.0 (#13): the invocation's lexical arguments from the durable
906
+ * log — the tool_call_input_delta events between ITS tool_call_start
907
+ * and ITS tool_call_end (anchored by invocationSeq, never "the latest
908
+ * same callId"), the window the kernel buffered on the fresh path.
909
+ * Undefined when no delta was streamed, or when the invocation's seq is
910
+ * unknown. */
911
+ function rawInputOf(events, callId, invocationSeq) {
912
+ if (invocationSeq === undefined)
913
+ return undefined;
914
+ const end = events.findIndex((e) => e.seq === invocationSeq);
915
+ if (end < 0)
916
+ return undefined;
917
+ let text;
918
+ for (let i = end - 1; i >= 0; i -= 1) {
919
+ const e = events[i];
920
+ if (e.type === "tool_call_start" && e.callId === callId)
921
+ break;
922
+ if (e.type === "stop" || e.type === "user_input" || e.type === "terminal" || e.type === "assistant_start")
923
+ break;
924
+ if (e.type === "tool_call_input_delta" && e.callId === callId)
925
+ text = e.inputJsonDelta + (text ?? "");
926
+ }
927
+ return text;
928
+ }
package/dist/session.d.ts CHANGED
@@ -29,7 +29,6 @@
29
29
  */
30
30
  import { EventLog, type AbortSignalLike, type Adapter, type Event, type KisoExtension, type Message, type PermissionDecision, type Tool, type ToolSpec, type EventInput } from "@vincemakes/kiso-core";
31
31
  import { type OverflowMeasure } from "./overflow-belt.js";
32
- import { type TaskAssessment } from "./task-assessment.js";
33
32
  import { type ContinuationScope } from "@vincemakes/kiso-core";
34
33
  import { type ReasoningSetting } from "./provider/metadata.js";
35
34
  import { type SessionStore, type StoreRecord } from "./store.js";
@@ -375,20 +374,6 @@ export declare class AgentSession {
375
374
  approve(decisionId: string, allow: boolean, reason?: string): Promise<void>;
376
375
  /** Executions that started but never reported a result (crash window). */
377
376
  uncertainExecutions(): import("./ledger.js").ExecutionRecord[];
378
- /**
379
- * TV-1A — assess the task claims and their evidence freshness over THIS
380
- * session's durable log. The non-mutating set comes from the live tools'
381
- * own `effects.precommitSafe` certificates (one direction of truth,
382
- * never a second declaration) — the read-only+free+local contract, the
383
- * only certificate that proves the world untouched. `concurrency:
384
- * "shared"` is a SCHEDULING promise and never feeds this set (TV-1C —
385
- * slow_touch is shared and writes). The evidence policy defaults to
386
- * {"shell"} — the convention the task extension's own "make the LAST
387
- * item a verification step" guidance produces.
388
- */
389
- assessTasks(opts?: {
390
- readonly evidenceTools?: ReadonlySet<string>;
391
- }): TaskAssessment;
392
377
  /**
393
378
  * The human's verdict on an interrupted execution, keyed by EXECUTION ID
394
379
  * (B group): "rerun" (the human says the side effect did NOT happen — the
package/dist/session.js CHANGED
@@ -31,11 +31,6 @@ import { EventLog, projectMessages, SUMMARY_FRAMING, } from "@vincemakes/kiso-co
31
31
  import { executionLedger } from "./ledger.js";
32
32
  import { overflowBelt } from "./overflow-belt.js";
33
33
  import { windowLearner } from "./window-learner.js";
34
- import { assessTasks } from "./task-assessment.js";
35
- /** TV-1A — the session-level evidence policy: the PURE projection defaults
36
- * to ∅ (never inventing evidence); the session names the one built-in
37
- * verification surface. Override per call for custom evidence tools. */
38
- const DEFAULT_EVIDENCE_TOOLS = new Set(["shell"]);
39
34
  import { denialResult } from "@vincemakes/kiso-core";
40
35
  import { buildProfile, readProfile, writeProfile, writeSummary } from "./profile.js";
41
36
  import { summarizeEvents } from "./session-summary.js";
@@ -1064,28 +1059,6 @@ export class AgentSession {
1064
1059
  uncertainExecutions() {
1065
1060
  return [...executionLedger(this.log.all).values()].filter((r) => r.status === "uncertain");
1066
1061
  }
1067
- /**
1068
- * TV-1A — assess the task claims and their evidence freshness over THIS
1069
- * session's durable log. The non-mutating set comes from the live tools'
1070
- * own `effects.precommitSafe` certificates (one direction of truth,
1071
- * never a second declaration) — the read-only+free+local contract, the
1072
- * only certificate that proves the world untouched. `concurrency:
1073
- * "shared"` is a SCHEDULING promise and never feeds this set (TV-1C —
1074
- * slow_touch is shared and writes). The evidence policy defaults to
1075
- * {"shell"} — the convention the task extension's own "make the LAST
1076
- * item a verification step" guidance produces.
1077
- */
1078
- assessTasks(opts) {
1079
- const nonMutatingTools = new Set();
1080
- for (const tool of this.#config.registry.list()) {
1081
- if (tool.effects?.precommitSafe === true)
1082
- nonMutatingTools.add(tool.name);
1083
- }
1084
- return assessTasks(this.log.all, {
1085
- nonMutatingTools,
1086
- evidenceTools: opts?.evidenceTools ?? DEFAULT_EVIDENCE_TOOLS,
1087
- });
1088
- }
1089
1062
  /**
1090
1063
  * The human's verdict on an interrupted execution, keyed by EXECUTION ID
1091
1064
  * (B group): "rerun" (the human says the side effect did NOT happen — the
@@ -270,8 +270,9 @@ export declare function estimateEventTokens(ev: Event): number;
270
270
  * rounds exist (nothing worth covering yet).
271
271
  *
272
272
  * ⑥ (task round): a tool result tagged do-not-compact is DURABLE work
273
- * memory (the task_set echo) — the summary must never cover its round,
274
- * or the model loses the current list. When the base boundary would
273
+ * memory (a tool declares it so; the retired task extension's list was
274
+ * the first) — the summary must never cover its round, or the model
275
+ * loses it. When the base boundary would
275
276
  * cover such a result, the boundary pulls back to just before the round
276
277
  * containing the LATEST one (still a turn boundary). A protected round
277
278
  * as the FIRST uncovered round leaves nothing before it to cover →
package/dist/summarize.js CHANGED
@@ -501,8 +501,9 @@ export function estimateEventTokens(ev) {
501
501
  * rounds exist (nothing worth covering yet).
502
502
  *
503
503
  * ⑥ (task round): a tool result tagged do-not-compact is DURABLE work
504
- * memory (the task_set echo) — the summary must never cover its round,
505
- * or the model loses the current list. When the base boundary would
504
+ * memory (a tool declares it so; the retired task extension's list was
505
+ * the first) — the summary must never cover its round, or the model
506
+ * loses it. When the base boundary would
506
507
  * cover such a result, the boundary pulls back to just before the round
507
508
  * containing the LATEST one (still a turn boundary). A protected round
508
509
  * as the FIRST uncovered round leaves nothing before it to cover →
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-runtime",
3
- "version": "0.42.3",
3
+ "version": "0.44.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,12 +25,12 @@
25
25
  "test": "vitest run"
26
26
  },
27
27
  "dependencies": {
28
- "@vincemakes/kiso-core": "0.42.3"
28
+ "@vincemakes/kiso-core": "0.44.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "@vincemakes/kiso-provider-anthropic": "0.42.3",
32
- "@vincemakes/kiso-provider-openai": "0.42.3",
33
- "@vincemakes/kiso-provider-openai-responses": "0.42.3"
31
+ "@vincemakes/kiso-provider-anthropic": "0.44.0",
32
+ "@vincemakes/kiso-provider-openai": "0.44.0",
33
+ "@vincemakes/kiso-provider-openai-responses": "0.44.0"
34
34
  },
35
35
  "peerDependenciesMeta": {
36
36
  "@vincemakes/kiso-provider-anthropic": {
@@ -44,7 +44,7 @@
44
44
  }
45
45
  },
46
46
  "devDependencies": {
47
- "@vincemakes/kiso-evals": "0.42.3",
47
+ "@vincemakes/kiso-evals": "0.44.0",
48
48
  "@types/node": "^26.1.2",
49
49
  "typescript": "^5.7.2",
50
50
  "vitest": "^3.0.0"
@@ -1,93 +0,0 @@
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;
@@ -1,165 +0,0 @@
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
- }