@alisio/core 0.1.0-alpha.22 → 0.1.0-alpha.24

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.
@@ -13,6 +13,7 @@ import { createArtifactPublisher } from "./artifacts/publisher.js";
13
13
  import { ArtifactStore } from "./artifacts/store.js";
14
14
  import { configFile, configHome, loadConfigWithProvenance, overridesSavedProviderProfile, setConfigValue, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, stateHome, } from "./config.js";
15
15
  import { completeText } from "./core/compaction.js";
16
+ import { DEFAULT_MAX_OUTPUT_TOKENS } from "./core/output-limit.js";
16
17
  import { ToolRegistry } from "./core/registry.js";
17
18
  import { AgentRunner } from "./core/runner.js";
18
19
  import { HerdrBridge } from "./integrations/herdr.js";
@@ -638,6 +639,39 @@ export async function createApplication(options = {}) {
638
639
  return {};
639
640
  }
640
641
  };
642
+ /**
643
+ * The maximum output a model's catalog declares (`ModelInfo.maxOutputTokens`), for the budget
644
+ * of each request. Looks in the active provider's catalog first (loading it once, bounded, if
645
+ * the first request comes before it); then in the catalogs of the configured profiles that
646
+ * were already listed, taking the smallest declaration when several providers list the same
647
+ * model id so a derived budget never exceeds any of them. Unknown stays unknown.
648
+ */
649
+ const declaredOutputLimit = async (model) => {
650
+ if (!models.has(model)) {
651
+ const loading = ensureModels();
652
+ if (loading) {
653
+ let timer;
654
+ await Promise.race([
655
+ loading.catch(() => undefined),
656
+ new Promise((resolve) => {
657
+ timer = setTimeout(resolve, 2_000);
658
+ }),
659
+ ]);
660
+ if (timer)
661
+ clearTimeout(timer);
662
+ }
663
+ }
664
+ const own = models.get(model)?.maxOutputTokens;
665
+ if (own !== undefined)
666
+ return own;
667
+ const declared = catalogCache
668
+ ? availableProviderModels(catalogCache.value)
669
+ .filter((entry) => entry.model.id === model)
670
+ .map((entry) => entry.model.maxOutputTokens)
671
+ .filter((value) => typeof value === "number" && value > 0)
672
+ : [];
673
+ return declared.length ? Math.min(...declared) : undefined;
674
+ };
641
675
  const runner = new AgentRunner({
642
676
  provider,
643
677
  providerFor,
@@ -650,6 +684,14 @@ export async function createApplication(options = {}) {
650
684
  compaction: config.compaction,
651
685
  extensions: plugins,
652
686
  contextWindow,
687
+ modelOutputLimit: declaredOutputLimit,
688
+ fallbackMaxOutputTokens: DEFAULT_MAX_OUTPUT_TOKENS,
689
+ effortLevels: (model) => {
690
+ const known = models.get(model)?.effort;
691
+ if (!known)
692
+ void ensureModels();
693
+ return known;
694
+ },
653
695
  ...(options.approve && !options.readOnly ? { approve: options.approve } : {}),
654
696
  pathAccess,
655
697
  ...(analysisEnabled
@@ -983,6 +1025,9 @@ export async function createApplication(options = {}) {
983
1025
  case "limits.maxTurns":
984
1026
  case "limits.maxOutputTokens":
985
1027
  case "limits.maxContextChars":
1028
+ case "limits.firstTokenTimeoutMs":
1029
+ case "limits.firstTokenRetries":
1030
+ case "limits.truncationRecoveries":
986
1031
  case "limits.timeoutMs": {
987
1032
  const patch = limitLeaf(key.slice("limits.".length));
988
1033
  config.limits = { ...config.limits, ...patch };
package/dist/config.d.ts CHANGED
@@ -175,8 +175,11 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
175
175
  limits: z.ZodDefault<z.ZodObject<{
176
176
  maxTurns: z.ZodDefault<z.ZodNumber>;
177
177
  timeoutMs: z.ZodDefault<z.ZodNumber>;
178
+ firstTokenTimeoutMs: z.ZodDefault<z.ZodNumber>;
179
+ firstTokenRetries: z.ZodDefault<z.ZodNumber>;
180
+ truncationRecoveries: z.ZodDefault<z.ZodNumber>;
178
181
  maxContextChars: z.ZodDefault<z.ZodNumber>;
179
- maxOutputTokens: z.ZodDefault<z.ZodNumber>;
182
+ maxOutputTokens: z.ZodOptional<z.ZodNumber>;
180
183
  maxTokens: z.ZodOptional<z.ZodNumber>;
181
184
  }, z.core.$strip>>;
182
185
  analysis: z.ZodDefault<z.ZodObject<{
@@ -270,8 +273,11 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
270
273
  limits: {
271
274
  maxTurns: number;
272
275
  timeoutMs: number;
276
+ firstTokenTimeoutMs: number;
277
+ firstTokenRetries: number;
278
+ truncationRecoveries: number;
273
279
  maxContextChars: number;
274
- maxOutputTokens: number;
280
+ maxOutputTokens?: number | undefined;
275
281
  maxTokens?: number | undefined;
276
282
  };
277
283
  analysis: {
@@ -442,8 +448,11 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
442
448
  limits: {
443
449
  maxTurns: number;
444
450
  timeoutMs: number;
451
+ firstTokenTimeoutMs: number;
452
+ firstTokenRetries: number;
453
+ truncationRecoveries: number;
445
454
  maxContextChars: number;
446
- maxOutputTokens: number;
455
+ maxOutputTokens?: number | undefined;
447
456
  maxTokens?: number | undefined;
448
457
  };
449
458
  analysis: {
@@ -552,9 +561,12 @@ declare const SETTABLE_KEYS: {
552
561
  readonly "context.claudeMdFallback": z.ZodDefault<z.ZodBoolean>;
553
562
  readonly "context.maxBytes": z.ZodDefault<z.ZodNumber>;
554
563
  readonly "limits.maxTurns": z.ZodDefault<z.ZodNumber>;
555
- readonly "limits.maxOutputTokens": z.ZodDefault<z.ZodNumber>;
564
+ readonly "limits.maxOutputTokens": z.ZodOptional<z.ZodNumber>;
556
565
  readonly "limits.maxContextChars": z.ZodDefault<z.ZodNumber>;
557
566
  readonly "limits.timeoutMs": z.ZodDefault<z.ZodNumber>;
567
+ readonly "limits.firstTokenTimeoutMs": z.ZodDefault<z.ZodNumber>;
568
+ readonly "limits.firstTokenRetries": z.ZodDefault<z.ZodNumber>;
569
+ readonly "limits.truncationRecoveries": z.ZodDefault<z.ZodNumber>;
558
570
  readonly "pluginHooks.timeoutMs": z.ZodDefault<z.ZodNumber>;
559
571
  readonly "tui.paddingX": z.ZodDefault<z.ZodNumber>;
560
572
  readonly "tui.contentPaddingX": z.ZodDefault<z.ZodNumber>;
package/dist/config.js CHANGED
@@ -173,21 +173,47 @@ const configObjectSchema = z
173
173
  limits: z
174
174
  .object({
175
175
  maxTurns: z.number().int().min(1).max(100).default(100),
176
- timeoutMs: z.number().int().min(100).default(300000),
176
+ timeoutMs: z.number().int().min(100).default(600000),
177
+ /**
178
+ * Stop a model request that stays completely silent (no text, reasoning or tool-call
179
+ * delta) this long, so a queued or stuck provider is cut after 90 s instead of the
180
+ * whole-run `timeoutMs`. 0 disables it (models that do not stream their reasoning can stay
181
+ * silent for minutes: raise or disable it for those).
182
+ */
183
+ firstTokenTimeoutMs: z.number().int().min(0).default(90000),
184
+ /**
185
+ * How many times the SAME request is sent again after it stayed silent for
186
+ * `firstTokenTimeoutMs` (a stalled connection usually answers on the next try). 0
187
+ * disables the retry. Never counts as a turn.
188
+ */
189
+ firstTokenRetries: z.number().int().min(0).max(3).default(1),
190
+ /**
191
+ * How many times a turn is requested again after the model's response was cut off by
192
+ * `maxOutputTokens` before it was usable (no text and no tool call, or a tool call with
193
+ * incomplete arguments). The truncated calls are never executed. 0 disables the
194
+ * recovery. Never counts as a turn.
195
+ */
196
+ truncationRecoveries: z.number().int().min(0).max(5).default(2),
177
197
  maxContextChars: z.number().int().positive().default(800000),
178
198
  /**
179
- * Per-call output token budget for one agent turn. 4096 starves reasoning-heavy models,
180
- * which can spend the whole budget on `reasoning_content` before any usable text arrives.
199
+ * Per-call output token budget for one agent turn. Deliberately WITHOUT a schema default:
200
+ * when unset, the budget of each request is the model catalog's declared maximum (capped
201
+ * at 65536) or 16384 (see `core/output-limit.ts`), so "set by the user" stays
202
+ * distinguishable from "defaulted". A value set here always wins. 4096 starves
203
+ * reasoning-heavy models, which can spend the whole budget on `reasoning_content`
204
+ * before any usable text arrives.
181
205
  */
182
- maxOutputTokens: z.number().int().positive().default(16384),
206
+ maxOutputTokens: z.number().int().positive().optional(),
183
207
  /** Cumulative tokens per run; default is proportional to the context window. */
184
208
  maxTokens: z.number().int().positive().optional(),
185
209
  })
186
210
  .default(() => ({
187
211
  maxTurns: 100,
188
- timeoutMs: 300000,
212
+ timeoutMs: 600000,
213
+ firstTokenTimeoutMs: 90000,
214
+ firstTokenRetries: 1,
215
+ truncationRecoveries: 2,
189
216
  maxContextChars: 800000,
190
- maxOutputTokens: 16384,
191
217
  })),
192
218
  /**
193
219
  * Python analysis and downloadable artifacts. `enabled: false` registers none of the
@@ -686,6 +712,9 @@ const SETTABLE_KEYS = {
686
712
  "limits.maxOutputTokens": SETTABLE_SECTIONS.limits.shape.maxOutputTokens,
687
713
  "limits.maxContextChars": SETTABLE_SECTIONS.limits.shape.maxContextChars,
688
714
  "limits.timeoutMs": SETTABLE_SECTIONS.limits.shape.timeoutMs,
715
+ "limits.firstTokenTimeoutMs": SETTABLE_SECTIONS.limits.shape.firstTokenTimeoutMs,
716
+ "limits.firstTokenRetries": SETTABLE_SECTIONS.limits.shape.firstTokenRetries,
717
+ "limits.truncationRecoveries": SETTABLE_SECTIONS.limits.shape.truncationRecoveries,
689
718
  "pluginHooks.timeoutMs": SETTABLE_SECTIONS.pluginHooks.shape.timeoutMs,
690
719
  "tui.paddingX": SETTABLE_SECTIONS.tui.shape.paddingX,
691
720
  "tui.contentPaddingX": SETTABLE_SECTIONS.tui.shape.contentPaddingX,
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The output-token budget of one model request.
3
+ *
4
+ * Priority: a value the user set explicitly (`limits.maxOutputTokens` in any config layer,
5
+ * `/settings`, a per-run option) always wins, even above the model's declared maximum (they asked
6
+ * for it, and a provider that rejects it says so); otherwise the maximum the model's catalog
7
+ * declares (`ModelInfo.maxOutputTokens`), capped at `MAX_AUTO_OUTPUT_TOKENS`; otherwise
8
+ * `DEFAULT_MAX_OUTPUT_TOKENS`. The value is resolved for the model of each request, so switching
9
+ * model within a session changes it.
10
+ */
11
+ /** Output budget used when neither the user nor the model catalog says anything. */
12
+ export declare const DEFAULT_MAX_OUTPUT_TOKENS = 16384;
13
+ /**
14
+ * Upper bound of a budget taken from a model's declared maximum. Catalogs declare the model's
15
+ * hard limit (hundreds of thousands of tokens for some reasoning models), which is far more than
16
+ * a single agent turn should be allowed to stream; an explicit user value is never capped by it.
17
+ */
18
+ export declare const MAX_AUTO_OUTPUT_TOKENS = 65536;
19
+ /** Where the effective budget came from. */
20
+ export type OutputLimitSource = "user" | "model" | "default";
21
+ export interface OutputLimit {
22
+ value: number;
23
+ source: OutputLimitSource;
24
+ }
25
+ export declare function resolveMaxOutputTokens(input: {
26
+ /** Set by the user (config, settings or a per-run option); `undefined` when only defaulted. */
27
+ explicit?: number | undefined;
28
+ /** `ModelInfo.maxOutputTokens` of the model of the request, when its catalog declares one. */
29
+ declared?: number | undefined;
30
+ /** Used when nothing else applies; `DEFAULT_MAX_OUTPUT_TOKENS` unless the host knows better. */
31
+ fallback?: number | undefined;
32
+ }): OutputLimit;
33
+ /** A short English phrase for messages and logs. */
34
+ export declare function describeOutputLimitSource(source: OutputLimitSource | undefined): string;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The output-token budget of one model request.
3
+ *
4
+ * Priority: a value the user set explicitly (`limits.maxOutputTokens` in any config layer,
5
+ * `/settings`, a per-run option) always wins, even above the model's declared maximum (they asked
6
+ * for it, and a provider that rejects it says so); otherwise the maximum the model's catalog
7
+ * declares (`ModelInfo.maxOutputTokens`), capped at `MAX_AUTO_OUTPUT_TOKENS`; otherwise
8
+ * `DEFAULT_MAX_OUTPUT_TOKENS`. The value is resolved for the model of each request, so switching
9
+ * model within a session changes it.
10
+ */
11
+ /** Output budget used when neither the user nor the model catalog says anything. */
12
+ export const DEFAULT_MAX_OUTPUT_TOKENS = 16_384;
13
+ /**
14
+ * Upper bound of a budget taken from a model's declared maximum. Catalogs declare the model's
15
+ * hard limit (hundreds of thousands of tokens for some reasoning models), which is far more than
16
+ * a single agent turn should be allowed to stream; an explicit user value is never capped by it.
17
+ */
18
+ export const MAX_AUTO_OUTPUT_TOKENS = 65_536;
19
+ const positive = (value) => typeof value === "number" && Number.isFinite(value) && value >= 1;
20
+ export function resolveMaxOutputTokens(input) {
21
+ if (positive(input.explicit))
22
+ return { value: Math.floor(input.explicit), source: "user" };
23
+ if (positive(input.declared))
24
+ return { value: Math.min(Math.floor(input.declared), MAX_AUTO_OUTPUT_TOKENS), source: "model" };
25
+ return {
26
+ value: positive(input.fallback) ? Math.floor(input.fallback) : DEFAULT_MAX_OUTPUT_TOKENS,
27
+ source: "default",
28
+ };
29
+ }
30
+ /** A short English phrase for messages and logs. */
31
+ export function describeOutputLimitSource(source) {
32
+ if (source === "user")
33
+ return "set by you";
34
+ if (source === "model")
35
+ return "from the model catalog";
36
+ if (source === "default")
37
+ return "default";
38
+ return "current limit";
39
+ }
@@ -24,8 +24,35 @@ export interface RunnerOptions {
24
24
  workspace: string;
25
25
  maxTurns?: number;
26
26
  timeoutMs?: number;
27
+ /** See `RunnerSettingsPatch.firstTokenTimeoutMs`. */
28
+ firstTokenTimeoutMs?: number;
29
+ /** See `RunnerSettingsPatch.firstTokenRetries`. */
30
+ firstTokenRetries?: number;
31
+ /** See `RunnerSettingsPatch.truncationRecoveries`. */
32
+ truncationRecoveries?: number;
33
+ /**
34
+ * Reasoning effort levels a model advertises (`ModelInfo.effort`), when known. Lets a recovery
35
+ * from an exhausted reasoning budget request one level lower; without it effort is unchanged.
36
+ */
37
+ effortLevels?: (model: string) => {
38
+ supportedLevels: string[];
39
+ defaultLevel?: string;
40
+ } | undefined;
27
41
  maxContextChars?: number;
42
+ /**
43
+ * Output-token budget the USER set (`limits.maxOutputTokens`). Left unset, the budget of each
44
+ * request is the model's declared maximum (`modelOutputLimit`, capped), else
45
+ * `fallbackMaxOutputTokens`. See `resolveMaxOutputTokens`.
46
+ */
28
47
  maxOutputTokens?: number;
48
+ /**
49
+ * The maximum output a model's catalog declares (`ModelInfo.maxOutputTokens`), when known.
50
+ * May be async (a catalog still loading); a failure counts as unknown. Called for the model
51
+ * of every request, so a model switch changes the budget.
52
+ */
53
+ modelOutputLimit?: (model: string) => number | undefined | Promise<number | undefined>;
54
+ /** Budget when neither the user nor the catalog says anything (4096 for bare embedders). */
55
+ fallbackMaxOutputTokens?: number;
29
56
  maxTokens?: number;
30
57
  onEvent?: (event: RunEvent) => void;
31
58
  readConcurrency?: number;
@@ -70,6 +97,22 @@ export interface RunnerSettingsPatch {
70
97
  maxContextChars?: number;
71
98
  /** Per-run timeout in milliseconds; the next run honors it. */
72
99
  timeoutMs?: number;
100
+ /**
101
+ * Stop a model request that stays completely silent (no text, reasoning or tool-call delta)
102
+ * for this many milliseconds. `0` or unset disables it: only the run timeout applies.
103
+ */
104
+ firstTokenTimeoutMs?: number;
105
+ /**
106
+ * How many times the same request is sent again after a silent timeout (`firstTokenTimeoutMs`).
107
+ * `0` or unset disables it. A retry is not a turn; it only happens while nothing at all was
108
+ * received for the request.
109
+ */
110
+ firstTokenRetries?: number;
111
+ /**
112
+ * How many times a turn is requested again after the response was cut off by the output-token
113
+ * limit before it was usable. `0` or unset disables it. A recovery is not a turn.
114
+ */
115
+ truncationRecoveries?: number;
73
116
  }
74
117
  /**
75
118
  * Per-run overrides used by embedders and child sessions. Callers must only NARROW: `policy`
@@ -97,6 +140,12 @@ export interface RunOptions {
97
140
  /** Per-call output token budget for this run; beats the runner-level budget when set. */
98
141
  maxOutputTokens?: number;
99
142
  timeoutMs?: number;
143
+ /** Silent-request limit for this run; beats the runner-level `firstTokenTimeoutMs`. */
144
+ firstTokenTimeoutMs?: number;
145
+ /** Silent-request retries for this run; beats the runner-level `firstTokenRetries`. */
146
+ firstTokenRetries?: number;
147
+ /** Output-truncation recoveries for this run; beats the runner-level `truncationRecoveries`. */
148
+ truncationRecoveries?: number;
100
149
  /** Reasoning effort level for this run; beats the runner-level default when set. */
101
150
  reasoningEffort?: string;
102
151
  /** Run id to use for this run's events (e.g. preassigned by an embedder); a UUID otherwise. */
@@ -183,6 +232,8 @@ export declare class AgentRunner {
183
232
  contextWindow?: number;
184
233
  reasoningEffort?: string;
185
234
  }>;
235
+ /** The output-token budget for `model`: user value, else catalog maximum, else fallback. */
236
+ private outputLimit;
186
237
  /** Change the model used by subsequent turns of a session; recorded in the store. */
187
238
  setModel(sessionId: string, model: string): void;
188
239
  /** Manually compact a session outside a run. */
@@ -1,5 +1,25 @@
1
1
  import { isEphemeralRunEventType, textProjection, textResult, } from "@alisio/sdk";
2
2
  import { checkpointInstructions, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, reduceMessagesToBudget, shouldCompactContext, summarize, summaryMessage, } from "./compaction.js";
3
+ import { resolveMaxOutputTokens } from "./output-limit.js";
4
+ import { isTimeoutReason, providerLabel, RunTimeoutError } from "./timeout.js";
5
+ import { incompleteCalls, isOutputTruncationError, lowerEffort, RunTruncationError, TRUNCATION_NOTICE, } from "./truncation.js";
6
+ /** Pause before a silent request is sent again; short, and cut off by a stop or the run limit. */
7
+ const FIRST_TOKEN_RETRY_PAUSE_MS = 250;
8
+ /** Pause before a cut-off turn is requested again; short, and cut off by a stop or the run limit. */
9
+ const TRUNCATION_RETRY_PAUSE_MS = 100;
10
+ function abortableDelay(ms, signal) {
11
+ return new Promise((resolve) => {
12
+ if (signal.aborted)
13
+ return resolve();
14
+ const done = () => {
15
+ clearTimeout(timer);
16
+ signal.removeEventListener("abort", done);
17
+ resolve();
18
+ };
19
+ const timer = setTimeout(done, ms);
20
+ signal.addEventListener("abort", done, { once: true });
21
+ });
22
+ }
3
23
  /**
4
24
  * Default cumulative token budget for one run: several context windows (each turn re-sends the
5
25
  * context), clamped; 1M tokens when the window is unknown.
@@ -195,11 +215,21 @@ export class AgentRunner {
195
215
  instructions: await o.context.instructions(sessionId),
196
216
  messages: o.store.messages(sessionId),
197
217
  maxContextChars: o.maxContextChars ?? 800_000,
198
- maxOutputTokens: o.maxOutputTokens ?? 4096,
218
+ maxOutputTokens: (await this.outputLimit(session.model)).value,
199
219
  timeoutMs: o.timeoutMs ?? 300_000,
200
220
  ...(o.reasoningEffort ? { reasoningEffort: o.reasoningEffort } : {}),
201
221
  };
202
222
  }
223
+ /** The output-token budget for `model`: user value, else catalog maximum, else fallback. */
224
+ async outputLimit(model, perRun) {
225
+ const o = this.options;
226
+ const declared = await Promise.resolve(o.modelOutputLimit?.(model)).catch(() => undefined);
227
+ return resolveMaxOutputTokens({
228
+ explicit: perRun ?? o.maxOutputTokens,
229
+ declared,
230
+ fallback: o.fallbackMaxOutputTokens ?? 4096,
231
+ });
232
+ }
203
233
  /** Change the model used by subsequent turns of a session; recorded in the store. */
204
234
  setModel(sessionId, model) {
205
235
  const id = model.trim();
@@ -593,7 +623,8 @@ export class AgentRunner {
593
623
  /* A journal write failure cannot invalidate an executed run. */
594
624
  }
595
625
  };
596
- const timeout = AbortSignal.timeout(options.timeoutMs ?? o.timeoutMs ?? 300_000);
626
+ const runTimeoutMs = options.timeoutMs ?? o.timeoutMs ?? 300_000;
627
+ const timeout = AbortSignal.timeout(runTimeoutMs);
597
628
  const combined = AbortSignal.any([
598
629
  controller.signal,
599
630
  timeout,
@@ -656,6 +687,17 @@ export class AgentRunner {
656
687
  return { runId, status: "completed", result };
657
688
  }
658
689
  catch (error) {
690
+ if (combined.aborted && isTimeoutReason(combined.reason)) {
691
+ const failure = new RunTimeoutError({
692
+ kind: "run",
693
+ ms: runTimeoutMs,
694
+ stage: "tool",
695
+ tool: name,
696
+ });
697
+ emit("run_failed", { error: failure.message, code: "timeout", timeout: failure.info });
698
+ finish("failed", failure.message);
699
+ throw failure;
700
+ }
659
701
  const cause = combined.aborted ? (combined.reason ?? error) : error;
660
702
  const message = cause instanceof Error ? cause.message : String(cause);
661
703
  emit(combined.aborted ? "run_cancelled" : "run_failed", { error: message });
@@ -684,8 +726,27 @@ export class AgentRunner {
684
726
  const limit = o.maxContextChars ?? 800_000;
685
727
  let tokens = 0, lastText = "", acquired = false;
686
728
  const usageTotal = { input: 0, output: 0 };
687
- const timeout = AbortSignal.timeout(options.timeoutMs ?? o.timeoutMs ?? 300_000);
729
+ const runTimeoutMs = options.timeoutMs ?? o.timeoutMs ?? 300_000;
730
+ const firstTokenMs = options.firstTokenTimeoutMs ?? o.firstTokenTimeoutMs ?? 0;
731
+ const firstTokenRetries = firstTokenMs > 0 ? Math.max(0, options.firstTokenRetries ?? o.firstTokenRetries ?? 0) : 0;
732
+ const truncationRecoveries = Math.max(0, options.truncationRecoveries ?? o.truncationRecoveries ?? 0);
733
+ /** Responses of this run cut off by the output limit (the first plus every recovery). */
734
+ let truncations = 0;
735
+ /** Continuation notice and lowered effort for the request that follows a cut-off response. */
736
+ let recoveryNotice;
737
+ let recoveryEffort;
738
+ const runStartedAt = Date.now();
739
+ const timeout = AbortSignal.timeout(runTimeoutMs);
688
740
  const combined = AbortSignal.any([controller.signal, timeout, ...(signal ? [signal] : [])]);
741
+ // What the run was doing, so a timeout can say where it was stuck (see `RunTimeoutError`).
742
+ let stage = "other";
743
+ let stageTool;
744
+ let modelId;
745
+ let providerName;
746
+ let requests = 0;
747
+ let silentTimedOut = false;
748
+ /** Sends of the request that is (or was last) waiting for its first token. */
749
+ let silentAttempts = 0;
689
750
  const context = options.context ?? o.context;
690
751
  const workspace = options.workspace ?? o.workspace;
691
752
  const pathAccess = o.pathAccess;
@@ -712,6 +773,8 @@ export class AgentRunner {
712
773
  throw new Error("Session provider/workspace mismatch");
713
774
  // The session records the model; switching models is explicit via setModel.
714
775
  const model = session.model;
776
+ modelId = model;
777
+ providerName = providerLabel(provider.id);
715
778
  combined.throwIfAborted();
716
779
  if (o.extensions && o.store.messages(sessionId).length === 0) {
717
780
  // New session: extensions may inject budgeted context once, persisted with the session.
@@ -822,38 +885,170 @@ export class AgentRunner {
822
885
  // display-only extension of the persisted transcript and never reach a model prompt
823
886
  // (raw image bytes included). The store keeps the rich parts for TUI replay.
824
887
  const providerMessages = messages.map((m) => m.role === "tool" ? { ...m, result: textProjection(m.result) } : m);
825
- const requested = Date.now();
888
+ // After a cut-off response the request carries a transient continuation notice (never
889
+ // persisted: the stored transcript only ever holds what the model really answered).
890
+ if (recoveryNotice)
891
+ providerMessages.push({ role: "user", text: recoveryNotice });
892
+ const requestEffort = recoveryEffort ?? options.reasoningEffort ?? o.reasoningEffort;
893
+ recoveryNotice = undefined;
894
+ recoveryEffort = undefined;
895
+ // Resolved for the model of THIS request (a catalog may still be loading on the first).
896
+ const outputLimit = await this.outputLimit(model, options.maxOutputTokens);
897
+ combined.throwIfAborted();
898
+ /** The provider threw the "cut off by the output limit" signal instead of completing. */
899
+ let cutByError = false;
900
+ let requested = Date.now();
826
901
  let firstDelta;
827
- for await (const e of provider.stream({
828
- instructions,
829
- messages: providerMessages,
830
- tools,
831
- maxOutputTokens: options.maxOutputTokens ?? o.maxOutputTokens ?? 4096,
832
- signal: combined,
833
- model,
834
- sessionId,
835
- ...(o.nativeTools?.length ? { nativeTools: o.nativeTools } : {}),
836
- ...((options.reasoningEffort ?? o.reasoningEffort)
837
- ? { reasoningEffort: options.reasoningEffort ?? o.reasoningEffort }
838
- : {}),
839
- })) {
902
+ stage = "waiting_model";
903
+ requests++;
904
+ silentAttempts = 0;
905
+ // The same request is sent again only while the provider stayed completely silent until
906
+ // `firstTokenTimeoutMs`. Nothing was received, appended or accounted for the aborted
907
+ // attempt, so the retry reuses the exact same inputs inside this turn.
908
+ for (;;) {
909
+ silentAttempts++;
910
+ silentTimedOut = false;
911
+ requested = Date.now();
912
+ // A request that stays completely silent can be stopped early (`firstTokenTimeoutMs`).
913
+ const silent = new AbortController();
914
+ let silentTimer = firstTokenMs > 0
915
+ ? setTimeout(() => {
916
+ silentTimedOut = true;
917
+ silent.abort();
918
+ }, firstTokenMs)
919
+ : undefined;
920
+ const stopSilentTimer = () => {
921
+ if (silentTimer)
922
+ clearTimeout(silentTimer);
923
+ silentTimer = undefined;
924
+ };
925
+ let failure;
926
+ try {
927
+ for await (const e of provider.stream({
928
+ instructions,
929
+ messages: providerMessages,
930
+ tools,
931
+ maxOutputTokens: outputLimit.value,
932
+ signal: firstTokenMs > 0 ? AbortSignal.any([combined, silent.signal]) : combined,
933
+ model,
934
+ sessionId,
935
+ ...(o.nativeTools?.length ? { nativeTools: o.nativeTools } : {}),
936
+ ...(requestEffort ? { reasoningEffort: requestEffort } : {}),
937
+ })) {
938
+ combined.throwIfAborted();
939
+ stopSilentTimer();
940
+ if (stage === "waiting_model")
941
+ stage = "streaming";
942
+ if (e.type !== "completed")
943
+ firstDelta ??= Date.now();
944
+ if (e.type === "text_delta")
945
+ emit("text_delta", { delta: e.delta });
946
+ else if (e.type === "reasoning_delta")
947
+ emit("reasoning_delta", { delta: e.delta });
948
+ else {
949
+ if (completion)
950
+ throw new Error("Provider emitted multiple completions");
951
+ completion = e.message;
952
+ truncated = e.message.truncated === true;
953
+ usage = e.usage;
954
+ tokens += (e.usage?.input ?? 0) + (e.usage?.output ?? 0);
955
+ usageTotal.input += e.usage?.input ?? 0;
956
+ usageTotal.output += e.usage?.output ?? 0;
957
+ }
958
+ }
959
+ }
960
+ catch (error) {
961
+ failure = error;
962
+ }
963
+ finally {
964
+ stopSilentTimer();
965
+ }
966
+ const silentFailure = silentTimedOut &&
967
+ firstDelta === undefined &&
968
+ !completion &&
969
+ !combined.aborted &&
970
+ silentAttempts <= firstTokenRetries;
971
+ if (!silentFailure) {
972
+ if (failure !== undefined) {
973
+ // A response cut off before anything usable is recovered like a truncated one.
974
+ if (!completion && !combined.aborted && isOutputTruncationError(failure)) {
975
+ cutByError = true;
976
+ break;
977
+ }
978
+ throw failure;
979
+ }
980
+ break;
981
+ }
982
+ // Do not start an attempt the whole-run limit could not let finish.
983
+ const retryPauseMs = FIRST_TOKEN_RETRY_PAUSE_MS;
984
+ if (runTimeoutMs - (Date.now() - runStartedAt) < retryPauseMs + firstTokenMs) {
985
+ if (failure !== undefined)
986
+ throw failure;
987
+ break;
988
+ }
989
+ // From here the silence is handled: a later stop or run timeout is judged on its own.
990
+ silentTimedOut = false;
991
+ emit("request_retry", {
992
+ attempt: silentAttempts,
993
+ of: firstTokenRetries,
994
+ reason: "first_token_timeout",
995
+ afterMs: Date.now() - requested,
996
+ });
997
+ await abortableDelay(retryPauseMs, combined);
840
998
  combined.throwIfAborted();
841
- if (e.type !== "completed")
842
- firstDelta ??= Date.now();
843
- if (e.type === "text_delta")
844
- emit("text_delta", { delta: e.delta });
845
- else if (e.type === "reasoning_delta")
846
- emit("reasoning_delta", { delta: e.delta });
847
- else {
848
- if (completion)
849
- throw new Error("Provider emitted multiple completions");
850
- completion = e.message;
851
- truncated = e.message.truncated === true;
852
- usage = e.usage;
853
- tokens += (e.usage?.input ?? 0) + (e.usage?.output ?? 0);
854
- usageTotal.input += e.usage?.input ?? 0;
855
- usageTotal.output += e.usage?.output ?? 0;
999
+ stage = "waiting_model";
1000
+ }
1001
+ stage = "other";
1002
+ // A response cut off by the output limit that cannot be used as it is: no visible text
1003
+ // and no tool call, or a tool call whose arguments are incomplete. Complete calls of a
1004
+ // cut response, and a cut response with usable text, keep their normal handling below.
1005
+ let cut;
1006
+ if (cutByError)
1007
+ cut = "empty_response";
1008
+ else if (!completion)
1009
+ throw new Error("Provider stream ended without a completed response");
1010
+ else if (truncated) {
1011
+ if (incompleteCalls(completion.calls))
1012
+ cut = "tool_call_cut";
1013
+ else if (!completion.text.trim() && !completion.calls.length)
1014
+ cut = "empty_response";
1015
+ }
1016
+ if (cut) {
1017
+ const maxOutputTokens = outputLimit.value;
1018
+ truncations++;
1019
+ // Visible text is kept as a normal assistant message; the cut calls are discarded:
1020
+ // never executed, never persisted (they would have no result), no provider data.
1021
+ const keep = completion?.text ?? "";
1022
+ if (keep.trim()) {
1023
+ o.store.append(sessionId, { role: "assistant", text: keep, calls: [] });
1024
+ lastText = keep;
856
1025
  }
1026
+ if (truncations > truncationRecoveries)
1027
+ throw new RunTruncationError({
1028
+ attempts: truncations,
1029
+ maxOutputTokens,
1030
+ source: outputLimit.source,
1031
+ ...(modelId ? { model: modelId } : {}),
1032
+ });
1033
+ // Reasoning exhausted the budget (nothing at all came out): ask for less of it once.
1034
+ const levels = o.effortLevels?.(model);
1035
+ const lowered = truncations === 1 && cut === "empty_response"
1036
+ ? lowerEffort(requestEffort ?? levels?.defaultLevel, levels?.supportedLevels)
1037
+ : undefined;
1038
+ emit("truncation_recovery", {
1039
+ attempt: truncations,
1040
+ of: truncationRecoveries,
1041
+ reason: cut,
1042
+ maxOutputTokens,
1043
+ ...(lowered ? { effort: lowered } : {}),
1044
+ });
1045
+ recoveryNotice = TRUNCATION_NOTICE;
1046
+ recoveryEffort = lowered;
1047
+ await abortableDelay(TRUNCATION_RETRY_PAUSE_MS, combined);
1048
+ combined.throwIfAborted();
1049
+ // The recovery is not a turn.
1050
+ turn--;
1051
+ continue;
857
1052
  }
858
1053
  if (!completion)
859
1054
  throw new Error("Provider stream ended without a completed response");
@@ -891,7 +1086,8 @@ export class AgentRunner {
891
1086
  // The adapter kept a usable answer, but the token budget cut it off.
892
1087
  emit("response_truncated", {
893
1088
  turn: turn + 1,
894
- maxOutputTokens: options.maxOutputTokens ?? o.maxOutputTokens ?? 4096,
1089
+ maxOutputTokens: outputLimit.value,
1090
+ source: outputLimit.source,
895
1091
  });
896
1092
  final.truncated = true;
897
1093
  }
@@ -944,6 +1140,8 @@ export class AgentRunner {
944
1140
  };
945
1141
  },
946
1142
  };
1143
+ stage = "tool";
1144
+ stageTool = prepared[0]?.call.name;
947
1145
  const execute = (p) => this.executeCall(scope, p);
948
1146
  const results = [];
949
1147
  // Bounded parallel batches for consecutive read or explicitly concurrent tools.
@@ -974,6 +1172,8 @@ export class AgentRunner {
974
1172
  if (c && r)
975
1173
  o.store.append(sessionId, { role: "tool", callId: c.id, result: r });
976
1174
  }
1175
+ stage = "other";
1176
+ stageTool = undefined;
977
1177
  if (tokens >= budget)
978
1178
  throw new Error("Token budget exhausted");
979
1179
  }
@@ -986,6 +1186,33 @@ export class AgentRunner {
986
1186
  return { sessionId, text: lastText, status: "turns-exceeded", usage: usageTotal };
987
1187
  }
988
1188
  catch (error) {
1189
+ const timedOut = silentTimedOut
1190
+ ? { kind: "first_token", ms: firstTokenMs, attempts: Math.max(1, silentAttempts) }
1191
+ : combined.aborted && isTimeoutReason(combined.reason)
1192
+ ? { kind: "run", ms: runTimeoutMs }
1193
+ : undefined;
1194
+ if (timedOut && !(combined.aborted && !isTimeoutReason(combined.reason))) {
1195
+ const failure = new RunTimeoutError({
1196
+ ...timedOut,
1197
+ ...(modelId ? { model: modelId } : {}),
1198
+ ...(providerName ? { provider: providerName } : {}),
1199
+ stage,
1200
+ ...(stage === "tool" && stageTool ? { tool: stageTool } : {}),
1201
+ ...(requests <= 1 ? { firstRequest: true } : {}),
1202
+ });
1203
+ emit("run_failed", { error: failure.message, code: "timeout", timeout: failure.info });
1204
+ finish("failed", failure.message);
1205
+ throw failure;
1206
+ }
1207
+ if (error instanceof RunTruncationError) {
1208
+ emit("run_failed", {
1209
+ error: error.message,
1210
+ code: "output_truncated",
1211
+ truncation: error.info,
1212
+ });
1213
+ finish("failed", error.message);
1214
+ throw error;
1215
+ }
989
1216
  // On cancellation report the abort reason, not the transport's secondary error.
990
1217
  const cause = combined.aborted ? (combined.reason ?? error) : error;
991
1218
  const message = cause instanceof Error ? cause.message : String(cause);
@@ -0,0 +1,15 @@
1
+ import type { RunTimeoutInfo } from "@alisio/sdk";
2
+ /** A run stopped by a time limit; the message says which limit, which model and what to do. */
3
+ export declare class RunTimeoutError extends Error {
4
+ readonly info: RunTimeoutInfo;
5
+ constructor(info: RunTimeoutInfo);
6
+ }
7
+ /** `AbortSignal.timeout` aborts with a `TimeoutError`; an explicit `abort()` does not. */
8
+ export declare function isTimeoutReason(reason: unknown): boolean;
9
+ /**
10
+ * A short, human label for a provider id: the host of an OpenAI-compatible endpoint
11
+ * (`openai-compatible:chat:https://api.example.com/v1` → `api.example.com`), else the id itself.
12
+ */
13
+ export declare function providerLabel(id: string | undefined): string | undefined;
14
+ /** Plain-text message of a timeout (TUI, headless, logs, the `runs.error` column). English only. */
15
+ export declare function describeTimeout(info: RunTimeoutInfo): string;
@@ -0,0 +1,61 @@
1
+ /** A run stopped by a time limit; the message says which limit, which model and what to do. */
2
+ export class RunTimeoutError extends Error {
3
+ info;
4
+ constructor(info) {
5
+ super(describeTimeout(info));
6
+ this.name = "RunTimeoutError";
7
+ this.info = info;
8
+ }
9
+ }
10
+ /** `AbortSignal.timeout` aborts with a `TimeoutError`; an explicit `abort()` does not. */
11
+ export function isTimeoutReason(reason) {
12
+ return (typeof reason === "object" &&
13
+ reason !== null &&
14
+ reason.name === "TimeoutError");
15
+ }
16
+ /**
17
+ * A short, human label for a provider id: the host of an OpenAI-compatible endpoint
18
+ * (`openai-compatible:chat:https://api.example.com/v1` → `api.example.com`), else the id itself.
19
+ */
20
+ export function providerLabel(id) {
21
+ if (!id)
22
+ return undefined;
23
+ const url = /(https?:\/\/[^\s]+)/.exec(id)?.[1];
24
+ if (url)
25
+ try {
26
+ return new URL(url).host;
27
+ }
28
+ catch {
29
+ /* fall through to the raw id */
30
+ }
31
+ return id;
32
+ }
33
+ const seconds = (ms) => Math.round(ms / 1000);
34
+ /** Plain-text message of a timeout (TUI, headless, logs, the `runs.error` column). English only. */
35
+ export function describeTimeout(info) {
36
+ const who = info.model
37
+ ? `The model ${info.model}${info.provider ? ` (${info.provider})` : ""}`
38
+ : "The model";
39
+ const limit = `${seconds(info.ms)} s`;
40
+ if (info.kind === "first_token" && info.attempts && info.attempts > 1)
41
+ return (`${who} did not respond after ${info.attempts} attempts of ${limit} each: no tokens were received, so the run was stopped. ` +
42
+ "Retry, switch to another model, or raise or disable limits.firstTokenTimeoutMs (0 disables it); " +
43
+ "limits.firstTokenRetries sets how many times a silent request is sent again.");
44
+ if (info.kind === "first_token")
45
+ return (`${who} sent nothing within ${limit} of the request, so the run was stopped. ` +
46
+ "Retry, switch to another model, or raise or disable limits.firstTokenTimeoutMs (0 disables it).");
47
+ if (info.stage === "waiting_model")
48
+ return info.firstRequest
49
+ ? `${who} did not respond within ${limit}: no tokens were received. ` +
50
+ "Retry, switch to another model, or raise limits.timeoutMs."
51
+ : `The run reached its ${limit} limit while waiting for ${info.model ? `the model ${info.model}` : "the model"}` +
52
+ `${info.provider ? ` (${info.provider})` : ""}. ` +
53
+ "Everything produced so far is kept: prompt again to continue, switch model, or raise limits.timeoutMs.";
54
+ const doing = info.stage === "tool" && info.tool
55
+ ? `while running ${info.tool}`
56
+ : info.stage === "streaming"
57
+ ? "while the model was answering"
58
+ : "";
59
+ return (`The run reached its ${limit} limit${doing ? ` ${doing}` : ""}. ` +
60
+ "Everything produced so far is kept: prompt again to continue, or raise limits.timeoutMs.");
61
+ }
@@ -0,0 +1,30 @@
1
+ import { type RunTruncationInfo, type ToolCall } from "@alisio/sdk";
2
+ /**
3
+ * Recovery from a model response that the output-token limit (`limits.maxOutputTokens`) cut
4
+ * off before it was usable. Provider-agnostic: the runner decides from the completed message
5
+ * (`truncated: true`) or from the typed `OutputTruncatedError` signal, never from SDK types.
6
+ */
7
+ /** The run failed because every response was cut off; carries the numbers for the UIs. */
8
+ export declare class RunTruncationError extends Error {
9
+ readonly code = "output_truncated";
10
+ readonly info: RunTruncationInfo;
11
+ constructor(info: RunTruncationInfo);
12
+ }
13
+ /** Plain-text failure message (TUI, headless, logs). English only; the web localizes it. */
14
+ export declare function describeTruncation(info: RunTruncationInfo): string;
15
+ /** Whether a provider failure means "the response was cut off by the output-token limit". */
16
+ export declare function isOutputTruncationError(error: unknown): boolean;
17
+ /**
18
+ * Whether a tool call of a truncated response cannot be trusted: missing id or name, or
19
+ * arguments that are not a complete JSON object. Empty arguments only count as incomplete for
20
+ * the LAST call (the one being streamed when the limit hit); an earlier call with no arguments
21
+ * is how a provider reports a call without parameters.
22
+ */
23
+ export declare function incompleteCalls(calls: ToolCall[]): boolean;
24
+ /** What the model is told on the request that follows a truncated response. */
25
+ export declare const TRUNCATION_NOTICE: string;
26
+ /**
27
+ * One step below `current` among the model's supported levels, or `undefined` when that cannot
28
+ * be decided cleanly (unknown level names, no current level, or already the lowest).
29
+ */
30
+ export declare function lowerEffort(current: string | undefined, supported: readonly string[] | undefined): string | undefined;
@@ -0,0 +1,83 @@
1
+ import { OUTPUT_TRUNCATED_CODE } from "@alisio/sdk";
2
+ import { describeOutputLimitSource } from "./output-limit.js";
3
+ /**
4
+ * Recovery from a model response that the output-token limit (`limits.maxOutputTokens`) cut
5
+ * off before it was usable. Provider-agnostic: the runner decides from the completed message
6
+ * (`truncated: true`) or from the typed `OutputTruncatedError` signal, never from SDK types.
7
+ */
8
+ /** The run failed because every response was cut off; carries the numbers for the UIs. */
9
+ export class RunTruncationError extends Error {
10
+ code = OUTPUT_TRUNCATED_CODE;
11
+ info;
12
+ constructor(info) {
13
+ super(describeTruncation(info));
14
+ this.name = "RunTruncationError";
15
+ this.info = info;
16
+ }
17
+ }
18
+ /** Plain-text failure message (TUI, headless, logs). English only; the web localizes it. */
19
+ export function describeTruncation(info) {
20
+ const who = info.model ? `The model ${info.model}` : "The model";
21
+ const times = info.attempts === 1 ? "once" : `${info.attempts} times`;
22
+ return (`${who} was cut off ${times} by the output-token limit (${info.maxOutputTokens} tokens, ${describeOutputLimitSource(info.source)}) ` +
23
+ "before it produced a usable response, so the run was stopped. " +
24
+ "Raise limits.maxOutputTokens (/settings or the config file), lower the reasoning effort (/effort), " +
25
+ "or ask for the result in smaller parts.");
26
+ }
27
+ /**
28
+ * COMPATIBILITY: provider plugins published before the typed signal (for example
29
+ * `@alisio/plugin-deepseek` 0.1.1) throw a plain `Error` with this text when a response is cut
30
+ * off before any visible text. It is the only place the text is matched; new providers should
31
+ * yield `completed` with `truncated: true` or throw `OutputTruncatedError` from `@alisio/sdk`.
32
+ */
33
+ const LEGACY_TRUNCATION_TEXT = "cut off by max output tokens before any usable content";
34
+ /** Whether a provider failure means "the response was cut off by the output-token limit". */
35
+ export function isOutputTruncationError(error) {
36
+ if (typeof error !== "object" || error === null)
37
+ return false;
38
+ const { code, message } = error;
39
+ return (code === OUTPUT_TRUNCATED_CODE ||
40
+ (typeof message === "string" && message.includes(LEGACY_TRUNCATION_TEXT)));
41
+ }
42
+ /**
43
+ * Whether a tool call of a truncated response cannot be trusted: missing id or name, or
44
+ * arguments that are not a complete JSON object. Empty arguments only count as incomplete for
45
+ * the LAST call (the one being streamed when the limit hit); an earlier call with no arguments
46
+ * is how a provider reports a call without parameters.
47
+ */
48
+ export function incompleteCalls(calls) {
49
+ return calls.some((call, index) => {
50
+ if (!call.id || !call.name)
51
+ return true;
52
+ const raw = call.arguments.trim();
53
+ if (!raw)
54
+ return index === calls.length - 1;
55
+ try {
56
+ const parsed = JSON.parse(raw);
57
+ return typeof parsed !== "object" || parsed === null || Array.isArray(parsed);
58
+ }
59
+ catch {
60
+ return true;
61
+ }
62
+ });
63
+ }
64
+ /** What the model is told on the request that follows a truncated response. */
65
+ export const TRUNCATION_NOTICE = "Your previous reply was cut off by the output-token limit, so it was discarded. " +
66
+ "Do not repeat what you already produced. Work in smaller steps: write long files in several parts " +
67
+ "(for example save the script or data to files in several tool calls, or build the dashboard with " +
68
+ "the compact alisio_runtime.html and svg helpers), keep your reasoning short, and continue from where you stopped.";
69
+ const EFFORT_RANK = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
70
+ /**
71
+ * One step below `current` among the model's supported levels, or `undefined` when that cannot
72
+ * be decided cleanly (unknown level names, no current level, or already the lowest).
73
+ */
74
+ export function lowerEffort(current, supported) {
75
+ if (!current || !supported?.length)
76
+ return undefined;
77
+ const rank = (level) => EFFORT_RANK.indexOf(level);
78
+ if (rank(current) < 0 || supported.some((level) => rank(level) < 0))
79
+ return undefined;
80
+ const lower = [...supported].filter((level) => rank(level) < rank(current));
81
+ lower.sort((a, b) => rank(b) - rank(a));
82
+ return lower[0];
83
+ }
package/dist/index.d.ts CHANGED
@@ -27,6 +27,7 @@ export { BUILTIN_COMMANDS, type BuiltinCommand, CommandCatalog, type CommandExec
27
27
  export { type Config, type ConfigLoadResult, type ConfigProvenance, configFile, configHome, configSchema, isSettableSettingKey, type LoadedConfig, loadConfig, loadConfigWithProvenance, type McpServerSource, OCI_IMAGE_PATTERN, overridesSavedProviderProfile, type SettableSettingInfo, type SettableSettingKey, setConfigValue, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, settableSettings, stateHome, } from "./config.ts";
28
28
  export { type ContextBudget, checkpointInstructions, completeText, effectiveContextBudget, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, renderCheckpoint, serializeForSummary, shouldCompact, shouldCompactContext, summarize, } from "./core/compaction.ts";
29
29
  export type { ApprovalDecision, ApprovalHandler, ApprovalRequest, ArtifactPublisherFactory, BeginRunInput, CapabilityGate, ContextSource, CoreArtifactPublisher, EndRunInput, EventPage, HookFailure, MessagePage, PageOptions, Policy, PublishedArtifactInfo, RunnerExtensions, RunRecord, RunStatus, Session, SessionStore, StoredEvent, TerminalRunStatus, ToolCallMeta, } from "./core/contracts.ts";
30
+ export { DEFAULT_MAX_OUTPUT_TOKENS, describeOutputLimitSource, MAX_AUTO_OUTPUT_TOKENS, type OutputLimit, type OutputLimitSource, resolveMaxOutputTokens, } from "./core/output-limit.ts";
30
31
  export { ToolRegistry } from "./core/registry.ts";
31
32
  export { AgentRunner, type CompactionResult, defaultTokenBudget, type RunnerOptions, type RunnerSettingsPatch, type RunOptions, } from "./core/runner.ts";
32
33
  export { type ExtensionConflict, ExtensionRegistry } from "./extensions/registry.ts";
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ export { ArtifactRejected, ArtifactStore, readHead as readArtifactHead, slugify
26
26
  export { BUILTIN_COMMANDS, CommandCatalog, } from "./commands/catalog.js";
27
27
  export { configFile, configHome, configSchema, isSettableSettingKey, loadConfig, loadConfigWithProvenance, OCI_IMAGE_PATTERN, overridesSavedProviderProfile, setConfigValue, setGlobalMcpAllow, setMcpServerEnabled, setProjectPluginEnabled, setProjectSkillEnabled, settableSettings, stateHome, } from "./config.js";
28
28
  export { checkpointInstructions, completeText, effectiveContextBudget, estimateTokens, MAX_TRUSTED_WINDOW, parseCheckpointOutput, planCompaction, renderCheckpoint, serializeForSummary, shouldCompact, shouldCompactContext, summarize, } from "./core/compaction.js";
29
+ export { DEFAULT_MAX_OUTPUT_TOKENS, describeOutputLimitSource, MAX_AUTO_OUTPUT_TOKENS, resolveMaxOutputTokens, } from "./core/output-limit.js";
29
30
  export { ToolRegistry } from "./core/registry.js";
30
31
  export { AgentRunner, defaultTokenBudget, } from "./core/runner.js";
31
32
  export { ExtensionRegistry } from "./extensions/registry.js";
@@ -125,6 +125,7 @@ function pythonRunDescription(deps) {
125
125
  "data_inspect) to $ALISIO_INPUT_DIR/<name>.sqlite: alisio_runtime.datasets.open(name) returns a",
126
126
  "read-only sqlite3 connection (table `data` or `s_<sheet>`). HTML is shown offline: embed data and scripts",
127
127
  "inline (no fetch, no CDN). Nothing is published when the script fails unless publishOnError.",
128
+ "Keep each call's code short: the model's output is capped per response, so split large scripts or dashboards into several calls or files.",
128
129
  "To run an earlier analysis again unchanged (same script, inputs checked by hash, new artifacts) pass rerunOf: an artifact or execution id of this session, instead of code.",
129
130
  container ? CONTAINER_NOTE : NOT_SANDBOXED,
130
131
  ...(deps.startup.candidates || container
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alisio/core",
3
- "version": "0.1.0-alpha.22",
3
+ "version": "0.1.0-alpha.24",
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.18",
44
+ "@alisio/sdk": "0.1.0-alpha.20",
45
45
  "@mixmark-io/domino": "2.2.0",
46
46
  "@modelcontextprotocol/client": "2.1.0",
47
47
  "ajv": "8.20.0",