@vincemakes/kiso-core 0.1.4 → 0.1.6
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 +15 -0
- package/dist/kernel/loop.js +88 -17
- package/dist/protocol/events.d.ts +3 -0
- package/dist/protocol/events.js +2 -1
- package/dist/protocol/extension.d.ts +62 -0
- package/dist/protocol/extension.js +10 -0
- package/dist/protocol/index.d.ts +1 -0
- package/dist/protocol/index.js +1 -0
- package/package.json +3 -3
package/dist/kernel/loop.d.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { type Adapter, type AbortSignalLike } from "../protocol/adapter.js";
|
|
30
30
|
import type { Event, StructuredError } from "../protocol/events.js";
|
|
31
|
+
import type { ApprovalPolicy } from "../protocol/extension.js";
|
|
31
32
|
import { EventLog } from "./event-log.js";
|
|
32
33
|
import type { EventInput } from "./event-log.js";
|
|
33
34
|
import type { AssistantBlock, AssistantMessage, Message, ToolResultMessage } from "../protocol/messages.js";
|
|
@@ -68,6 +69,7 @@ export interface LoopConfig {
|
|
|
68
69
|
*/
|
|
69
70
|
readonly microcompact?: {
|
|
70
71
|
readonly thresholdTokens: number;
|
|
72
|
+
readonly keepResults?: number;
|
|
71
73
|
};
|
|
72
74
|
readonly signal?: AbortSignalLike;
|
|
73
75
|
readonly temperature?: number;
|
|
@@ -95,6 +97,19 @@ export interface LoopConfig {
|
|
|
95
97
|
readonly resolveUncertainty?: (executionId: string) => Promise<"rerun" | "abandoned">;
|
|
96
98
|
/** 第四轮(对抗): the uncertainty twin of `approvalVerdict`. */
|
|
97
99
|
readonly uncertaintyVerdict?: (executionId: string) => "rerun" | "abandoned" | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* E1: extension approval policies, tagged by their owning extension —
|
|
102
|
+
* decided BEFORE the human flow. Any deny wins (the FIRST denial's
|
|
103
|
+
* reason); else any ask falls into the existing flow; all allow
|
|
104
|
+
* auto-approves. A policy that throws counts as ask. Allow/deny are
|
|
105
|
+
* recorded durably with decidedBy = the extension's name, never pausing
|
|
106
|
+
* for a human; a durable decision already recorded (resume) takes
|
|
107
|
+
* effect and the chain never re-runs.
|
|
108
|
+
*/
|
|
109
|
+
readonly approvalPolicies?: readonly {
|
|
110
|
+
readonly extension: string;
|
|
111
|
+
readonly policy: ApprovalPolicy;
|
|
112
|
+
}[];
|
|
98
113
|
}
|
|
99
114
|
export declare const DEFAULT_MAX_TURNS = 10;
|
|
100
115
|
export declare const DEFAULT_MAX_RETRIES = 2;
|
package/dist/kernel/loop.js
CHANGED
|
@@ -151,7 +151,7 @@ export async function* loop(config) {
|
|
|
151
151
|
}
|
|
152
152
|
// ── C 区: one-shot microcompact boundary when over the threshold ──
|
|
153
153
|
if (config.microcompact !== undefined && estimateTokens(messages) > config.microcompact.thresholdTokens) {
|
|
154
|
-
const beforeSeq = microcompactBoundarySeq(log.all);
|
|
154
|
+
const beforeSeq = microcompactBoundarySeq(log.all, config.microcompact.keepResults ?? KEEP_COMPACTABLE_RESULTS);
|
|
155
155
|
if (beforeSeq !== undefined) {
|
|
156
156
|
const full = log.append({ type: "microcompacted", beforeSeq });
|
|
157
157
|
if (hooks.onEvent)
|
|
@@ -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)) {
|
|
366
|
+
for await (const ev of executeOne(call, registry, hooks, { signal: signal ?? NEVER_ABORT }, 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).
|
|
@@ -531,7 +531,7 @@ function terminalForStop(reason) {
|
|
|
531
531
|
* blocks with a precondition result — the handler never auto-runs a
|
|
532
532
|
* possibly-executed side effect.
|
|
533
533
|
*/
|
|
534
|
-
async function* executeOne(call, registry, hooks, ctx, log, resolveApproval, resolveApprovalVerdict, signal) {
|
|
534
|
+
async function* executeOne(call, registry, hooks, ctx, log, resolveApproval, resolveApprovalVerdict, signal, approvalPolicies) {
|
|
535
535
|
const payload = {
|
|
536
536
|
callId: call.callId,
|
|
537
537
|
name: call.name,
|
|
@@ -588,10 +588,80 @@ 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
|
+
// ── E1: the extension policy chain, decided BEFORE the human flow ─────
|
|
592
|
+
// A durable POLICY decision for THIS call takes effect on resume — the
|
|
593
|
+
// chain never re-runs when its verdict is already in the log (同构
|
|
594
|
+
// alreadyReplaced: the persisted fact speaks for the call). The match is
|
|
595
|
+
// the same logical call: same callId, decidedBy set (a policy verdict,
|
|
596
|
+
// never a human's), and input identical to the original tool_call_end —
|
|
597
|
+
// a re-issued call with different arguments is a NEW call and re-decided.
|
|
598
|
+
// Composition: deny > ask > allow — any deny wins (the FIRST denial's
|
|
599
|
+
// reason), else any ask falls into the existing human flow below, and
|
|
600
|
+
// only an ALL-allow chain auto-approves: recorded durably with decidedBy
|
|
601
|
+
// = the extension's name, never a human-visible pause. A policy that
|
|
602
|
+
// throws counts as ask.
|
|
603
|
+
const originalCall = [...log.all].reverse().find((e) => e.type === "tool_call_end" && e.callId === call.callId);
|
|
604
|
+
const durable = originalCall !== undefined && JSON.stringify(originalCall.input) === JSON.stringify(call.input)
|
|
605
|
+
? log.all.find((e) => e.type === "permission_decided" && e.decidedBy !== undefined && e.callId === call.callId)
|
|
606
|
+
: undefined;
|
|
607
|
+
let chainVerdict;
|
|
608
|
+
let deniedReason;
|
|
609
|
+
let deniedBy;
|
|
610
|
+
if (durable === undefined && approvalPolicies !== undefined && approvalPolicies.length > 0) {
|
|
611
|
+
for (const { extension, policy } of approvalPolicies) {
|
|
612
|
+
let v;
|
|
613
|
+
try {
|
|
614
|
+
v = await raceAbort(Promise.resolve(policy.decide(payload, ctx)), signal);
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
v = { action: "ask" }; // 抛错 = 该扩展计为 ask
|
|
618
|
+
}
|
|
619
|
+
if (v.action === "deny") {
|
|
620
|
+
deniedBy ??= extension;
|
|
621
|
+
deniedReason ??= v.reason; // the FIRST denial's reason
|
|
622
|
+
}
|
|
623
|
+
else if (v.action === "ask") {
|
|
624
|
+
chainVerdict = { action: "ask" };
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (deniedBy !== undefined) {
|
|
628
|
+
chainVerdict = { action: "deny", reason: deniedReason ?? "denied" };
|
|
629
|
+
}
|
|
630
|
+
else if (chainVerdict === undefined) {
|
|
631
|
+
chainVerdict = { action: "allow" };
|
|
632
|
+
}
|
|
633
|
+
if (chainVerdict.action !== "ask") {
|
|
634
|
+
// allow/deny are PERSISTED FACTS (decidedBy = the extension) —
|
|
635
|
+
// never a human pause.
|
|
636
|
+
yield log.append({
|
|
637
|
+
type: "permission_decided",
|
|
638
|
+
decisionId: `d-${log.lastSeq + 1}`,
|
|
639
|
+
callId: call.callId,
|
|
640
|
+
decision: chainVerdict.action === "allow" ? "approved" : "denied",
|
|
641
|
+
...(chainVerdict.action === "deny" ? { reason: chainVerdict.reason } : {}),
|
|
642
|
+
decidedBy: deniedBy ?? approvalPolicies[0].extension,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (chainVerdict?.action === "deny") {
|
|
647
|
+
yield emitResult(denialResult(chainVerdict.reason));
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (durable !== undefined && durable.decision === "denied") {
|
|
651
|
+
yield emitResult(denialResult(durable.reason ?? "denied"));
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (chainVerdict?.action === "ask" && hooks.onPreTool === undefined) {
|
|
655
|
+
// No human flow exists — the ask degrades to an honest denial, never
|
|
656
|
+
// an unasked execution (mirrors the defer-without-channel path).
|
|
657
|
+
yield emitResult(denialResult("a policy asked for a human decision, but no approval flow is configured"));
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
591
660
|
// Permission negotiation — defer is a REAL pause (Phase D). C 组: the
|
|
592
661
|
// hook itself is cancelable (a slow policy query must not outlive an
|
|
593
|
-
// abort), and the signal is re-checked after it returns.
|
|
594
|
-
|
|
662
|
+
// abort), and the signal is re-checked after it returns. Skipped when
|
|
663
|
+
// the policy chain already decided (durable or all-allow).
|
|
664
|
+
if (durable === undefined && chainVerdict?.action !== "allow" && hooks.onPreTool) {
|
|
595
665
|
const decision = await raceAbort(hooks.onPreTool(payload, ctx), signal);
|
|
596
666
|
if (signal?.aborted)
|
|
597
667
|
throw ABORTED;
|
|
@@ -797,23 +867,24 @@ function sleep(ms, signal) {
|
|
|
797
867
|
});
|
|
798
868
|
}
|
|
799
869
|
/**
|
|
800
|
-
* C 区 (自举 #3): how many of the NEWEST compactable tool
|
|
801
|
-
*
|
|
802
|
-
*
|
|
870
|
+
* C 区 (自举 #3): the DEFAULT for how many of the NEWEST compactable tool
|
|
871
|
+
* results survive a microcompact boundary (overridable per config via
|
|
872
|
+
* microcompact.keepResults) — the model must keep reasoning over the
|
|
873
|
+
* recent results, whatever turn they belong to.
|
|
803
874
|
*/
|
|
804
875
|
const KEEP_COMPACTABLE_RESULTS = 4;
|
|
805
876
|
/**
|
|
806
877
|
* C 区: the boundary seq for a microcompact — drawn by COMPACTABLE-RESULT
|
|
807
878
|
* recentness, never user turns: a SINGLE user turn that reads several big
|
|
808
879
|
* files (the coding agent's main overflow shape) crosses the threshold and
|
|
809
|
-
* must trigger. The newest
|
|
810
|
-
*
|
|
811
|
-
*
|
|
812
|
-
*
|
|
813
|
-
*
|
|
814
|
-
*
|
|
880
|
+
* must trigger. The newest `keepResults` still-visible compactable tool
|
|
881
|
+
* results stay intact; the boundary points AT the (K+1)th-newest of them,
|
|
882
|
+
* so it and everything older is cleared. Results already cleared by an
|
|
883
|
+
* earlier boundary do not count toward the kept window — each new boundary
|
|
884
|
+
* makes progress. Undefined when fewer than keepResults+1 compactable
|
|
885
|
+
* results remain (the kept window is the whole context).
|
|
815
886
|
*/
|
|
816
|
-
function microcompactBoundarySeq(events) {
|
|
887
|
+
function microcompactBoundarySeq(events, keepResults) {
|
|
817
888
|
const callName = new Map();
|
|
818
889
|
let lastCleared = -1;
|
|
819
890
|
for (const ev of events) {
|
|
@@ -830,9 +901,9 @@ function microcompactBoundarySeq(events) {
|
|
|
830
901
|
if (name !== undefined && MICROCOMPACTABLE.has(name))
|
|
831
902
|
visible.push(ev.seq);
|
|
832
903
|
}
|
|
833
|
-
if (visible.length <=
|
|
904
|
+
if (visible.length <= keepResults)
|
|
834
905
|
return undefined;
|
|
835
|
-
return visible[visible.length -
|
|
906
|
+
return visible[visible.length - keepResults - 1];
|
|
836
907
|
}
|
|
837
908
|
/** A signal that never aborts — for executions outside any abort scope. */
|
|
838
909
|
const NEVER_ABORT = {
|
|
@@ -268,6 +268,9 @@ export interface PermissionDecided {
|
|
|
268
268
|
readonly callId?: string;
|
|
269
269
|
readonly decision: "approved" | "denied";
|
|
270
270
|
readonly reason?: string;
|
|
271
|
+
/** E1: the deciding extension's name — present ONLY on policy decisions;
|
|
272
|
+
* absent = the human decided (old logs stay compatible). */
|
|
273
|
+
readonly decidedBy?: string;
|
|
271
274
|
}
|
|
272
275
|
/**
|
|
273
276
|
* A permission request was CLOSED because its run terminated first (B 组):
|
package/dist/protocol/events.js
CHANGED
|
@@ -209,7 +209,8 @@ const EVENT_VALIDATORS = {
|
|
|
209
209
|
permission_decided: (v) => typeof v.decisionId === "string" &&
|
|
210
210
|
(v.decision === "approved" || v.decision === "denied") &&
|
|
211
211
|
(v.callId === undefined || typeof v.callId === "string") &&
|
|
212
|
-
(v.reason === undefined || typeof v.reason === "string")
|
|
212
|
+
(v.reason === undefined || typeof v.reason === "string") &&
|
|
213
|
+
(v.decidedBy === undefined || typeof v.decidedBy === "string"),
|
|
213
214
|
permission_expired: (v) => typeof v.decisionId === "string" && typeof v.reason === "string",
|
|
214
215
|
uncertain_pending: (v) => typeof v.executionId === "string" && typeof v.callId === "string" && typeof v.name === "string" && typeof v.error === "string",
|
|
215
216
|
microcompacted: (v) => isNonNegativeInt(v.beforeSeq),
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E1 — extension approval policies: pure types, no runtime.
|
|
3
|
+
*
|
|
4
|
+
* An extension is a named bundle of optional capabilities: hooks (composed
|
|
5
|
+
* AFTER the harness's own — 既有先行), tools (merged into the registry), and
|
|
6
|
+
* approval policies (the loop's policy chain, decided BEFORE the human
|
|
7
|
+
* flow). This file is types-only: loading and composition live in the
|
|
8
|
+
* runtime package (loadExtensions) and the kernel loop.
|
|
9
|
+
*/
|
|
10
|
+
import type { HookHost } from "../kernel/hooks.js";
|
|
11
|
+
import type { Tool, ToolContext } from "../tools/tool.js";
|
|
12
|
+
/** The call a policy decides on — the tool's name and parsed input. */
|
|
13
|
+
export interface PolicyCall {
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly input: Readonly<Record<string, unknown>>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A policy's verdict. `ask` defers to the existing human approval flow;
|
|
19
|
+
* `deny` carries the reason the model sees; `allow` auto-approves.
|
|
20
|
+
*/
|
|
21
|
+
export type PolicyVerdict = {
|
|
22
|
+
readonly action: "allow";
|
|
23
|
+
} | {
|
|
24
|
+
readonly action: "deny";
|
|
25
|
+
readonly reason: string;
|
|
26
|
+
} | {
|
|
27
|
+
readonly action: "ask";
|
|
28
|
+
};
|
|
29
|
+
/** One approval policy — a pure decide function over a tool call. */
|
|
30
|
+
export interface ApprovalPolicy {
|
|
31
|
+
readonly decide: (call: PolicyCall, ctx: ToolContext) => PolicyVerdict | Promise<PolicyVerdict>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A loaded extension. `name` is unique per installation (the loader rejects
|
|
35
|
+
* duplicates loudly); hooks/tools/approvals are all optional.
|
|
36
|
+
*/
|
|
37
|
+
export interface KisoExtension {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly hooks?: HookHost;
|
|
40
|
+
readonly tools?: readonly Tool[];
|
|
41
|
+
readonly approvals?: readonly ApprovalPolicy[];
|
|
42
|
+
/**
|
|
43
|
+
* E2: the extension's compaction config — supplies the loop's microcompact
|
|
44
|
+
* parameters (threshold + optional keepResults) when the session config
|
|
45
|
+
* does not set its own microcompact.
|
|
46
|
+
*/
|
|
47
|
+
readonly compaction?: {
|
|
48
|
+
readonly thresholdTokens?: number;
|
|
49
|
+
readonly keepResults?: number;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* E2: EXTEND the system prompt — append-only, never replace (a replace
|
|
53
|
+
* is a footgun; appends guarantee "adding an extension never removes
|
|
54
|
+
* existing guidance" — the monotonicity family of the approval chain's
|
|
55
|
+
* deny>ask>allow and the veto short-circuit). The session's own
|
|
56
|
+
* systemPrompt comes first, then each extension's append in load order,
|
|
57
|
+
* \n\n-joined.
|
|
58
|
+
*/
|
|
59
|
+
readonly systemPrompt?: {
|
|
60
|
+
readonly append: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E1 — extension approval policies: pure types, no runtime.
|
|
3
|
+
*
|
|
4
|
+
* An extension is a named bundle of optional capabilities: hooks (composed
|
|
5
|
+
* AFTER the harness's own — 既有先行), tools (merged into the registry), and
|
|
6
|
+
* approval policies (the loop's policy chain, decided BEFORE the human
|
|
7
|
+
* flow). This file is types-only: loading and composition live in the
|
|
8
|
+
* runtime package (loadExtensions) and the kernel loop.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
package/dist/protocol/index.d.ts
CHANGED
package/dist/protocol/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-core",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "kiso(
|
|
3
|
+
"version": "0.1.6",
|
|
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",
|
|
7
7
|
"exports": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"openai"
|
|
34
34
|
],
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@vincemakes/kiso-evals": "0.1.
|
|
36
|
+
"@vincemakes/kiso-evals": "0.1.6",
|
|
37
37
|
"@types/node": "^26.1.2",
|
|
38
38
|
"typescript": "^5.7.2",
|
|
39
39
|
"vitest": "^3.0.0"
|