@vincemakes/kiso-core 0.1.0 → 0.1.1

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.
@@ -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;
@@ -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,
@@ -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.
@@ -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.
@@ -68,6 +100,12 @@ export function projectMessages(events) {
68
100
  });
69
101
  }
70
102
  }
103
+ // C 区: callId → {name, input} for the microcompact placeholder.
104
+ const callMeta = new Map();
105
+ for (const ev of events) {
106
+ if (ev.type === "tool_call_end")
107
+ callMeta.set(ev.callId, { name: ev.name, input: ev.input });
108
+ }
71
109
  let explicitAssistant = false;
72
110
  for (const ev of events) {
73
111
  switch (ev.type) {
@@ -171,6 +209,27 @@ export function projectMessages(events) {
171
209
  out.push(message);
172
210
  break;
173
211
  }
212
+ case "microcompacted": {
213
+ flushAssistant();
214
+ // C 区: replace every eligible OLD tool result with the fixed
215
+ // placeholder. Eligibility: the result's own event seq <= the
216
+ // boundary, its tool in the whitelist, and no do-not-compact
217
+ // tag. Deterministic — the boundary event IS the decision.
218
+ const replaced = out.map((m) => {
219
+ if (m.role !== "tool")
220
+ return m;
221
+ if (m.eventSeq === undefined || m.eventSeq > ev.beforeSeq)
222
+ return m;
223
+ const meta = callMeta.get(m.callId);
224
+ if (meta === undefined || !MICROCOMPACTABLE.has(meta.name))
225
+ return m;
226
+ if ((m.tags ?? []).includes(DO_NOT_COMPACT))
227
+ return m;
228
+ return { ...m, content: microPlaceholder(meta.name, primaryArg(meta.input)) };
229
+ });
230
+ out.splice(0, out.length, ...replaced);
231
+ break;
232
+ }
174
233
  case "compacted": {
175
234
  flushAssistant();
176
235
  // Apply the EXACT persisted replacements — never re-run the
@@ -200,6 +259,7 @@ export function projectMessages(events) {
200
259
  case "usage":
201
260
  case "stop":
202
261
  case "terminal":
262
+ case "microcompacted":
203
263
  case "tool_execution_started":
204
264
  case "tool_execution_succeeded":
205
265
  case "tool_execution_failed":
@@ -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.
@@ -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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-core",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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.0",
36
+ "@vincemakes/kiso-evals": "0.1.1",
37
37
  "@types/node": "^26.1.2",
38
38
  "typescript": "^5.7.2",
39
39
  "vitest": "^3.0.0"