@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.
- package/dist/application.js +45 -0
- package/dist/config.d.ts +16 -4
- package/dist/config.js +35 -6
- package/dist/core/output-limit.d.ts +34 -0
- package/dist/core/output-limit.js +39 -0
- package/dist/core/runner.d.ts +51 -0
- package/dist/core/runner.js +260 -33
- package/dist/core/timeout.d.ts +15 -0
- package/dist/core/timeout.js +61 -0
- 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
|
|
@@ -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.
|
|
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
|
|
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
|
|
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.
|
|
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(
|
|
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.
|
|
180
|
-
*
|
|
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().
|
|
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:
|
|
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
|
+
}
|
package/dist/core/runner.d.ts
CHANGED
|
@@ -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. */
|
package/dist/core/runner.js
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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:
|
|
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";
|
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",
|