@alisio/core 0.1.0-alpha.3 → 0.1.0-alpha.5

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.
@@ -167,6 +167,12 @@ export declare function createApplication(options?: AppOptions): Promise<{
167
167
  runner: AgentRunner;
168
168
  herdr: HerdrBridge;
169
169
  contextWindow: (model: string) => number | undefined;
170
+ /** Effective context budget + compaction point shared by the runner and the TUI bar. */
171
+ contextBudget: (model: string) => {
172
+ total: number;
173
+ basis: "window" | "chars";
174
+ compactionAt: number;
175
+ };
170
176
  loadModels: (signal: AbortSignal) => Promise<ModelInfo[]>;
171
177
  /** Creates a provider for validation/model discovery without changing the active provider. */
172
178
  probeProvider(id: string, profile: Record<string, import("@alisio/sdk").ProviderConfigurationValue>, credentials: Record<string, string>, signal: AbortSignal): Promise<ModelInfo[]>;
@@ -635,6 +635,8 @@ export async function createApplication(options = {}) {
635
635
  runner,
636
636
  herdr,
637
637
  contextWindow,
638
+ /** Effective context budget + compaction point shared by the runner and the TUI bar. */
639
+ contextBudget: (model) => runner.contextBudget(model),
638
640
  loadModels,
639
641
  /** Creates a provider for validation/model discovery without changing the active provider. */
640
642
  async probeProvider(id, profile, credentials, signal) {
package/dist/config.d.ts CHANGED
@@ -149,6 +149,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
149
149
  auto: z.ZodDefault<z.ZodBoolean>;
150
150
  threshold: z.ZodDefault<z.ZodNumber>;
151
151
  keepTurns: z.ZodDefault<z.ZodNumber>;
152
+ maxOutputTokens: z.ZodDefault<z.ZodNumber>;
152
153
  }, z.core.$strict>>;
153
154
  websearch: z.ZodDefault<z.ZodObject<{
154
155
  provider: z.ZodOptional<z.ZodEnum<{
@@ -206,6 +207,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
206
207
  auto: boolean;
207
208
  threshold: number;
208
209
  keepTurns: number;
210
+ maxOutputTokens: number;
209
211
  };
210
212
  websearch: {
211
213
  provider?: "brave" | "duckduckgo-instant" | "native" | "searxng" | "serpapi" | "tavily" | undefined;
@@ -337,6 +339,7 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
337
339
  auto: boolean;
338
340
  threshold: number;
339
341
  keepTurns: number;
342
+ maxOutputTokens: number;
340
343
  };
341
344
  websearch: {
342
345
  provider?: "brave" | "duckduckgo-instant" | "native" | "searxng" | "serpapi" | "tavily" | undefined;
package/dist/config.js CHANGED
@@ -122,9 +122,14 @@ const configObjectSchema = z
122
122
  auto: z.boolean().default(true),
123
123
  threshold: z.number().min(0.1).max(0.99).default(0.85),
124
124
  keepTurns: z.number().int().min(0).max(20).default(2),
125
+ /**
126
+ * Output token budget for the summarizer call. Larger than the agent-loop budget on
127
+ * purpose: a truncated summary is kept as partial, never re-run.
128
+ */
129
+ maxOutputTokens: z.number().int().positive().default(16_000),
125
130
  })
126
131
  .strict()
127
- .default(() => ({ auto: true, threshold: 0.85, keepTurns: 2 })),
132
+ .default(() => ({ auto: true, threshold: 0.85, keepTurns: 2, maxOutputTokens: 16_000 })),
128
133
  websearch: z
129
134
  .object({
130
135
  provider: z
@@ -2,6 +2,36 @@ import type { CompactionCheckpoint, CompletionRequest, Message, ModelProvider }
2
2
  /** Rough provider-agnostic estimate: ~4 characters per token. */
3
3
  export declare function estimateTokens(value: string | Message[]): number;
4
4
  export declare function shouldCompact(used: number, window: number | undefined, threshold: number): boolean;
5
+ /**
6
+ * Declared context windows above this many tokens are treated as unknown for auto-compaction and
7
+ * for the context bar: a `window * threshold` trigger would dwarf the char-budget fallback and
8
+ * hide real pressure (e.g. an 8M-token window with a default 160k-char budget would never even
9
+ * reach 2% before running out of space). Documented in docs/compaction.md.
10
+ */
11
+ export declare const MAX_TRUSTED_WINDOW = 2000000;
12
+ export interface ContextBudget {
13
+ /** Effective total in tokens the context bar and auto-compaction measure against. */
14
+ total: number;
15
+ /** What the total is derived from: the model window, or the char-budget fallback. */
16
+ basis: "window" | "chars";
17
+ }
18
+ /**
19
+ * The single effective budget shared by the runner's auto-compaction and the TUI context bar.
20
+ * A known window (>= 1 token and <= MAX_TRUSTED_WINDOW) is used directly; otherwise the char
21
+ * budget is converted to estimated tokens (about 4 characters per token) so the bar fills where
22
+ * the char fallback actually compacts instead of showing an almost-empty bar next to an early
23
+ * compaction (the DeepSeek ~1M-window vs 160k-char budget mismatch).
24
+ */
25
+ export declare function effectiveContextBudget(window: number | undefined, maxContextChars: number): ContextBudget;
26
+ /**
27
+ * Whether to auto-compact before the next model call. Uses ONE effective budget: a known window
28
+ * triggers at `used >= window * threshold`; an unknown or guard-bounded window falls back to the
29
+ * char budget (`estimatedChars / 4 >= maxContextChars / 4`, keeping the old ~4-characters-per-token
30
+ * protection). `used` (provider-reported when available) feeds the window branch and the context
31
+ * bar; `estimatedChars` is the raw character count of the next request and feeds the fallback, so
32
+ * a provider's token report can never push an unknown-window session into needless compaction.
33
+ */
34
+ export declare function shouldCompactContext(used: number, estimatedChars: number, window: number | undefined, maxContextChars: number, threshold: number): boolean;
5
35
  export interface CompactionOptions {
6
36
  /** Recent user turns kept verbatim. */
7
37
  keepTurns?: number;
@@ -31,6 +61,11 @@ export interface CheckpointOutput {
31
61
  text: string;
32
62
  }
33
63
  export declare function parseCheckpointOutput(raw: string, fields?: string[]): CheckpointOutput;
64
+ export interface TextCompletion {
65
+ text: string;
66
+ /** Provider reported an output-token cut (finish "length"): text may be partial. */
67
+ truncated: boolean;
68
+ }
34
69
  export declare function summarize(provider: ModelProvider, request: {
35
70
  instructions: string;
36
71
  messages: Message[];
@@ -39,7 +74,7 @@ export declare function summarize(provider: ModelProvider, request: {
39
74
  maxOutputTokens: number;
40
75
  signal: AbortSignal;
41
76
  sessionId?: string;
42
- }): Promise<string>;
77
+ }): Promise<TextCompletion>;
43
78
  /** Narrow provider-agnostic completion used by compaction and the plugin model service. */
44
79
  export declare function completeText(provider: ModelProvider, request: CompletionRequest & {
45
80
  signal: AbortSignal;
@@ -7,6 +7,39 @@ export function estimateTokens(value) {
7
7
  export function shouldCompact(used, window, threshold) {
8
8
  return !!window && window > 0 && used >= window * threshold;
9
9
  }
10
+ /**
11
+ * Declared context windows above this many tokens are treated as unknown for auto-compaction and
12
+ * for the context bar: a `window * threshold` trigger would dwarf the char-budget fallback and
13
+ * hide real pressure (e.g. an 8M-token window with a default 160k-char budget would never even
14
+ * reach 2% before running out of space). Documented in docs/compaction.md.
15
+ */
16
+ export const MAX_TRUSTED_WINDOW = 2_000_000;
17
+ /**
18
+ * The single effective budget shared by the runner's auto-compaction and the TUI context bar.
19
+ * A known window (>= 1 token and <= MAX_TRUSTED_WINDOW) is used directly; otherwise the char
20
+ * budget is converted to estimated tokens (about 4 characters per token) so the bar fills where
21
+ * the char fallback actually compacts instead of showing an almost-empty bar next to an early
22
+ * compaction (the DeepSeek ~1M-window vs 160k-char budget mismatch).
23
+ */
24
+ export function effectiveContextBudget(window, maxContextChars) {
25
+ if (window !== undefined && window > 0 && window <= MAX_TRUSTED_WINDOW)
26
+ return { total: window, basis: "window" };
27
+ return { total: Math.max(1, Math.round(maxContextChars / 4)), basis: "chars" };
28
+ }
29
+ /**
30
+ * Whether to auto-compact before the next model call. Uses ONE effective budget: a known window
31
+ * triggers at `used >= window * threshold`; an unknown or guard-bounded window falls back to the
32
+ * char budget (`estimatedChars / 4 >= maxContextChars / 4`, keeping the old ~4-characters-per-token
33
+ * protection). `used` (provider-reported when available) feeds the window branch and the context
34
+ * bar; `estimatedChars` is the raw character count of the next request and feeds the fallback, so
35
+ * a provider's token report can never push an unknown-window session into needless compaction.
36
+ */
37
+ export function shouldCompactContext(used, estimatedChars, window, maxContextChars, threshold) {
38
+ const budget = effectiveContextBudget(window, maxContextChars);
39
+ if (budget.basis === "window")
40
+ return used >= budget.total * threshold;
41
+ return estimatedChars / 4 >= budget.total;
42
+ }
10
43
  /**
11
44
  * Indexes where history can be split without separating an assistant tool call from its
12
45
  * results: every call before the index is answered before the index, and the kept part
@@ -154,26 +187,41 @@ export async function summarize(provider, request) {
154
187
  const focus = request.focus?.trim()
155
188
  ? `\n\nAdditional focus requested by the user: ${request.focus.trim()}`
156
189
  : "";
157
- const text = await completeText(provider, {
158
- system: request.instructions,
159
- messages: [
160
- {
161
- role: "user",
162
- text: `Checkpoint this session transcript.${focus}\n\n<transcript>\n${serializeForSummary(request.messages)}\n</transcript>`,
163
- },
164
- ],
165
- maxTokens: request.maxOutputTokens,
166
- signal: request.signal,
167
- ...(request.model ? { model: request.model } : {}),
168
- ...(request.sessionId ? { sessionId: request.sessionId } : {}),
169
- });
170
- if (!text)
190
+ let completion;
191
+ try {
192
+ completion = await completeTextWithMeta(provider, {
193
+ system: request.instructions,
194
+ messages: [
195
+ {
196
+ role: "user",
197
+ text: `Checkpoint this session transcript.${focus}\n\n<transcript>\n${serializeForSummary(request.messages)}\n</transcript>`,
198
+ },
199
+ ],
200
+ maxTokens: request.maxOutputTokens,
201
+ signal: request.signal,
202
+ ...(request.model ? { model: request.model } : {}),
203
+ ...(request.sessionId ? { sessionId: request.sessionId } : {}),
204
+ });
205
+ }
206
+ catch (error) {
207
+ // The adapters report an empty "length" cut with a message about max output tokens.
208
+ if (!request.signal.aborted && String(error).includes("max output tokens"))
209
+ throw new Error("Compaction summary was cut off before any usable text was produced; raise compaction.maxOutputTokens");
210
+ throw error;
211
+ }
212
+ if (completion.truncated && !completion.text)
213
+ throw new Error("Compaction summary was cut off before any usable text was produced; raise compaction.maxOutputTokens");
214
+ if (!completion.text)
171
215
  throw new Error("Summarizer returned an empty summary");
172
- return text;
216
+ return completion;
173
217
  }
174
218
  /** Narrow provider-agnostic completion used by compaction and the plugin model service. */
175
219
  export async function completeText(provider, request) {
176
- let text = "", completed = false;
220
+ return (await completeTextWithMeta(provider, request)).text;
221
+ }
222
+ /** Like `completeText`, but also reports whether the provider cut the response by tokens. */
223
+ async function completeTextWithMeta(provider, request) {
224
+ let text = "", completed = false, truncated = false;
177
225
  for await (const event of provider.stream({
178
226
  instructions: request.system,
179
227
  messages: request.messages.map((m) => m.role === "user"
@@ -188,6 +236,7 @@ export async function completeText(provider, request) {
188
236
  request.signal.throwIfAborted();
189
237
  if (event.type === "completed") {
190
238
  completed = true;
239
+ truncated = event.message.truncated === true;
191
240
  text = event.message.text || text;
192
241
  }
193
242
  else if (event.type === "text_delta")
@@ -195,7 +244,7 @@ export async function completeText(provider, request) {
195
244
  }
196
245
  if (!completed)
197
246
  throw new Error("Completion ended without a completed response");
198
- return text.trim();
247
+ return { text: text.trim(), truncated };
199
248
  }
200
249
  export function summaryMessage(body, replaced) {
201
250
  return {
@@ -6,6 +6,11 @@ export interface CompactionSettings {
6
6
  auto?: boolean;
7
7
  threshold?: number;
8
8
  keepTurns?: number;
9
+ /**
10
+ * Per-call output token budget for the summarizer. Defaults to 4096 when unset; never falls
11
+ * back to the agent-loop `maxOutputTokens`. A truncated summary is accepted as partial.
12
+ */
13
+ maxOutputTokens?: number;
9
14
  }
10
15
  export interface RunnerOptions {
11
16
  /** Provider bound to this persisted session. */
@@ -23,7 +28,8 @@ export interface RunnerOptions {
23
28
  maxTokens?: number;
24
29
  onEvent?: (event: RunEvent) => void;
25
30
  readConcurrency?: number;
26
- /** Context window in tokens for a model, when known. Unknown disables auto compaction. */
31
+ /** Context window in tokens for a model, when known. Drives auto-compaction at `threshold`; when
32
+ * unknown (or absurdly large, see `MAX_TRUSTED_WINDOW`) the char budget falls back. */
27
33
  contextWindow?: (model: string) => number | undefined;
28
34
  compaction?: CompactionSettings;
29
35
  /** When set, write/process tools not allowed by the policy are offered and ask first. */
@@ -101,6 +107,17 @@ export declare class AgentRunner {
101
107
  private toolsText;
102
108
  /** Estimated tokens the next request of a session would send. */
103
109
  estimateContext(sessionId: string): Promise<number>;
110
+ /**
111
+ * The effective context budget a session's auto-compaction and the TUI context bar both use:
112
+ * the model window when known (and not beyond `MAX_TRUSTED_WINDOW`), otherwise the char budget
113
+ * converted to estimated tokens. `compactionAt` is the percentage of `total` where auto
114
+ * compaction triggers, so the bar turns red exactly where the engine compacts.
115
+ */
116
+ contextBudget(model: string): {
117
+ total: number;
118
+ basis: "window" | "chars";
119
+ compactionAt: number;
120
+ };
104
121
  /** Change the model used by subsequent turns of a session; recorded in the store. */
105
122
  setModel(sessionId: string, model: string): void;
106
123
  /** Manually compact a session outside a run. */
@@ -1,5 +1,5 @@
1
1
  import { textResult, } from "@alisio/sdk";
2
- import { checkpointInstructions, estimateTokens, parseCheckpointOutput, planCompaction, shouldCompact, summarize, summaryMessage, } from "./compaction.js";
2
+ import { checkpointInstructions, effectiveContextBudget, estimateTokens, parseCheckpointOutput, planCompaction, 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.
@@ -111,6 +111,21 @@ export class AgentRunner {
111
111
  estimateTokens(this.options.store.messages(sessionId)) +
112
112
  estimateTokens(this.toolsText(this.availableTools())));
113
113
  }
114
+ /**
115
+ * The effective context budget a session's auto-compaction and the TUI context bar both use:
116
+ * the model window when known (and not beyond `MAX_TRUSTED_WINDOW`), otherwise the char budget
117
+ * converted to estimated tokens. `compactionAt` is the percentage of `total` where auto
118
+ * compaction triggers, so the bar turns red exactly where the engine compacts.
119
+ */
120
+ contextBudget(model) {
121
+ const budget = effectiveContextBudget(this.options.contextWindow?.(model), this.options.maxContextChars ?? 160_000);
122
+ return {
123
+ ...budget,
124
+ compactionAt: budget.basis === "window"
125
+ ? Math.round((this.options.compaction?.threshold ?? 0.85) * 100)
126
+ : 100,
127
+ };
128
+ }
114
129
  /** Change the model used by subsequent turns of a session; recorded in the store. */
115
130
  setModel(sessionId, model) {
116
131
  const id = model.trim();
@@ -183,12 +198,12 @@ export class AgentRunner {
183
198
  reportFailures(contribution.failures);
184
199
  const fields = Object.fromEntries(Object.entries(contribution.fields).map(([name, f]) => [name, f.description]));
185
200
  const provider = await (o.providerFor?.(o.store.get(sessionId)) ?? o.provider);
186
- const raw = await summarize(provider, {
201
+ const { text: raw, truncated: summaryTruncated } = await summarize(provider, {
187
202
  instructions: checkpointInstructions(contribution.instructions, fields),
188
203
  messages: plan.summarized,
189
204
  focus,
190
205
  model,
191
- maxOutputTokens: o.maxOutputTokens ?? 4096,
206
+ maxOutputTokens: o.compaction?.maxOutputTokens ?? 4096,
192
207
  signal,
193
208
  sessionId,
194
209
  });
@@ -230,6 +245,7 @@ export class AgentRunner {
230
245
  summarizedTokens: estimateTokens(plan.summarized),
231
246
  checkpointTokens: estimateTokens(summary.text),
232
247
  plugins: after.reports,
248
+ ...(summaryTruncated ? { partial: true } : {}),
233
249
  });
234
250
  return { replaced: plan.cut, before, after: afterTokens, summary };
235
251
  }
@@ -327,10 +343,12 @@ export class AgentRunner {
327
343
  ? last.tokens + estimateTokens(messages.slice(last.count))
328
344
  : estimateTokens(instructions) + estimateTokens(messages) + estimateTokens(toolsText);
329
345
  };
330
- const overChars = chars() > (o.maxContextChars ?? 160_000);
331
- const overWindow = compaction.auto !== false &&
332
- shouldCompact(used(), o.contextWindow?.(model), compaction.threshold ?? 0.85);
333
- if (overWindow || (overChars && compaction.auto !== false)) {
346
+ // One effective budget: a known window triggers at `used >= window * threshold`; an
347
+ // unknown (or absurdly large, see MAX_TRUSTED_WINDOW) window falls back to the char
348
+ // estimate (`chars / 4 >= maxContextChars / 4`). `maxContextChars` below stays as the
349
+ // post-compaction hard limit.
350
+ if (compaction.auto !== false &&
351
+ shouldCompactContext(used(), chars(), o.contextWindow?.(model), o.maxContextChars ?? 160_000, compaction.threshold ?? 0.85)) {
334
352
  await this.compactLocked(sessionId, emit, model, combined, "auto");
335
353
  instructions = await withPersona();
336
354
  messages = o.store.messages(sessionId);
@@ -338,6 +356,7 @@ export class AgentRunner {
338
356
  if (chars() > (o.maxContextChars ?? 160_000))
339
357
  throw new Error("Context budget exceeded and compaction could not reduce it; start a new session.");
340
358
  let completion;
359
+ let truncated = false;
341
360
  let usage;
342
361
  for await (const e of provider.stream({
343
362
  instructions,
@@ -358,6 +377,7 @@ export class AgentRunner {
358
377
  if (completion)
359
378
  throw new Error("Provider emitted multiple completions");
360
379
  completion = e.message;
380
+ truncated = e.message.truncated === true;
361
381
  usage = e.usage;
362
382
  tokens += (e.usage?.input ?? 0) + (e.usage?.output ?? 0);
363
383
  usageTotal.input += e.usage?.input ?? 0;
@@ -389,7 +409,19 @@ export class AgentRunner {
389
409
  ...(usage ? { usage } : {}),
390
410
  });
391
411
  if (!completion.calls.length) {
392
- emit("run_completed", { tokens, text: lastText });
412
+ const final = {
413
+ tokens,
414
+ text: lastText,
415
+ };
416
+ if (truncated) {
417
+ // The adapter kept a usable answer, but the token budget cut it off.
418
+ emit("response_truncated", {
419
+ turn: turn + 1,
420
+ maxOutputTokens: o.maxOutputTokens ?? 4096,
421
+ });
422
+ final.truncated = true;
423
+ }
424
+ emit("run_completed", final);
393
425
  return { sessionId, text: lastText, status: "completed", usage: usageTotal };
394
426
  }
395
427
  const prepared = completion.calls.map((call) => {
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  export * from "@alisio/sdk";
3
3
  export { type AppOptions, type BuiltinContext, type BuiltinPlugin, createApplication, enabledBuiltins, type PluginCatalogEntry, type PluginCatalogStatus, } from "./application.ts";
4
4
  export { type Config, type ConfigLoadResult, type ConfigProvenance, configFile, configHome, configSchema, type LoadedConfig, loadConfig, loadConfigWithProvenance, type McpServerSource, overridesSavedProviderProfile, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, stateHome, } from "./config.ts";
5
- export { checkpointInstructions, completeText, estimateTokens, parseCheckpointOutput, planCompaction, renderCheckpoint, serializeForSummary, shouldCompact, } from "./core/compaction.ts";
5
+ export { type ContextBudget, checkpointInstructions, completeText, effectiveContextBudget, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, renderCheckpoint, serializeForSummary, shouldCompact, shouldCompactContext, summarize, } from "./core/compaction.ts";
6
6
  export type { ApprovalDecision, ApprovalHandler, ApprovalRequest, ContextSource, HookFailure, Policy, RunnerExtensions, Session, SessionStore, } from "./core/contracts.ts";
7
7
  export { ToolRegistry } from "./core/registry.ts";
8
8
  export { AgentRunner, type CompactionResult, defaultTokenBudget, type RunnerOptions, type RunOptions, } from "./core/runner.ts";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  export * from "@alisio/sdk";
3
3
  export { createApplication, enabledBuiltins, } from "./application.js";
4
4
  export { configFile, configHome, configSchema, loadConfig, loadConfigWithProvenance, overridesSavedProviderProfile, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, stateHome, } from "./config.js";
5
- export { checkpointInstructions, completeText, estimateTokens, parseCheckpointOutput, planCompaction, renderCheckpoint, serializeForSummary, shouldCompact, } from "./core/compaction.js";
5
+ export { checkpointInstructions, completeText, effectiveContextBudget, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, renderCheckpoint, serializeForSummary, shouldCompact, shouldCompactContext, summarize, } from "./core/compaction.js";
6
6
  export { ToolRegistry } from "./core/registry.js";
7
7
  export { AgentRunner, defaultTokenBudget, } from "./core/runner.js";
8
8
  export { ExtensionRegistry } from "./extensions/registry.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alisio/core",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0-alpha.5",
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",
@@ -41,7 +41,7 @@
41
41
  "node": ">=22.16"
42
42
  },
43
43
  "dependencies": {
44
- "@alisio/sdk": "0.1.0-alpha.2",
44
+ "@alisio/sdk": "0.1.0-alpha.5",
45
45
  "@mixmark-io/domino": "2.2.0",
46
46
  "@modelcontextprotocol/client": "2.1.0",
47
47
  "ajv": "8.20.0",