@alisio/core 0.1.0-alpha.23 → 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.
- package/dist/application.js +43 -0
- package/dist/config.d.ts +8 -4
- package/dist/config.js +16 -4
- package/dist/core/output-limit.d.ts +34 -0
- package/dist/core/output-limit.js +39 -0
- package/dist/core/runner.d.ts +32 -0
- package/dist/core/runner.js +104 -7
- package/dist/core/truncation.d.ts +30 -0
- package/dist/core/truncation.js +83 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/tools/analysis.js +1 -0
- package/package.json +2 -2
package/dist/application.js
CHANGED
|
@@ -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
|
|
@@ -985,6 +1027,7 @@ export async function createApplication(options = {}) {
|
|
|
985
1027
|
case "limits.maxContextChars":
|
|
986
1028
|
case "limits.firstTokenTimeoutMs":
|
|
987
1029
|
case "limits.firstTokenRetries":
|
|
1030
|
+
case "limits.truncationRecoveries":
|
|
988
1031
|
case "limits.timeoutMs": {
|
|
989
1032
|
const patch = limitLeaf(key.slice("limits.".length));
|
|
990
1033
|
config.limits = { ...config.limits, ...patch };
|
package/dist/config.d.ts
CHANGED
|
@@ -177,8 +177,9 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
177
177
|
timeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
178
178
|
firstTokenTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
179
179
|
firstTokenRetries: z.ZodDefault<z.ZodNumber>;
|
|
180
|
+
truncationRecoveries: z.ZodDefault<z.ZodNumber>;
|
|
180
181
|
maxContextChars: z.ZodDefault<z.ZodNumber>;
|
|
181
|
-
maxOutputTokens: z.
|
|
182
|
+
maxOutputTokens: z.ZodOptional<z.ZodNumber>;
|
|
182
183
|
maxTokens: z.ZodOptional<z.ZodNumber>;
|
|
183
184
|
}, z.core.$strip>>;
|
|
184
185
|
analysis: z.ZodDefault<z.ZodObject<{
|
|
@@ -274,8 +275,9 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
274
275
|
timeoutMs: number;
|
|
275
276
|
firstTokenTimeoutMs: number;
|
|
276
277
|
firstTokenRetries: number;
|
|
278
|
+
truncationRecoveries: number;
|
|
277
279
|
maxContextChars: number;
|
|
278
|
-
maxOutputTokens
|
|
280
|
+
maxOutputTokens?: number | undefined;
|
|
279
281
|
maxTokens?: number | undefined;
|
|
280
282
|
};
|
|
281
283
|
analysis: {
|
|
@@ -448,8 +450,9 @@ export declare const configSchema: z.ZodPipe<z.ZodObject<{
|
|
|
448
450
|
timeoutMs: number;
|
|
449
451
|
firstTokenTimeoutMs: number;
|
|
450
452
|
firstTokenRetries: number;
|
|
453
|
+
truncationRecoveries: number;
|
|
451
454
|
maxContextChars: number;
|
|
452
|
-
maxOutputTokens
|
|
455
|
+
maxOutputTokens?: number | undefined;
|
|
453
456
|
maxTokens?: number | undefined;
|
|
454
457
|
};
|
|
455
458
|
analysis: {
|
|
@@ -558,11 +561,12 @@ declare const SETTABLE_KEYS: {
|
|
|
558
561
|
readonly "context.claudeMdFallback": z.ZodDefault<z.ZodBoolean>;
|
|
559
562
|
readonly "context.maxBytes": z.ZodDefault<z.ZodNumber>;
|
|
560
563
|
readonly "limits.maxTurns": z.ZodDefault<z.ZodNumber>;
|
|
561
|
-
readonly "limits.maxOutputTokens": z.
|
|
564
|
+
readonly "limits.maxOutputTokens": z.ZodOptional<z.ZodNumber>;
|
|
562
565
|
readonly "limits.maxContextChars": z.ZodDefault<z.ZodNumber>;
|
|
563
566
|
readonly "limits.timeoutMs": z.ZodDefault<z.ZodNumber>;
|
|
564
567
|
readonly "limits.firstTokenTimeoutMs": z.ZodDefault<z.ZodNumber>;
|
|
565
568
|
readonly "limits.firstTokenRetries": z.ZodDefault<z.ZodNumber>;
|
|
569
|
+
readonly "limits.truncationRecoveries": z.ZodDefault<z.ZodNumber>;
|
|
566
570
|
readonly "pluginHooks.timeoutMs": z.ZodDefault<z.ZodNumber>;
|
|
567
571
|
readonly "tui.paddingX": z.ZodDefault<z.ZodNumber>;
|
|
568
572
|
readonly "tui.contentPaddingX": z.ZodDefault<z.ZodNumber>;
|
package/dist/config.js
CHANGED
|
@@ -187,12 +187,23 @@ const configObjectSchema = z
|
|
|
187
187
|
* disables the retry. Never counts as a turn.
|
|
188
188
|
*/
|
|
189
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),
|
|
190
197
|
maxContextChars: z.number().int().positive().default(800000),
|
|
191
198
|
/**
|
|
192
|
-
* Per-call output token budget for one agent turn.
|
|
193
|
-
*
|
|
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.
|
|
194
205
|
*/
|
|
195
|
-
maxOutputTokens: z.number().int().positive().
|
|
206
|
+
maxOutputTokens: z.number().int().positive().optional(),
|
|
196
207
|
/** Cumulative tokens per run; default is proportional to the context window. */
|
|
197
208
|
maxTokens: z.number().int().positive().optional(),
|
|
198
209
|
})
|
|
@@ -201,8 +212,8 @@ const configObjectSchema = z
|
|
|
201
212
|
timeoutMs: 600000,
|
|
202
213
|
firstTokenTimeoutMs: 90000,
|
|
203
214
|
firstTokenRetries: 1,
|
|
215
|
+
truncationRecoveries: 2,
|
|
204
216
|
maxContextChars: 800000,
|
|
205
|
-
maxOutputTokens: 16384,
|
|
206
217
|
})),
|
|
207
218
|
/**
|
|
208
219
|
* Python analysis and downloadable artifacts. `enabled: false` registers none of the
|
|
@@ -703,6 +714,7 @@ const SETTABLE_KEYS = {
|
|
|
703
714
|
"limits.timeoutMs": SETTABLE_SECTIONS.limits.shape.timeoutMs,
|
|
704
715
|
"limits.firstTokenTimeoutMs": SETTABLE_SECTIONS.limits.shape.firstTokenTimeoutMs,
|
|
705
716
|
"limits.firstTokenRetries": SETTABLE_SECTIONS.limits.shape.firstTokenRetries,
|
|
717
|
+
"limits.truncationRecoveries": SETTABLE_SECTIONS.limits.shape.truncationRecoveries,
|
|
706
718
|
"pluginHooks.timeoutMs": SETTABLE_SECTIONS.pluginHooks.shape.timeoutMs,
|
|
707
719
|
"tui.paddingX": SETTABLE_SECTIONS.tui.shape.paddingX,
|
|
708
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
|
+
}
|
package/dist/core/runner.d.ts
CHANGED
|
@@ -28,8 +28,31 @@ export interface RunnerOptions {
|
|
|
28
28
|
firstTokenTimeoutMs?: number;
|
|
29
29
|
/** See `RunnerSettingsPatch.firstTokenRetries`. */
|
|
30
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;
|
|
31
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
|
+
*/
|
|
32
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;
|
|
33
56
|
maxTokens?: number;
|
|
34
57
|
onEvent?: (event: RunEvent) => void;
|
|
35
58
|
readConcurrency?: number;
|
|
@@ -85,6 +108,11 @@ export interface RunnerSettingsPatch {
|
|
|
85
108
|
* received for the request.
|
|
86
109
|
*/
|
|
87
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;
|
|
88
116
|
}
|
|
89
117
|
/**
|
|
90
118
|
* Per-run overrides used by embedders and child sessions. Callers must only NARROW: `policy`
|
|
@@ -116,6 +144,8 @@ export interface RunOptions {
|
|
|
116
144
|
firstTokenTimeoutMs?: number;
|
|
117
145
|
/** Silent-request retries for this run; beats the runner-level `firstTokenRetries`. */
|
|
118
146
|
firstTokenRetries?: number;
|
|
147
|
+
/** Output-truncation recoveries for this run; beats the runner-level `truncationRecoveries`. */
|
|
148
|
+
truncationRecoveries?: number;
|
|
119
149
|
/** Reasoning effort level for this run; beats the runner-level default when set. */
|
|
120
150
|
reasoningEffort?: string;
|
|
121
151
|
/** Run id to use for this run's events (e.g. preassigned by an embedder); a UUID otherwise. */
|
|
@@ -202,6 +232,8 @@ export declare class AgentRunner {
|
|
|
202
232
|
contextWindow?: number;
|
|
203
233
|
reasoningEffort?: string;
|
|
204
234
|
}>;
|
|
235
|
+
/** The output-token budget for `model`: user value, else catalog maximum, else fallback. */
|
|
236
|
+
private outputLimit;
|
|
205
237
|
/** Change the model used by subsequent turns of a session; recorded in the store. */
|
|
206
238
|
setModel(sessionId: string, model: string): void;
|
|
207
239
|
/** Manually compact a session outside a run. */
|
package/dist/core/runner.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
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";
|
|
3
4
|
import { isTimeoutReason, providerLabel, RunTimeoutError } from "./timeout.js";
|
|
5
|
+
import { incompleteCalls, isOutputTruncationError, lowerEffort, RunTruncationError, TRUNCATION_NOTICE, } from "./truncation.js";
|
|
4
6
|
/** Pause before a silent request is sent again; short, and cut off by a stop or the run limit. */
|
|
5
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;
|
|
6
10
|
function abortableDelay(ms, signal) {
|
|
7
11
|
return new Promise((resolve) => {
|
|
8
12
|
if (signal.aborted)
|
|
@@ -211,11 +215,21 @@ export class AgentRunner {
|
|
|
211
215
|
instructions: await o.context.instructions(sessionId),
|
|
212
216
|
messages: o.store.messages(sessionId),
|
|
213
217
|
maxContextChars: o.maxContextChars ?? 800_000,
|
|
214
|
-
maxOutputTokens:
|
|
218
|
+
maxOutputTokens: (await this.outputLimit(session.model)).value,
|
|
215
219
|
timeoutMs: o.timeoutMs ?? 300_000,
|
|
216
220
|
...(o.reasoningEffort ? { reasoningEffort: o.reasoningEffort } : {}),
|
|
217
221
|
};
|
|
218
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
|
+
}
|
|
219
233
|
/** Change the model used by subsequent turns of a session; recorded in the store. */
|
|
220
234
|
setModel(sessionId, model) {
|
|
221
235
|
const id = model.trim();
|
|
@@ -715,6 +729,12 @@ export class AgentRunner {
|
|
|
715
729
|
const runTimeoutMs = options.timeoutMs ?? o.timeoutMs ?? 300_000;
|
|
716
730
|
const firstTokenMs = options.firstTokenTimeoutMs ?? o.firstTokenTimeoutMs ?? 0;
|
|
717
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;
|
|
718
738
|
const runStartedAt = Date.now();
|
|
719
739
|
const timeout = AbortSignal.timeout(runTimeoutMs);
|
|
720
740
|
const combined = AbortSignal.any([controller.signal, timeout, ...(signal ? [signal] : [])]);
|
|
@@ -865,6 +885,18 @@ export class AgentRunner {
|
|
|
865
885
|
// display-only extension of the persisted transcript and never reach a model prompt
|
|
866
886
|
// (raw image bytes included). The store keeps the rich parts for TUI replay.
|
|
867
887
|
const providerMessages = messages.map((m) => m.role === "tool" ? { ...m, result: textProjection(m.result) } : m);
|
|
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;
|
|
868
900
|
let requested = Date.now();
|
|
869
901
|
let firstDelta;
|
|
870
902
|
stage = "waiting_model";
|
|
@@ -896,14 +928,12 @@ export class AgentRunner {
|
|
|
896
928
|
instructions,
|
|
897
929
|
messages: providerMessages,
|
|
898
930
|
tools,
|
|
899
|
-
maxOutputTokens:
|
|
931
|
+
maxOutputTokens: outputLimit.value,
|
|
900
932
|
signal: firstTokenMs > 0 ? AbortSignal.any([combined, silent.signal]) : combined,
|
|
901
933
|
model,
|
|
902
934
|
sessionId,
|
|
903
935
|
...(o.nativeTools?.length ? { nativeTools: o.nativeTools } : {}),
|
|
904
|
-
...(
|
|
905
|
-
? { reasoningEffort: options.reasoningEffort ?? o.reasoningEffort }
|
|
906
|
-
: {}),
|
|
936
|
+
...(requestEffort ? { reasoningEffort: requestEffort } : {}),
|
|
907
937
|
})) {
|
|
908
938
|
combined.throwIfAborted();
|
|
909
939
|
stopSilentTimer();
|
|
@@ -939,8 +969,14 @@ export class AgentRunner {
|
|
|
939
969
|
!combined.aborted &&
|
|
940
970
|
silentAttempts <= firstTokenRetries;
|
|
941
971
|
if (!silentFailure) {
|
|
942
|
-
if (failure !== undefined)
|
|
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
|
+
}
|
|
943
978
|
throw failure;
|
|
979
|
+
}
|
|
944
980
|
break;
|
|
945
981
|
}
|
|
946
982
|
// Do not start an attempt the whole-run limit could not let finish.
|
|
@@ -963,6 +999,57 @@ export class AgentRunner {
|
|
|
963
999
|
stage = "waiting_model";
|
|
964
1000
|
}
|
|
965
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;
|
|
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;
|
|
1052
|
+
}
|
|
966
1053
|
if (!completion)
|
|
967
1054
|
throw new Error("Provider stream ended without a completed response");
|
|
968
1055
|
const durationMs = Date.now() - requested;
|
|
@@ -999,7 +1086,8 @@ export class AgentRunner {
|
|
|
999
1086
|
// The adapter kept a usable answer, but the token budget cut it off.
|
|
1000
1087
|
emit("response_truncated", {
|
|
1001
1088
|
turn: turn + 1,
|
|
1002
|
-
maxOutputTokens:
|
|
1089
|
+
maxOutputTokens: outputLimit.value,
|
|
1090
|
+
source: outputLimit.source,
|
|
1003
1091
|
});
|
|
1004
1092
|
final.truncated = true;
|
|
1005
1093
|
}
|
|
@@ -1116,6 +1204,15 @@ export class AgentRunner {
|
|
|
1116
1204
|
finish("failed", failure.message);
|
|
1117
1205
|
throw failure;
|
|
1118
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
|
+
}
|
|
1119
1216
|
// On cancellation report the abort reason, not the transport's secondary error.
|
|
1120
1217
|
const cause = combined.aborted ? (combined.reason ?? error) : error;
|
|
1121
1218
|
const message = cause instanceof Error ? cause.message : String(cause);
|
|
@@ -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";
|
package/dist/tools/analysis.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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",
|