@vincemakes/kiso-runtime 0.42.2 → 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.
- package/dist/recovery-plan.js +27 -12
- package/dist/run.js +72 -10
- package/package.json +6 -6
package/dist/recovery-plan.js
CHANGED
|
@@ -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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
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 &&
|
|
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,
|
|
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.
|
|
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.
|
|
28
|
+
"@vincemakes/kiso-core": "0.43.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@vincemakes/kiso-provider-anthropic": "0.
|
|
32
|
-
"@vincemakes/kiso-provider-openai": "0.
|
|
33
|
-
"@vincemakes/kiso-provider-openai-responses": "0.
|
|
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.
|
|
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"
|