@vincemakes/kiso-runtime 0.42.3 → 0.43.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.
@@ -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";
@@ -494,6 +494,17 @@ 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
+ // 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
+ }
497
508
  const payload = { callId: call.callId, name: call.name, input: call.input ?? {} };
498
509
  const policyCall = { name: payload.name, input: payload.input };
499
510
  let verdict;
@@ -525,13 +536,26 @@ export class Run {
525
536
  }
526
537
  if (verdict === undefined)
527
538
  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.
539
+ // 0430-F1: a verdict WITHOUT a speaker — the default allow, an
540
+ // onPreTool allow or deny — is not a durable fact on the fresh path
541
+ // and is not one here: allow goes straight to the execution (the
542
+ // plan no longer re-decides a started invocation), deny straight to
543
+ // the denial result. Only a speaking chain's verdict is recorded,
544
+ // exactly as the fresh path records it. (Before: a fabricated
545
+ // "mode:default" decision, written so the plan could make progress.)
546
+ if (!("decidedBy" in verdict)) {
547
+ if (verdict.action === "allow") {
548
+ yield* this.#executeInvocation(call.seq, scope, log, signal);
549
+ return;
550
+ }
551
+ if (verdict.action === "deny") {
552
+ yield* this.#denialResult(call.callId, ("reason" in verdict && verdict.reason !== undefined ? verdict.reason : undefined) ?? "denied", call.seq);
553
+ return;
554
+ }
555
+ }
556
+ // A speaking chain's decision binds the call (the plan's E1 rule: a
557
+ // decidedBy-carrying decision binds the call — a human verdict binds
558
+ // its request, never the call).
535
559
  const decisionId = `d-${log.all.length + 1}`;
536
560
  if (verdict.action === "allow") {
537
561
  yield log.append({
@@ -655,6 +679,9 @@ export class Run {
655
679
  ? {
656
680
  type: "tool_result",
657
681
  callId: ev.callId,
682
+ // 0430-F1: the receipt's invocation identity rides the fill, as
683
+ // a fresh result carries call.seq — bound exactly ever after.
684
+ ...(ev.invocationSeq !== undefined ? { invocationSeq: ev.invocationSeq } : {}),
658
685
  content: ev.result.content,
659
686
  isError: false,
660
687
  // round 8: the repaired result reproduces the normal path
@@ -665,6 +692,7 @@ export class Run {
665
692
  : {
666
693
  type: "tool_result",
667
694
  callId: ev.callId,
695
+ ...(ev.invocationSeq !== undefined ? { invocationSeq: ev.invocationSeq } : {}),
668
696
  content: ev.error,
669
697
  isError: true,
670
698
  ...(ev.errorKind !== undefined ? { errorKind: ev.errorKind } : {}),
@@ -687,7 +715,10 @@ export class Run {
687
715
  const decided = log.all.find((e) => e.type === "permission_decided" &&
688
716
  (request !== undefined
689
717
  ? e.decisionId === request.decisionId
690
- : call !== undefined && e.callId === call.callId && e.seq > call.seq && e.decidedBy !== undefined));
718
+ : call !== undefined &&
719
+ e.decidedBy !== undefined &&
720
+ // 0430-F1: exact invocation first; an old log's decision by callId
721
+ (e.invocationSeq !== undefined ? e.invocationSeq === call.seq : e.callId === call.callId && e.seq > call.seq)));
691
722
  if (decided === undefined || (request?.callId ?? call?.callId) === undefined) {
692
723
  throw new Error(`the recovery plan derived a denial without its decision (seq ${invocationSeq})`);
693
724
  }
@@ -755,6 +786,12 @@ export class Run {
755
786
  const log = this.#session.log;
756
787
  const tool = this.#config.registry.get(name);
757
788
  const executionId = `ex-${log.lastSeq + 1}`;
789
+ // 0.43.0 (#13, 0420-F1): ToolContext is a function of the durable
790
+ // invocation and execution, never of fresh vs recovery — the same
791
+ // sessionId, callId, executionId and lexical arguments the fresh path
792
+ // hands the handler, the last derived from THIS invocation's deltas.
793
+ const rawInput = rawInputOf(log.all, callId, invocationSeq);
794
+ const ctx = { signal, sessionId: this.#session.id, callId, executionId, ...(rawInput !== undefined ? { rawInput } : {}) };
758
795
  // An abort that landed while the decision was being applied must
759
796
  // not start the side effect (finding 3).
760
797
  if (signal.aborted)
@@ -777,7 +814,7 @@ export class Run {
777
814
  result = { content: "aborted before execution", isError: true, errorKind: "fatal" };
778
815
  }
779
816
  else {
780
- result = await tool.execute(input, { signal });
817
+ result = await tool.execute(input, ctx);
781
818
  }
782
819
  }
783
820
  catch (err) {
@@ -850,6 +887,7 @@ export class Run {
850
887
  content: denial.content,
851
888
  isError: true,
852
889
  errorKind: denial.errorKind,
890
+ tags: denial.tags, // 0430-F1: the fresh path's denial carries them
853
891
  });
854
892
  }
855
893
  }
@@ -861,3 +899,27 @@ function resolveReasoningOrThrow(model, setting, endpoint) {
861
899
  throw new Error(`the session's recorded reasoning setting cannot run here: ${r.reason}`);
862
900
  return Object.keys(r.wire).length > 0 ? { reasoning: r.wire } : {};
863
901
  }
902
+ /** 0.43.0 (#13): the invocation's lexical arguments from the durable
903
+ * log — the tool_call_input_delta events between ITS tool_call_start
904
+ * and ITS tool_call_end (anchored by invocationSeq, never "the latest
905
+ * same callId"), the window the kernel buffered on the fresh path.
906
+ * Undefined when no delta was streamed, or when the invocation's seq is
907
+ * unknown. */
908
+ function rawInputOf(events, callId, invocationSeq) {
909
+ if (invocationSeq === undefined)
910
+ return undefined;
911
+ const end = events.findIndex((e) => e.seq === invocationSeq);
912
+ if (end < 0)
913
+ return undefined;
914
+ let text;
915
+ for (let i = end - 1; i >= 0; i -= 1) {
916
+ const e = events[i];
917
+ if (e.type === "tool_call_start" && e.callId === callId)
918
+ break;
919
+ if (e.type === "stop" || e.type === "user_input" || e.type === "terminal" || e.type === "assistant_start")
920
+ break;
921
+ if (e.type === "tool_call_input_delta" && e.callId === callId)
922
+ text = e.inputJsonDelta + (text ?? "");
923
+ }
924
+ return text;
925
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-runtime",
3
- "version": "0.42.3",
3
+ "version": "0.43.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.43.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.43.0",
32
+ "@vincemakes/kiso-provider-openai": "0.43.0",
33
+ "@vincemakes/kiso-provider-openai-responses": "0.43.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.43.0",
48
48
  "@types/node": "^26.1.2",
49
49
  "typescript": "^5.7.2",
50
50
  "vitest": "^3.0.0"