@vincemakes/kiso-core 0.1.0 → 0.1.2
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
CHANGED
|
@@ -58,6 +58,17 @@ export interface LoopConfig {
|
|
|
58
58
|
readonly compaction?: {
|
|
59
59
|
readonly thresholdTokens: number;
|
|
60
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* C 区: MICROCOMPACT — when the projected context exceeds the threshold,
|
|
63
|
+
* append ONE durable `microcompacted` boundary (clearing compactable tool
|
|
64
|
+
* results older than the recent turns). The decision is a persisted fact:
|
|
65
|
+
* the projection derives the same cleared view from the same events,
|
|
66
|
+
* byte for byte, across crash/resume. Never a per-turn progressive
|
|
67
|
+
* clearing.
|
|
68
|
+
*/
|
|
69
|
+
readonly microcompact?: {
|
|
70
|
+
readonly thresholdTokens: number;
|
|
71
|
+
};
|
|
61
72
|
readonly signal?: AbortSignalLike;
|
|
62
73
|
readonly temperature?: number;
|
|
63
74
|
readonly maxTokens?: number;
|
package/dist/kernel/loop.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* re-stream that duplicates output or tool calls.
|
|
28
28
|
*/
|
|
29
29
|
import { isAdapterEvent } from "../protocol/adapter.js";
|
|
30
|
-
import { estimateTokens, microcompact } from "./compaction.js";
|
|
30
|
+
import { estimateTokens, KEEP_RECENT_TURNS, 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";
|
|
@@ -149,6 +149,17 @@ export async function* loop(config) {
|
|
|
149
149
|
await hooks.onPostCompact(messages, {}).catch(() => { });
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
+
// ── C 区: one-shot microcompact boundary when over the threshold ──
|
|
153
|
+
if (config.microcompact !== undefined && estimateTokens(messages) > config.microcompact.thresholdTokens) {
|
|
154
|
+
const beforeSeq = microcompactBoundarySeq(log.all);
|
|
155
|
+
if (beforeSeq !== undefined) {
|
|
156
|
+
const full = log.append({ type: "microcompacted", beforeSeq });
|
|
157
|
+
if (hooks.onEvent)
|
|
158
|
+
await hooks.onEvent(full, {}).catch(() => { });
|
|
159
|
+
yield full;
|
|
160
|
+
messages = derive();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
152
163
|
if (hooks.onPreLlm)
|
|
153
164
|
await hooks.onPreLlm({ model: config.model, turns }, {});
|
|
154
165
|
if (aborted()) {
|
|
@@ -785,6 +796,21 @@ function sleep(ms, signal) {
|
|
|
785
796
|
}, { once: true });
|
|
786
797
|
});
|
|
787
798
|
}
|
|
799
|
+
/**
|
|
800
|
+
* C 区: the boundary seq for a microcompact — the seq of the user input
|
|
801
|
+
* KEEP_RECENT_TURNS+1 places from the end (everything BEFORE that user
|
|
802
|
+
* input is old enough to clear; the recent turns stay intact). Undefined
|
|
803
|
+
* when the history is too short to clear anything.
|
|
804
|
+
*/
|
|
805
|
+
function microcompactBoundarySeq(events) {
|
|
806
|
+
const userSeqs = [];
|
|
807
|
+
for (const ev of events) {
|
|
808
|
+
if (ev.type === "user_input")
|
|
809
|
+
userSeqs.push(ev.seq);
|
|
810
|
+
}
|
|
811
|
+
const boundary = userSeqs[userSeqs.length - KEEP_RECENT_TURNS - 1];
|
|
812
|
+
return boundary !== undefined ? boundary - 1 : undefined;
|
|
813
|
+
}
|
|
788
814
|
/** A signal that never aborts — for executions outside any abort scope. */
|
|
789
815
|
const NEVER_ABORT = {
|
|
790
816
|
aborted: false,
|
package/dist/kernel/project.d.ts
CHANGED
|
@@ -22,9 +22,15 @@
|
|
|
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
|
+
/** The tag that makes a tool result un-clearable (C 区). */
|
|
26
|
+
export declare const DO_NOT_COMPACT = "do-not-compact";
|
|
25
27
|
/**
|
|
26
28
|
* Rebuild the message array from events. Deterministic and order-sensitive:
|
|
27
|
-
* replaying the same log always produces the same messages
|
|
29
|
+
* replaying the same log always produces the same messages — BYTE FOR BYTE
|
|
30
|
+
* (D 区): the same event prefix derives the same message prefix; the only
|
|
31
|
+
* events that change already-derived messages are `microcompacted`
|
|
32
|
+
* boundaries, which are themselves persisted facts (their replay derives
|
|
33
|
+
* the same projection every time).
|
|
28
34
|
*
|
|
29
35
|
* Text block boundaries are preserved: `text_end` closes the current text
|
|
30
36
|
* block (an explicit boundary); `text_start` after a block opens a new one.
|
package/dist/kernel/project.js
CHANGED
|
@@ -19,9 +19,41 @@
|
|
|
19
19
|
* microcompact is deterministic and idempotent, so the replay equals the
|
|
20
20
|
* live run. See ADR-0002.
|
|
21
21
|
*/
|
|
22
|
+
/**
|
|
23
|
+
* C 区: tools whose output is eligible for microcompact clearing — reads,
|
|
24
|
+
* listings, searches, and shell output. write/edit outputs are short and
|
|
25
|
+
* never cleared.
|
|
26
|
+
*/
|
|
27
|
+
const MICROCOMPACTABLE = new Set(["read_file", "list_dir", "search_text", "shell"]);
|
|
28
|
+
/** The tag that makes a tool result un-clearable (C 区). */
|
|
29
|
+
export const DO_NOT_COMPACT = "do-not-compact";
|
|
30
|
+
/**
|
|
31
|
+
* C 区: the fixed placeholder for a cleared tool output, derived ONLY from
|
|
32
|
+
* the event stream (deterministic across replay): the tool's name and its
|
|
33
|
+
* primary argument (the first string-valued field of its input).
|
|
34
|
+
*/
|
|
35
|
+
function microPlaceholder(name, arg) {
|
|
36
|
+
return arg === undefined
|
|
37
|
+
? `[old tool output cleared: ${name}]`
|
|
38
|
+
: `[old tool output cleared: ${name} ${arg}]`;
|
|
39
|
+
}
|
|
40
|
+
/** The first string-valued argument of a tool input, if any. */
|
|
41
|
+
function primaryArg(input) {
|
|
42
|
+
if (input === null || input === undefined)
|
|
43
|
+
return undefined;
|
|
44
|
+
for (const value of Object.values(input)) {
|
|
45
|
+
if (typeof value === "string" && value !== "")
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
22
50
|
/**
|
|
23
51
|
* Rebuild the message array from events. Deterministic and order-sensitive:
|
|
24
|
-
* replaying the same log always produces the same messages
|
|
52
|
+
* replaying the same log always produces the same messages — BYTE FOR BYTE
|
|
53
|
+
* (D 区): the same event prefix derives the same message prefix; the only
|
|
54
|
+
* events that change already-derived messages are `microcompacted`
|
|
55
|
+
* boundaries, which are themselves persisted facts (their replay derives
|
|
56
|
+
* the same projection every time).
|
|
25
57
|
*
|
|
26
58
|
* Text block boundaries are preserved: `text_end` closes the current text
|
|
27
59
|
* block (an explicit boundary); `text_start` after a block opens a new one.
|
|
@@ -39,16 +71,23 @@ export function projectMessages(events) {
|
|
|
39
71
|
text = null;
|
|
40
72
|
}
|
|
41
73
|
};
|
|
74
|
+
// 自举 P1: the reasoning of the turn being built, accumulated from its
|
|
75
|
+
// `thinking` events and attached to the assistant message at flush —
|
|
76
|
+
// deterministic (same events → same messages → same request body, D 区).
|
|
77
|
+
let pendingReasoning = null;
|
|
42
78
|
const flushAssistant = () => {
|
|
43
79
|
pushText();
|
|
44
80
|
if (blocks.length === 0) {
|
|
45
81
|
assistantSource = undefined;
|
|
46
82
|
return;
|
|
47
83
|
}
|
|
84
|
+
const reasoning = pendingReasoning;
|
|
85
|
+
pendingReasoning = null;
|
|
48
86
|
out.push({
|
|
49
87
|
role: "assistant",
|
|
50
88
|
blocks: [...blocks],
|
|
51
89
|
...(assistantSource !== undefined ? { source: assistantSource } : {}),
|
|
90
|
+
...(reasoning !== null ? { reasoning } : {}),
|
|
52
91
|
});
|
|
53
92
|
blocks = [];
|
|
54
93
|
assistantSource = undefined;
|
|
@@ -68,6 +107,12 @@ export function projectMessages(events) {
|
|
|
68
107
|
});
|
|
69
108
|
}
|
|
70
109
|
}
|
|
110
|
+
// C 区: callId → {name, input} for the microcompact placeholder.
|
|
111
|
+
const callMeta = new Map();
|
|
112
|
+
for (const ev of events) {
|
|
113
|
+
if (ev.type === "tool_call_end")
|
|
114
|
+
callMeta.set(ev.callId, { name: ev.name, input: ev.input });
|
|
115
|
+
}
|
|
71
116
|
let explicitAssistant = false;
|
|
72
117
|
for (const ev of events) {
|
|
73
118
|
switch (ev.type) {
|
|
@@ -171,6 +216,27 @@ export function projectMessages(events) {
|
|
|
171
216
|
out.push(message);
|
|
172
217
|
break;
|
|
173
218
|
}
|
|
219
|
+
case "microcompacted": {
|
|
220
|
+
flushAssistant();
|
|
221
|
+
// C 区: replace every eligible OLD tool result with the fixed
|
|
222
|
+
// placeholder. Eligibility: the result's own event seq <= the
|
|
223
|
+
// boundary, its tool in the whitelist, and no do-not-compact
|
|
224
|
+
// tag. Deterministic — the boundary event IS the decision.
|
|
225
|
+
const replaced = out.map((m) => {
|
|
226
|
+
if (m.role !== "tool")
|
|
227
|
+
return m;
|
|
228
|
+
if (m.eventSeq === undefined || m.eventSeq > ev.beforeSeq)
|
|
229
|
+
return m;
|
|
230
|
+
const meta = callMeta.get(m.callId);
|
|
231
|
+
if (meta === undefined || !MICROCOMPACTABLE.has(meta.name))
|
|
232
|
+
return m;
|
|
233
|
+
if ((m.tags ?? []).includes(DO_NOT_COMPACT))
|
|
234
|
+
return m;
|
|
235
|
+
return { ...m, content: microPlaceholder(meta.name, primaryArg(meta.input)) };
|
|
236
|
+
});
|
|
237
|
+
out.splice(0, out.length, ...replaced);
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
174
240
|
case "compacted": {
|
|
175
241
|
flushAssistant();
|
|
176
242
|
// Apply the EXACT persisted replacements — never re-run the
|
|
@@ -197,9 +263,16 @@ export function projectMessages(events) {
|
|
|
197
263
|
break;
|
|
198
264
|
}
|
|
199
265
|
case "thinking":
|
|
266
|
+
// 自举 P1: accumulate the turn's reasoning — the flush (an
|
|
267
|
+
// empty one at the turn's start) keeps the pending text, and
|
|
268
|
+
// the assistant message that follows carries it.
|
|
269
|
+
flushAssistant();
|
|
270
|
+
pendingReasoning = (pendingReasoning ?? "") + ev.text;
|
|
271
|
+
break;
|
|
200
272
|
case "usage":
|
|
201
273
|
case "stop":
|
|
202
274
|
case "terminal":
|
|
275
|
+
case "microcompacted":
|
|
203
276
|
case "tool_execution_started":
|
|
204
277
|
case "tool_execution_succeeded":
|
|
205
278
|
case "tool_execution_failed":
|
|
@@ -237,6 +310,11 @@ export function messagesToEvents(messages) {
|
|
|
237
310
|
case "assistant": {
|
|
238
311
|
// D 组: an explicit assistant_start/assistant_end pair frames
|
|
239
312
|
// the message — adjacent and empty assistants round-trip.
|
|
313
|
+
// 自举 P1: the reasoning re-enters the log as its ORIGINAL
|
|
314
|
+
// thinking event — the projection re-attaches it identically.
|
|
315
|
+
if (msg.reasoning !== undefined) {
|
|
316
|
+
out.push({ type: "thinking", text: msg.reasoning });
|
|
317
|
+
}
|
|
240
318
|
out.push({
|
|
241
319
|
type: "assistant_start",
|
|
242
320
|
...(msg.source !== undefined ? { source: msg.source } : {}),
|
|
@@ -310,6 +310,20 @@ export interface UserInputReplaced {
|
|
|
310
310
|
/** Provenance of the replacement — preserved from the hook (三). */
|
|
311
311
|
readonly source?: import("./messages.js").MessageSource;
|
|
312
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* C 区: a MICROCOMPACT boundary — the durable record of a context-clearing
|
|
315
|
+
* decision. `beforeSeq` is the event seq up to which eligible tool results
|
|
316
|
+
* are cleared: the projection replaces every tool_result with seq <=
|
|
317
|
+
* beforeSeq (whose tool is in the compactable whitelist and carries no
|
|
318
|
+
* do-not-compact tag) with a fixed placeholder derived from the stream
|
|
319
|
+
* itself. The decision is a PERSISTED FACT — replaying the same events
|
|
320
|
+
* derives the same messages, byte for byte, after a crash or resume.
|
|
321
|
+
*/
|
|
322
|
+
export interface MicroCompactEvent {
|
|
323
|
+
readonly seq: number;
|
|
324
|
+
readonly type: "microcompacted";
|
|
325
|
+
readonly beforeSeq: number;
|
|
326
|
+
}
|
|
313
327
|
/** Extended-thinking content. Providers without it emit nothing here. */
|
|
314
328
|
export interface Thinking {
|
|
315
329
|
readonly seq: number;
|
|
@@ -400,7 +414,7 @@ export interface TerminalEvent {
|
|
|
400
414
|
* The union. Consume it with a `switch (event.type)`; with
|
|
401
415
|
* `strictNullChecks` on, an unhandled variant is a compile error.
|
|
402
416
|
*/
|
|
403
|
-
export type Event = AssistantStart | AssistantEnd | TextStart | TextDelta | TextEnd | ToolCallStart | ToolCallInputDelta | ToolCallEnd | ToolResultEvent | Thinking | Usage | Stop | UserInputEvent | CompactedEvent | ToolExecutionStarted | ToolExecutionSucceeded | ToolExecutionFailed | ToolExecutionResolved | PermissionRequested | PermissionDecided | PermissionExpired | UncertainPending | UserInputReplaced | TerminalEvent;
|
|
417
|
+
export type Event = AssistantStart | AssistantEnd | TextStart | TextDelta | TextEnd | ToolCallStart | ToolCallInputDelta | ToolCallEnd | ToolResultEvent | Thinking | Usage | Stop | UserInputEvent | CompactedEvent | ToolExecutionStarted | ToolExecutionSucceeded | ToolExecutionFailed | ToolExecutionResolved | PermissionRequested | PermissionDecided | PermissionExpired | UncertainPending | UserInputReplaced | MicroCompactEvent | TerminalEvent;
|
|
404
418
|
/**
|
|
405
419
|
* Runtime type guard for the union. The store validates every JSONL record
|
|
406
420
|
* with it: valid JSON that is not a kiso event is corruption, not history.
|
package/dist/protocol/events.js
CHANGED
|
@@ -212,6 +212,7 @@ const EVENT_VALIDATORS = {
|
|
|
212
212
|
(v.reason === undefined || typeof v.reason === "string"),
|
|
213
213
|
permission_expired: (v) => typeof v.decisionId === "string" && typeof v.reason === "string",
|
|
214
214
|
uncertain_pending: (v) => typeof v.executionId === "string" && typeof v.callId === "string" && typeof v.name === "string" && typeof v.error === "string",
|
|
215
|
+
microcompacted: (v) => isNonNegativeInt(v.beforeSeq),
|
|
215
216
|
user_input_replaced: (v) => isNonNegativeInt(v.replaces) && (v.content === null || isContent(v.content)) && isSource(v),
|
|
216
217
|
terminal: (v) => isTerminal(v.outcome),
|
|
217
218
|
};
|
|
@@ -71,6 +71,12 @@ export interface AssistantMessage {
|
|
|
71
71
|
readonly role: "assistant";
|
|
72
72
|
readonly blocks: readonly AssistantBlock[];
|
|
73
73
|
readonly source?: MessageSource;
|
|
74
|
+
/**
|
|
75
|
+
* 自举 P1: the turn's reasoning, derived deterministically from its
|
|
76
|
+
* `thinking` events (DeepSeek's thinking mode requires it back on
|
|
77
|
+
* follow-up requests). Present only when the turn actually reasoned.
|
|
78
|
+
*/
|
|
79
|
+
readonly reasoning?: string;
|
|
74
80
|
}
|
|
75
81
|
/**
|
|
76
82
|
* Sent back to the model after a tool ran.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "kiso(\u57fa\u790e) 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.1.
|
|
36
|
+
"@vincemakes/kiso-evals": "0.1.2",
|
|
37
37
|
"@types/node": "^26.1.2",
|
|
38
38
|
"typescript": "^5.7.2",
|
|
39
39
|
"vitest": "^3.0.0"
|