@alisio/core 0.1.0-alpha.6 → 0.1.0-alpha.7

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.
@@ -44,6 +44,31 @@ export interface CompactionPlan {
44
44
  kept: Message[];
45
45
  }
46
46
  export declare function planCompaction(messages: Message[], options?: CompactionOptions): CompactionPlan | undefined;
47
+ /** Marker appended to message content cut by the context-budget reduction. */
48
+ export declare const TRUNCATION_MARKER = "\u2026 [truncated by context budget]";
49
+ /** Default per-message cap for kept tool result text after a failed compaction. */
50
+ export declare const MAX_TOOL_RESULT_CHARS = 8000;
51
+ /** Default per-message cap for kept user/assistant text after a failed compaction. */
52
+ export declare const MAX_TEXT_CHARS = 16000;
53
+ export interface MessageReductionOptions {
54
+ /** Keep at most this many characters of text per tool result (default 8_000). */
55
+ maxToolResultChars?: number;
56
+ /** Keep at most this many characters per user/assistant text (default 16_000). */
57
+ maxTextChars?: number;
58
+ }
59
+ export interface MessageReduction {
60
+ messages: Message[];
61
+ /** Number of messages whose content was truncated. */
62
+ truncated: number;
63
+ }
64
+ /**
65
+ * Cuts oversized retained content so the session fits the context-budget hard limit after a
66
+ * compaction could not. Only message CONTENT changes (user/assistant `text` and the text parts
67
+ * of tool results); roles, call IDs, order and message boundaries stay identical, so the
68
+ * transcript remains valid and replayable and a tool call is never separated from its results.
69
+ * Checkpoint summaries (`summary: true`) are bounded by design and never truncated.
70
+ */
71
+ export declare function reduceMessageSizes(messages: Message[], options?: MessageReductionOptions): MessageReduction;
47
72
  /**
48
73
  * Plain-text transcript used as summarizer input. Provider continuation data (for example
49
74
  * encrypted reasoning) is opaque and is intentionally not included.
@@ -91,6 +91,60 @@ export function planCompaction(messages, options = {}) {
91
91
  return { cut, summarized: messages.slice(0, cut), kept: messages.slice(cut) };
92
92
  }
93
93
  const clip = (text, max) => text.length > max ? `${text.slice(0, max)}… [truncated]` : text;
94
+ /** Marker appended to message content cut by the context-budget reduction. */
95
+ export const TRUNCATION_MARKER = "… [truncated by context budget]";
96
+ /** Default per-message cap for kept tool result text after a failed compaction. */
97
+ export const MAX_TOOL_RESULT_CHARS = 8_000;
98
+ /** Default per-message cap for kept user/assistant text after a failed compaction. */
99
+ export const MAX_TEXT_CHARS = 16_000;
100
+ /**
101
+ * Cuts oversized retained content so the session fits the context-budget hard limit after a
102
+ * compaction could not. Only message CONTENT changes (user/assistant `text` and the text parts
103
+ * of tool results); roles, call IDs, order and message boundaries stay identical, so the
104
+ * transcript remains valid and replayable and a tool call is never separated from its results.
105
+ * Checkpoint summaries (`summary: true`) are bounded by design and never truncated.
106
+ */
107
+ export function reduceMessageSizes(messages, options = {}) {
108
+ const maxToolResultChars = options.maxToolResultChars ?? MAX_TOOL_RESULT_CHARS;
109
+ const maxTextChars = options.maxTextChars ?? MAX_TEXT_CHARS;
110
+ let truncated = 0;
111
+ const reduced = messages.map((m) => {
112
+ if (m.role === "user") {
113
+ if (m.summary || m.text.length <= maxTextChars)
114
+ return m;
115
+ truncated++;
116
+ return { ...m, text: `${m.text.slice(0, maxTextChars)}\n${TRUNCATION_MARKER}` };
117
+ }
118
+ if (m.role === "assistant") {
119
+ if (m.text.length <= maxTextChars)
120
+ return m;
121
+ truncated++;
122
+ return { ...m, text: `${m.text.slice(0, maxTextChars)}\n${TRUNCATION_MARKER}` };
123
+ }
124
+ let used = 0;
125
+ let changed = false;
126
+ const content = m.result.content.map((part) => {
127
+ const room = maxToolResultChars - used;
128
+ const text = room <= 0
129
+ ? TRUNCATION_MARKER
130
+ : part.text.length > room
131
+ ? `${part.text.slice(0, room)}\n${TRUNCATION_MARKER}`
132
+ : part.text;
133
+ if (text !== part.text) {
134
+ changed = true;
135
+ used = maxToolResultChars;
136
+ }
137
+ else
138
+ used += part.text.length;
139
+ return { type: "text", text };
140
+ });
141
+ if (!changed)
142
+ return m;
143
+ truncated++;
144
+ return { ...m, result: { ...m.result, content } };
145
+ });
146
+ return truncated ? { messages: reduced, truncated } : { messages, truncated: 0 };
147
+ }
94
148
  /**
95
149
  * Plain-text transcript used as summarizer input. Provider continuation data (for example
96
150
  * encrypted reasoning) is opaque and is intentionally not included.
@@ -56,6 +56,12 @@ export interface SessionStore {
56
56
  * rest verbatim and in order. Callers must never split a tool call from its results.
57
57
  */
58
58
  compact(id: string, replaced: number, summary: Message): void;
59
+ /**
60
+ * Atomically replace the whole active history with `messages` (like `compact` without a
61
+ * summary): used after a context-budget reduction so the persisted transcript matches what
62
+ * was actually sent. Callers must never split a tool call from its results.
63
+ */
64
+ overwrite(id: string, messages: Message[]): void;
59
65
  acquire(id: string): void;
60
66
  release(id: string): void;
61
67
  beginCall(session: string, call: ToolCall): void;
@@ -1,5 +1,5 @@
1
1
  import { textResult, } from "@alisio/sdk";
2
- import { checkpointInstructions, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, shouldCompactContext, summarize, summaryMessage, } from "./compaction.js";
2
+ import { checkpointInstructions, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, reduceMessageSizes, shouldCompactContext, summarize, summaryMessage, } from "./compaction.js";
3
3
  /**
4
4
  * Default cumulative token budget for one run: several context windows (each turn re-sends the
5
5
  * context), clamped; 1M tokens when the window is unknown.
@@ -263,6 +263,7 @@ export class AgentRunner {
263
263
  async run(sessionId, prompt, signal, options = {}) {
264
264
  const controller = this.claim(sessionId);
265
265
  const o = this.options, emit = this.emitter(sessionId);
266
+ const limit = o.maxContextChars ?? 160_000;
266
267
  let tokens = 0, lastText = "", acquired = false;
267
268
  const usageTotal = { input: 0, output: 0 };
268
269
  const timeout = AbortSignal.timeout(options.timeoutMs ?? o.timeoutMs ?? 300_000);
@@ -351,13 +352,27 @@ export class AgentRunner {
351
352
  // estimate (`chars / 4 >= maxContextChars / 4`). `maxContextChars` below stays as the
352
353
  // post-compaction hard limit.
353
354
  if (compaction.auto !== false &&
354
- shouldCompactContext(used(), chars(), o.contextWindow?.(model), o.maxContextChars ?? 160_000, compaction.threshold ?? 0.85)) {
355
+ shouldCompactContext(used(), chars(), o.contextWindow?.(model), limit, compaction.threshold ?? 0.85)) {
355
356
  await this.compactLocked(sessionId, emit, model, combined, "auto");
356
357
  instructions = await withPersona();
357
358
  messages = o.store.messages(sessionId);
358
359
  }
359
- if (chars() > (o.maxContextChars ?? 160_000))
360
- throw new Error("Context budget exceeded and compaction could not reduce it; start a new session.");
360
+ // A compaction that still leaves the session over the hard limit (typically a kept tail
361
+ // holding huge tool outputs) is reduced in place instead of failing outright: oversized
362
+ // kept content is cut per message (roles/callIds/boundaries untouched, so the transcript
363
+ // stays valid and replayable). Only if even the reduced tail exceeds the limit the run
364
+ // fails with an actionable error; the reduction is persisted either way, so the session
365
+ // stays usable for later prompts.
366
+ if (chars() > limit) {
367
+ const reduction = reduceMessageSizes(messages);
368
+ if (reduction.truncated > 0) {
369
+ o.store.overwrite(sessionId, reduction.messages);
370
+ messages = reduction.messages;
371
+ emit("context_reduced", { messages: reduction.truncated });
372
+ }
373
+ if (chars() > limit)
374
+ throw new Error(`Context budget exceeded and compaction could not reduce it (approximately ${chars()} characters; limit ${limit}). Run /compact, trim large tool outputs, or start a new session.`);
375
+ }
361
376
  let completion;
362
377
  let truncated = false;
363
378
  let usage;
@@ -25,6 +25,7 @@ export declare class SQLiteStore implements SessionStore {
25
25
  append(id: string, message: Message): void;
26
26
  setModel(id: string, model: string): void;
27
27
  compact(id: string, replaced: number, summary: Message): void;
28
+ overwrite(id: string, messages: Message[]): void;
28
29
  acquire(id: string): void;
29
30
  release(id: string): void;
30
31
  beginCall(id: string, call: ToolCall): void;
@@ -168,6 +168,15 @@ export class SQLiteStore {
168
168
  this.db.prepare("INSERT INTO messages(session,body) VALUES(?,?)").run(id, row.body);
169
169
  });
170
170
  }
171
+ overwrite(id, messages) {
172
+ this.db.transaction(() => {
173
+ // Retire the active rows (originals stay marked compacted for auditing), then rewrite
174
+ // the reduced transcript in the same order.
175
+ this.db.prepare("UPDATE messages SET compacted=1 WHERE session=? AND compacted=0").run(id);
176
+ for (const message of messages)
177
+ this.append(id, message);
178
+ });
179
+ }
171
180
  acquire(id) {
172
181
  this.db.transaction(() => {
173
182
  this.get(id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alisio/core",
3
- "version": "0.1.0-alpha.6",
3
+ "version": "0.1.0-alpha.7",
4
4
  "description": "Embeddable provider-agnostic Alisio agent core: tool loop, context compaction, local tools, MCP, plugin host and SQLite session store. Runs on Node >=22.16 and Bun.",
5
5
  "author": "Gustavo Gutiérrez",
6
6
  "license": "MIT",