@vincemakes/kiso-core 0.1.6 → 0.1.8
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 +2 -0
- package/dist/kernel/loop.js +88 -64
- package/dist/protocol/extension.d.ts +6 -0
- package/package.json +2 -2
package/dist/kernel/loop.d.ts
CHANGED
|
@@ -110,6 +110,8 @@ export interface LoopConfig {
|
|
|
110
110
|
readonly extension: string;
|
|
111
111
|
readonly policy: ApprovalPolicy;
|
|
112
112
|
}[];
|
|
113
|
+
/** P3: the session's id — carried to tools via ToolContext.sessionId. */
|
|
114
|
+
readonly sessionId?: string;
|
|
113
115
|
}
|
|
114
116
|
export declare const DEFAULT_MAX_TURNS = 10;
|
|
115
117
|
export declare const DEFAULT_MAX_RETRIES = 2;
|
package/dist/kernel/loop.js
CHANGED
|
@@ -363,7 +363,7 @@ export async function* loop(config) {
|
|
|
363
363
|
}
|
|
364
364
|
let currentExecutionId;
|
|
365
365
|
try {
|
|
366
|
-
for await (const ev of executeOne(call, registry, hooks, { signal: signal ?? NEVER_ABORT }, log, config.resolveApproval, config.approvalVerdict, signal, config.approvalPolicies)) {
|
|
366
|
+
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
367
|
// 四: the identity of THIS execution comes from the stream —
|
|
368
368
|
// a historical same-callId execution must never be mistaken
|
|
369
369
|
// for this call's (the provider callId may repeat across runs).
|
|
@@ -588,6 +588,70 @@ async function* executeOne(call, registry, hooks, ctx, log, resolveApproval, res
|
|
|
588
588
|
// all. Checked again here, after any permission path.
|
|
589
589
|
if (signal?.aborted)
|
|
590
590
|
throw ABORTED;
|
|
591
|
+
/**
|
|
592
|
+
* The human approval pause (Phase D / 裁决 A): register the resolver
|
|
593
|
+
* BEFORE announcing the request (a consumer that answers the moment it
|
|
594
|
+
* sees the event must find the resolver already waiting — no deadlock
|
|
595
|
+
* between yield and await), persist the request, yield it, await the
|
|
596
|
+
* human's decision — abortable (an abort during the wait ends the run;
|
|
597
|
+
* a verdict given in the same instant is still recorded exactly once) —
|
|
598
|
+
* then persist and yield the decision. Returns the human's verdict.
|
|
599
|
+
*/
|
|
600
|
+
async function* awaitHumanApproval(decisionId) {
|
|
601
|
+
const pendingDecision = resolveApproval !== undefined
|
|
602
|
+
? resolveApproval(decisionId)
|
|
603
|
+
: Promise.resolve({ action: "deny", reason: "no approval channel configured" });
|
|
604
|
+
const requested = log.append({
|
|
605
|
+
type: "permission_requested",
|
|
606
|
+
decisionId,
|
|
607
|
+
callId: call.callId,
|
|
608
|
+
name: call.name,
|
|
609
|
+
input: payload.input,
|
|
610
|
+
});
|
|
611
|
+
if (hooks.onPause)
|
|
612
|
+
await hooks.onPause("awaiting approval", {}).catch(() => { });
|
|
613
|
+
yield requested;
|
|
614
|
+
// Area 4: the pause is abortable — a cancel during the human's wait
|
|
615
|
+
// ends the run now; the request stays durable and pending.
|
|
616
|
+
let finalDecision;
|
|
617
|
+
try {
|
|
618
|
+
finalDecision = await raceAbort(pendingDecision, signal);
|
|
619
|
+
}
|
|
620
|
+
catch (err) {
|
|
621
|
+
if (err === ABORTED) {
|
|
622
|
+
// 第四轮(对抗): the human may have answered in the same instant
|
|
623
|
+
// the abort landed — a CONSUMED verdict must be recorded
|
|
624
|
+
// (exactly once), never lost; the abort then ends the run with
|
|
625
|
+
// its honest aborted terminal.
|
|
626
|
+
const verdict = resolveApprovalVerdict?.(decisionId);
|
|
627
|
+
if (verdict !== undefined) {
|
|
628
|
+
yield log.append({
|
|
629
|
+
type: "permission_decided",
|
|
630
|
+
decisionId,
|
|
631
|
+
callId: call.callId,
|
|
632
|
+
decision: verdict ? "approved" : "denied",
|
|
633
|
+
...(verdict ? {} : { reason: "denied by user" }),
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
throw err;
|
|
638
|
+
}
|
|
639
|
+
// The approval channel (session.approve) persists the decision
|
|
640
|
+
// write-ahead BEFORE waking the resolver (Area 2): if it already
|
|
641
|
+
// landed in the log, this is the same decision, not a duplicate.
|
|
642
|
+
const decided = log.all.find((e) => e.type === "permission_decided" && e.decisionId === decisionId) ??
|
|
643
|
+
log.append({
|
|
644
|
+
type: "permission_decided",
|
|
645
|
+
decisionId,
|
|
646
|
+
callId: call.callId, // binds the decision to the invocation (B 组)
|
|
647
|
+
decision: finalDecision.action === "allow" ? "approved" : "denied",
|
|
648
|
+
...(finalDecision.action === "deny" && finalDecision.reason !== undefined
|
|
649
|
+
? { reason: finalDecision.reason }
|
|
650
|
+
: {}),
|
|
651
|
+
});
|
|
652
|
+
yield decided;
|
|
653
|
+
return finalDecision;
|
|
654
|
+
}
|
|
591
655
|
// ── E1: the extension policy chain, decided BEFORE the human flow ─────
|
|
592
656
|
// A durable POLICY decision for THIS call takes effect on resume — the
|
|
593
657
|
// chain never re-runs when its verdict is already in the log (同构
|
|
@@ -651,77 +715,37 @@ async function* executeOne(call, registry, hooks, ctx, log, resolveApproval, res
|
|
|
651
715
|
yield emitResult(denialResult(durable.reason ?? "denied"));
|
|
652
716
|
return;
|
|
653
717
|
}
|
|
654
|
-
if (chainVerdict?.action === "ask"
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
|
|
658
|
-
|
|
718
|
+
if (chainVerdict?.action === "ask") {
|
|
719
|
+
// 裁决 A (E1 ask 语义修正): an ask means "a HUMAN must decide" — it
|
|
720
|
+
// routes DIRECTLY to the human approval pause, never through
|
|
721
|
+
// onPreTool: a static automated policy (e.g. the CLI's default deny
|
|
722
|
+
// for unknown tools) must not answer for the human. No approval
|
|
723
|
+
// channel configured → an honest denial (judged by resolveApproval,
|
|
724
|
+
// not by the hook's presence).
|
|
725
|
+
if (resolveApproval === undefined) {
|
|
726
|
+
yield emitResult(denialResult("a policy asked for a human decision, but no approval flow is configured"));
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const decisionId = `d-${log.lastSeq + 1}`;
|
|
730
|
+
const finalDecision = yield* awaitHumanApproval(decisionId);
|
|
731
|
+
if (finalDecision.action !== "allow") {
|
|
732
|
+
yield emitResult(denialResult(finalDecision.reason ?? "denied"));
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
659
735
|
}
|
|
660
736
|
// Permission negotiation — defer is a REAL pause (Phase D). C 组: the
|
|
661
737
|
// hook itself is cancelable (a slow policy query must not outlive an
|
|
662
|
-
// abort), and the signal is re-checked after it returns.
|
|
663
|
-
// the policy chain
|
|
664
|
-
|
|
738
|
+
// abort), and the signal is re-checked after it returns. Runs only when
|
|
739
|
+
// the policy chain did not run at all (裁决 A: an ask was already
|
|
740
|
+
// resolved by the human pause above — the static hook never speaks for
|
|
741
|
+
// it, and a durable decision already spoke for the call).
|
|
742
|
+
if (durable === undefined && chainVerdict === undefined && hooks.onPreTool) {
|
|
665
743
|
const decision = await raceAbort(hooks.onPreTool(payload, ctx), signal);
|
|
666
744
|
if (signal?.aborted)
|
|
667
745
|
throw ABORTED;
|
|
668
746
|
if (decision.action === "defer") {
|
|
669
747
|
const decisionId = `d-${log.lastSeq + 1}`;
|
|
670
|
-
|
|
671
|
-
// that answers the request the moment it sees it must find the
|
|
672
|
-
// resolver already waiting (no deadlock between yield and await).
|
|
673
|
-
const pendingDecision = resolveApproval !== undefined
|
|
674
|
-
? resolveApproval(decisionId)
|
|
675
|
-
: Promise.resolve({ action: "deny", reason: "no approval channel configured" });
|
|
676
|
-
const requested = log.append({
|
|
677
|
-
type: "permission_requested",
|
|
678
|
-
decisionId,
|
|
679
|
-
callId: call.callId,
|
|
680
|
-
name: call.name,
|
|
681
|
-
input: payload.input,
|
|
682
|
-
});
|
|
683
|
-
if (hooks.onPause)
|
|
684
|
-
await hooks.onPause("awaiting approval", {}).catch(() => { });
|
|
685
|
-
yield requested;
|
|
686
|
-
// Area 4: the pause is abortable — a cancel during the human's
|
|
687
|
-
// wait ends the run now; the request stays durable and pending.
|
|
688
|
-
let finalDecision;
|
|
689
|
-
try {
|
|
690
|
-
finalDecision = await raceAbort(pendingDecision, signal);
|
|
691
|
-
}
|
|
692
|
-
catch (err) {
|
|
693
|
-
if (err === ABORTED) {
|
|
694
|
-
// 第四轮(对抗): the human may have answered in the same
|
|
695
|
-
// instant the abort landed — a CONSUMED verdict must be
|
|
696
|
-
// recorded (exactly once), never lost; the abort then
|
|
697
|
-
// ends the run with its honest aborted terminal.
|
|
698
|
-
const verdict = resolveApprovalVerdict?.(decisionId);
|
|
699
|
-
if (verdict !== undefined) {
|
|
700
|
-
yield log.append({
|
|
701
|
-
type: "permission_decided",
|
|
702
|
-
decisionId,
|
|
703
|
-
callId: call.callId,
|
|
704
|
-
decision: verdict ? "approved" : "denied",
|
|
705
|
-
...(verdict ? {} : { reason: "denied by user" }),
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
throw err;
|
|
710
|
-
}
|
|
711
|
-
// The approval channel (session.approve) persists the decision
|
|
712
|
-
// write-ahead BEFORE waking the resolver (Area 2): if it already
|
|
713
|
-
// landed in the log, this is the same decision, not a duplicate.
|
|
714
|
-
const decided = log.all.find((e) => e.type === "permission_decided" && e.decisionId === decisionId) ??
|
|
715
|
-
log.append({
|
|
716
|
-
type: "permission_decided",
|
|
717
|
-
decisionId,
|
|
718
|
-
callId: call.callId, // binds the decision to the invocation (B 组)
|
|
719
|
-
decision: finalDecision.action === "allow" ? "approved" : "denied",
|
|
720
|
-
...(finalDecision.action === "deny" && finalDecision.reason !== undefined
|
|
721
|
-
? { reason: finalDecision.reason }
|
|
722
|
-
: {}),
|
|
723
|
-
});
|
|
724
|
-
yield decided;
|
|
748
|
+
const finalDecision = yield* awaitHumanApproval(decisionId);
|
|
725
749
|
if (finalDecision.action !== "allow") {
|
|
726
750
|
yield emitResult(denialResult(finalDecision.reason ?? "denied"));
|
|
727
751
|
return;
|
|
@@ -59,4 +59,10 @@ export interface KisoExtension {
|
|
|
59
59
|
readonly systemPrompt?: {
|
|
60
60
|
readonly append: string;
|
|
61
61
|
};
|
|
62
|
+
/**
|
|
63
|
+
* 发现#8 (P1): the extension's shutdown action — the closing of external
|
|
64
|
+
* resources it holds (child processes, connections). The LOADER is
|
|
65
|
+
* responsible for calling it.
|
|
66
|
+
*/
|
|
67
|
+
readonly dispose?: () => Promise<void> | void;
|
|
62
68
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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.
|
|
36
|
+
"@vincemakes/kiso-evals": "0.1.8",
|
|
37
37
|
"@types/node": "^26.1.2",
|
|
38
38
|
"typescript": "^5.7.2",
|
|
39
39
|
"vitest": "^3.0.0"
|