@vincemakes/kiso-core 0.1.10 → 0.1.12

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.
@@ -17,8 +17,10 @@
17
17
  * Status derivation:
18
18
  * started, no terminal event yet → "uncertain" (interrupted: human)
19
19
  * succeeded → "succeeded" (confirmed, never re-run)
20
- * failed, safeToRetry (idempotent) → "failed" (clean failure)
21
- * failed, not safeToRetry → "uncertain" (side effects possible)
20
+ * failed (any) → "failed" (a complete receipt IS
21
+ * the outcome 裁决 #12 / ADR-0038;
22
+ * safeToRetry stays on the event for
23
+ * history, it no longer feeds status)
22
24
  * resolved "rerun" → "rerun" (human cleared it)
23
25
  * resolved "abandoned" → "abandoned" (human killed it)
24
26
  */
@@ -17,8 +17,10 @@
17
17
  * Status derivation:
18
18
  * started, no terminal event yet → "uncertain" (interrupted: human)
19
19
  * succeeded → "succeeded" (confirmed, never re-run)
20
- * failed, safeToRetry (idempotent) → "failed" (clean failure)
21
- * failed, not safeToRetry → "uncertain" (side effects possible)
20
+ * failed (any) → "failed" (a complete receipt IS
21
+ * the outcome 裁决 #12 / ADR-0038;
22
+ * safeToRetry stays on the event for
23
+ * history, it no longer feeds status)
22
24
  * resolved "rerun" → "rerun" (human cleared it)
23
25
  * resolved "abandoned" → "abandoned" (human killed it)
24
26
  */
@@ -48,10 +50,10 @@ export function executionLedger(events) {
48
50
  if (prior) {
49
51
  ledger.set(ev.executionId, {
50
52
  ...prior,
51
- // Area 3: only a tool that proved safe-to-retry gets a
52
- // clean "failed"; everything else may have produced a
53
- // side effect and is uncertain until a human decides.
54
- status: ev.safeToRetry ? "failed" : "uncertain",
53
+ // 裁决 #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",
55
57
  ...(ev.error !== undefined ? { error: ev.error } : {}),
56
58
  });
57
59
  }
@@ -361,14 +361,8 @@ export async function* loop(config) {
361
361
  yield await terminal({ kind: "aborted", by: "user" });
362
362
  return;
363
363
  }
364
- let currentExecutionId;
365
364
  try {
366
365
  for await (const ev of executeOne(call, registry, hooks, { signal: signal ?? NEVER_ABORT, ...(config.sessionId !== undefined ? { sessionId: config.sessionId } : {}) }, log, config.resolveApproval, config.approvalVerdict, signal, config.approvalPolicies)) {
367
- // 四: the identity of THIS execution comes from the stream —
368
- // a historical same-callId execution must never be mistaken
369
- // for this call's (the provider callId may repeat across runs).
370
- if (ev.type === "tool_execution_started")
371
- currentExecutionId = ev.executionId;
372
366
  if (hooks.onEvent)
373
367
  await hooks.onEvent(ev, {}).catch(() => { });
374
368
  yield ev;
@@ -383,87 +377,14 @@ export async function* loop(config) {
383
377
  }
384
378
  throw err;
385
379
  }
386
- // C 组: a failed NON-idempotent execution is a persistent
387
- // uncertain PAUSE no sibling tool, no auto-retry, and the next
388
- // model turn waits for the human verdict. 四: the failed event is
389
- // found by THIS execution's id never by the repeatable callId,
390
- // which would let a historical same-callId failure pollute a fresh
391
- // successful execution with a stale uncertain pause.
392
- const failed = currentExecutionId === undefined
393
- ? undefined
394
- : [...log.all]
395
- .reverse()
396
- .find((e) => e.type === "tool_execution_failed" && e.executionId === currentExecutionId);
397
- if (failed !== undefined && !failed.safeToRetry) {
398
- // Register the human channel BEFORE announcing the pause —
399
- // a consumer that answers the moment it sees the event must
400
- // find the resolver already waiting (no deadlock between
401
- // yield and await, mirroring the approval pause).
402
- const pendingResolution = config.resolveUncertainty !== undefined ? config.resolveUncertainty(failed.executionId) : undefined;
403
- const pendingUncertain = log.append({
404
- type: "uncertain_pending",
405
- executionId: failed.executionId,
406
- callId: call.callId,
407
- name: call.name,
408
- error: failed.error,
409
- });
410
- if (hooks.onEvent)
411
- await hooks.onEvent(pendingUncertain, {}).catch(() => { });
412
- yield pendingUncertain;
413
- let resolution;
414
- if (pendingResolution !== undefined) {
415
- try {
416
- resolution = await raceAbort(pendingResolution, signal);
417
- }
418
- catch (err) {
419
- if (err === ABORTED) {
420
- // 第四轮(对抗): the human may have answered in the
421
- // same instant the abort landed — a CONSUMED verdict
422
- // must be recorded (exactly once), never lost. It is
423
- // appended here, then the run ends with its honest
424
- // aborted terminal; the execution is resolved, not
425
- // bricked.
426
- const verdict = config.uncertaintyVerdict?.(failed.executionId);
427
- if (verdict !== undefined) {
428
- const verdictEvent = log.append({
429
- type: "tool_execution_resolved",
430
- executionId: failed.executionId,
431
- callId: call.callId,
432
- resolution: verdict,
433
- });
434
- if (hooks.onEvent)
435
- await hooks.onEvent(verdictEvent, {}).catch(() => { });
436
- yield verdictEvent;
437
- }
438
- yield await terminal({ kind: "aborted", by: "user" });
439
- return;
440
- }
441
- throw err;
442
- }
443
- }
444
- else {
445
- // No channel: record the conservative verdict — the
446
- // failure is NEVER auto-retried, and the ledger stays
447
- // consistent for future resumes.
448
- resolution = "abandoned";
449
- }
450
- // 七: the LOOP owns the resolution event — appended and
451
- // yielded on EVERY verdict path (channel or not), so the Run
452
- // persists it and the consumer's stream has no hidden gap.
453
- // A live resolveUncertain() only passed the verdict; the
454
- // event itself is created here, exactly once.
455
- const resolvedEvent = log.append({
456
- type: "tool_execution_resolved",
457
- executionId: failed.executionId,
458
- callId: call.callId,
459
- resolution,
460
- });
461
- if (hooks.onEvent)
462
- await hooks.onEvent(resolvedEvent, {}).catch(() => { });
463
- yield resolvedEvent;
464
- // Either verdict ends the pending list: siblings never run.
465
- break;
466
- }
380
+ // 裁决 #12 (ADR-0038): the failed-receipt uncertain PAUSE is
381
+ // REMOVEDwith a complete receipt (succeeded or failed) the
382
+ // outcome is KNOWN, and uncertainty belongs to the crash window
383
+ // alone (started, no receipt; surfaced through the ledger's
384
+ // uncertainExecutions and resolved offline). A retry is a NEW
385
+ // call it passes the approval chain again, which is the correct
386
+ // guard for partial side effects; the honest note on
387
+ // non-idempotent failures rides the result (修正一).
467
388
  }
468
389
  // ── Advance history: the log grew; re-derive for the next turn ─────
469
390
  messages = derive();
@@ -794,6 +715,18 @@ async function* executeOne(call, registry, hooks, ctx, log, resolveApproval, res
794
715
  if (hooks.onPostTool) {
795
716
  result = await hooks.onPostTool(payload, result, ctx);
796
717
  }
718
+ // 裁决 #12 修正一: a non-idempotent failure's side effects may have
719
+ // partially applied — an honest note rides the RESULT (and the failed
720
+ // receipt below, losslessly — a crash-window repair of the tool_result
721
+ // reproduces the normal path). Idempotent failures carry no note; the
722
+ // MCP bridge maps tools without declaring idempotency, so unknown
723
+ // idempotency = the note applies (honest).
724
+ if (result.isError && tool.idempotent !== true) {
725
+ result = {
726
+ ...result,
727
+ content: `${result.content}\n[non-idempotent tool failed — its side effects may have partially applied; verify before retrying]`,
728
+ };
729
+ }
797
730
  if (result.isError) {
798
731
  // Area 3: only a tool that PROVED safe-to-retry (idempotent) gets a
799
732
  // clean failure; a non-idempotent failure may have produced a side
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-core",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "kiso(基礎) core — protocol, event log, loop, hooks, modes, permissions, compaction, delivery truth. The 2,000-line kernel at the bottom of the kiso framework.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  "openai"
34
34
  ],
35
35
  "devDependencies": {
36
- "@vincemakes/kiso-evals": "0.1.10",
36
+ "@vincemakes/kiso-evals": "0.1.12",
37
37
  "@types/node": "^26.1.2",
38
38
  "typescript": "^5.7.2",
39
39
  "vitest": "^3.0.0"