@vincemakes/kiso-core 0.1.31 → 0.1.32
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/kernel/loop.d.ts +11 -12
- package/dist/kernel/loop.js +31 -49
- package/dist/protocol/events.d.ts +6 -3
- package/dist/protocol/events.js +4 -3
- package/dist/protocol/extension.d.ts +23 -0
- package/dist/protocol/messages.d.ts +2 -2
- package/dist/tools/registry.d.ts +3 -3
- package/dist/tools/registry.js +3 -3
- package/dist/tools/tool.d.ts +2 -2
- package/package.json +2 -2
package/dist/kernel/loop.d.ts
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
*/
|
|
35
35
|
import { type Adapter, type AbortSignalLike } from "../protocol/adapter.js";
|
|
36
36
|
import type { Event, StructuredError } from "../protocol/events.js";
|
|
37
|
-
import type {
|
|
37
|
+
import type { ApprovalChain } from "../protocol/extension.js";
|
|
38
38
|
import { EventLog } from "./event-log.js";
|
|
39
39
|
import type { EventInput } from "./event-log.js";
|
|
40
40
|
import type { AssistantBlock, AssistantMessage, Message, ToolResultMessage } from "../protocol/messages.js";
|
|
@@ -109,18 +109,17 @@ export interface LoopConfig {
|
|
|
109
109
|
/** round 4 (adversarial): the uncertainty twin of `approvalVerdict`. */
|
|
110
110
|
readonly uncertaintyVerdict?: (executionId: string) => "rerun" | "abandoned" | undefined;
|
|
111
111
|
/**
|
|
112
|
-
* E1:
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
112
|
+
* E1: the COMPOSED approval chain — the runtime composes the
|
|
113
|
+
* extensions' policies (deny > allow > ask, the R3 ruling) into ONE
|
|
114
|
+
* decide; the kernel's gate calls it BEFORE the human flow. Allow/deny
|
|
115
|
+
* are recorded durably with decidedBy = the deciding extension, never
|
|
116
|
+
* pausing for a human; an ask falls into the human flow (its speaker
|
|
117
|
+
* names the first non-abstain — the panel's why-asked line). A
|
|
118
|
+
* throwing chain counts as ask. Absent, no chain runs. A durable
|
|
119
|
+
* decision already recorded (resume) takes effect and the chain never
|
|
120
|
+
* re-runs.
|
|
119
121
|
*/
|
|
120
|
-
readonly
|
|
121
|
-
readonly extension: string;
|
|
122
|
-
readonly policy: ApprovalPolicy;
|
|
123
|
-
}[];
|
|
122
|
+
readonly approvalPolicy?: ApprovalChain;
|
|
124
123
|
/** P3: the session's id — carried to tools via ToolContext.sessionId. */
|
|
125
124
|
readonly sessionId?: string;
|
|
126
125
|
}
|
package/dist/kernel/loop.js
CHANGED
|
@@ -225,7 +225,7 @@ export async function* loop(config) {
|
|
|
225
225
|
try {
|
|
226
226
|
await acquireWindow();
|
|
227
227
|
decideChain = decideChain.then(async () => {
|
|
228
|
-
const v = await decideCall(call, registry, hooks, { signal: signal ?? NEVER_ABORT, ...(config.sessionId !== undefined ? { sessionId: config.sessionId } : {}) }, log, config.resolveApproval, config.approvalVerdict, signal, config.
|
|
228
|
+
const v = await decideCall(call, registry, hooks, { signal: signal ?? NEVER_ABORT, ...(config.sessionId !== undefined ? { sessionId: config.sessionId } : {}) }, log, config.resolveApproval, config.approvalVerdict, signal, config.approvalPolicy, nextDecisionId, pushExec);
|
|
229
229
|
if (v.action === "ask") {
|
|
230
230
|
askGate = new Promise((res) => {
|
|
231
231
|
askRelease = res;
|
|
@@ -247,7 +247,7 @@ export async function* loop(config) {
|
|
|
247
247
|
// ordering: the successors then proceed with their own
|
|
248
248
|
// verdicts).
|
|
249
249
|
const decision = await Promise.race([
|
|
250
|
-
humanPause(call, verdict.decisionId, hooks, log, config.resolveApproval, config.approvalVerdict, signal, pushExec),
|
|
250
|
+
humanPause(call, verdict.decisionId, hooks, log, config.resolveApproval, config.approvalVerdict, signal, pushExec, verdict.speaker),
|
|
251
251
|
violatedP,
|
|
252
252
|
]);
|
|
253
253
|
askRelease?.();
|
|
@@ -604,7 +604,7 @@ function resultEvent(call, result, executionId) {
|
|
|
604
604
|
* caller runs it (conservative ordering: the calls after an ask wait for
|
|
605
605
|
* its resolution).
|
|
606
606
|
*/
|
|
607
|
-
async function decideCall(call, registry, hooks, ctx, log, resolveApproval, resolveApprovalVerdict, signal,
|
|
607
|
+
async function decideCall(call, registry, hooks, ctx, log, resolveApproval, resolveApprovalVerdict, signal, approvalPolicy, nextDecisionId, push) {
|
|
608
608
|
const payload = {
|
|
609
609
|
callId: call.callId,
|
|
610
610
|
name: call.name,
|
|
@@ -632,59 +632,30 @@ async function decideCall(call, registry, hooks, ctx, log, resolveApproval, reso
|
|
|
632
632
|
// all. Checked again here, after any permission path.
|
|
633
633
|
if (signal?.aborted)
|
|
634
634
|
throw ABORTED;
|
|
635
|
-
// ── E1: the
|
|
635
|
+
// ── E1: the composed approval chain, decided BEFORE the human flow ────
|
|
636
636
|
// A durable POLICY decision for THIS call takes effect on resume — the
|
|
637
637
|
// chain never re-runs when its verdict is already in the log (isomorphic
|
|
638
638
|
// alreadyReplaced: the persisted fact speaks for the call). The match is
|
|
639
639
|
// the same logical call: same callId, decidedBy set (a policy verdict,
|
|
640
640
|
// never a human's), and input identical to the original tool_call_end —
|
|
641
641
|
// a re-issued call with different arguments is a NEW call and re-decided.
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
//
|
|
642
|
+
// The chain itself is COMPOSED by the runtime (composeApprovalChain —
|
|
643
|
+
// the R3 ruling: deny > allow > ask, any deny wins, a LATER allow beats
|
|
644
|
+
// an EARLIER ask, an all-abstain asks per ADR-0042); the kernel consumes
|
|
645
|
+
// its verdict. A throwing chain counts as ask. Allow/deny are recorded
|
|
646
|
+
// durably with decidedBy = the deciding extension, never pausing for a
|
|
647
|
+
// human.
|
|
647
648
|
const originalCall = [...log.all].reverse().find((e) => e.type === "tool_call_end" && e.callId === call.callId);
|
|
648
649
|
const durable = originalCall !== undefined && JSON.stringify(originalCall.input) === JSON.stringify(call.input)
|
|
649
650
|
? log.all.find((e) => e.type === "permission_decided" && e.decidedBy !== undefined && e.callId === call.callId)
|
|
650
651
|
: undefined;
|
|
651
652
|
let chainVerdict;
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
let anySpoke = false;
|
|
656
|
-
if (durable === undefined && approvalPolicies !== undefined && approvalPolicies.length > 0) {
|
|
657
|
-
for (const { extension, policy } of approvalPolicies) {
|
|
658
|
-
let v;
|
|
659
|
-
try {
|
|
660
|
-
v = await raceAbort(Promise.resolve(policy.decide(payload, ctx)), signal);
|
|
661
|
-
}
|
|
662
|
-
catch {
|
|
663
|
-
v = { action: "ask" }; // a throwing policy counts as ask — it speaks, never silently
|
|
664
|
-
}
|
|
665
|
-
if (v.action === "abstain")
|
|
666
|
-
continue; // no opinion — not a verdict
|
|
667
|
-
anySpoke = true;
|
|
668
|
-
firstSpeaker ??= extension;
|
|
669
|
-
if (v.action === "deny") {
|
|
670
|
-
deniedBy ??= extension;
|
|
671
|
-
deniedReason ??= v.reason; // the FIRST denial's reason
|
|
672
|
-
}
|
|
673
|
-
else if (v.action === "ask") {
|
|
674
|
-
chainVerdict = { action: "ask" };
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
if (deniedBy !== undefined) {
|
|
678
|
-
chainVerdict = { action: "deny", reason: deniedReason ?? "denied" };
|
|
679
|
-
}
|
|
680
|
-
else if (chainVerdict === undefined && anySpoke) {
|
|
681
|
-
chainVerdict = { action: "allow" }; // every speaker allows
|
|
653
|
+
if (durable === undefined && approvalPolicy !== undefined) {
|
|
654
|
+
try {
|
|
655
|
+
chainVerdict = await raceAbort(Promise.resolve(approvalPolicy.decide(payload, ctx)), signal);
|
|
682
656
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
// the ask flow below, never to a silent auto-approve. The human
|
|
686
|
-
// decides; absent a channel, humanPause's honest denial.
|
|
687
|
-
chainVerdict = { action: "ask" };
|
|
657
|
+
catch {
|
|
658
|
+
chainVerdict = { action: "ask" }; // a throwing chain counts as ask — it speaks, never silently
|
|
688
659
|
}
|
|
689
660
|
if (chainVerdict.action !== "ask") {
|
|
690
661
|
// allow/deny are PERSISTED FACTS (decidedBy = a SPEAKING
|
|
@@ -695,13 +666,15 @@ async function decideCall(call, registry, hooks, ctx, log, resolveApproval, reso
|
|
|
695
666
|
decisionId: nextDecisionId(),
|
|
696
667
|
callId: call.callId,
|
|
697
668
|
decision: chainVerdict.action === "allow" ? "approved" : "denied",
|
|
698
|
-
...(chainVerdict.action === "deny"
|
|
699
|
-
|
|
669
|
+
...(chainVerdict.action === "deny" && chainVerdict.reason !== undefined
|
|
670
|
+
? { reason: chainVerdict.reason }
|
|
671
|
+
: {}),
|
|
672
|
+
decidedBy: chainVerdict.decidedBy,
|
|
700
673
|
});
|
|
701
674
|
}
|
|
702
675
|
}
|
|
703
676
|
if (chainVerdict?.action === "deny") {
|
|
704
|
-
return { action: "deny", result: resultEvent(call, denialResult(chainVerdict.reason)) };
|
|
677
|
+
return { action: "deny", result: resultEvent(call, denialResult(chainVerdict.reason ?? "denied")) };
|
|
705
678
|
}
|
|
706
679
|
if (durable !== undefined && durable.decision === "denied") {
|
|
707
680
|
return { action: "deny", result: resultEvent(call, denialResult(durable.reason ?? "denied")) };
|
|
@@ -716,7 +689,13 @@ async function decideCall(call, registry, hooks, ctx, log, resolveApproval, reso
|
|
|
716
689
|
if (resolveApproval === undefined) {
|
|
717
690
|
return { action: "deny", result: resultEvent(call, denialResult("a policy asked for a human decision, but no approval flow is configured")) };
|
|
718
691
|
}
|
|
719
|
-
|
|
692
|
+
// W21: the speaker rides the ask verdict — the panel's why-asked
|
|
693
|
+
// line names the extension that asked (the composed chain's first
|
|
694
|
+
// non-abstain; exactOptionalPropertyTypes: omitted, never
|
|
695
|
+
// `undefined`).
|
|
696
|
+
return chainVerdict.speaker === undefined
|
|
697
|
+
? { action: "ask", decisionId: nextDecisionId() }
|
|
698
|
+
: { action: "ask", decisionId: nextDecisionId(), speaker: chainVerdict.speaker };
|
|
720
699
|
}
|
|
721
700
|
// Permission negotiation — defer is a REAL pause (Phase D). C group: the
|
|
722
701
|
// hook itself is cancelable (a slow policy query must not outlive an
|
|
@@ -746,7 +725,7 @@ async function decideCall(call, registry, hooks, ctx, log, resolveApproval, reso
|
|
|
746
725
|
* verdict given in the same instant is still recorded exactly once) —
|
|
747
726
|
* then persist the decision. Returns "approved" | "denied".
|
|
748
727
|
*/
|
|
749
|
-
async function humanPause(call, decisionId, hooks, log, resolveApproval, resolveApprovalVerdict, signal, push) {
|
|
728
|
+
async function humanPause(call, decisionId, hooks, log, resolveApproval, resolveApprovalVerdict, signal, push, speaker) {
|
|
750
729
|
const pendingDecision = resolveApproval !== undefined
|
|
751
730
|
? resolveApproval(decisionId)
|
|
752
731
|
: Promise.resolve({ action: "deny", reason: "no approval channel configured" });
|
|
@@ -756,6 +735,9 @@ async function humanPause(call, decisionId, hooks, log, resolveApproval, resolve
|
|
|
756
735
|
callId: call.callId,
|
|
757
736
|
name: call.name,
|
|
758
737
|
input: call.input ?? {},
|
|
738
|
+
// W21: the ask verdict's speaker — the panel's why-asked line (a
|
|
739
|
+
// static-hook ask has none).
|
|
740
|
+
...(speaker !== undefined ? { speaker } : {}),
|
|
759
741
|
});
|
|
760
742
|
if (hooks.onPause)
|
|
761
743
|
await hooks.onPause("awaiting approval", {}).catch(() => { });
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* assigned by the kernel's EventLog at append time. Consumers (surfaces,
|
|
17
17
|
* persistence, eval) sync by `seq`; a trajectory is the complete replay of
|
|
18
18
|
* `seq` 0..N. Without `seq`, "what happened" can only be reconstructed by
|
|
19
|
-
* array-shape heuristics — the exact failure
|
|
20
|
-
* lives in. See ADR-0002.
|
|
19
|
+
* array-shape heuristics — the exact failure the reference implementation's
|
|
20
|
+
* transcript sync lives in. See ADR-0002.
|
|
21
21
|
*
|
|
22
22
|
* This module is almost types-only: the only runtime value it emits is
|
|
23
23
|
* `isKisoEvent`, the type guard the session store validates records with.
|
|
@@ -258,6 +258,9 @@ export interface PermissionRequested {
|
|
|
258
258
|
readonly callId: string;
|
|
259
259
|
readonly name: string;
|
|
260
260
|
readonly input: Readonly<Record<string, unknown>>;
|
|
261
|
+
/** W21: the first non-abstain extension's name — the panel's why-asked
|
|
262
|
+
* line. Absent on static-hook asks and old logs. */
|
|
263
|
+
readonly speaker?: string;
|
|
261
264
|
}
|
|
262
265
|
/** The durable answer to a PermissionRequested. */
|
|
263
266
|
export interface PermissionDecided {
|
|
@@ -400,7 +403,7 @@ export interface StructuredError {
|
|
|
400
403
|
*
|
|
401
404
|
* Every consumer switches on `kind`; with `exactOptionalPropertyTypes` and
|
|
402
405
|
* `strictNullChecks` on, a terminal that nobody handles is a compile error,
|
|
403
|
-
* not a production mystery.
|
|
406
|
+
* not a production mystery. The reference implementation's query() returns 11 different reasons that
|
|
404
407
|
* every consumer discards — here the terminal is an event like any other,
|
|
405
408
|
* so it cannot be lost. See ADR-0004.
|
|
406
409
|
*
|
package/dist/protocol/events.js
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* assigned by the kernel's EventLog at append time. Consumers (surfaces,
|
|
17
17
|
* persistence, eval) sync by `seq`; a trajectory is the complete replay of
|
|
18
18
|
* `seq` 0..N. Without `seq`, "what happened" can only be reconstructed by
|
|
19
|
-
* array-shape heuristics — the exact failure
|
|
20
|
-
* lives in. See ADR-0002.
|
|
19
|
+
* array-shape heuristics — the exact failure the reference implementation's
|
|
20
|
+
* transcript sync lives in. See ADR-0002.
|
|
21
21
|
*
|
|
22
22
|
* This module is almost types-only: the only runtime value it emits is
|
|
23
23
|
* `isKisoEvent`, the type guard the session store validates records with.
|
|
@@ -205,7 +205,8 @@ const EVENT_VALIDATORS = {
|
|
|
205
205
|
permission_requested: (v) => typeof v.decisionId === "string" &&
|
|
206
206
|
typeof v.callId === "string" &&
|
|
207
207
|
typeof v.name === "string" &&
|
|
208
|
-
isPlainObject(v.input)
|
|
208
|
+
isPlainObject(v.input) &&
|
|
209
|
+
(v.speaker === undefined || typeof v.speaker === "string"),
|
|
209
210
|
permission_decided: (v) => typeof v.decisionId === "string" &&
|
|
210
211
|
(v.decision === "approved" || v.decision === "denied") &&
|
|
211
212
|
(v.callId === undefined || typeof v.callId === "string") &&
|
|
@@ -38,6 +38,29 @@ export type PolicyVerdict = {
|
|
|
38
38
|
export interface ApprovalPolicy {
|
|
39
39
|
readonly decide: (call: PolicyCall, ctx: ToolContext) => PolicyVerdict | Promise<PolicyVerdict>;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The COMPOSED chain's verdict — what the kernel's gate consumes. The
|
|
43
|
+
* runtime composes the extensions' policies (deny > allow > ask, the R3
|
|
44
|
+
* ruling); allow/deny always carry decidedBy — the deciding extension —
|
|
45
|
+
* and an ask may carry the speaker (the first non-abstain — the panel's
|
|
46
|
+
* why-asked line). The attribution is durable audit (rides
|
|
47
|
+
* permission_decided, never a human pause).
|
|
48
|
+
*/
|
|
49
|
+
export type ChainVerdict = {
|
|
50
|
+
readonly action: "deny";
|
|
51
|
+
readonly reason?: string;
|
|
52
|
+
readonly decidedBy: string;
|
|
53
|
+
} | {
|
|
54
|
+
readonly action: "ask";
|
|
55
|
+
readonly speaker?: string;
|
|
56
|
+
} | {
|
|
57
|
+
readonly action: "allow";
|
|
58
|
+
readonly decidedBy: string;
|
|
59
|
+
};
|
|
60
|
+
/** The approval chain as the kernel sees it — ONE composed policy. */
|
|
61
|
+
export interface ApprovalChain {
|
|
62
|
+
readonly decide: (call: PolicyCall, ctx: ToolContext) => ChainVerdict | Promise<ChainVerdict>;
|
|
63
|
+
}
|
|
41
64
|
/**
|
|
42
65
|
* A loaded extension. `name` is unique per installation (the loader rejects
|
|
43
66
|
* duplicates loudly); hooks/tools/approvals are all optional.
|
|
@@ -45,8 +45,8 @@ export type ContentBlock = TextContentBlock | ImageContentBlock;
|
|
|
45
45
|
*
|
|
46
46
|
* The model treats this as evidence about intent; a "user" line that the UI
|
|
47
47
|
* recycled from a suggestion chip is NOT user intent, and a model that cannot
|
|
48
|
-
* tell the difference drifts on its own recycled wording (
|
|
49
|
-
* #60087). The kernel preserves the label; product code decides how to render
|
|
48
|
+
* tell the difference drifts on its own recycled wording (the reference
|
|
49
|
+
* implementation's #60087). The kernel preserves the label; product code decides how to render
|
|
50
50
|
* it. Defaults (when absent): role `user` → "user", role `assistant` →
|
|
51
51
|
* "model", role `tool` → "tool_result".
|
|
52
52
|
*/
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* One registry per agent. It is the ONLY place the kernel learns which tools
|
|
5
5
|
* exist: nothing is assembled from a list maintained elsewhere, because a
|
|
6
|
-
* second list is a second truth (the failure class behind
|
|
7
|
-
* hand-maintained agent-tool sets and
|
|
8
|
-
* list — see ADR-0001).
|
|
6
|
+
* second list is a second truth (the failure class behind the reference
|
|
7
|
+
* implementation's hand-maintained agent-tool sets and its six copies of the
|
|
8
|
+
* default tool list — see ADR-0001).
|
|
9
9
|
*
|
|
10
10
|
* `subset()` is the structural tool filter: a mode or a subagent gets a
|
|
11
11
|
* registry whose tool table PHYSICALLY lacks the tools it must not see. The
|
package/dist/tools/registry.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* One registry per agent. It is the ONLY place the kernel learns which tools
|
|
5
5
|
* exist: nothing is assembled from a list maintained elsewhere, because a
|
|
6
|
-
* second list is a second truth (the failure class behind
|
|
7
|
-
* hand-maintained agent-tool sets and
|
|
8
|
-
* list — see ADR-0001).
|
|
6
|
+
* second list is a second truth (the failure class behind the reference
|
|
7
|
+
* implementation's hand-maintained agent-tool sets and its six copies of the
|
|
8
|
+
* default tool list — see ADR-0001).
|
|
9
9
|
*
|
|
10
10
|
* `subset()` is the structural tool filter: a mode or a subagent gets a
|
|
11
11
|
* registry whose tool table PHYSICALLY lacks the tools it must not see. The
|
package/dist/tools/tool.d.ts
CHANGED
|
@@ -49,8 +49,8 @@ export interface Tool<I = unknown> {
|
|
|
49
49
|
/** JSON Schema (draft-07 subset). Validated before execute. */
|
|
50
50
|
readonly parameters: Readonly<Record<string, unknown>>;
|
|
51
51
|
/**
|
|
52
|
-
* Per-call concurrency predicate —
|
|
53
|
-
* executionMode cannot express: the same tool may be parallel-safe for one
|
|
52
|
+
* Per-call concurrency predicate — a shape the reference implementation's
|
|
53
|
+
* static executionMode cannot express: the same tool may be parallel-safe for one
|
|
54
54
|
* input and must be serial for another (generate_image with
|
|
55
55
|
* `chain_to_previous`). Absent = safe when true-ish; see ADR-0015.
|
|
56
56
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.32",
|
|
4
4
|
"description": "kiso (foundation) 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.
|
|
36
|
+
"@vincemakes/kiso-evals": "0.1.33",
|
|
37
37
|
"@types/node": "^26.1.2",
|
|
38
38
|
"typescript": "^5.7.2",
|
|
39
39
|
"vitest": "^3.0.0"
|