@vincemakes/kiso-runtime 0.1.34 → 0.1.36
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 +1 -0
- package/dist/index.js +1 -0
- package/dist/ledger.d.ts +45 -0
- package/dist/ledger.js +90 -0
- package/dist/run.js +222 -10
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -1
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/ledger.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 — the execution ledger: exactly-once side effects from the event log.
|
|
3
|
+
*
|
|
4
|
+
* Every tool execution writes `tool_execution_started` before the handler
|
|
5
|
+
* and `tool_execution_succeeded` / `tool_execution_failed` after
|
|
6
|
+
* (kernel/loop.ts). From those events alone — no second store — this module
|
|
7
|
+
* answers the recovery questions:
|
|
8
|
+
*
|
|
9
|
+
* 1. What is the durable status of execution X? (`executionLedger`)
|
|
10
|
+
* 2. What is the latest execution of call Y? (`executionForCallId`)
|
|
11
|
+
*
|
|
12
|
+
* IDENTITY (Area 3): the ledger is keyed by `executionId` — a persistent,
|
|
13
|
+
* framework-generated id unique per log (one per started event). The
|
|
14
|
+
* provider's `callId` is correlation only and may repeat; two logical calls
|
|
15
|
+
* with identical (name, input) are two executions.
|
|
16
|
+
*
|
|
17
|
+
* Status derivation:
|
|
18
|
+
* started, no terminal event yet → "uncertain" (interrupted: human)
|
|
19
|
+
* succeeded → "succeeded" (confirmed, never re-run)
|
|
20
|
+
* failed (any) → "failed" (a complete receipt IS
|
|
21
|
+
* the outcome — ruling #12 / ADR-0038;
|
|
22
|
+
* safeToRetry stays on the event for
|
|
23
|
+
* history, it no longer feeds status)
|
|
24
|
+
* resolved "rerun" → "rerun" (human cleared it)
|
|
25
|
+
* resolved "abandoned" → "abandoned" (human killed it)
|
|
26
|
+
*/
|
|
27
|
+
import type { Event } from "@vincemakes/kiso-core";
|
|
28
|
+
export type ExecutionStatus = "uncertain" | "succeeded" | "failed" | "rerun" | "abandoned";
|
|
29
|
+
export interface ExecutionRecord {
|
|
30
|
+
readonly executionId: string;
|
|
31
|
+
readonly callId: string;
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly input: Readonly<Record<string, unknown>>;
|
|
34
|
+
readonly status: ExecutionStatus;
|
|
35
|
+
/** Present when `status` is "succeeded" — the durable result to replay. */
|
|
36
|
+
readonly result?: {
|
|
37
|
+
readonly content: string;
|
|
38
|
+
readonly isError: false;
|
|
39
|
+
};
|
|
40
|
+
readonly error?: string;
|
|
41
|
+
}
|
|
42
|
+
/** executionId → durable status, rebuilt purely from events (ADR-0002). */
|
|
43
|
+
export declare function executionLedger(events: readonly Event[]): Map<string, ExecutionRecord>;
|
|
44
|
+
/** The LATEST execution record for a provider call id (correlation only). */
|
|
45
|
+
export declare function executionForCallId(events: readonly Event[], callId: string): ExecutionRecord | undefined;
|
package/dist/ledger.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 — the execution ledger: exactly-once side effects from the event log.
|
|
3
|
+
*
|
|
4
|
+
* Every tool execution writes `tool_execution_started` before the handler
|
|
5
|
+
* and `tool_execution_succeeded` / `tool_execution_failed` after
|
|
6
|
+
* (kernel/loop.ts). From those events alone — no second store — this module
|
|
7
|
+
* answers the recovery questions:
|
|
8
|
+
*
|
|
9
|
+
* 1. What is the durable status of execution X? (`executionLedger`)
|
|
10
|
+
* 2. What is the latest execution of call Y? (`executionForCallId`)
|
|
11
|
+
*
|
|
12
|
+
* IDENTITY (Area 3): the ledger is keyed by `executionId` — a persistent,
|
|
13
|
+
* framework-generated id unique per log (one per started event). The
|
|
14
|
+
* provider's `callId` is correlation only and may repeat; two logical calls
|
|
15
|
+
* with identical (name, input) are two executions.
|
|
16
|
+
*
|
|
17
|
+
* Status derivation:
|
|
18
|
+
* started, no terminal event yet → "uncertain" (interrupted: human)
|
|
19
|
+
* succeeded → "succeeded" (confirmed, never re-run)
|
|
20
|
+
* failed (any) → "failed" (a complete receipt IS
|
|
21
|
+
* the outcome — ruling #12 / ADR-0038;
|
|
22
|
+
* safeToRetry stays on the event for
|
|
23
|
+
* history, it no longer feeds status)
|
|
24
|
+
* resolved "rerun" → "rerun" (human cleared it)
|
|
25
|
+
* resolved "abandoned" → "abandoned" (human killed it)
|
|
26
|
+
*/
|
|
27
|
+
/** executionId → durable status, rebuilt purely from events (ADR-0002). */
|
|
28
|
+
export function executionLedger(events) {
|
|
29
|
+
const ledger = new Map();
|
|
30
|
+
for (const ev of events) {
|
|
31
|
+
switch (ev.type) {
|
|
32
|
+
case "tool_execution_started":
|
|
33
|
+
ledger.set(ev.executionId, {
|
|
34
|
+
executionId: ev.executionId,
|
|
35
|
+
callId: ev.callId,
|
|
36
|
+
name: ev.name,
|
|
37
|
+
input: ev.input,
|
|
38
|
+
status: "uncertain",
|
|
39
|
+
});
|
|
40
|
+
break;
|
|
41
|
+
case "tool_execution_succeeded": {
|
|
42
|
+
const prior = ledger.get(ev.executionId);
|
|
43
|
+
if (prior) {
|
|
44
|
+
ledger.set(ev.executionId, { ...prior, status: "succeeded", result: ev.result });
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
case "tool_execution_failed": {
|
|
49
|
+
const prior = ledger.get(ev.executionId);
|
|
50
|
+
if (prior) {
|
|
51
|
+
ledger.set(ev.executionId, {
|
|
52
|
+
...prior,
|
|
53
|
+
// ruling #12 (ADR-0038): a complete receipt IS the outcome —
|
|
54
|
+
// failed is "failed", never "uncertain"; uncertainty
|
|
55
|
+
// belongs to the crash window alone (started, no receipt).
|
|
56
|
+
status: "failed",
|
|
57
|
+
...(ev.error !== undefined ? { error: ev.error } : {}),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case "tool_execution_resolved": {
|
|
63
|
+
const prior = ledger.get(ev.executionId);
|
|
64
|
+
if (prior) {
|
|
65
|
+
ledger.set(ev.executionId, {
|
|
66
|
+
...prior,
|
|
67
|
+
status: ev.resolution === "rerun" ? "rerun" : "abandoned",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
default:
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return ledger;
|
|
77
|
+
}
|
|
78
|
+
/** The LATEST execution record for a provider call id (correlation only). */
|
|
79
|
+
export function executionForCallId(events, callId) {
|
|
80
|
+
const ledger = executionLedger(events);
|
|
81
|
+
let found;
|
|
82
|
+
for (const ev of events) {
|
|
83
|
+
if (ev.type !== "tool_execution_started")
|
|
84
|
+
continue;
|
|
85
|
+
if (ev.callId !== callId)
|
|
86
|
+
continue;
|
|
87
|
+
found = ledger.get(ev.executionId);
|
|
88
|
+
}
|
|
89
|
+
return found;
|
|
90
|
+
}
|
package/dist/run.js
CHANGED
|
@@ -176,7 +176,7 @@ export class Run {
|
|
|
176
176
|
if (ev.seq > baseSeq)
|
|
177
177
|
await this.#session.persist(this.runId, ev);
|
|
178
178
|
};
|
|
179
|
-
for await (const ev of this.#recover(log, signal, lastOpen.events)) {
|
|
179
|
+
for await (const ev of this.#recover(log, signal, lastOpen.events, approvalChain, this.#config.hooks)) {
|
|
180
180
|
await persist(ev);
|
|
181
181
|
yield ev;
|
|
182
182
|
}
|
|
@@ -242,10 +242,211 @@ export class Run {
|
|
|
242
242
|
* re-approved); a denial writes its tool result; a succeeded/failed
|
|
243
243
|
* execution whose tool_result never landed is completed from the
|
|
244
244
|
* receipt. Undecided requests pause and await approve().
|
|
245
|
+
*
|
|
246
|
+
* R-E 0.1.43 (Gap A): a committed turn's tool_call_end with no durable
|
|
247
|
+
* decision and no execution is UNDECIDED — recovery re-enters it into
|
|
248
|
+
* the approval pipeline before anything else (only a durable
|
|
249
|
+
* permission_decided authorizes an effect).
|
|
245
250
|
*/
|
|
246
|
-
async *#recover(log, signal, scope) {
|
|
247
|
-
|
|
251
|
+
async *#recover(log, signal, scope, approvalChain, hooks) {
|
|
252
|
+
// ── Gap B: the tail draft is abandoned — never committed history ──
|
|
253
|
+
// "A model output suffix without a committed stop is an incomplete
|
|
254
|
+
// draft and must never become committed provider history." The
|
|
255
|
+
// resume appends the abandon marker FIRST: it voids the range after
|
|
256
|
+
// the last committed boundary (stop / user_input / terminal /
|
|
257
|
+
// compaction / summarized — or an earlier marker), so the projection
|
|
258
|
+
// excludes the draft and a call inside it is never executed (the
|
|
259
|
+
// boundary clause: the two Gaps divide at the stop). The audit bytes
|
|
260
|
+
// stay; a marker is kernel-exclusive (the AdapterEvent whitelist).
|
|
261
|
+
// Idempotent: an already-voided draft has a marker as its last
|
|
262
|
+
// boundary — the detection finds no output after it, and the
|
|
263
|
+
// recovered events (decided/started/result) are no draft.
|
|
264
|
+
const boundary = [...scope].reverse().find((e) => e.type === "stop" ||
|
|
265
|
+
e.type === "user_input" ||
|
|
266
|
+
e.type === "terminal" ||
|
|
267
|
+
e.type === "microcompacted" ||
|
|
268
|
+
e.type === "compacted" ||
|
|
269
|
+
e.type === "summarized" ||
|
|
270
|
+
e.type === "model_output_abandoned");
|
|
271
|
+
if (boundary !== undefined) {
|
|
272
|
+
// R-E 0.1.44 (verified against sentence 2): the draft detection
|
|
273
|
+
// stays text-only — a bare tool-call suffix [tool_call_end,
|
|
274
|
+
// permission_requested] with no text is the legal approval-panel
|
|
275
|
+
// pause (the Area 2 contract: the pending request binds and
|
|
276
|
+
// executes, pair-closed — the extensions-e2e gate). The finding's
|
|
277
|
+
// shapes all carry text, and the VOID SCOPE (what the marker
|
|
278
|
+
// covers) is the type filter at project.ts: model output dies
|
|
279
|
+
// with the draft, the framework's facts never do.
|
|
280
|
+
const draft = scope.some((e) => (e.type === "text_delta" || e.type === "thinking") && e.seq > boundary.seq);
|
|
281
|
+
if (draft) {
|
|
282
|
+
yield log.append({
|
|
283
|
+
type: "model_output_abandoned",
|
|
284
|
+
voidFromSeq: boundary.seq,
|
|
285
|
+
reason: "a model output suffix without a committed stop — abandoned on resume",
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// The invocation's framework identity (R-E 0.1.43): the
|
|
290
|
+
// tool_call_end's seq — carried by new logs, derived by
|
|
291
|
+
// callId+proximity for old ones (the last such call before the seq).
|
|
292
|
+
const callSeqOf = (callId, before) => {
|
|
293
|
+
let seq;
|
|
294
|
+
for (const e of scope) {
|
|
295
|
+
if (e.type === "tool_call_end" && e.callId === callId && e.seq < before)
|
|
296
|
+
seq = e.seq;
|
|
297
|
+
}
|
|
298
|
+
return seq;
|
|
299
|
+
};
|
|
300
|
+
// ── Gap A: a committed turn's UNDECIDED invocation ────────────────
|
|
301
|
+
// A durable stop with a bare tool_call_end (no decision, no
|
|
302
|
+
// execution) re-enters the approval pipeline: the composed chain
|
|
303
|
+
// decides — allow → durable permission_decided (decidedBy
|
|
304
|
+
// faithfully) + the persisted execution; deny → decided + the
|
|
305
|
+
// denial result; ask/all-abstain → permission_requested (the
|
|
306
|
+
// requests pass below announces it and waits for the human). No
|
|
307
|
+
// guessing, no inheriting, no retro-authorization — re-decide.
|
|
308
|
+
// A durable POLICY verdict (E1: decidedBy set) newer than the call
|
|
309
|
+
// binds it — the chain never re-runs for a decided invocation.
|
|
310
|
+
const gapAsks = [];
|
|
311
|
+
for (const call of scope) {
|
|
312
|
+
if (call.type !== "tool_call_end")
|
|
313
|
+
continue;
|
|
314
|
+
// The boundary clause (the directive): a call whose turn has no
|
|
315
|
+
// legal stop is a DRAFT's call — Gap B voids it (never executed,
|
|
316
|
+
// never in the provider projection); this stage never touches it.
|
|
317
|
+
// The two Gaps divide at the stop; no mixing.
|
|
318
|
+
const turnEnd = scope.find((e) => e.type === "user_input" && e.seq > call.seq)?.seq ?? Number.POSITIVE_INFINITY;
|
|
319
|
+
const turnStop = scope.some((e) => e.type === "stop" && e.seq > call.seq && e.seq < turnEnd);
|
|
320
|
+
if (!turnStop)
|
|
321
|
+
continue;
|
|
322
|
+
// The requests pass below owns request-tracked invocations (it
|
|
323
|
+
// binds the stored request by decisionId, or pauses for the
|
|
324
|
+
// human) — Gap A must never re-decide over a stored
|
|
325
|
+
// permission_requested. "Only a durable permission_decided
|
|
326
|
+
// authorizes an effect": a pending request is not a decision.
|
|
327
|
+
const hasRequest = log.all.some((e) => e.type === "permission_requested" && e.callId === call.callId && e.seq > call.seq);
|
|
328
|
+
if (hasRequest)
|
|
329
|
+
continue;
|
|
330
|
+
const decided = log.all.find((e) => e.type === "permission_decided" && e.callId === call.callId && e.seq > call.seq && e.decidedBy !== undefined);
|
|
331
|
+
const hasExecution = log.all.some((e) => e.type === "tool_execution_started" && e.callId === call.callId && e.seq > call.seq);
|
|
332
|
+
const hasResult = log.all.some((e) => e.type === "tool_result" && e.callId === call.callId && e.seq > call.seq);
|
|
333
|
+
if (hasResult)
|
|
334
|
+
continue; // closed — nothing to fill
|
|
335
|
+
if (decided !== undefined) {
|
|
336
|
+
// E1: the durable verdict speaks for the call — apply it
|
|
337
|
+
// without re-running the chain.
|
|
338
|
+
if (signal.aborted)
|
|
339
|
+
return;
|
|
340
|
+
if (decided.decision === "approved" && !hasExecution) {
|
|
341
|
+
yield* this.#executePersisted(call.callId, call.name, call.input ?? {}, call.seq, signal);
|
|
342
|
+
}
|
|
343
|
+
else if (!hasResult) {
|
|
344
|
+
yield* this.#denialResult(call.callId, decided.reason ?? "denied by user", call.seq);
|
|
345
|
+
}
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
// UNDECIDED — re-enter the approval pipeline with the LIVE
|
|
349
|
+
// decision order (loop.ts decideCall): the composed chain
|
|
350
|
+
// first; only when no chain exists do the hooks' onPreTool
|
|
351
|
+
// speak (defer → ask, deny → deny, allow → allow); no policies
|
|
352
|
+
// at all → the kernel's default allow. Same semantics as the
|
|
353
|
+
// live path — a defer policy must not collapse into an
|
|
354
|
+
// auto-allow on resume. A throwing chain counts as ask: it
|
|
355
|
+
// speaks, never silently (the live parity).
|
|
356
|
+
const payload = { callId: call.callId, name: call.name, input: call.input ?? {} };
|
|
357
|
+
// The chain's PolicyCall carries name+input only — callId is the
|
|
358
|
+
// framework's, the hook's is the provider-facing ToolCallPayload.
|
|
359
|
+
const policyCall = { name: payload.name, input: payload.input };
|
|
360
|
+
let verdict;
|
|
361
|
+
try {
|
|
362
|
+
if (approvalChain !== undefined) {
|
|
363
|
+
const chainVerdict = await abortable(Promise.resolve(approvalChain.decide(policyCall, { signal, sessionId: this.#session.id })), signal);
|
|
364
|
+
if (chainVerdict === ABORTED)
|
|
365
|
+
return;
|
|
366
|
+
verdict = chainVerdict;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
verdict = { action: "ask" };
|
|
371
|
+
}
|
|
372
|
+
if (verdict === undefined && hooks?.onPreTool !== undefined) {
|
|
373
|
+
const decision = await abortable(Promise.resolve(hooks.onPreTool(payload, { sessionId: this.#session.id })), signal);
|
|
374
|
+
if (decision === ABORTED)
|
|
375
|
+
return;
|
|
376
|
+
if (decision.action === "defer")
|
|
377
|
+
verdict = { action: "ask" };
|
|
378
|
+
else if (decision.action !== "allow")
|
|
379
|
+
verdict = { action: "deny", reason: decision.reason ?? "denied" };
|
|
380
|
+
else
|
|
381
|
+
verdict = { action: "allow" };
|
|
382
|
+
}
|
|
383
|
+
if (verdict === undefined)
|
|
384
|
+
verdict = { action: "allow" }; // no policies — the kernel's default allow
|
|
385
|
+
const decisionId = `d-${log.all.length + 1}`;
|
|
386
|
+
if (verdict.action === "allow") {
|
|
387
|
+
yield log.append({
|
|
388
|
+
type: "permission_decided",
|
|
389
|
+
decisionId,
|
|
390
|
+
callId: call.callId,
|
|
391
|
+
invocationSeq: call.seq,
|
|
392
|
+
decision: "approved",
|
|
393
|
+
...("decidedBy" in verdict ? { decidedBy: verdict.decidedBy } : {}),
|
|
394
|
+
});
|
|
395
|
+
if (signal.aborted)
|
|
396
|
+
return;
|
|
397
|
+
yield* this.#executePersisted(call.callId, call.name, call.input ?? {}, call.seq, signal);
|
|
398
|
+
}
|
|
399
|
+
else if (verdict.action === "deny") {
|
|
400
|
+
yield log.append({
|
|
401
|
+
type: "permission_decided",
|
|
402
|
+
decisionId,
|
|
403
|
+
callId: call.callId,
|
|
404
|
+
invocationSeq: call.seq,
|
|
405
|
+
decision: "denied",
|
|
406
|
+
...("reason" in verdict && verdict.reason !== undefined ? { reason: verdict.reason } : {}),
|
|
407
|
+
...("decidedBy" in verdict ? { decidedBy: verdict.decidedBy } : {}),
|
|
408
|
+
});
|
|
409
|
+
yield* this.#denialResult(call.callId, ("reason" in verdict && verdict.reason) || "denied", call.seq);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
// ask / all-abstain — the requests pass below announces the
|
|
413
|
+
// stored request and waits for the human.
|
|
414
|
+
const appended = log.append({
|
|
415
|
+
type: "permission_requested",
|
|
416
|
+
decisionId,
|
|
417
|
+
callId: call.callId,
|
|
418
|
+
invocationSeq: call.seq,
|
|
419
|
+
name: call.name,
|
|
420
|
+
input: call.input ?? {},
|
|
421
|
+
});
|
|
422
|
+
gapAsks.push(appended);
|
|
423
|
+
yield appended;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const requests = [...scope.filter((e) => e.type === "permission_requested"), ...gapAsks];
|
|
248
427
|
for (const pending of requests) {
|
|
428
|
+
// R-E 0.1.43: the writes below carry the invocation's framework
|
|
429
|
+
// identity — the request's own, or the callId+proximity
|
|
430
|
+
// fallback for old logs (the compat contract).
|
|
431
|
+
const invocationSeq = pending.invocationSeq ?? callSeqOf(pending.callId, pending.seq);
|
|
432
|
+
// R-E 0.1.44 (sentence 3): a stored request whose invocation is
|
|
433
|
+
// VOIDED is expired — never re-presented, never executed (the
|
|
434
|
+
// dead-run expiry precedent above). The void ranges come from
|
|
435
|
+
// log.all — the marker THIS recovery appended is included. The
|
|
436
|
+
// receipt stays in the audit; only the presentation is gone.
|
|
437
|
+
// An identity-less request (an old log where neither the field nor
|
|
438
|
+
// the callId+proximity fallback yields a seq) is never proven
|
|
439
|
+
// voided — it keeps the pre-0.1.44 behavior.
|
|
440
|
+
const voided = invocationSeq !== undefined &&
|
|
441
|
+
log.all.some((e) => e.type === "model_output_abandoned" && invocationSeq > e.voidFromSeq && invocationSeq <= e.seq);
|
|
442
|
+
if (voided) {
|
|
443
|
+
yield log.append({
|
|
444
|
+
type: "permission_expired",
|
|
445
|
+
decisionId: pending.decisionId,
|
|
446
|
+
reason: "the invocation was abandoned with an incomplete draft — never re-presented, never executed",
|
|
447
|
+
});
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
249
450
|
const decided = log.all.find((e) => e.type === "permission_decided" && e.decisionId === pending.decisionId);
|
|
250
451
|
// round 4: paired by events NEWER than the request — a historical
|
|
251
452
|
// same-callId execution from an earlier run must not count as THIS
|
|
@@ -303,10 +504,10 @@ export class Run {
|
|
|
303
504
|
});
|
|
304
505
|
if (final.action === "allow") {
|
|
305
506
|
if (!hasExecution)
|
|
306
|
-
yield* this.#executePersisted(pending.callId, pending.name, pending.input, signal);
|
|
507
|
+
yield* this.#executePersisted(pending.callId, pending.name, pending.input, invocationSeq, signal);
|
|
307
508
|
}
|
|
308
509
|
else if (!hasResult) {
|
|
309
|
-
yield* this.#denialResult(pending.callId, final.reason ?? "denied by user");
|
|
510
|
+
yield* this.#denialResult(pending.callId, final.reason ?? "denied by user", invocationSeq);
|
|
310
511
|
}
|
|
311
512
|
}
|
|
312
513
|
else if (decided.decision === "approved") {
|
|
@@ -316,10 +517,10 @@ export class Run {
|
|
|
316
517
|
if (signal.aborted)
|
|
317
518
|
return;
|
|
318
519
|
if (!hasExecution)
|
|
319
|
-
yield* this.#executePersisted(pending.callId, pending.name, pending.input, signal);
|
|
520
|
+
yield* this.#executePersisted(pending.callId, pending.name, pending.input, invocationSeq, signal);
|
|
320
521
|
}
|
|
321
522
|
else if (!hasResult) {
|
|
322
|
-
yield* this.#denialResult(pending.callId, decided.reason ?? "denied by user");
|
|
523
|
+
yield* this.#denialResult(pending.callId, decided.reason ?? "denied by user", invocationSeq);
|
|
323
524
|
}
|
|
324
525
|
}
|
|
325
526
|
// Receipt repair: an execution that reached a terminal state but
|
|
@@ -385,7 +586,7 @@ export class Run {
|
|
|
385
586
|
* the permission hook (it was decided) and the model (it was never
|
|
386
587
|
* asked to re-issue). Full ledgered lifecycle.
|
|
387
588
|
*/
|
|
388
|
-
async *#executePersisted(callId, name, input, signal) {
|
|
589
|
+
async *#executePersisted(callId, name, input, invocationSeq, signal) {
|
|
389
590
|
const log = this.#session.log;
|
|
390
591
|
const tool = this.#config.registry.get(name);
|
|
391
592
|
const executionId = `ex-${log.lastSeq + 1}`;
|
|
@@ -393,7 +594,14 @@ export class Run {
|
|
|
393
594
|
// not start the side effect (finding 3).
|
|
394
595
|
if (signal.aborted)
|
|
395
596
|
return;
|
|
396
|
-
yield log.append({
|
|
597
|
+
yield log.append({
|
|
598
|
+
type: "tool_execution_started",
|
|
599
|
+
executionId,
|
|
600
|
+
callId,
|
|
601
|
+
...(invocationSeq !== undefined ? { invocationSeq } : {}),
|
|
602
|
+
name,
|
|
603
|
+
input,
|
|
604
|
+
});
|
|
397
605
|
let result;
|
|
398
606
|
if (tool === undefined) {
|
|
399
607
|
result = { content: `Unknown tool: ${name}`, isError: true, errorKind: "invalid_input" };
|
|
@@ -432,6 +640,7 @@ export class Run {
|
|
|
432
640
|
type: "tool_execution_failed",
|
|
433
641
|
executionId,
|
|
434
642
|
callId,
|
|
643
|
+
...(invocationSeq !== undefined ? { invocationSeq } : {}),
|
|
435
644
|
error: result.content,
|
|
436
645
|
// P1-9: errorKind only exists on errors — runtime-guarded too.
|
|
437
646
|
...(result.isError && result.errorKind !== undefined ? { errorKind: result.errorKind } : {}),
|
|
@@ -444,6 +653,7 @@ export class Run {
|
|
|
444
653
|
type: "tool_execution_succeeded",
|
|
445
654
|
executionId,
|
|
446
655
|
callId,
|
|
656
|
+
...(invocationSeq !== undefined ? { invocationSeq } : {}),
|
|
447
657
|
result: { content: result.content, isError: false },
|
|
448
658
|
...(result.tags !== undefined ? { tags: result.tags } : {}),
|
|
449
659
|
});
|
|
@@ -451,6 +661,7 @@ export class Run {
|
|
|
451
661
|
yield log.append({
|
|
452
662
|
type: "tool_result",
|
|
453
663
|
callId,
|
|
664
|
+
...(invocationSeq !== undefined ? { invocationSeq } : {}),
|
|
454
665
|
content: result.content,
|
|
455
666
|
isError: result.isError,
|
|
456
667
|
// P1-9: errorKind only exists on errors — runtime-guarded too.
|
|
@@ -465,11 +676,12 @@ export class Run {
|
|
|
465
676
|
// alone. A retry passes the approval chain again.
|
|
466
677
|
}
|
|
467
678
|
/** The model-facing result of a durable denial — no execution happened. */
|
|
468
|
-
async *#denialResult(callId, reason) {
|
|
679
|
+
async *#denialResult(callId, reason, invocationSeq) {
|
|
469
680
|
const denial = denialResult(reason);
|
|
470
681
|
yield this.#session.log.append({
|
|
471
682
|
type: "tool_result",
|
|
472
683
|
callId,
|
|
684
|
+
...(invocationSeq !== undefined ? { invocationSeq } : {}),
|
|
473
685
|
content: denial.content,
|
|
474
686
|
isError: true,
|
|
475
687
|
errorKind: denial.errorKind,
|
package/dist/session.d.ts
CHANGED
|
@@ -145,7 +145,7 @@ export declare class AgentSession {
|
|
|
145
145
|
*/
|
|
146
146
|
approve(decisionId: string, allow: boolean, reason?: string): Promise<void>;
|
|
147
147
|
/** Executions that started but never reported a result (crash window). */
|
|
148
|
-
uncertainExecutions(): import("
|
|
148
|
+
uncertainExecutions(): import("./ledger.js").ExecutionRecord[];
|
|
149
149
|
/**
|
|
150
150
|
* The human's verdict on an interrupted execution, keyed by EXECUTION ID
|
|
151
151
|
* (B group): "rerun" (the human says the side effect did NOT happen — the
|
package/dist/session.js
CHANGED
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
* support in recovery.ts, the E1/E2 composition helpers in compose.ts —
|
|
28
28
|
* same package, same exports (index.ts re-exports all four).
|
|
29
29
|
*/
|
|
30
|
-
import { EventLog,
|
|
30
|
+
import { EventLog, projectMessages, } from "@vincemakes/kiso-core";
|
|
31
|
+
import { executionLedger } from "./ledger.js";
|
|
31
32
|
import { denialResult } from "@vincemakes/kiso-core";
|
|
32
33
|
import { estimateSummarySavings, KEEP_RECENT_ROUNDS, lastSummaryPoint, summarizeConversation, summaryBoundarySeq, } from "./summarize.js";
|
|
33
34
|
import { estimateTokens } from "@vincemakes/kiso-core";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
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",
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
"test": "vitest run"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@vincemakes/kiso-core": "0.1.
|
|
24
|
+
"@vincemakes/kiso-core": "0.1.35"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
|
-
"@vincemakes/kiso-provider-anthropic": "0.1.
|
|
28
|
-
"@vincemakes/kiso-provider-openai": "0.1.
|
|
27
|
+
"@vincemakes/kiso-provider-anthropic": "0.1.36",
|
|
28
|
+
"@vincemakes/kiso-provider-openai": "0.1.36"
|
|
29
29
|
},
|
|
30
30
|
"peerDependenciesMeta": {
|
|
31
31
|
"@vincemakes/kiso-provider-anthropic": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@vincemakes/kiso-evals": "0.1.
|
|
39
|
+
"@vincemakes/kiso-evals": "0.1.36",
|
|
40
40
|
"@types/node": "^26.1.2",
|
|
41
41
|
"typescript": "^5.7.2",
|
|
42
42
|
"vitest": "^3.0.0"
|