@alisio/core 0.4.2 → 0.4.4

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.
@@ -64,14 +64,17 @@ export const PURPOSE_WORDS = [
64
64
  /** Goal words that name a measure, with the column words that satisfy them. */
65
65
  export const MEASURE_SYNONYMS = [
66
66
  {
67
- goal: ["sales", "ventas", "venta", "ingreso", "revenue"],
68
- column: words("sales sale ventas venta ingresos ingreso revenue revenues amount amounts monto montos importe importes"),
67
+ goal: ["sales", "ventas", "venta", "ingreso", "revenue", "facturacion"],
68
+ column: words("sales sale ventas venta ingresos ingreso revenue revenues facturacion amount amounts monto montos importe importes"),
69
69
  },
70
70
  {
71
- goal: ["profit", "rentabilidad", "utilidad", "ganancia", "margin", "margen"],
72
- column: words("profit profits rentabilidad utilidad utilidades ganancia ganancias margin margen"),
71
+ goal: ["profit", "rentabilidad", "utilidad", "ganancia", "beneficio", "margin", "margen"],
72
+ column: words("profit profits rentabilidad utilidad utilidades ganancia ganancias beneficio beneficios margin margen"),
73
+ },
74
+ {
75
+ goal: ["cost", "costo", "coste", "gasto"],
76
+ column: words("cost costs costo costos coste costes gasto gastos"),
73
77
  },
74
- { goal: ["cost", "costo", "gasto"], column: words("cost costs costo costos gasto gastos") },
75
78
  {
76
79
  goal: ["quantity", "cantidad", "unidades", "units"],
77
80
  column: words("quantity qty cantidad units unidades"),
@@ -83,11 +86,14 @@ export function columnTokens(name, label) {
83
86
  }
84
87
  /** Dimension words that mean the same in English and Spanish (spec 6.2, goal-aware planning). */
85
88
  export const DIMENSION_SYNONYMS = [
89
+ // qualifiers of a measure: a goal that says "ventas netas" names net_sales, never gross_sales
90
+ words("net neto neta netos netas"),
91
+ words("gross bruto bruta brutos brutas"),
86
92
  words("category categories categoria categorias"),
87
93
  words("status estado"),
88
94
  words("seller salesperson salesman rep vendedor vendedora vendedores"),
89
95
  words("type tipo"),
90
- words("region regiones"),
96
+ words("region regiones regions"),
91
97
  words("channel canal"),
92
98
  words("segment segmento"),
93
99
  words("country pais"),
@@ -95,7 +101,7 @@ export const DIMENSION_SYNONYMS = [
95
101
  words("product producto"),
96
102
  words("customer client cliente"),
97
103
  words("payment pago"),
98
- words("discount descuento"),
104
+ words("discount descuento descuentos"),
99
105
  words("date fecha"),
100
106
  words("month mes"),
101
107
  words("year ano anio"),
@@ -2,6 +2,37 @@
2
2
  export const CHANGELOG_DATA = {
3
3
  schema: 1,
4
4
  entries: [
5
+ {
6
+ version: "0.4.4",
7
+ date: "2026-10-04",
8
+ sections: [
9
+ {
10
+ title: "Fixed",
11
+ items: [
12
+ 'Automatic compaction now also triggers at 85 % of `limits.maxContextChars`, so a long conversation with a large-window model is compacted before the run fails instead of stopping with "Context budget exceeded" until you ran `/compact` yourself. A last-chance compaction runs once before a run fails, and the final error now says whether compaction ran, was skipped, failed or is disabled.',
13
+ 'The web shows "Compacting context…" during a manual `/compact` or an automatic one, and blocks sending (keeping your draft) until it finishes.',
14
+ ],
15
+ },
16
+ ],
17
+ },
18
+ {
19
+ version: "0.4.3",
20
+ date: "2026-10-04",
21
+ sections: [
22
+ {
23
+ title: "Added",
24
+ items: [
25
+ 'When you run the setup command of a decision provider plugin and it asks to become the decision provider, the question now starts on "Yes (recommended)". The web question panel also starts a single-choice question on its recommended option, as the terminal already did.',
26
+ ],
27
+ },
28
+ {
29
+ title: "Fixed",
30
+ items: [
31
+ 'Smart Dashboard now understands Spanish net and gross phrasing: asking for "ventas netas" or "ingresos netos" charts the net sales column instead of falling back to gross sales.',
32
+ ],
33
+ },
34
+ ],
35
+ },
5
36
  {
6
37
  version: "0.4.2",
7
38
  date: "2026-10-04",
@@ -302,7 +302,7 @@ export interface HookFailure {
302
302
  export interface RunnerExtensions {
303
303
  beforeCompact(input: {
304
304
  sessionId: string;
305
- reason: "manual" | "auto";
305
+ reason: "manual" | "auto" | "budget";
306
306
  messages: readonly Message[];
307
307
  focus?: string;
308
308
  }): Promise<{
@@ -316,7 +316,7 @@ export interface RunnerExtensions {
316
316
  }>;
317
317
  afterCompact(input: {
318
318
  sessionId: string;
319
- reason: "manual" | "auto";
319
+ reason: "manual" | "auto" | "budget";
320
320
  messages: readonly Message[];
321
321
  focus?: string;
322
322
  model: string;
@@ -189,6 +189,8 @@ export declare class AgentRunner {
189
189
  private inbox;
190
190
  /** Last provider-reported context size per session and the history length it covered. */
191
191
  private reported;
192
+ /** Conversation size in characters right after the last automatic compaction, per session. */
193
+ private compactedChars;
192
194
  /** Where human waits are opened; the clocks of the active runs listen to it. */
193
195
  readonly humanWaits: HumanWaits;
194
196
  constructor(options: RunnerOptions);
@@ -41,6 +41,13 @@ const MAX_INJECT_CHARS = 24_000;
41
41
  * fails with an actionable error instead of silently losing the whole history.
42
42
  */
43
43
  const MIN_MESSAGE_BUDGET_FLOOR = 4_000;
44
+ /**
45
+ * Fraction of the character limit the conversation must grow beyond its post-compaction size
46
+ * before the CHARACTER-based auto-compaction trigger may fire again. When the kept tail alone
47
+ * sits at or above the trigger, summarizing gains nothing, so without this margin every turn
48
+ * would pay for a summarizer call. The hard-limit last-chance compaction ignores it.
49
+ */
50
+ const RECOMPACT_GROWTH_MARGIN = 0.1;
44
51
  const allowed = (policy, effect) => effect === "read" ||
45
52
  effect === "internal" ||
46
53
  ((effect === "write" || effect === "process" || effect === "external") && !!policy[effect]);
@@ -62,6 +69,8 @@ export class AgentRunner {
62
69
  inbox = new Map();
63
70
  /** Last provider-reported context size per session and the history length it covered. */
64
71
  reported = new Map();
72
+ /** Conversation size in characters right after the last automatic compaction, per session. */
73
+ compactedChars = new Map();
65
74
  /** Where human waits are opened; the clocks of the active runs listen to it. */
66
75
  humanWaits;
67
76
  constructor(options) {
@@ -907,11 +916,24 @@ export class AgentRunner {
907
916
  // unknown (or absurdly large, see MAX_TRUSTED_WINDOW) window falls back to the char
908
917
  // estimate (`chars / 4 >= maxContextChars / 4`). `maxContextChars` below stays as the
909
918
  // post-compaction hard limit.
919
+ // The character budget is an independent trigger: `maxContextChars` is a hard limit for
920
+ // every model, so a large-window model must compact at `limit * threshold` characters
921
+ // too instead of reaching the hard check with compaction never having run.
922
+ const threshold = compaction.threshold ?? 0.85;
923
+ // What automatic compaction did this turn (at most ONE attempt per turn).
924
+ let attempt = "none";
925
+ let attemptError = "";
910
926
  if (compaction.auto !== false &&
911
- shouldCompactContext(used(), chars(), o.contextWindow?.(model), limit, compaction.threshold ?? 0.85)) {
912
- await this.compactLocked(sessionId, emit, model, combined, "auto");
927
+ (shouldCompactContext(used(), chars(), o.contextWindow?.(model), limit, threshold) ||
928
+ (charsConversation() >= limit * threshold &&
929
+ charsConversation() >=
930
+ (this.compactedChars.get(sessionId) ?? 0) + limit * RECOMPACT_GROWTH_MARGIN))) {
931
+ const result = await this.compactLocked(sessionId, emit, model, combined, "auto");
932
+ attempt = result ? "ran" : "skipped";
913
933
  instructions = await withPersona();
914
934
  messages = o.store.messages(sessionId);
935
+ if (result)
936
+ this.compactedChars.set(sessionId, charsConversation());
915
937
  }
916
938
  // A compaction that still leaves the session over the hard limit is reduced in place
917
939
  // instead of failing outright: retained content is clipped against a TOTAL character
@@ -925,15 +947,45 @@ export class AgentRunner {
925
947
  // prompts. Only if even the reduction floor cannot fit (instructions alone crowd out the
926
948
  // limit) the run fails with an actionable error.
927
949
  if (charsConversation() > limit) {
928
- const targetChars = Math.max(MIN_MESSAGE_BUDGET_FLOOR, limit - instructions.length);
929
- const reduction = reduceMessagesToBudget(messages, targetChars);
930
- if (reduction.truncated > 0) {
931
- o.store.overwrite(sessionId, reduction.messages);
932
- messages = reduction.messages;
933
- emit("context_reduced", { messages: reduction.truncated });
950
+ const reduceInPlace = () => {
951
+ const targetChars = Math.max(MIN_MESSAGE_BUDGET_FLOOR, limit - instructions.length);
952
+ const reduction = reduceMessagesToBudget(messages, targetChars);
953
+ if (reduction.truncated > 0) {
954
+ o.store.overwrite(sessionId, reduction.messages);
955
+ messages = reduction.messages;
956
+ emit("context_reduced", { messages: reduction.truncated });
957
+ }
958
+ };
959
+ reduceInPlace();
960
+ // Last chance: clipping alone was not enough and no compaction was attempted this
961
+ // turn, so summarize history once (reason "budget") and reduce again before failing.
962
+ if (charsConversation() > limit && compaction.auto !== false && attempt === "none") {
963
+ try {
964
+ const result = await this.compactLocked(sessionId, emit, model, combined, "budget");
965
+ attempt = result ? "ran" : "skipped";
966
+ }
967
+ catch (error) {
968
+ combined.throwIfAborted();
969
+ attempt = "failed";
970
+ attemptError = error instanceof Error ? error.message : String(error);
971
+ }
972
+ instructions = await withPersona();
973
+ messages = o.store.messages(sessionId);
974
+ if (charsConversation() > limit)
975
+ reduceInPlace();
976
+ if (attempt === "ran")
977
+ this.compactedChars.set(sessionId, charsConversation());
978
+ }
979
+ if (charsConversation() > limit) {
980
+ const what = compaction.auto === false
981
+ ? "Automatic compaction is disabled (compaction.auto is false)."
982
+ : attempt === "ran"
983
+ ? "Automatic compaction ran but could not reduce it enough."
984
+ : attempt === "failed"
985
+ ? `Automatic compaction failed (${attemptError}).`
986
+ : "Automatic compaction was skipped because there is no safe boundary to summarize (the conversation is a single long turn or too short).";
987
+ throw new Error(`Context budget exceeded (approximately ${charsConversation()} characters of conversation; limit ${limit}). ${what} Run /compact, trim large tool outputs, start a new session, or disable unneeded MCP servers with /plugins.`);
934
988
  }
935
- if (charsConversation() > limit)
936
- throw new Error(`Context budget exceeded and compaction could not reduce it (approximately ${charsConversation()} characters of conversation; limit ${limit}). Run /compact, trim large tool outputs, start a new session, or disable unneeded MCP servers with /plugins.`);
937
989
  }
938
990
  let completion;
939
991
  let truncated = false;
@@ -1,4 +1,4 @@
1
- import type { AskQuestionsRequest, AskQuestionsResult, DecisionActivationResult } from "@alisio/sdk";
1
+ import type { AskQuestionsRequest, AskQuestionsResult, DecisionActivationOptions, DecisionActivationResult } from "@alisio/sdk";
2
2
  import type { DecisionRegistry } from "./registry.ts";
3
3
  import type { DecisionsConfig } from "./service.ts";
4
4
  export interface DecisionActivationDeps {
@@ -15,4 +15,4 @@ export interface DecisionActivationDeps {
15
15
  * A plugin asks the host to activate one of ITS providers. The host never overwrites a configured
16
16
  * provider, asks the user exactly once, and only then persists. Never throws.
17
17
  */
18
- export declare function requestDecisionActivation(owner: string, providerId: unknown, deps: DecisionActivationDeps): Promise<DecisionActivationResult>;
18
+ export declare function requestDecisionActivation(owner: string, providerId: unknown, deps: DecisionActivationDeps, options?: DecisionActivationOptions): Promise<DecisionActivationResult>;
@@ -3,7 +3,7 @@ const QUESTION_ID = "activate";
3
3
  * A plugin asks the host to activate one of ITS providers. The host never overwrites a configured
4
4
  * provider, asks the user exactly once, and only then persists. Never throws.
5
5
  */
6
- export async function requestDecisionActivation(owner, providerId, deps) {
6
+ export async function requestDecisionActivation(owner, providerId, deps, options) {
7
7
  if (typeof providerId !== "string" || deps.registry.get(providerId)?.owner !== owner)
8
8
  return {
9
9
  status: "unavailable",
@@ -18,6 +18,10 @@ export async function requestDecisionActivation(owner, providerId, deps) {
18
18
  return { status: "other_provider_active", active: config.provider };
19
19
  if (!deps.interactive())
20
20
  return { status: "needs_confirmation" };
21
+ // Only an explicit `recommend: true` moves the pre-selection to Yes; anything else is ignored.
22
+ const recommendYes = typeof options === "object" &&
23
+ options !== null &&
24
+ options.recommend === true;
21
25
  let answer;
22
26
  try {
23
27
  answer = await deps.ask({
@@ -28,12 +32,17 @@ export async function requestDecisionActivation(owner, providerId, deps) {
28
32
  header: "Decision provider",
29
33
  question: `Plugin ${owner} wants to become the decision provider. Dashboards will send your request goal and column names, never values, to it. Activate it?`,
30
34
  options: [
31
- { value: "yes", label: "Yes", description: "Save it in your global configuration." },
35
+ {
36
+ value: "yes",
37
+ label: "Yes",
38
+ description: "Save it in your global configuration.",
39
+ ...(recommendYes ? { recommended: true } : {}),
40
+ },
32
41
  {
33
42
  value: "no",
34
43
  label: "No",
35
44
  description: "Keep decisions unchanged.",
36
- recommended: true,
45
+ ...(recommendYes ? {} : { recommended: true }),
37
46
  },
38
47
  ],
39
48
  },
@@ -439,13 +439,13 @@ export class PluginHost {
439
439
  ...(activation
440
440
  ? {
441
441
  // Only providers THIS plugin registered; the user confirms once.
442
- activate: (providerId) => requestDecisionActivation(plugin.id, providerId, {
442
+ activate: (providerId, options) => requestDecisionActivation(plugin.id, providerId, {
443
443
  registry,
444
444
  config: activation.config,
445
445
  interactive: () => !!this.uiImpl,
446
446
  ask: (request) => this.uiImpl ? this.uiImpl.askQuestions(request) : Promise.resolve({}),
447
447
  persist: activation.persist,
448
- }),
448
+ }, options),
449
449
  }
450
450
  : {}),
451
451
  }))(this.decisionsImpl),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alisio/core",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
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.4.2",
44
+ "@alisio/sdk": "0.4.4",
45
45
  "@mixmark-io/domino": "2.2.0",
46
46
  "@modelcontextprotocol/client": "2.1.0",
47
47
  "ajv": "8.20.0",