@vincemakes/kiso-core 0.42.1 → 0.42.2

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.
Files changed (2) hide show
  1. package/dist/kernel/loop.js +33 -46
  2. package/package.json +2 -2
@@ -112,6 +112,15 @@ export async function* loop(config) {
112
112
  return full;
113
113
  };
114
114
  const aborted = () => signal?.aborted === true;
115
+ /** append → observe → yield, for a producer with NOTHING between the
116
+ * three (never the execution queue's ack, the stream's launch, or a
117
+ * producer without an observer). */
118
+ async function* emit(e) {
119
+ const full = log.append(e);
120
+ if (hooks.onEvent)
121
+ await hooks.onEvent(full, {}).catch(() => { });
122
+ yield full;
123
+ }
115
124
  // Assemble: the incoming user message may be rewritten or vetoed.
116
125
  // C group: the outcome is PERSISTED as a user_input_replaced event — the
117
126
  // projection renders the final replacement AT THE INPUT'S POSITION (or
@@ -502,12 +511,8 @@ export async function* loop(config) {
502
511
  // ── ADR-0055 (A1b): the compaction point, before every request ──
503
512
  if (config.compact !== undefined) {
504
513
  const added = await config.compact(log.all, messages, overflow === null ? "request" : "overflow");
505
- for (const e of added) {
506
- const full = log.append(e);
507
- if (hooks.onEvent)
508
- await hooks.onEvent(full, {}).catch(() => { });
509
- yield full;
510
- }
514
+ for (const e of added)
515
+ yield* emit(e);
511
516
  if (overflow !== null && added.length === 0) {
512
517
  yield await terminal({ kind: "error", error: overflow });
513
518
  return;
@@ -573,12 +578,8 @@ export async function* loop(config) {
573
578
  yield* drainSettled();
574
579
  if (launchError !== null)
575
580
  throw launchError;
576
- if (log.lastSeq > turnStart && !unsafeStartedInDraft()) {
577
- const marker = log.append({ type: "model_output_abandoned", voidFromSeq: turnStart, reason });
578
- if (hooks.onEvent)
579
- await hooks.onEvent(marker, {}).catch(() => { });
580
- yield marker;
581
- }
581
+ if (log.lastSeq > turnStart && !unsafeStartedInDraft())
582
+ yield* emit({ type: "model_output_abandoned", voidFromSeq: turnStart, reason });
582
583
  };
583
584
  while (true) {
584
585
  // Area 4: the backoff is abortable — a cancel landing during a
@@ -867,11 +868,8 @@ export async function* loop(config) {
867
868
  return;
868
869
  }
869
870
  if (voided === null && heldStop !== null) {
870
- const full = log.append(heldStop);
871
- if (hooks.onEvent)
872
- await hooks.onEvent(full, {}).catch(() => { });
873
- committed = true;
874
- yield full;
871
+ committed = true; // a local flag; set before the emit, read only after it (the observer cannot throw)
872
+ yield* emit(heldStop);
875
873
  }
876
874
  settleTurn();
877
875
  // A turn with no tool calls ends on its OWN stop reason (Phase B,
@@ -969,6 +967,20 @@ function terminalForStop(reason) {
969
967
  };
970
968
  }
971
969
  }
970
+ /** The permission_decided event for an invocation — the fields in the
971
+ * order the log has always written them. The CALLER decides: approved or
972
+ * not, the reason, the speaker; this builds, never classifies. */
973
+ function decidedEvent(call, decisionId, approved, reason, decidedBy) {
974
+ return {
975
+ type: "permission_decided",
976
+ decisionId,
977
+ callId: call.callId,
978
+ invocationSeq: call.seq,
979
+ decision: approved ? "approved" : "denied",
980
+ ...(reason !== undefined ? { reason } : {}),
981
+ ...(decidedBy !== undefined ? { decidedBy } : {}),
982
+ };
983
+ }
972
984
  /** The tool_result event for a call — the shared shape (executionId rides
973
985
  * it as the durable correlation, round 5; invocationSeq = the framework
974
986
  * identity, R-E 0.1.43). */
@@ -1101,17 +1113,7 @@ async function decideCall(call, registry, hooks, ctx, log, resolveApproval, reso
1101
1113
  // allow/deny are PERSISTED FACTS (decidedBy = a SPEAKING
1102
1114
  // extension — never the chain head on behalf of a non-speaker)
1103
1115
  // — never a human pause.
1104
- push({
1105
- type: "permission_decided",
1106
- decisionId: nextDecisionId(),
1107
- callId: call.callId,
1108
- invocationSeq: call.seq,
1109
- decision: chainVerdict.action === "allow" ? "approved" : "denied",
1110
- ...(chainVerdict.action === "deny" && chainVerdict.reason !== undefined
1111
- ? { reason: chainVerdict.reason }
1112
- : {}),
1113
- decidedBy: chainVerdict.decidedBy,
1114
- });
1116
+ push(decidedEvent(call, nextDecisionId(), chainVerdict.action === "allow", chainVerdict.action === "deny" ? chainVerdict.reason : undefined, chainVerdict.decidedBy));
1115
1117
  }
1116
1118
  }
1117
1119
  if (chainVerdict?.action === "deny") {
@@ -1197,14 +1199,7 @@ async function humanPause(call, decisionId, hooks, log, resolveApproval, resolve
1197
1199
  // its honest aborted terminal.
1198
1200
  const verdict = resolveApprovalVerdict?.(decisionId);
1199
1201
  if (verdict !== undefined) {
1200
- push({
1201
- type: "permission_decided",
1202
- decisionId,
1203
- callId: call.callId,
1204
- invocationSeq: call.seq,
1205
- decision: verdict ? "approved" : "denied",
1206
- ...(verdict ? {} : { reason: "denied by user" }),
1207
- });
1202
+ push(decidedEvent(call, decisionId, verdict, verdict ? undefined : "denied by user"));
1208
1203
  }
1209
1204
  }
1210
1205
  throw err;
@@ -1213,16 +1208,8 @@ async function humanPause(call, decisionId, hooks, log, resolveApproval, resolve
1213
1208
  // write-ahead BEFORE waking the resolver (Area 2): if it already
1214
1209
  // landed in the log, this is the same decision, not a duplicate.
1215
1210
  if (log.all.find((e) => e.type === "permission_decided" && e.decisionId === decisionId) === undefined) {
1216
- push({
1217
- type: "permission_decided",
1218
- decisionId,
1219
- callId: call.callId, // binds the decision to the invocation (B group)
1220
- invocationSeq: call.seq,
1221
- decision: finalDecision.action === "allow" ? "approved" : "denied",
1222
- ...(finalDecision.action === "deny" && finalDecision.reason !== undefined
1223
- ? { reason: finalDecision.reason }
1224
- : {}),
1225
- });
1211
+ // callId binds the decision to the invocation (B group)
1212
+ push(decidedEvent(call, decisionId, finalDecision.action === "allow", finalDecision.action === "deny" ? finalDecision.reason : undefined));
1226
1213
  }
1227
1214
  return finalDecision;
1228
1215
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-core",
3
- "version": "0.42.1",
3
+ "version": "0.42.2",
4
4
  "description": "kiso core \u2014 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.42.1",
36
+ "@vincemakes/kiso-evals": "0.42.2",
37
37
  "@types/node": "^26.1.2",
38
38
  "typescript": "^5.7.2",
39
39
  "vitest": "^3.0.0"