@vincemakes/kiso-core 0.44.0 → 0.45.0
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/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/kernel/loop.d.ts +21 -2
- package/dist/kernel/loop.js +48 -15
- package/package.json +2 -2
- package/dist/kernel/compaction.d.ts +0 -28
- package/dist/kernel/compaction.js +0 -90
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/kernel/loop.d.ts
CHANGED
|
@@ -33,14 +33,25 @@
|
|
|
33
33
|
* re-stream that duplicates output or tool calls.
|
|
34
34
|
*/
|
|
35
35
|
import { type Adapter, type AbortSignalLike } from "../protocol/adapter.js";
|
|
36
|
-
import type { ContinuationScope, Event, StructuredError } from "../protocol/events.js";
|
|
36
|
+
import type { ContinuationScope, Event, StructuredError, UserInputVia } from "../protocol/events.js";
|
|
37
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
|
-
import type { AssistantBlock, AssistantMessage, Message, ToolResultMessage } from "../protocol/messages.js";
|
|
40
|
+
import type { AssistantBlock, AssistantMessage, ContentBlock, Message, MessageSource, ToolResultMessage } from "../protocol/messages.js";
|
|
41
41
|
import { ToolRegistry } from "../tools/registry.js";
|
|
42
42
|
import type { HookHost } from "./hooks.js";
|
|
43
43
|
import { type PermissionDecision } from "./permission.js";
|
|
44
|
+
/**
|
|
45
|
+
* ADR-0057 (Safe Admission): input handed to a running run at a quiescent
|
|
46
|
+
* boundary. A "human" input passes onUserMessage once; a "runtime" input —
|
|
47
|
+
* a fact the runtime owes the model — never does.
|
|
48
|
+
*/
|
|
49
|
+
export interface AdmissionInput {
|
|
50
|
+
readonly kind: "human" | "runtime";
|
|
51
|
+
readonly content: string | readonly ContentBlock[];
|
|
52
|
+
readonly source?: MessageSource;
|
|
53
|
+
readonly via?: UserInputVia;
|
|
54
|
+
}
|
|
44
55
|
export interface LoopConfig {
|
|
45
56
|
readonly adapter: Adapter;
|
|
46
57
|
readonly model: string;
|
|
@@ -78,6 +89,14 @@ export interface LoopConfig {
|
|
|
78
89
|
* run that gets nothing back then ends on that refusal.
|
|
79
90
|
*/
|
|
80
91
|
readonly compact?: (events: readonly Event[], messages: readonly Message[], why: "request" | "overflow") => Promise<readonly EventInput[]>;
|
|
92
|
+
/**
|
|
93
|
+
* ADR-0057 (Safe Admission): the run's ingress. The kernel asks at the
|
|
94
|
+
* three sites — "take", or "takeOrSeal" where the run would otherwise
|
|
95
|
+
* end — and every terminal asks "seal" first. What it returns is
|
|
96
|
+
* appended as `user_input`. Absent: nothing is admitted, and no request
|
|
97
|
+
* byte changes.
|
|
98
|
+
*/
|
|
99
|
+
readonly admit?: (mode: "take" | "takeOrSeal" | "seal") => Promise<readonly AdmissionInput[]>;
|
|
81
100
|
readonly signal?: AbortSignalLike;
|
|
82
101
|
readonly temperature?: number;
|
|
83
102
|
readonly maxTokens?: number;
|
package/dist/kernel/loop.js
CHANGED
|
@@ -105,6 +105,7 @@ export async function* loop(config) {
|
|
|
105
105
|
const derive = () => projectMessages(log.all);
|
|
106
106
|
/** Yield a terminal: onStop (lifecycle) → event → onEvent (observer). */
|
|
107
107
|
const terminal = async (outcome) => {
|
|
108
|
+
await config.admit?.("seal"); // ADR-0057: the ingress closes before onStop can be awaited
|
|
108
109
|
if (hooks.onStop)
|
|
109
110
|
await hooks.onStop(outcome.kind, {}).catch(() => { });
|
|
110
111
|
const full = log.append({ type: "terminal", outcome });
|
|
@@ -129,6 +130,35 @@ export async function* loop(config) {
|
|
|
129
130
|
// every later turn of the run sees.
|
|
130
131
|
let messages = derive();
|
|
131
132
|
let vetoed = false;
|
|
133
|
+
// ADR-0057 §4 — the hook speaks ONCE per human input, the run's own or
|
|
134
|
+
// one admitted mid-run; its outcome is persisted at the input's
|
|
135
|
+
// position. Returns true for a veto.
|
|
136
|
+
async function* hook(seq, msg) {
|
|
137
|
+
const rewritten = await hooks.onUserMessage(msg, {});
|
|
138
|
+
yield log.append({ type: "user_input_replaced", replaces: seq, content: rewritten?.content ?? null, ...(rewritten !== null && rewritten.source !== undefined ? { source: rewritten.source } : {}) });
|
|
139
|
+
messages = derive();
|
|
140
|
+
return rewritten === null;
|
|
141
|
+
}
|
|
142
|
+
// An input admitted mid-run follows a committed model turn of the same run
|
|
143
|
+
// (a stop after the last terminal); the run's own input — or a seeded
|
|
144
|
+
// history, which carries no stop — never does. A veto of the run's own
|
|
145
|
+
// input ends the run; a veto of an admitted one drops only that input.
|
|
146
|
+
const midRun = (seq) => {
|
|
147
|
+
const after = [...log.all].reverse().find((e) => e.type === "terminal")?.seq ?? -1;
|
|
148
|
+
return log.all.some((e) => e.type === "stop" && e.seq > after && e.seq < seq);
|
|
149
|
+
};
|
|
150
|
+
/** ADR-0057 — admit what has arrived; returns how many inputs landed (a veto does not). */
|
|
151
|
+
async function* admit(mode) {
|
|
152
|
+
let landed = 0;
|
|
153
|
+
for (const input of (await config.admit?.(mode)) ?? []) {
|
|
154
|
+
const src = input.source !== undefined ? { source: input.source } : {};
|
|
155
|
+
yield* emit({ type: "user_input", content: input.content, ...src, ...(input.via !== undefined ? { via: input.via } : {}) });
|
|
156
|
+
messages = derive();
|
|
157
|
+
if (!(input.kind === "human" && hooks.onUserMessage && (yield* hook(log.lastSeq, { role: "user", content: input.content, ...src }))))
|
|
158
|
+
landed += 1;
|
|
159
|
+
}
|
|
160
|
+
return landed;
|
|
161
|
+
}
|
|
132
162
|
if (hooks.onUserMessage && messages.length > 0) {
|
|
133
163
|
const last = messages.at(-1);
|
|
134
164
|
if (last?.role === "user") {
|
|
@@ -147,21 +177,12 @@ export async function* loop(config) {
|
|
|
147
177
|
if (replacement.content === null)
|
|
148
178
|
vetoed = true;
|
|
149
179
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
replaces: inputEvent.seq,
|
|
157
|
-
content: rewritten?.content ?? null,
|
|
158
|
-
...(rewritten !== null && rewritten.source !== undefined ? { source: rewritten.source } : {}),
|
|
159
|
-
});
|
|
160
|
-
messages = derive();
|
|
161
|
-
yield replaced;
|
|
162
|
-
if (rewritten === null)
|
|
163
|
-
vetoed = true; // round 3: a true veto ends the run
|
|
164
|
-
}
|
|
180
|
+
// round 1: the rewrite/veto is a NORMAL stream event — persisted by
|
|
181
|
+
// the harness and visible to consumers, never a hidden append.
|
|
182
|
+
// round 3: a true veto ends the run — unless the input was admitted
|
|
183
|
+
// mid-run (ADR-0057: a resume decides as the live path did).
|
|
184
|
+
if (replacement === undefined && inputEvent && (yield* hook(inputEvent.seq, last)) && !midRun(inputEvent.seq))
|
|
185
|
+
vetoed = true;
|
|
165
186
|
}
|
|
166
187
|
}
|
|
167
188
|
// round 3: a true veto ends the run — the provider is NEVER called, even
|
|
@@ -491,6 +512,12 @@ export async function* loop(config) {
|
|
|
491
512
|
yield await terminal({ kind: "aborted", by: "user" });
|
|
492
513
|
return;
|
|
493
514
|
}
|
|
515
|
+
// ADR-0057 site A: the turn head — quiescent, eligible (maxTurns wins),
|
|
516
|
+
// and BEFORE the END_TURN scan, which stops at an admitted input. The
|
|
517
|
+
// overflow retry is left alone (request mode only).
|
|
518
|
+
const requestMode = overflow === null;
|
|
519
|
+
if (requestMode && turns < maxTurns)
|
|
520
|
+
yield* admit("take");
|
|
494
521
|
// 0.42.0: a settled result tagged END_TURN in the OPEN turn — after
|
|
495
522
|
// the most recent stop, user_input or terminal — completes the run
|
|
496
523
|
// instead of asking again. Read from the log at the HEAD of every
|
|
@@ -522,6 +549,9 @@ export async function* loop(config) {
|
|
|
522
549
|
if (added.length > 0)
|
|
523
550
|
messages = derive();
|
|
524
551
|
}
|
|
552
|
+
// ADR-0057 site C: what arrived during a request-mode compaction.
|
|
553
|
+
if (requestMode)
|
|
554
|
+
yield* admit("take");
|
|
525
555
|
if (hooks.onPreLlm)
|
|
526
556
|
await hooks.onPreLlm({ model: config.model, turns }, {});
|
|
527
557
|
if (aborted()) {
|
|
@@ -891,6 +921,9 @@ export async function* loop(config) {
|
|
|
891
921
|
overflow = t.error;
|
|
892
922
|
continue;
|
|
893
923
|
}
|
|
924
|
+
// ADR-0057 site B: the run would end — take what arrived, or seal.
|
|
925
|
+
if (t.kind === "completed" && turns < maxTurns && (yield* admit("takeOrSeal")) > 0)
|
|
926
|
+
continue;
|
|
894
927
|
yield await terminal(t);
|
|
895
928
|
return;
|
|
896
929
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
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.
|
|
36
|
+
"@vincemakes/kiso-evals": "0.45.0",
|
|
37
37
|
"@types/node": "^26.1.2",
|
|
38
38
|
"typescript": "^5.7.2",
|
|
39
39
|
"vitest": "^3.0.0"
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* L2 — context-economy primitives, the MECHANICAL half.
|
|
3
|
-
*
|
|
4
|
-
* The kernel's compaction policy is identity preservation, not summary
|
|
5
|
-
* (the predecessor's ADR-0007): keep the message SHELL (id, role, position), replace
|
|
6
|
-
* the content with a marker, zero LLM calls. A summary is a NEW message; it
|
|
7
|
-
* never rewrites an old one. Messages are immutable (ADR-0002) — "clearing"
|
|
8
|
-
* is append, not mutation.
|
|
9
|
-
*
|
|
10
|
-
* ADR-0044 merged the classic auto-compaction (`config.compaction` +
|
|
11
|
-
* `compacted` events) INTO the microcompact boundary: the loop no longer
|
|
12
|
-
* produces `compacted` events (the boundary's projection derives the same
|
|
13
|
-
* cleared view deterministically), and this module now holds only what the
|
|
14
|
-
* live path shares. Old sessions' `compacted` events still replay verbatim
|
|
15
|
-
* — see kernel/project.ts.
|
|
16
|
-
*
|
|
17
|
-
* The model-generated half of context economy (the /compact summary layer)
|
|
18
|
-
* lives in kernel/summarize.ts.
|
|
19
|
-
*/
|
|
20
|
-
import type { Message } from "../protocol/messages.js";
|
|
21
|
-
/**
|
|
22
|
-
* Rough token estimate (chars/4 + structural overhead). Calibration-free on
|
|
23
|
-
* purpose: context economy only needs a stable MONOTONE proxy, not an exact
|
|
24
|
-
* count — the threshold absorbs the error (the predecessor's ADR-0007). The proxy's
|
|
25
|
-
* three-word contract, and the pins that hold it, are
|
|
26
|
-
* packages/core/tests/sc1b-estimator.test.ts.
|
|
27
|
-
*/
|
|
28
|
-
export declare function estimateTokens(messages: readonly Message[]): number;
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* L2 — context-economy primitives, the MECHANICAL half.
|
|
3
|
-
*
|
|
4
|
-
* The kernel's compaction policy is identity preservation, not summary
|
|
5
|
-
* (the predecessor's ADR-0007): keep the message SHELL (id, role, position), replace
|
|
6
|
-
* the content with a marker, zero LLM calls. A summary is a NEW message; it
|
|
7
|
-
* never rewrites an old one. Messages are immutable (ADR-0002) — "clearing"
|
|
8
|
-
* is append, not mutation.
|
|
9
|
-
*
|
|
10
|
-
* ADR-0044 merged the classic auto-compaction (`config.compaction` +
|
|
11
|
-
* `compacted` events) INTO the microcompact boundary: the loop no longer
|
|
12
|
-
* produces `compacted` events (the boundary's projection derives the same
|
|
13
|
-
* cleared view deterministically), and this module now holds only what the
|
|
14
|
-
* live path shares. Old sessions' `compacted` events still replay verbatim
|
|
15
|
-
* — see kernel/project.ts.
|
|
16
|
-
*
|
|
17
|
-
* The model-generated half of context economy (the /compact summary layer)
|
|
18
|
-
* lives in kernel/summarize.ts.
|
|
19
|
-
*/
|
|
20
|
-
/**
|
|
21
|
-
* What a non-text block (an image) contributes. There is no character count
|
|
22
|
-
* to proxy, and this module refuses to guess a provider's image
|
|
23
|
-
* tokenization; the figure exists so a block is never worth ZERO, which is
|
|
24
|
-
* what MONOTONE requires. Deliberately small: under-stating an image is a
|
|
25
|
-
* threshold that fires slightly late, over-stating it is one that fires on
|
|
26
|
-
* a conversation that was never large.
|
|
27
|
-
*/
|
|
28
|
-
const NON_TEXT_BLOCK_TOKENS = 8;
|
|
29
|
-
/**
|
|
30
|
-
* chars/4 over a `string | ContentBlock[]` content field.
|
|
31
|
-
*
|
|
32
|
-
* SC-1b ①: the array arm must be summed BLOCK BY BLOCK. Reading `.length`
|
|
33
|
-
* off it yields the block COUNT — a five-block 50 KB message scored ~2
|
|
34
|
-
* tokens, and the live microcompact threshold believed it.
|
|
35
|
-
*/
|
|
36
|
-
function contentTokens(content) {
|
|
37
|
-
if (typeof content === "string")
|
|
38
|
-
return textTokens(content);
|
|
39
|
-
let tokens = 0;
|
|
40
|
-
for (const block of content) {
|
|
41
|
-
tokens += block.type === "text" ? textTokens(block.text) : NON_TEXT_BLOCK_TOKENS;
|
|
42
|
-
}
|
|
43
|
-
return tokens;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* 0.40.0 (the owner's session): chars/4, except that a CJK character counts
|
|
47
|
-
* as one token — which is about what a tokenizer spends on one. Plain
|
|
48
|
-
* chars/4 read a Chinese-heavy 730k context as ~470k, so the thresholds
|
|
49
|
-
* never fired and the ctx row lied. Text without CJK scores exactly what
|
|
50
|
-
* it scored before. A counting loop, not a regex match: the text can be
|
|
51
|
-
* megabytes, and a match array would hold one entry per character.
|
|
52
|
-
*/
|
|
53
|
-
function textTokens(text) {
|
|
54
|
-
let cjk = 0;
|
|
55
|
-
for (let i = 0; i < text.length; i++) {
|
|
56
|
-
const c = text.charCodeAt(i);
|
|
57
|
-
// kana, CJK ext A, unified ideographs, Hangul syllables,
|
|
58
|
-
// compatibility ideographs, full-width forms
|
|
59
|
-
if ((c >= 0x3040 && c <= 0x30ff) || (c >= 0x3400 && c <= 0x4dbf) || (c >= 0x4e00 && c <= 0x9fff) || (c >= 0xac00 && c <= 0xd7af) || (c >= 0xf900 && c <= 0xfaff) || (c >= 0xff00 && c <= 0xffef))
|
|
60
|
-
cjk++;
|
|
61
|
-
}
|
|
62
|
-
return cjk + Math.ceil((text.length - cjk) / 4);
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Rough token estimate (chars/4 + structural overhead). Calibration-free on
|
|
66
|
-
* purpose: context economy only needs a stable MONOTONE proxy, not an exact
|
|
67
|
-
* count — the threshold absorbs the error (the predecessor's ADR-0007). The proxy's
|
|
68
|
-
* three-word contract, and the pins that hold it, are
|
|
69
|
-
* packages/core/tests/sc1b-estimator.test.ts.
|
|
70
|
-
*/
|
|
71
|
-
export function estimateTokens(messages) {
|
|
72
|
-
let total = 0;
|
|
73
|
-
for (const msg of messages) {
|
|
74
|
-
if (msg.role === "user") {
|
|
75
|
-
total += contentTokens(msg.content);
|
|
76
|
-
}
|
|
77
|
-
else if (msg.role === "assistant") {
|
|
78
|
-
for (const block of msg.blocks) {
|
|
79
|
-
total +=
|
|
80
|
-
block.type === "text"
|
|
81
|
-
? textTokens(block.text)
|
|
82
|
-
: textTokens(JSON.stringify(block.input)) + 20;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
total += contentTokens(msg.content) + 10;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return total;
|
|
90
|
-
}
|