@vincemakes/kiso-core 0.1.3 → 0.1.5
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 +14 -0
- package/dist/kernel/loop.js +108 -15
- package/dist/kernel/project.d.ts +7 -0
- package/dist/kernel/project.js +3 -2
- package/dist/protocol/events.d.ts +3 -0
- package/dist/protocol/events.js +2 -1
- package/dist/protocol/extension.d.ts +42 -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";
|
|
@@ -95,6 +96,19 @@ export interface LoopConfig {
|
|
|
95
96
|
readonly resolveUncertainty?: (executionId: string) => Promise<"rerun" | "abandoned">;
|
|
96
97
|
/** 第四轮(对抗): the uncertainty twin of `approvalVerdict`. */
|
|
97
98
|
readonly uncertaintyVerdict?: (executionId: string) => "rerun" | "abandoned" | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* E1: extension approval policies, tagged by their owning extension —
|
|
101
|
+
* decided BEFORE the human flow. Any deny wins (the FIRST denial's
|
|
102
|
+
* reason); else any ask falls into the existing flow; all allow
|
|
103
|
+
* auto-approves. A policy that throws counts as ask. Allow/deny are
|
|
104
|
+
* recorded durably with decidedBy = the extension's name, never pausing
|
|
105
|
+
* for a human; a durable decision already recorded (resume) takes
|
|
106
|
+
* effect and the chain never re-runs.
|
|
107
|
+
*/
|
|
108
|
+
readonly approvalPolicies?: readonly {
|
|
109
|
+
readonly extension: string;
|
|
110
|
+
readonly policy: ApprovalPolicy;
|
|
111
|
+
}[];
|
|
98
112
|
}
|
|
99
113
|
export declare const DEFAULT_MAX_TURNS = 10;
|
|
100
114
|
export declare const DEFAULT_MAX_RETRIES = 2;
|
package/dist/kernel/loop.js
CHANGED
|
@@ -27,14 +27,14 @@
|
|
|
27
27
|
* re-stream that duplicates output or tool calls.
|
|
28
28
|
*/
|
|
29
29
|
import { isAdapterEvent } from "../protocol/adapter.js";
|
|
30
|
-
import { estimateTokens,
|
|
30
|
+
import { estimateTokens, microcompact } from "./compaction.js";
|
|
31
31
|
import { EventLog } from "./event-log.js";
|
|
32
32
|
import { ToolRegistry } from "../tools/registry.js";
|
|
33
33
|
import { validateArgs } from "../tools/validate.js";
|
|
34
34
|
import { NoOpHooks } from "./hooks.js";
|
|
35
35
|
import { resolveModeProfile } from "./mode.js";
|
|
36
36
|
import { denialResult } from "./permission.js";
|
|
37
|
-
import { messagesToEvents, projectMessages } from "./project.js";
|
|
37
|
+
import { messagesToEvents, MICROCOMPACTABLE, projectMessages } from "./project.js";
|
|
38
38
|
export const DEFAULT_MAX_TURNS = 10;
|
|
39
39
|
export const DEFAULT_MAX_RETRIES = 2;
|
|
40
40
|
export async function* loop(config) {
|
|
@@ -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,19 +867,42 @@ function sleep(ms, signal) {
|
|
|
797
867
|
});
|
|
798
868
|
}
|
|
799
869
|
/**
|
|
800
|
-
* C
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
|
|
870
|
+
* C 区 (自举 #3): how many of the NEWEST compactable tool results survive a
|
|
871
|
+
* microcompact boundary — the model must keep reasoning over the recent
|
|
872
|
+
* results, whatever turn they belong to.
|
|
873
|
+
*/
|
|
874
|
+
const KEEP_COMPACTABLE_RESULTS = 4;
|
|
875
|
+
/**
|
|
876
|
+
* C 区: the boundary seq for a microcompact — drawn by COMPACTABLE-RESULT
|
|
877
|
+
* recentness, never user turns: a SINGLE user turn that reads several big
|
|
878
|
+
* files (the coding agent's main overflow shape) crosses the threshold and
|
|
879
|
+
* must trigger. The newest KEEP_COMPACTABLE_RESULTS still-visible
|
|
880
|
+
* compactable tool results stay intact; the boundary points AT the
|
|
881
|
+
* (K+1)th-newest of them, so it and everything older is cleared. Results
|
|
882
|
+
* already cleared by an earlier boundary do not count toward the kept
|
|
883
|
+
* window — each new boundary makes progress. Undefined when fewer than
|
|
884
|
+
* K+1 compactable results remain (the kept window is the whole context).
|
|
804
885
|
*/
|
|
805
886
|
function microcompactBoundarySeq(events) {
|
|
806
|
-
const
|
|
887
|
+
const callName = new Map();
|
|
888
|
+
let lastCleared = -1;
|
|
889
|
+
for (const ev of events) {
|
|
890
|
+
if (ev.type === "tool_call_end")
|
|
891
|
+
callName.set(ev.callId, ev.name);
|
|
892
|
+
if (ev.type === "microcompacted" && ev.beforeSeq > lastCleared)
|
|
893
|
+
lastCleared = ev.beforeSeq;
|
|
894
|
+
}
|
|
895
|
+
const visible = [];
|
|
807
896
|
for (const ev of events) {
|
|
808
|
-
if (ev.type
|
|
809
|
-
|
|
897
|
+
if (ev.type !== "tool_result" || ev.seq <= lastCleared)
|
|
898
|
+
continue;
|
|
899
|
+
const name = callName.get(ev.callId);
|
|
900
|
+
if (name !== undefined && MICROCOMPACTABLE.has(name))
|
|
901
|
+
visible.push(ev.seq);
|
|
810
902
|
}
|
|
811
|
-
|
|
812
|
-
|
|
903
|
+
if (visible.length <= KEEP_COMPACTABLE_RESULTS)
|
|
904
|
+
return undefined;
|
|
905
|
+
return visible[visible.length - KEEP_COMPACTABLE_RESULTS - 1];
|
|
813
906
|
}
|
|
814
907
|
/** A signal that never aborts — for executions outside any abort scope. */
|
|
815
908
|
const NEVER_ABORT = {
|
package/dist/kernel/project.d.ts
CHANGED
|
@@ -22,6 +22,13 @@
|
|
|
22
22
|
import type { Event } from "../protocol/events.js";
|
|
23
23
|
import type { EventInput } from "./event-log.js";
|
|
24
24
|
import type { AssistantBlock, Message, MessageSource } from "../protocol/messages.js";
|
|
25
|
+
/**
|
|
26
|
+
* C 区: tools whose output is eligible for microcompact clearing — reads,
|
|
27
|
+
* listings, searches, and shell output. write/edit outputs are short and
|
|
28
|
+
* never cleared. Exported so the loop's boundary computation (自举 #3)
|
|
29
|
+
* counts exactly the results the projection can clear.
|
|
30
|
+
*/
|
|
31
|
+
export declare const MICROCOMPACTABLE: Set<string>;
|
|
25
32
|
/** The tag that makes a tool result un-clearable (C 区). */
|
|
26
33
|
export declare const DO_NOT_COMPACT = "do-not-compact";
|
|
27
34
|
/**
|
package/dist/kernel/project.js
CHANGED
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
/**
|
|
23
23
|
* C 区: tools whose output is eligible for microcompact clearing — reads,
|
|
24
24
|
* listings, searches, and shell output. write/edit outputs are short and
|
|
25
|
-
* never cleared.
|
|
25
|
+
* never cleared. Exported so the loop's boundary computation (自举 #3)
|
|
26
|
+
* counts exactly the results the projection can clear.
|
|
26
27
|
*/
|
|
27
|
-
const MICROCOMPACTABLE = new Set(["read_file", "list_dir", "search_text", "shell"]);
|
|
28
|
+
export const MICROCOMPACTABLE = new Set(["read_file", "list_dir", "search_text", "shell"]);
|
|
28
29
|
/** The tag that makes a tool result un-clearable (C 区). */
|
|
29
30
|
export const DO_NOT_COMPACT = "do-not-compact";
|
|
30
31
|
/**
|
|
@@ -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,42 @@
|
|
|
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
|
+
}
|
|
@@ -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.5",
|
|
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.5",
|
|
37
37
|
"@types/node": "^26.1.2",
|
|
38
38
|
"typescript": "^5.7.2",
|
|
39
39
|
"vitest": "^3.0.0"
|