@vincemakes/kiso-core 0.1.18 → 0.1.20
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 +1 -0
- package/dist/index.js +1 -0
- package/dist/kernel/compaction.d.ts +16 -50
- package/dist/kernel/compaction.js +15 -88
- package/dist/kernel/hooks.d.ts +6 -2
- package/dist/kernel/loop.d.ts +10 -4
- package/dist/kernel/loop.js +4 -28
- package/dist/kernel/project.d.ts +10 -7
- package/dist/kernel/project.js +56 -7
- package/dist/kernel/summarize.d.ts +58 -0
- package/dist/kernel/summarize.js +106 -0
- package/dist/protocol/adapter.d.ts +5 -5
- package/dist/protocol/events.d.ts +24 -1
- package/dist/protocol/events.js +6 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from "./kernel/mode.js";
|
|
|
8
8
|
export * from "./kernel/permission.js";
|
|
9
9
|
export * from "./kernel/loop.js";
|
|
10
10
|
export * from "./kernel/compaction.js";
|
|
11
|
+
export * from "./kernel/summarize.js";
|
|
11
12
|
export * from "./kernel/project.js";
|
|
12
13
|
export * from "./kernel/ledger.js";
|
|
13
14
|
export * from "./governance/delivery.js";
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export * from "./kernel/mode.js";
|
|
|
8
8
|
export * from "./kernel/permission.js";
|
|
9
9
|
export * from "./kernel/loop.js";
|
|
10
10
|
export * from "./kernel/compaction.js";
|
|
11
|
+
export * from "./kernel/summarize.js";
|
|
11
12
|
export * from "./kernel/project.js";
|
|
12
13
|
export * from "./kernel/ledger.js";
|
|
13
14
|
export * from "./governance/delivery.js";
|
|
@@ -1,60 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* L2 —
|
|
2
|
+
* L2 — context-economy primitives, the MECHANICAL half.
|
|
3
3
|
*
|
|
4
4
|
* The kernel's compaction policy is identity preservation, not summary
|
|
5
|
-
* (mauri ADR-0007): keep the message SHELL (id, role, position), replace
|
|
6
|
-
* content with a marker, zero LLM calls. A summary is a NEW message; it
|
|
7
|
-
* rewrites an old one. Messages are immutable (ADR-0002) — "clearing"
|
|
8
|
-
* append, not mutation.
|
|
5
|
+
* (mauri 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
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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.
|
|
15
19
|
*/
|
|
16
|
-
import type {
|
|
17
|
-
/** Marker prefix for cleared tool results. Must be unambiguous and stable. */
|
|
18
|
-
export declare const CLEARED_MARKER_PREFIX = "[content cleared \u2014 reference by revision]";
|
|
19
|
-
export declare function isClearedMarker(content: string): boolean;
|
|
20
|
-
/** Idempotence gate: a message whose content is already the clear marker is
|
|
21
|
-
* never compacted again, never re-archived, never overwritten. */
|
|
22
|
-
export declare function shouldClearContent(content: string): boolean;
|
|
20
|
+
import type { Message } from "../protocol/messages.js";
|
|
23
21
|
/**
|
|
24
22
|
* Rough token estimate (chars/4 + structural overhead). Calibration-free on
|
|
25
|
-
* purpose:
|
|
23
|
+
* purpose: context economy only needs a stable MONOTONE proxy, not an exact
|
|
26
24
|
* count — the threshold absorbs the error (mauri ADR-0007).
|
|
27
25
|
*/
|
|
28
26
|
export declare function estimateTokens(messages: readonly Message[]): number;
|
|
29
|
-
/**
|
|
30
|
-
* Microcompact — zero-LLM context relief (ported shape from oohki runner,
|
|
31
|
-
* adapted to kiso's Message union; identity preservation, ADR-0007):
|
|
32
|
-
*
|
|
33
|
-
* 1. Find the boundary: the KEEP_RECENT_TURNS-th user message from the end.
|
|
34
|
-
* Recent turns stay fully intact — the model must reason about them.
|
|
35
|
-
* 2. Tool results BEFORE the boundary have their content replaced by a stub
|
|
36
|
-
* that keeps the name + char count (an information anchor, not a hole).
|
|
37
|
-
* 3. Idempotent: already-cleared content is never touched again (the
|
|
38
|
-
* compaction-regrowth incident's one-line fix, as a first-class rule).
|
|
39
|
-
* 4. Returns a NEW array; messages are immutable (ADR-0002).
|
|
40
|
-
*/
|
|
41
|
-
export declare const KEEP_RECENT_TURNS = 5;
|
|
42
|
-
export interface MicrocompactResult {
|
|
43
|
-
readonly messages: readonly Message[];
|
|
44
|
-
/** How much content (chars) was cleared this pass. */
|
|
45
|
-
readonly clearedChars: number;
|
|
46
|
-
/**
|
|
47
|
-
* ONLY the tool results cleared THIS pass — the delta, never the
|
|
48
|
-
* cumulative marker set (五: a replayable trajectory must not record the
|
|
49
|
-
* same replacement on every turn). `eventSeq` is the cleared result's
|
|
50
|
-
* stable identity (the tool_result event's seq, attached by the
|
|
51
|
-
* projection); it is undefined for hand-built messages outside the loop.
|
|
52
|
-
*/
|
|
53
|
-
readonly cleared: readonly {
|
|
54
|
-
readonly eventSeq?: number;
|
|
55
|
-
readonly callId: string;
|
|
56
|
-
readonly content: string;
|
|
57
|
-
}[];
|
|
58
|
-
}
|
|
59
|
-
export declare function microcompact(messages: readonly Message[]): MicrocompactResult;
|
|
60
|
-
export type { AssistantBlock };
|
|
@@ -1,31 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* L2 —
|
|
2
|
+
* L2 — context-economy primitives, the MECHANICAL half.
|
|
3
3
|
*
|
|
4
4
|
* The kernel's compaction policy is identity preservation, not summary
|
|
5
|
-
* (mauri ADR-0007): keep the message SHELL (id, role, position), replace
|
|
6
|
-
* content with a marker, zero LLM calls. A summary is a NEW message; it
|
|
7
|
-
* rewrites an old one. Messages are immutable (ADR-0002) — "clearing"
|
|
8
|
-
* append, not mutation.
|
|
5
|
+
* (mauri 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
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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.
|
|
15
19
|
*/
|
|
16
|
-
/** Marker prefix for cleared tool results. Must be unambiguous and stable. */
|
|
17
|
-
export const CLEARED_MARKER_PREFIX = "[content cleared — reference by revision]";
|
|
18
|
-
export function isClearedMarker(content) {
|
|
19
|
-
return content.startsWith(CLEARED_MARKER_PREFIX);
|
|
20
|
-
}
|
|
21
|
-
/** Idempotence gate: a message whose content is already the clear marker is
|
|
22
|
-
* never compacted again, never re-archived, never overwritten. */
|
|
23
|
-
export function shouldClearContent(content) {
|
|
24
|
-
return !isClearedMarker(content);
|
|
25
|
-
}
|
|
26
20
|
/**
|
|
27
21
|
* Rough token estimate (chars/4 + structural overhead). Calibration-free on
|
|
28
|
-
* purpose:
|
|
22
|
+
* purpose: context economy only needs a stable MONOTONE proxy, not an exact
|
|
29
23
|
* count — the threshold absorbs the error (mauri ADR-0007).
|
|
30
24
|
*/
|
|
31
25
|
export function estimateTokens(messages) {
|
|
@@ -48,70 +42,3 @@ export function estimateTokens(messages) {
|
|
|
48
42
|
}
|
|
49
43
|
return total;
|
|
50
44
|
}
|
|
51
|
-
/**
|
|
52
|
-
* Microcompact — zero-LLM context relief (ported shape from oohki runner,
|
|
53
|
-
* adapted to kiso's Message union; identity preservation, ADR-0007):
|
|
54
|
-
*
|
|
55
|
-
* 1. Find the boundary: the KEEP_RECENT_TURNS-th user message from the end.
|
|
56
|
-
* Recent turns stay fully intact — the model must reason about them.
|
|
57
|
-
* 2. Tool results BEFORE the boundary have their content replaced by a stub
|
|
58
|
-
* that keeps the name + char count (an information anchor, not a hole).
|
|
59
|
-
* 3. Idempotent: already-cleared content is never touched again (the
|
|
60
|
-
* compaction-regrowth incident's one-line fix, as a first-class rule).
|
|
61
|
-
* 4. Returns a NEW array; messages are immutable (ADR-0002).
|
|
62
|
-
*/
|
|
63
|
-
export const KEEP_RECENT_TURNS = 5;
|
|
64
|
-
export function microcompact(messages) {
|
|
65
|
-
const userIndices = [];
|
|
66
|
-
for (let i = 0; i < messages.length; i++) {
|
|
67
|
-
if (messages[i]?.role === "user")
|
|
68
|
-
userIndices.push(i);
|
|
69
|
-
}
|
|
70
|
-
if (userIndices.length <= KEEP_RECENT_TURNS) {
|
|
71
|
-
return { messages, clearedChars: 0, cleared: [] };
|
|
72
|
-
}
|
|
73
|
-
const recentBoundary = userIndices[userIndices.length - KEEP_RECENT_TURNS];
|
|
74
|
-
const nameByCallId = buildToolNameMap(messages);
|
|
75
|
-
let clearedChars = 0;
|
|
76
|
-
const cleared = [];
|
|
77
|
-
let changed = false;
|
|
78
|
-
const result = messages.map((msg, i) => {
|
|
79
|
-
if (i >= recentBoundary || msg.role !== "tool")
|
|
80
|
-
return msg;
|
|
81
|
-
if (typeof msg.content !== "string")
|
|
82
|
-
return msg; // binary content is untouched
|
|
83
|
-
if (!shouldClearContent(msg.content))
|
|
84
|
-
return msg; // idempotence gate
|
|
85
|
-
const toolName = nameByCallId.get(msg.callId) ?? "unknown";
|
|
86
|
-
const eventSeq = msg.eventSeq;
|
|
87
|
-
clearedChars += msg.content.length;
|
|
88
|
-
cleared.push({
|
|
89
|
-
...(eventSeq !== undefined ? { eventSeq } : {}),
|
|
90
|
-
callId: msg.callId,
|
|
91
|
-
content: `${CLEARED_MARKER_PREFIX} ${toolName} returned ${msg.content.length.toLocaleString()} chars — compacted`,
|
|
92
|
-
});
|
|
93
|
-
changed = true;
|
|
94
|
-
return {
|
|
95
|
-
...msg,
|
|
96
|
-
content: `${CLEARED_MARKER_PREFIX} ${toolName} returned ${msg.content.length.toLocaleString()} chars — compacted`,
|
|
97
|
-
};
|
|
98
|
-
});
|
|
99
|
-
return {
|
|
100
|
-
messages: changed ? result : messages,
|
|
101
|
-
clearedChars,
|
|
102
|
-
cleared,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
/** callId → tool name, from assistant tool_use blocks (for the stub). */
|
|
106
|
-
function buildToolNameMap(messages) {
|
|
107
|
-
const map = new Map();
|
|
108
|
-
for (const msg of messages) {
|
|
109
|
-
if (msg.role !== "assistant")
|
|
110
|
-
continue;
|
|
111
|
-
for (const block of msg.blocks) {
|
|
112
|
-
if (block.type === "tool_use")
|
|
113
|
-
map.set(block.callId, block.name);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return map;
|
|
117
|
-
}
|
package/dist/kernel/hooks.d.ts
CHANGED
|
@@ -39,9 +39,13 @@ export interface HookHost {
|
|
|
39
39
|
onPreTool?(call: ToolCallPayload, ctx: HookContext): Promise<PermissionDecision>;
|
|
40
40
|
/** Execute: rewrite the result a tool returns. */
|
|
41
41
|
onPostTool?(call: ToolCallPayload, result: ToolResult, ctx: HookContext): Promise<ToolResult>;
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* DEPRECATED (ADR-0044): the classic auto-compaction path is retired —
|
|
44
|
+
* the loop never fires these (the microcompact boundary replaced it).
|
|
45
|
+
* Kept so the extension contract type-checks; inert. Removed at 1.0.
|
|
46
|
+
*/
|
|
43
47
|
onPreCompact?(messages: readonly Message[], ctx: HookContext): Promise<void>;
|
|
44
|
-
/**
|
|
48
|
+
/** DEPRECATED (ADR-0044): see onPreCompact — never invoked. */
|
|
45
49
|
onPostCompact?(messages: readonly Message[], ctx: HookContext): Promise<void>;
|
|
46
50
|
/** Lifecycle: the loop paused (human decision pending). */
|
|
47
51
|
onPause?(reason: string, ctx: HookContext): Promise<void>;
|
package/dist/kernel/loop.d.ts
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* stored alongside it — every adapter call derives them via
|
|
10
10
|
* `projectMessages(log.all)` (kernel/project.ts). A fresh log encodes the
|
|
11
11
|
* seed `messages` into events first, so even a one-shot call replays
|
|
12
|
-
* exactly. Compaction is recorded as a `
|
|
13
|
-
*
|
|
12
|
+
* exactly. Compaction is recorded as a `microcompacted` boundary (old
|
|
13
|
+
* sessions: a `compacted` event) and re-applied by the projection, keeping
|
|
14
|
+
* the replay identical to the live run.
|
|
14
15
|
*
|
|
15
16
|
* Per iteration:
|
|
16
17
|
* assemble (onUserMessage / onPreLlm)
|
|
@@ -54,8 +55,13 @@ export interface LoopConfig {
|
|
|
54
55
|
readonly messages?: readonly Message[];
|
|
55
56
|
/** The run's event log. Pass the session's log to make this run durable. */
|
|
56
57
|
readonly log?: EventLog;
|
|
57
|
-
/**
|
|
58
|
-
*
|
|
58
|
+
/**
|
|
59
|
+
* DEPRECATED (ADR-0044): the classic auto-compaction path is retired —
|
|
60
|
+
* the loop no longer produces `compacted` events; the microcompact
|
|
61
|
+
* boundary (below) absorbed the responsibility. Kept so old configs
|
|
62
|
+
* type-check; IGNORED. Old sessions' `compacted` events still replay
|
|
63
|
+
* verbatim (the projection). Removed at 1.0.
|
|
64
|
+
*/
|
|
59
65
|
readonly compaction?: {
|
|
60
66
|
readonly thresholdTokens: number;
|
|
61
67
|
};
|
package/dist/kernel/loop.js
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* stored alongside it — every adapter call derives them via
|
|
10
10
|
* `projectMessages(log.all)` (kernel/project.ts). A fresh log encodes the
|
|
11
11
|
* seed `messages` into events first, so even a one-shot call replays
|
|
12
|
-
* exactly. Compaction is recorded as a `
|
|
13
|
-
*
|
|
12
|
+
* exactly. Compaction is recorded as a `microcompacted` boundary (old
|
|
13
|
+
* sessions: a `compacted` event) and re-applied by the projection, keeping
|
|
14
|
+
* the replay identical to the live run.
|
|
14
15
|
*
|
|
15
16
|
* Per iteration:
|
|
16
17
|
* assemble (onUserMessage / onPreLlm)
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
* re-stream that duplicates output or tool calls.
|
|
28
29
|
*/
|
|
29
30
|
import { isAdapterEvent } from "../protocol/adapter.js";
|
|
30
|
-
import { estimateTokens
|
|
31
|
+
import { estimateTokens } from "./compaction.js";
|
|
31
32
|
import { EventLog } from "./event-log.js";
|
|
32
33
|
import { ToolRegistry } from "../tools/registry.js";
|
|
33
34
|
import { validateArgs } from "../tools/validate.js";
|
|
@@ -124,31 +125,6 @@ export async function* loop(config) {
|
|
|
124
125
|
return;
|
|
125
126
|
}
|
|
126
127
|
turns += 1;
|
|
127
|
-
// ── Auto-compaction: ONLY this turn's NEWLY cleared results are
|
|
128
|
-
// persisted, keyed by the replaced tool-result event's seq; the
|
|
129
|
-
// projection applies them verbatim (A 组/D 组/五).
|
|
130
|
-
if (config.compaction && estimateTokens(messages) > config.compaction.thresholdTokens) {
|
|
131
|
-
if (hooks.onPreCompact)
|
|
132
|
-
await hooks.onPreCompact(messages, {}).catch(() => { });
|
|
133
|
-
const result = microcompact(messages);
|
|
134
|
-
// 五: the delta only — messages already carrying the clear marker
|
|
135
|
-
// are never re-cleared (microcompact's idempotence gate), so the
|
|
136
|
-
// same replacement is never recorded twice across turns.
|
|
137
|
-
const cleared = result.cleared.map((c) => ({
|
|
138
|
-
eventSeq: c.eventSeq,
|
|
139
|
-
callId: c.callId,
|
|
140
|
-
content: c.content,
|
|
141
|
-
}));
|
|
142
|
-
if (cleared.length > 0) {
|
|
143
|
-
const full = log.append({ type: "compacted", cleared });
|
|
144
|
-
if (hooks.onEvent)
|
|
145
|
-
await hooks.onEvent(full, {}).catch(() => { });
|
|
146
|
-
yield full;
|
|
147
|
-
messages = derive();
|
|
148
|
-
if (hooks.onPostCompact)
|
|
149
|
-
await hooks.onPostCompact(messages, {}).catch(() => { });
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
128
|
// ── C 区: one-shot microcompact boundary when over the threshold ──
|
|
153
129
|
if (config.microcompact !== undefined && estimateTokens(messages) > config.microcompact.thresholdTokens) {
|
|
154
130
|
const beforeSeq = microcompactBoundarySeq(log.all, config.microcompact.keepResults ?? KEEP_COMPACTABLE_RESULTS);
|
package/dist/kernel/project.d.ts
CHANGED
|
@@ -13,11 +13,14 @@
|
|
|
13
13
|
* derives from the log, so there is one store and the replay of `seq` 0..N
|
|
14
14
|
* reproduces the run exactly.
|
|
15
15
|
*
|
|
16
|
-
* Events with no message shape (usage, stop, thinking, terminal,
|
|
17
|
-
* own
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* Events with no message shape (usage, stop, thinking, terminal, the
|
|
17
|
+
* compaction events' own records) are skipped by the projection;
|
|
18
|
+
* `compacted` applies the EXACT persisted replacements verbatim — it never
|
|
19
|
+
* re-runs the compaction algorithm (a future version could differ, A 组/D
|
|
20
|
+
* 组); `microcompacted` boundaries re-derive the cleared view from the
|
|
21
|
+
* stream itself (deterministic and idempotent); `summarized` (ADR-0044)
|
|
22
|
+
* replaces its covered range with one assistant summary message. All
|
|
23
|
+
* three are persisted facts — the replay equals the live run. See ADR-0002.
|
|
21
24
|
*/
|
|
22
25
|
import type { Event } from "../protocol/events.js";
|
|
23
26
|
import type { EventInput } from "./event-log.js";
|
|
@@ -36,8 +39,8 @@ export declare const DO_NOT_COMPACT = "do-not-compact";
|
|
|
36
39
|
* replaying the same log always produces the same messages — BYTE FOR BYTE
|
|
37
40
|
* (D 区): the same event prefix derives the same message prefix; the only
|
|
38
41
|
* events that change already-derived messages are `microcompacted`
|
|
39
|
-
* boundaries, which are themselves persisted facts
|
|
40
|
-
* the same projection every time).
|
|
42
|
+
* boundaries and `summarized` facts, which are themselves persisted facts
|
|
43
|
+
* (their replay derives the same projection every time).
|
|
41
44
|
*
|
|
42
45
|
* Text block boundaries are preserved: `text_end` closes the current text
|
|
43
46
|
* block (an explicit boundary); `text_start` after a block opens a new one.
|
package/dist/kernel/project.js
CHANGED
|
@@ -13,11 +13,14 @@
|
|
|
13
13
|
* derives from the log, so there is one store and the replay of `seq` 0..N
|
|
14
14
|
* reproduces the run exactly.
|
|
15
15
|
*
|
|
16
|
-
* Events with no message shape (usage, stop, thinking, terminal,
|
|
17
|
-
* own
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* Events with no message shape (usage, stop, thinking, terminal, the
|
|
17
|
+
* compaction events' own records) are skipped by the projection;
|
|
18
|
+
* `compacted` applies the EXACT persisted replacements verbatim — it never
|
|
19
|
+
* re-runs the compaction algorithm (a future version could differ, A 组/D
|
|
20
|
+
* 组); `microcompacted` boundaries re-derive the cleared view from the
|
|
21
|
+
* stream itself (deterministic and idempotent); `summarized` (ADR-0044)
|
|
22
|
+
* replaces its covered range with one assistant summary message. All
|
|
23
|
+
* three are persisted facts — the replay equals the live run. See ADR-0002.
|
|
21
24
|
*/
|
|
22
25
|
/**
|
|
23
26
|
* C 区: tools whose output is eligible for microcompact clearing — reads,
|
|
@@ -53,8 +56,8 @@ function primaryArg(input) {
|
|
|
53
56
|
* replaying the same log always produces the same messages — BYTE FOR BYTE
|
|
54
57
|
* (D 区): the same event prefix derives the same message prefix; the only
|
|
55
58
|
* events that change already-derived messages are `microcompacted`
|
|
56
|
-
* boundaries, which are themselves persisted facts
|
|
57
|
-
* the same projection every time).
|
|
59
|
+
* boundaries and `summarized` facts, which are themselves persisted facts
|
|
60
|
+
* (their replay derives the same projection every time).
|
|
58
61
|
*
|
|
59
62
|
* Text block boundaries are preserved: `text_end` closes the current text
|
|
60
63
|
* block (an explicit boundary); `text_start` after a block opens a new one.
|
|
@@ -114,8 +117,49 @@ export function projectMessages(events) {
|
|
|
114
117
|
if (ev.type === "tool_call_end")
|
|
115
118
|
callMeta.set(ev.callId, { name: ev.name, input: ev.input });
|
|
116
119
|
}
|
|
120
|
+
// ADR-0044: summarized coverage — each `summarized` event covers the
|
|
121
|
+
// range (previous coversToSeq, coversToSeq] of ORDINARY events. The
|
|
122
|
+
// ranges are disjoint and in seq order; boundaries are turn boundaries
|
|
123
|
+
// by construction (summaryBoundarySeq cuts before a user_input), so a
|
|
124
|
+
// skipped event never splits a message. Each summary message renders
|
|
125
|
+
// AT ITS BOUNDARY — the first event after the covered range — NOT at
|
|
126
|
+
// the summarized event itself: the event sits at the log's END (the
|
|
127
|
+
// kept rounds live between the boundary and it), and the summary must
|
|
128
|
+
// precede the kept conversation in reading order.
|
|
129
|
+
const summaryRanges = [];
|
|
130
|
+
{
|
|
131
|
+
let prev = -1;
|
|
132
|
+
for (const ev of events) {
|
|
133
|
+
if (ev.type === "summarized") {
|
|
134
|
+
summaryRanges.push({ from: prev, to: ev.coversToSeq, summary: ev.summary });
|
|
135
|
+
prev = ev.coversToSeq;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const isCovered = (seq) => summaryRanges.some((r) => seq > r.from && seq <= r.to);
|
|
140
|
+
// Summaries render in range order as the pass crosses their boundaries.
|
|
141
|
+
let renderedSummaries = 0;
|
|
117
142
|
let explicitAssistant = false;
|
|
118
143
|
for (const ev of events) {
|
|
144
|
+
// ADR-0044: covered events are replaced by their summary — seed
|
|
145
|
+
// events (EventInput, no seq) precede any summarized fact and are
|
|
146
|
+
// never covered. The summarized events themselves are exempt (their
|
|
147
|
+
// own event sits inside the NEXT range's coverage; the boundary
|
|
148
|
+
// render below is where their message lands).
|
|
149
|
+
if (ev.type !== "summarized" && isCovered(ev.seq ?? -1))
|
|
150
|
+
continue;
|
|
151
|
+
// The boundary render: every range whose end this event crosses
|
|
152
|
+
// yields its assistant summary message, before this event renders.
|
|
153
|
+
while (renderedSummaries < summaryRanges.length &&
|
|
154
|
+
ev.seq !== undefined &&
|
|
155
|
+
ev.seq > summaryRanges[renderedSummaries].to) {
|
|
156
|
+
flushAssistant();
|
|
157
|
+
out.push({
|
|
158
|
+
role: "assistant",
|
|
159
|
+
blocks: [{ type: "text", text: summaryRanges[renderedSummaries].summary }],
|
|
160
|
+
});
|
|
161
|
+
renderedSummaries += 1;
|
|
162
|
+
}
|
|
119
163
|
switch (ev.type) {
|
|
120
164
|
case "user_input": {
|
|
121
165
|
// 六: the final replacement renders HERE, at the input's own
|
|
@@ -270,6 +314,11 @@ export function projectMessages(events) {
|
|
|
270
314
|
flushAssistant();
|
|
271
315
|
pendingReasoning = (pendingReasoning ?? "") + ev.text;
|
|
272
316
|
break;
|
|
317
|
+
case "summarized":
|
|
318
|
+
// ADR-0044: this event produced nothing itself — its summary
|
|
319
|
+
// message was rendered at the boundary render above, in the
|
|
320
|
+
// covered range's position.
|
|
321
|
+
break;
|
|
273
322
|
case "usage":
|
|
274
323
|
case "stop":
|
|
275
324
|
case "terminal":
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 — the /compact summary layer (ADR-0044): the MODEL-GENERATED half of
|
|
3
|
+
* context economy. The mechanical half (microcompact, compaction.ts)
|
|
4
|
+
* clears TOOL RESULTS only; this layer compresses the CONVERSATION itself
|
|
5
|
+
* into one durable `summarized` event per call, replacing the covered
|
|
6
|
+
* range with a single assistant summary message in the projection.
|
|
7
|
+
*
|
|
8
|
+
* The summary call is OFF-LOOP: it goes through the session's OWN adapter
|
|
9
|
+
* (no new dependency), writes no events, and never touches the log — a
|
|
10
|
+
* failure throws, the caller reports it honestly, and the session is
|
|
11
|
+
* unchanged ("nothing happened"). Only the generated `summarized` event
|
|
12
|
+
* lands on disk; the original events stay there forever.
|
|
13
|
+
*/
|
|
14
|
+
import type { AbortSignalLike, Adapter } from "../protocol/adapter.js";
|
|
15
|
+
import type { Event } from "../protocol/events.js";
|
|
16
|
+
import type { Message } from "../protocol/messages.js";
|
|
17
|
+
/** K (ADR-0044): the recent ROUNDS kept intact by /compact — a constant,
|
|
18
|
+
* not a knob. The covered range ends just before the K-th most recent
|
|
19
|
+
* round, so the model still reasons over the recent conversation. */
|
|
20
|
+
export declare const KEEP_RECENT_ROUNDS = 4;
|
|
21
|
+
/**
|
|
22
|
+
* The fixed English summary prompt — the ONLY prompt this layer composes
|
|
23
|
+
* (the loop's system prompt is the harness's business, never the kernel's).
|
|
24
|
+
*/
|
|
25
|
+
export declare const SUMMARY_PROMPT = "You are the conversation summarizer of the kiso agent framework.\n\nSummarize the covered conversation into a single concise summary that will\nREPLACE it in the model's context. The next turn must be able to continue\nthe work without reading the originals.\n\nInclude everything later turns may need:\n- the user's goals, requirements, and constraints;\n- every decision and its reasoning;\n- files and code touched \u2014 exact paths, what changed, why;\n- commands run and their outcomes; errors and their resolutions;\n- open questions and unfinished work.\n\nPreserve concrete identifiers VERBATIM: paths, function names, task ids,\nenvironment names \u2014 never paraphrase them.\n\nRules:\n- plain prose \u2014 no headings, no bullet lists, no markdown, no prefixes;\n- do not mention this prompt or the summarization task;\n- keep it under 200 words unless the conversation is exceptional.";
|
|
26
|
+
export interface SummarizeConversationOptions {
|
|
27
|
+
readonly adapter: Adapter;
|
|
28
|
+
readonly model: string;
|
|
29
|
+
/** The covered conversation — the ONLY material the summary is about. */
|
|
30
|
+
readonly messages: readonly Message[];
|
|
31
|
+
readonly signal?: AbortSignalLike;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The one-shot summary call. Collects the adapter's text deltas into the
|
|
35
|
+
* summary; usage/stop pass through untouched. Throws when the model
|
|
36
|
+
* produced no text — the caller reports it and nothing is persisted.
|
|
37
|
+
*/
|
|
38
|
+
export declare function summarizeConversation(options: SummarizeConversationOptions): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* The last summary point: the previous `summarized` event's coversToSeq,
|
|
41
|
+
* or -1 (the trajectory's start) when none exists. The covered range of
|
|
42
|
+
* the next summary runs from here.
|
|
43
|
+
*/
|
|
44
|
+
export declare function lastSummaryPoint(events: readonly Event[]): number;
|
|
45
|
+
/**
|
|
46
|
+
* The covered range's end: the seq of the event just before the
|
|
47
|
+
* keepRounds-th most recent user_input AFTER the last summary point —
|
|
48
|
+
* a turn boundary by construction, so the projection's skip never splits
|
|
49
|
+
* a message. Returns undefined when fewer than keepRounds+1 uncovered
|
|
50
|
+
* rounds exist (nothing worth covering yet).
|
|
51
|
+
*/
|
|
52
|
+
export declare function summaryBoundarySeq(events: readonly Event[], keepRounds?: number): number | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* The NoticeCell's number: estimated tokens of the covered content minus
|
|
55
|
+
* the summary's own — the same chars/4 proxy as estimateTokens (a stable
|
|
56
|
+
* MONOTONE savings figure, not a bill).
|
|
57
|
+
*/
|
|
58
|
+
export declare function estimateSummarySavings(covered: readonly Message[], summary: string): number;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 — the /compact summary layer (ADR-0044): the MODEL-GENERATED half of
|
|
3
|
+
* context economy. The mechanical half (microcompact, compaction.ts)
|
|
4
|
+
* clears TOOL RESULTS only; this layer compresses the CONVERSATION itself
|
|
5
|
+
* into one durable `summarized` event per call, replacing the covered
|
|
6
|
+
* range with a single assistant summary message in the projection.
|
|
7
|
+
*
|
|
8
|
+
* The summary call is OFF-LOOP: it goes through the session's OWN adapter
|
|
9
|
+
* (no new dependency), writes no events, and never touches the log — a
|
|
10
|
+
* failure throws, the caller reports it honestly, and the session is
|
|
11
|
+
* unchanged ("nothing happened"). Only the generated `summarized` event
|
|
12
|
+
* lands on disk; the original events stay there forever.
|
|
13
|
+
*/
|
|
14
|
+
import { estimateTokens } from "./compaction.js";
|
|
15
|
+
/** K (ADR-0044): the recent ROUNDS kept intact by /compact — a constant,
|
|
16
|
+
* not a knob. The covered range ends just before the K-th most recent
|
|
17
|
+
* round, so the model still reasons over the recent conversation. */
|
|
18
|
+
export const KEEP_RECENT_ROUNDS = 4;
|
|
19
|
+
/**
|
|
20
|
+
* The fixed English summary prompt — the ONLY prompt this layer composes
|
|
21
|
+
* (the loop's system prompt is the harness's business, never the kernel's).
|
|
22
|
+
*/
|
|
23
|
+
export const SUMMARY_PROMPT = `You are the conversation summarizer of the kiso agent framework.
|
|
24
|
+
|
|
25
|
+
Summarize the covered conversation into a single concise summary that will
|
|
26
|
+
REPLACE it in the model's context. The next turn must be able to continue
|
|
27
|
+
the work without reading the originals.
|
|
28
|
+
|
|
29
|
+
Include everything later turns may need:
|
|
30
|
+
- the user's goals, requirements, and constraints;
|
|
31
|
+
- every decision and its reasoning;
|
|
32
|
+
- files and code touched — exact paths, what changed, why;
|
|
33
|
+
- commands run and their outcomes; errors and their resolutions;
|
|
34
|
+
- open questions and unfinished work.
|
|
35
|
+
|
|
36
|
+
Preserve concrete identifiers VERBATIM: paths, function names, task ids,
|
|
37
|
+
environment names — never paraphrase them.
|
|
38
|
+
|
|
39
|
+
Rules:
|
|
40
|
+
- plain prose — no headings, no bullet lists, no markdown, no prefixes;
|
|
41
|
+
- do not mention this prompt or the summarization task;
|
|
42
|
+
- keep it under 200 words unless the conversation is exceptional.`;
|
|
43
|
+
/**
|
|
44
|
+
* The one-shot summary call. Collects the adapter's text deltas into the
|
|
45
|
+
* summary; usage/stop pass through untouched. Throws when the model
|
|
46
|
+
* produced no text — the caller reports it and nothing is persisted.
|
|
47
|
+
*/
|
|
48
|
+
export async function summarizeConversation(options) {
|
|
49
|
+
const { adapter, model, messages } = options;
|
|
50
|
+
let text = "";
|
|
51
|
+
for await (const ev of adapter.stream({
|
|
52
|
+
model,
|
|
53
|
+
messages,
|
|
54
|
+
systemPrompt: SUMMARY_PROMPT,
|
|
55
|
+
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
56
|
+
})) {
|
|
57
|
+
if (ev.type === "text_delta")
|
|
58
|
+
text += ev.text;
|
|
59
|
+
}
|
|
60
|
+
const trimmed = text.trim();
|
|
61
|
+
if (trimmed === "") {
|
|
62
|
+
throw new Error("the summary call produced no text");
|
|
63
|
+
}
|
|
64
|
+
return trimmed;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The last summary point: the previous `summarized` event's coversToSeq,
|
|
68
|
+
* or -1 (the trajectory's start) when none exists. The covered range of
|
|
69
|
+
* the next summary runs from here.
|
|
70
|
+
*/
|
|
71
|
+
export function lastSummaryPoint(events) {
|
|
72
|
+
let prev = -1;
|
|
73
|
+
for (const ev of events) {
|
|
74
|
+
if (ev.type === "summarized" && ev.coversToSeq > prev)
|
|
75
|
+
prev = ev.coversToSeq;
|
|
76
|
+
}
|
|
77
|
+
return prev;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The covered range's end: the seq of the event just before the
|
|
81
|
+
* keepRounds-th most recent user_input AFTER the last summary point —
|
|
82
|
+
* a turn boundary by construction, so the projection's skip never splits
|
|
83
|
+
* a message. Returns undefined when fewer than keepRounds+1 uncovered
|
|
84
|
+
* rounds exist (nothing worth covering yet).
|
|
85
|
+
*/
|
|
86
|
+
export function summaryBoundarySeq(events, keepRounds = KEEP_RECENT_ROUNDS) {
|
|
87
|
+
const prevPoint = lastSummaryPoint(events);
|
|
88
|
+
const uncoveredInputs = [];
|
|
89
|
+
for (const ev of events) {
|
|
90
|
+
if (ev.type === "user_input" && ev.seq > prevPoint)
|
|
91
|
+
uncoveredInputs.push(ev.seq);
|
|
92
|
+
}
|
|
93
|
+
if (uncoveredInputs.length <= keepRounds)
|
|
94
|
+
return undefined;
|
|
95
|
+
// The input at m - keepRounds opens the FIRST KEPT round; everything
|
|
96
|
+
// before it (m - keepRounds ≥ 1 covered rounds) is summarizable.
|
|
97
|
+
return uncoveredInputs[uncoveredInputs.length - keepRounds] - 1;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The NoticeCell's number: estimated tokens of the covered content minus
|
|
101
|
+
* the summary's own — the same chars/4 proxy as estimateTokens (a stable
|
|
102
|
+
* MONOTONE savings figure, not a bill).
|
|
103
|
+
*/
|
|
104
|
+
export function estimateSummarySavings(covered, summary) {
|
|
105
|
+
return Math.max(0, estimateTokens(covered) - Math.ceil(summary.length / 4));
|
|
106
|
+
}
|
|
@@ -51,11 +51,11 @@ export interface StreamOptions {
|
|
|
51
51
|
/**
|
|
52
52
|
* The NARROW event set an adapter may produce (五). Everything else in the
|
|
53
53
|
* union is kernel-owned — `terminal`, `tool_execution_*`, `permission_*`,
|
|
54
|
-
* `user_input`, `compacted`, `
|
|
55
|
-
* `assistant_start`/`assistant_end` — and a
|
|
56
|
-
* those is FORGING kernel state. The type
|
|
57
|
-
* the loop ALSO enforces it at runtime (JS
|
|
58
|
-
* not trusted — see kernel/loop.ts).
|
|
54
|
+
* `user_input`, `compacted`, `summarized`, `uncertain_pending`,
|
|
55
|
+
* `user_input_replaced`, `assistant_start`/`assistant_end` — and a
|
|
56
|
+
* provider that yields any of those is FORGING kernel state. The type
|
|
57
|
+
* narrows the adapter contract; the loop ALSO enforces it at runtime (JS
|
|
58
|
+
* and third-party adapters are not trusted — see kernel/loop.ts).
|
|
59
59
|
*/
|
|
60
60
|
export type AdapterEvent = Extract<Event, {
|
|
61
61
|
type: "text_start" | "text_delta" | "text_end" | "tool_call_start" | "tool_call_input_delta" | "tool_call_end" | "thinking" | "usage" | "stop";
|
|
@@ -327,6 +327,29 @@ export interface MicroCompactEvent {
|
|
|
327
327
|
readonly type: "microcompacted";
|
|
328
328
|
readonly beforeSeq: number;
|
|
329
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* ADR-0044 — a model-generated summary replaced the covered conversation
|
|
332
|
+
* range. `coversToSeq` is the seq of the LAST covered event; the covered
|
|
333
|
+
* range runs from just past the previous `summarized` event's coversToSeq
|
|
334
|
+
* (or the trajectory's start for the first) up to coversToSeq. The
|
|
335
|
+
* projection replaces exactly those events with ONE assistant summary
|
|
336
|
+
* message (`summary`); every `summarized` event always renders its own
|
|
337
|
+
* message. Byte-stable: a summarized event is a persisted fact, so the
|
|
338
|
+
* same events derive the same messages on every replay.
|
|
339
|
+
*
|
|
340
|
+
* The summary is generated OFF-LOOP through the session's own adapter —
|
|
341
|
+
* the summary request itself never enters the log, and a failed summary
|
|
342
|
+
* never leaves a record ("nothing happened"). The ORIGINAL events stay on
|
|
343
|
+
* disk forever: /last, /think, and the raw log still reach them.
|
|
344
|
+
*/
|
|
345
|
+
export interface SummarizedEvent {
|
|
346
|
+
readonly seq: number;
|
|
347
|
+
readonly type: "summarized";
|
|
348
|
+
/** The last covered event's seq; the range is (previous coversToSeq, coversToSeq]. */
|
|
349
|
+
readonly coversToSeq: number;
|
|
350
|
+
/** The model's compression — replaces the covered range in the projection. */
|
|
351
|
+
readonly summary: string;
|
|
352
|
+
}
|
|
330
353
|
/** Extended-thinking content. Providers without it emit nothing here. */
|
|
331
354
|
export interface Thinking {
|
|
332
355
|
readonly seq: number;
|
|
@@ -417,7 +440,7 @@ export interface TerminalEvent {
|
|
|
417
440
|
* The union. Consume it with a `switch (event.type)`; with
|
|
418
441
|
* `strictNullChecks` on, an unhandled variant is a compile error.
|
|
419
442
|
*/
|
|
420
|
-
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;
|
|
443
|
+
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 | SummarizedEvent | TerminalEvent;
|
|
421
444
|
/**
|
|
422
445
|
* Runtime type guard for the union. The store validates every JSONL record
|
|
423
446
|
* with it: valid JSON that is not a kiso event is corruption, not history.
|
package/dist/protocol/events.js
CHANGED
|
@@ -214,6 +214,12 @@ const EVENT_VALIDATORS = {
|
|
|
214
214
|
permission_expired: (v) => typeof v.decisionId === "string" && typeof v.reason === "string",
|
|
215
215
|
uncertain_pending: (v) => typeof v.executionId === "string" && typeof v.callId === "string" && typeof v.name === "string" && typeof v.error === "string",
|
|
216
216
|
microcompacted: (v) => isNonNegativeInt(v.beforeSeq),
|
|
217
|
+
// ADR-0044: coversToSeq is a seq boundary BEFORE this event (a summary
|
|
218
|
+
// covers only what preceded it), and the summary is non-empty text.
|
|
219
|
+
summarized: (v) => isNonNegativeInt(v.coversToSeq) &&
|
|
220
|
+
typeof v.summary === "string" &&
|
|
221
|
+
v.summary.length > 0 &&
|
|
222
|
+
v.coversToSeq < v.seq,
|
|
217
223
|
user_input_replaced: (v) => isNonNegativeInt(v.replaces) && (v.content === null || isContent(v.content)) && isSource(v),
|
|
218
224
|
terminal: (v) => isTerminal(v.outcome),
|
|
219
225
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
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.20",
|
|
37
37
|
"@types/node": "^26.1.2",
|
|
38
38
|
"typescript": "^5.7.2",
|
|
39
39
|
"vitest": "^3.0.0"
|