@aragon-agent/cli 0.5.3 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +65 -2
- package/dist/cli.js +32 -5
- package/dist/commands/builtins.d.ts +2 -2
- package/dist/commands/builtins.js +51 -3
- package/dist/config/cli-commands.js +20 -2
- package/dist/config/env.d.ts +2 -1
- package/dist/config/env.js +17 -5
- package/dist/config/load.js +43 -2
- package/dist/config/schema.d.ts +64 -2
- package/dist/config/schema.js +73 -2
- package/dist/config/store.js +28 -1
- package/dist/ui/App.d.ts +22 -1
- package/dist/ui/App.js +48 -6
- package/dist/ui/overlays/HelpOverlay.js +1 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +12 -1
- package/dist/ui/overlays/SettingsScreen.js +38 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,35 @@ were written.
|
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
+
- **Output token limits that cannot themselves fail a run.** Every model and
|
|
13
|
+
every provider now defaults to 64000 output tokens, and that number is
|
|
14
|
+
configurable from the TUI, from a flag, from `ARAGON_MAX_TOKENS`, and by hand
|
|
15
|
+
in `~/.aragon-agent/config.json`. Leave it alone and it stays 64000.
|
|
16
|
+
|
|
17
|
+
What is new is that the number is an *ambition*. `gpt-4o` caps at 16384 and
|
|
18
|
+
`gemini-1.5-pro` at 8192; sending 64000 to either is an HTTP 400 that ends the
|
|
19
|
+
turn with a raw provider string in the transcript. The cap is now clamped down
|
|
20
|
+
to whatever the target model actually accepts, from a per-model table plus
|
|
21
|
+
whatever the provider's own model list reports. For a model nothing knows — a
|
|
22
|
+
proxy, a `--base-url` endpoint, something that shipped last week — the request
|
|
23
|
+
goes out at 64000, and if the provider objects, `aragon` reads the real ceiling
|
|
24
|
+
out of the error text, repairs the request, sends it exactly once more, and
|
|
25
|
+
remembers the answer for the rest of the session. One warning line; the run
|
|
26
|
+
continues.
|
|
27
|
+
|
|
28
|
+
Two combinations that were a guaranteed 400 on every request are fixed with
|
|
29
|
+
it: `--thinking xhigh` asks for a 65536-token budget while Anthropic requires
|
|
30
|
+
`max_tokens` to be strictly greater than it, and OpenAI's reasoning family
|
|
31
|
+
(`o1`, `o3`, `gpt-5`) rejects the `max_tokens` parameter name outright in
|
|
32
|
+
favour of `max_completion_tokens`.
|
|
33
|
+
|
|
34
|
+
New surfaces: `/max-tokens [<n>|auto|default]`, which with no argument reports
|
|
35
|
+
the setting *and* the effective cap for the live model; an `Effective:` line
|
|
36
|
+
under the settings screen's `Max tokens` field, so a value the model will not
|
|
37
|
+
honour is visible before you save it; `--max-tokens auto`; and
|
|
38
|
+
`aragon config set maxTokens <n|auto|default>`, which echoes the value that was
|
|
39
|
+
actually stored rather than the one you typed.
|
|
40
|
+
|
|
12
41
|
- **Team subagents.** The agent can now hand independent parts of a job to up to
|
|
13
42
|
five short-lived subagents that run in parallel, then read one combined report
|
|
14
43
|
of what each of them found. It decides for itself whether to delegate, so a
|
|
@@ -71,6 +100,14 @@ were written.
|
|
|
71
100
|
|
|
72
101
|
### Changed
|
|
73
102
|
|
|
103
|
+
- **`"maxTokens": null` in `config.json` now means AUTO** — use each model's own
|
|
104
|
+
ceiling, never above 64000. It previously resolved to 64000 by accident (the
|
|
105
|
+
layered resolver skipped `null`), and there was no way to express AUTO through
|
|
106
|
+
any supported write path. If you have a `null` there today, the only difference
|
|
107
|
+
is a downward clamp on models whose real ceiling is below 64000 — the direction
|
|
108
|
+
that prevents failures rather than causing them. Absent still means 64000, and
|
|
109
|
+
an explicit number still means that number.
|
|
110
|
+
|
|
74
111
|
- **`config.json` holds settings again.** The prompts you type are no longer
|
|
75
112
|
stored in it. They move to `~/.aragon-agent/prompt-history.jsonl` — one JSON
|
|
76
113
|
object per line, `0600` on POSIX — and the submit counter and the "notice
|
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ aragon --help | -h Print help
|
|
|
60
60
|
| `--base-url <url>` | Override the provider base URL (OpenAI-compatible endpoints) |
|
|
61
61
|
| `--api-key <key>` | One-shot key override (not persisted) |
|
|
62
62
|
| `--thinking <level>` | `off\|minimal\|low\|medium\|high\|xhigh` |
|
|
63
|
-
| `--max-tokens <n>` | Output token cap |
|
|
63
|
+
| `--max-tokens <n\|auto>` | Output token cap: a number, or `auto` for the model's own ceiling (default `64000`) |
|
|
64
64
|
| `--cwd <dir>` | Working directory for tools |
|
|
65
65
|
| `--confirm` | Confirm each mutating tool call |
|
|
66
66
|
| `--tool-timeout <ms>` | Per-tool executor ceiling (default 180000) |
|
|
@@ -91,6 +91,7 @@ Exit codes: `0` success · `1` agent/runtime error · `2` config/usage error ·
|
|
|
91
91
|
| `/model` | Open the model picker |
|
|
92
92
|
| `/settings` | Open the settings screen |
|
|
93
93
|
| `/thinking <level>` | Set the thinking level |
|
|
94
|
+
| `/max-tokens [<n>\|auto\|default]` | Output token cap; with no argument, reports the setting **and** the effective cap for the live model |
|
|
94
95
|
| `/tools` | List active tools |
|
|
95
96
|
| `/clear` | Clear the visible transcript |
|
|
96
97
|
| `/reset` | New conversation |
|
|
@@ -674,7 +675,9 @@ env / `.env` → CLI flags**.
|
|
|
674
675
|
|
|
675
676
|
- **Env / `.env`**: `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`,
|
|
676
677
|
`GOOGLE_API_KEY` / `GEMINI_API_KEY`, plus `ARAGON_PROVIDER`, `ARAGON_MODEL`,
|
|
677
|
-
`ARAGON_BASE_URL`, `ARAGON_THINKING`,
|
|
678
|
+
`ARAGON_BASE_URL`, `ARAGON_THINKING`,
|
|
679
|
+
`ARAGON_MAX_TOKENS` (a number, or `auto` / `0` for the per-model ceiling),
|
|
680
|
+
`ARAGON_THEME`,
|
|
678
681
|
`ARAGON_FULLSCREEN`, `ARAGON_PLAN` (`1` starts in plan mode),
|
|
679
682
|
`ARAGON_TEAM` (`0` disables team subagents), `ARAGON_TEAM_MAX` (fan-out width),
|
|
680
683
|
`ARAGON_SKILLS` (`0` disables), `ARAGON_SKILLS_PATH`,
|
|
@@ -692,6 +695,7 @@ env / `.env` → CLI flags**.
|
|
|
692
695
|
|
|
693
696
|
| Config key | Default | Meaning |
|
|
694
697
|
| --- | --- | --- |
|
|
698
|
+
| `maxTokens` | `64000` | Output token cap, clamped to `[256, 200000]`. `null` means **auto** — use each model's own ceiling, never above 64000. Absent means the default. See [Output token limits](#output-token-limits). |
|
|
695
699
|
| `fullscreen` | `true` | Use the full-screen TUI (still subject to the automatic downgrades). Setting it to `false` opts out permanently; leaving it `true` is *not* a force — only `--fullscreen` / `ARAGON_FULLSCREEN=1` override the heuristics. |
|
|
696
700
|
| `exitTranscript` | `true` | Replay a plain-text session summary after exiting (full-screen only). |
|
|
697
701
|
| `transcriptWindow` | `300` | How many trailing entries the full-screen viewport renders, clamped to `[50, 2000]`. The full history stays in memory and in `/save`. |
|
|
@@ -727,6 +731,65 @@ Byte, not character: `ToolExecutor` truncates at 100 000 **bytes**, so a
|
|
|
727
731
|
character-based budget would be roughly triple its nominal size for CJK text
|
|
728
732
|
and would get cut mid-tag.
|
|
729
733
|
|
|
734
|
+
### Output token limits
|
|
735
|
+
|
|
736
|
+
Every model and every provider defaults to **64000** output tokens. You never
|
|
737
|
+
have to configure this, and if you never touch it the number stays 64000.
|
|
738
|
+
|
|
739
|
+
The number you set is an **ambition, not a promise**: it is clamped down to
|
|
740
|
+
whatever the target model actually accepts. `gpt-4o` caps at 16384 and
|
|
741
|
+
`gemini-1.5-pro` at 8192, and sending 64000 to either is an HTTP 400 that ends
|
|
742
|
+
the turn — so `aragon` sends what the model will take, silently. When the model
|
|
743
|
+
is one it has never seen (a proxy, an endpoint behind `--base-url`, a model that
|
|
744
|
+
shipped last week), it sends 64000, and if the provider objects it reads the real
|
|
745
|
+
ceiling out of the provider's own error, repairs the request, sends it again, and
|
|
746
|
+
remembers the answer for the rest of the session. You see one warning line and
|
|
747
|
+
the run continues.
|
|
748
|
+
|
|
749
|
+
Four ways to change it, highest priority first:
|
|
750
|
+
|
|
751
|
+
```bash
|
|
752
|
+
aragon --max-tokens 32000 # this run only
|
|
753
|
+
aragon --max-tokens auto # this run only, per-model ceiling
|
|
754
|
+
ARAGON_MAX_TOKENS=32000 aragon # env layer, between the flag and the file
|
|
755
|
+
aragon config set maxTokens 32000 # persisted; prints "Set maxTokens = 32000"
|
|
756
|
+
aragon config set maxTokens auto # persisted as null
|
|
757
|
+
aragon config set maxTokens default # back to 64000
|
|
758
|
+
aragon config get maxTokens # "64000" | "auto"
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
Inside the TUI, `/max-tokens` reports the current setting **and** the effective
|
|
762
|
+
cap for the live model, and `/max-tokens <n|auto|default>` changes it. The
|
|
763
|
+
settings screen (`/settings`) has the same field with an `Effective:` line under
|
|
764
|
+
it, so a value the model will not honour is visible before you save it.
|
|
765
|
+
|
|
766
|
+
And by hand, in `~/.aragon-agent/config.json` (`aragon config path` prints the
|
|
767
|
+
exact location; `aragon config edit` opens it, and `/reload` picks up an edit
|
|
768
|
+
made in another window):
|
|
769
|
+
|
|
770
|
+
```jsonc
|
|
771
|
+
{
|
|
772
|
+
"version": 1,
|
|
773
|
+
"provider": "anthropic",
|
|
774
|
+
"model": "claude-sonnet-4-5-20250929",
|
|
775
|
+
|
|
776
|
+
// Output token cap.
|
|
777
|
+
// 64000 (or any 256..200000 integer) — an explicit cap, clamped down to
|
|
778
|
+
// the target model's real ceiling when that ceiling is lower.
|
|
779
|
+
// null — auto: use each model's own ceiling, never above 64000.
|
|
780
|
+
// absent — the product default, 64000.
|
|
781
|
+
"maxTokens": 64000
|
|
782
|
+
}
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
Out-of-range numbers are clamped on the way in **and** on the way out, so a
|
|
786
|
+
hand-edited `900000` becomes `200000` on disk rather than reverting to the
|
|
787
|
+
default on every launch.
|
|
788
|
+
|
|
789
|
+
Extended thinking is reconciled with the cap automatically: `--thinking xhigh`
|
|
790
|
+
asks for a 65536-token budget, Anthropic requires `max_tokens` to be strictly
|
|
791
|
+
greater than it, and the two are resolved together rather than left to collide.
|
|
792
|
+
|
|
730
793
|
These are additive: a config file written by an earlier version loads
|
|
731
794
|
unchanged, with no migration and no version bump.
|
|
732
795
|
|
package/dist/cli.js
CHANGED
|
@@ -27,7 +27,7 @@ import { LOG_CONFIG_SET_KEYS, TEAM_CONFIG_SET_KEYS, applyLogConfigSet, applyTeam
|
|
|
27
27
|
import { attachAgentEvents, attachTeamEvents, installLogging, setScreenRestore, setSignalTerminator, } from './logging/install.js';
|
|
28
28
|
import { getLogger } from './logging/logger.js';
|
|
29
29
|
import { runLogsCommand } from './logging/cli-commands.js';
|
|
30
|
-
import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampAskRounds, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampTranscriptWindow,
|
|
30
|
+
import { ADAPTER_PROVIDERS, clampDensity, clampTheme, clampThinkingLevel, clampAskRounds, clampHumanTimeout, clampSkillsIntegrity, clampSkillsToolPolicy, clampTranscriptWindow, coercePositiveInt, isAdapterProvider, parseMaxTokensInput, DEFAULT_MAX_TOKENS, DEFAULT_PLAN_HUMAN_TIMEOUT_MS, DEFAULT_PLAN_MAX_ASK_ROUNDS, DEFAULT_SKILLS_CONFIG, } from './config/schema.js';
|
|
31
31
|
import { makeGetApiKey } from './config/load.js';
|
|
32
32
|
import { AgentController, DENY_ALL_APPROVAL } from './agent/controller.js';
|
|
33
33
|
import { runSkillsCommand } from './skills/cli-commands.js';
|
|
@@ -397,6 +397,29 @@ const CONFIG_SET_KEYS = new Set([
|
|
|
397
397
|
// still printing `Set team.enabled = false`.
|
|
398
398
|
...TEAM_CONFIG_SET_KEYS,
|
|
399
399
|
]);
|
|
400
|
+
/**
|
|
401
|
+
* `aragon config set maxTokens <n|auto|default>`.
|
|
402
|
+
*
|
|
403
|
+
* REJECTS nonsense rather than clamping it, unlike `theme` and `thinkingLevel`.
|
|
404
|
+
* Those two clamp because their bad values are out-of-VOCABULARY; here a
|
|
405
|
+
* non-numeric argument is almost always a typo for a number the user meant, and
|
|
406
|
+
* storing 64000 for `3200O` would quietly discard their intent. An out-of-RANGE
|
|
407
|
+
* number is still clamped, in both directions, by the store's single gate.
|
|
408
|
+
*/
|
|
409
|
+
function runConfigSetMaxTokens(value) {
|
|
410
|
+
const raw = value.trim().toLowerCase();
|
|
411
|
+
const parsed = raw === 'default'
|
|
412
|
+
? { kind: 'value', value: DEFAULT_MAX_TOKENS, clamped: false }
|
|
413
|
+
: parseMaxTokensInput(value);
|
|
414
|
+
if (parsed.kind === 'invalid') {
|
|
415
|
+
process.stderr.write(`Invalid maxTokens "${value}". Use a number, "auto", or "default".\n`);
|
|
416
|
+
process.exitCode = 2;
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const stored = parsed.kind === 'auto' ? null : parsed.value;
|
|
420
|
+
const merged = updatePersistedConfig({ maxTokens: stored });
|
|
421
|
+
process.stdout.write(`Set maxTokens = ${merged.maxTokens ?? 'auto'}\n`);
|
|
422
|
+
}
|
|
400
423
|
function runConfigSet(key, value) {
|
|
401
424
|
if (!CONFIG_SET_KEYS.has(key)) {
|
|
402
425
|
process.stderr.write(`Unknown config key "${key}". Known keys: ${[...CONFIG_SET_KEYS].join(', ')}.\n`);
|
|
@@ -410,6 +433,13 @@ function runConfigSet(key, value) {
|
|
|
410
433
|
process.stdout.write(`Set ${key} = ${value}\n`);
|
|
411
434
|
return;
|
|
412
435
|
}
|
|
436
|
+
// Handled ahead of the switch because it needs to echo the STORED value, not
|
|
437
|
+
// the typed one: `config set maxTokens 900000` must not report a number that
|
|
438
|
+
// was never written. Same reason `team.*` has its own branch.
|
|
439
|
+
if (key === 'maxTokens') {
|
|
440
|
+
runConfigSetMaxTokens(value);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
413
443
|
const teamPatch = applyTeamConfigSet(key, value);
|
|
414
444
|
if (teamPatch) {
|
|
415
445
|
// The written value is echoed from the CLAMPED result rather than from the
|
|
@@ -434,9 +464,6 @@ function runConfigSet(key, value) {
|
|
|
434
464
|
case 'thinkingLevel':
|
|
435
465
|
patch.thinkingLevel = clampThinkingLevel(value, 'off');
|
|
436
466
|
break;
|
|
437
|
-
case 'maxTokens':
|
|
438
|
-
patch.maxTokens = coerceMaxTokens(value) ?? null;
|
|
439
|
-
break;
|
|
440
467
|
case 'theme':
|
|
441
468
|
patch.theme = clampTheme(value, 'auto');
|
|
442
469
|
break;
|
|
@@ -540,7 +567,7 @@ function buildProgram() {
|
|
|
540
567
|
.option('--base-url <url>', 'Override the provider base URL')
|
|
541
568
|
.option('--api-key <key>', 'One-shot API key override (not persisted)')
|
|
542
569
|
.option('--thinking <level>', 'off|minimal|low|medium|high|xhigh')
|
|
543
|
-
.option('--max-tokens <n>', 'Output token cap')
|
|
570
|
+
.option('--max-tokens <n|auto>', 'Output token cap: a number, or "auto" for the model ceiling (default 64000)')
|
|
544
571
|
.option('--cwd <dir>', 'Working directory for tools')
|
|
545
572
|
.option('--confirm', 'Confirm each mutating tool call')
|
|
546
573
|
.option('--tool-timeout <ms>', 'Per-tool executor ceiling (default 180000)')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in slash commands (spec §5.2):
|
|
3
|
-
* /help /model /settings /thinking /tools /clear /reset /cwd /save
|
|
4
|
-
* /copy /exit (/quit).
|
|
3
|
+
* /help /model /settings /thinking /max-tokens /tools /clear /reset /cwd /save
|
|
4
|
+
* /resume /copy /exit (/quit).
|
|
5
5
|
*/
|
|
6
6
|
import { CommandRegistry } from './registry.js';
|
|
7
7
|
export declare function registerBuiltinCommands(registry: CommandRegistry): void;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Built-in slash commands (spec §5.2):
|
|
3
|
-
* /help /model /settings /thinking /tools /clear /reset /cwd /save
|
|
4
|
-
* /copy /exit (/quit).
|
|
3
|
+
* /help /model /settings /thinking /max-tokens /tools /clear /reset /cwd /save
|
|
4
|
+
* /resume /copy /exit (/quit).
|
|
5
5
|
*/
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import { existsSync, statSync } from 'node:fs';
|
|
8
8
|
import { isAbsolute, resolve } from 'node:path';
|
|
9
9
|
import process from 'node:process';
|
|
10
|
-
import {
|
|
10
|
+
import { resolveOutputTokens } from '@aragon-agent/core';
|
|
11
|
+
import { clampThinkingLevel, clampTheme, isThemeName, parseMaxTokensInput, DEFAULT_MAX_TOKENS, LEGACY_DARK_THEME, THEME_NAMES, THINKING_LEVELS, } from '../config/schema.js';
|
|
11
12
|
import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
|
|
12
13
|
import { loadConfig } from '../config/load.js';
|
|
13
14
|
import { loadSession, resolveSessionPath, saveSession } from '../session/persist.js';
|
|
@@ -43,6 +44,53 @@ const COMMANDS = [
|
|
|
43
44
|
ctx.toast('info', `Thinking level set to "${clamped}".`);
|
|
44
45
|
},
|
|
45
46
|
},
|
|
47
|
+
{
|
|
48
|
+
name: 'max-tokens',
|
|
49
|
+
description: 'Show or set the output token cap (<n> | auto | default)',
|
|
50
|
+
/**
|
|
51
|
+
* Exists next to the settings screen for the reason `/theme` and `/plan` do:
|
|
52
|
+
* the slash palette is the discoverable surface, and this is one keystroke
|
|
53
|
+
* instead of five arrow presses.
|
|
54
|
+
*
|
|
55
|
+
* The no-argument branch reports the EFFECTIVE cap, not just the setting.
|
|
56
|
+
* "I set 64000" and "this model will produce 8192" are different facts, and
|
|
57
|
+
* only the second one explains a truncated answer.
|
|
58
|
+
*/
|
|
59
|
+
run: (ctx) => {
|
|
60
|
+
const arg = ctx.args.trim();
|
|
61
|
+
const cfg = ctx.controller.getConfig();
|
|
62
|
+
if (!arg) {
|
|
63
|
+
const resolution = resolveOutputTokens({
|
|
64
|
+
providerId: cfg.provider,
|
|
65
|
+
modelId: cfg.model,
|
|
66
|
+
...(cfg.maxTokens !== undefined ? { requested: cfg.maxTokens } : {}),
|
|
67
|
+
});
|
|
68
|
+
const setting = cfg.maxTokens === undefined ? 'auto' : String(cfg.maxTokens);
|
|
69
|
+
const why = resolution.clampedBy === 'ceiling' ? ` (${cfg.model} ceiling)` : '';
|
|
70
|
+
ctx.notify('info', `Max tokens: ${setting}. Effective for ${cfg.provider}:${cfg.model}: ` +
|
|
71
|
+
`${resolution.value}${why}.\nUse /max-tokens <n|auto|default>.`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const parsed = arg.toLowerCase() === 'default'
|
|
75
|
+
? { kind: 'value', value: DEFAULT_MAX_TOKENS, clamped: false }
|
|
76
|
+
: parseMaxTokensInput(arg);
|
|
77
|
+
if (parsed.kind === 'invalid') {
|
|
78
|
+
// Change NOTHING on a typo: silently resolving it to a default would
|
|
79
|
+
// discard the number the user meant to type.
|
|
80
|
+
ctx.notify('warn', `Invalid value "${arg}" - use /max-tokens <n|auto|default>.`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (parsed.kind === 'auto') {
|
|
84
|
+
ctx.controller.setMaxTokens(undefined);
|
|
85
|
+
ctx.persistConfig({ maxTokens: null });
|
|
86
|
+
ctx.toast('success', 'Max tokens: auto (per-model ceiling).');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
ctx.controller.setMaxTokens(parsed.value);
|
|
90
|
+
ctx.persistConfig({ maxTokens: parsed.value });
|
|
91
|
+
ctx.toast('success', `Max tokens set to ${parsed.value}.`);
|
|
92
|
+
},
|
|
93
|
+
},
|
|
46
94
|
{
|
|
47
95
|
name: 'theme',
|
|
48
96
|
description: 'Switch the color theme (auto|warm|cool|light)',
|
|
@@ -135,6 +135,20 @@ function maskConfig(config) {
|
|
|
135
135
|
}
|
|
136
136
|
return { ...config, apiKeys };
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* The human-readable label for a value whose raw form would mislead, or
|
|
140
|
+
* `undefined` to leave the caller's own formatting alone.
|
|
141
|
+
*
|
|
142
|
+
* `maxTokens: null` is AUTO, not "unset". Printing a bare `null` reads as a
|
|
143
|
+
* missing setting and sends the user looking for a bug that is not there.
|
|
144
|
+
* `get` and `list` format everything else differently (unquoted vs. JSON), so
|
|
145
|
+
* this deliberately overrides one case rather than replacing both renderers.
|
|
146
|
+
*/
|
|
147
|
+
function displayOverride(key, value) {
|
|
148
|
+
if (key === 'maxTokens' && value === null)
|
|
149
|
+
return 'auto';
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
138
152
|
/** Resolve a flat or one-level-dotted key against the persisted config. */
|
|
139
153
|
function readKey(config, key) {
|
|
140
154
|
const [head, tail] = key.split('.', 2);
|
|
@@ -153,10 +167,14 @@ export function runConfigGet(key) {
|
|
|
153
167
|
process.exitCode = 2;
|
|
154
168
|
return;
|
|
155
169
|
}
|
|
156
|
-
|
|
170
|
+
const shown = displayOverride(key, value)
|
|
171
|
+
?? (typeof value === 'string' ? value : JSON.stringify(value));
|
|
172
|
+
process.stdout.write(`${shown}\n`);
|
|
157
173
|
}
|
|
158
174
|
export function runConfigList(opts = {}) {
|
|
159
175
|
const masked = maskConfig(loadPersistedConfig());
|
|
176
|
+
// `--json` stays RAW on purpose: it mirrors the file, and a consumer parsing
|
|
177
|
+
// it must see the `null` that is actually on disk.
|
|
160
178
|
if (opts.json) {
|
|
161
179
|
process.stdout.write(`${JSON.stringify(masked, null, 2)}\n`);
|
|
162
180
|
return;
|
|
@@ -168,7 +186,7 @@ export function runConfigList(opts = {}) {
|
|
|
168
186
|
}
|
|
169
187
|
continue;
|
|
170
188
|
}
|
|
171
|
-
process.stdout.write(`${key} = ${JSON.stringify(value)}\n`);
|
|
189
|
+
process.stdout.write(`${key} = ${displayOverride(key, value) ?? JSON.stringify(value)}\n`);
|
|
172
190
|
}
|
|
173
191
|
}
|
|
174
192
|
// ---------------------------------------------------------------------------
|
package/dist/config/env.d.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* - Provider secrets: ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY /
|
|
6
6
|
* GEMINI_API_KEY.
|
|
7
7
|
* - Overrides: ARAGON_PROVIDER, ARAGON_MODEL, ARAGON_BASE_URL, ARAGON_THINKING,
|
|
8
|
-
* ARAGON_MAX_TOKENS
|
|
8
|
+
* ARAGON_MAX_TOKENS (a number, or `auto` / `0` for the per-model ceiling),
|
|
9
|
+
* ARAGON_THEME, ARAGON_MOUSE, ARAGON_PLAN, ARAGON_TEAM,
|
|
9
10
|
* ARAGON_TEAM_MAX, ARAGON_LOG_LEVEL, ARAGON_LOG_FILE, ARAGON_LOG_DIR.
|
|
10
11
|
*
|
|
11
12
|
* NOT here: `ARAGON_HOME`. It is read when `config/app-paths.ts` is first
|
package/dist/config/env.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* - Provider secrets: ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY /
|
|
6
6
|
* GEMINI_API_KEY.
|
|
7
7
|
* - Overrides: ARAGON_PROVIDER, ARAGON_MODEL, ARAGON_BASE_URL, ARAGON_THINKING,
|
|
8
|
-
* ARAGON_MAX_TOKENS
|
|
8
|
+
* ARAGON_MAX_TOKENS (a number, or `auto` / `0` for the per-model ceiling),
|
|
9
|
+
* ARAGON_THEME, ARAGON_MOUSE, ARAGON_PLAN, ARAGON_TEAM,
|
|
9
10
|
* ARAGON_TEAM_MAX, ARAGON_LOG_LEVEL, ARAGON_LOG_FILE, ARAGON_LOG_DIR.
|
|
10
11
|
*
|
|
11
12
|
* NOT here: `ARAGON_HOME`. It is read when `config/app-paths.ts` is first
|
|
@@ -18,7 +19,7 @@ import process from 'node:process';
|
|
|
18
19
|
import dotenv from 'dotenv';
|
|
19
20
|
import { registerSecretsFrom } from '../logging/secret-registry.js';
|
|
20
21
|
import { clampLogLevel } from '../logging/levels.js';
|
|
21
|
-
import { clampTheme, clampThinkingLevel,
|
|
22
|
+
import { clampMaxTokens, clampTheme, clampThinkingLevel, isAutoToken, } from './schema.js';
|
|
22
23
|
/**
|
|
23
24
|
* Load a project `.env` from the given directory into `process.env` (existing
|
|
24
25
|
* process env wins — dotenv never overrides already-set variables).
|
|
@@ -58,9 +59,20 @@ export function readEnvConfig() {
|
|
|
58
59
|
if (process.env.ARAGON_THINKING) {
|
|
59
60
|
partial.thinkingLevel = clampThinkingLevel(process.env.ARAGON_THINKING.trim(), 'off');
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// `auto` / `0` mean AUTO (`null`); a number is clamped to the accepted range.
|
|
63
|
+
// Anything else is LEFT ABSENT rather than written as `null`: a typo here used
|
|
64
|
+
// to become AUTO and silently discard the number in the config file under it,
|
|
65
|
+
// which is the opposite of what the user asked for.
|
|
66
|
+
const maxTokens = process.env.ARAGON_MAX_TOKENS?.trim();
|
|
67
|
+
if (maxTokens !== undefined && maxTokens.length > 0) {
|
|
68
|
+
if (isAutoToken(maxTokens)) {
|
|
69
|
+
partial.maxTokens = null;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const clamped = clampMaxTokens(maxTokens);
|
|
73
|
+
if (clamped !== undefined)
|
|
74
|
+
partial.maxTokens = clamped;
|
|
75
|
+
}
|
|
64
76
|
}
|
|
65
77
|
if (process.env.ARAGON_THEME) {
|
|
66
78
|
partial.theme = clampTheme(process.env.ARAGON_THEME.trim(), 'auto');
|
package/dist/config/load.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* legitimately long single tool run can never be watchdog-killed.
|
|
7
7
|
*/
|
|
8
8
|
import process from 'node:process';
|
|
9
|
-
import { DEFAULT_CONFIG, IDLE_TIMEOUT_MARGIN_MS, clampAskRounds, clampDensity, clampHumanTimeout, clampLogConfig, clampSkillsConfig, clampTeamConfig, clampTheme, clampThinkingLevel,
|
|
9
|
+
import { DEFAULT_CONFIG, IDLE_TIMEOUT_MARGIN_MS, clampAskRounds, clampDensity, clampHumanTimeout, clampLogConfig, clampSkillsConfig, clampTeamConfig, clampTheme, clampThinkingLevel, clampMaxTokens, clampTranscriptWindow, coercePositiveInt, isAutoToken, isSkillsToolPolicyMode, } from './schema.js';
|
|
10
10
|
import { loadDotenv, readEnvConfig, readEnvDisabledSkills } from './env.js';
|
|
11
11
|
import { readConfigFile } from './store.js';
|
|
12
12
|
import { getConfigPath } from './app-paths.js';
|
|
@@ -118,6 +118,44 @@ function pick(...vals) {
|
|
|
118
118
|
}
|
|
119
119
|
return undefined;
|
|
120
120
|
}
|
|
121
|
+
function hasOwn(obj, key) {
|
|
122
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Resolve the output cap: flag › env › file › default, where an EXPLICIT `null`
|
|
126
|
+
* at any layer means AUTO (`undefined` — let the resolver pick per model).
|
|
127
|
+
*
|
|
128
|
+
* THIS CANNOT USE `pick()`, and that is the whole point. `pick` skips `null`, so
|
|
129
|
+
* a config file holding `"maxTokens": null` silently resolves to 64000 and AUTO
|
|
130
|
+
* is unreachable through every supported write path. Detecting it needs an
|
|
131
|
+
* own-property test, not a truthiness test — `null` is exactly the value we are
|
|
132
|
+
* looking for.
|
|
133
|
+
*
|
|
134
|
+
* An unparseable layer falls through to the next one rather than becoming AUTO:
|
|
135
|
+
* a typo in `ARAGON_MAX_TOKENS` must not silently discard the number in the
|
|
136
|
+
* config file underneath it.
|
|
137
|
+
*/
|
|
138
|
+
function resolveMaxTokens(flags, env, file) {
|
|
139
|
+
const layers = [
|
|
140
|
+
{ name: '--max-tokens', value: flags.maxTokens },
|
|
141
|
+
{ name: 'ARAGON_MAX_TOKENS', value: hasOwn(env, 'maxTokens') ? env.maxTokens : undefined },
|
|
142
|
+
{ name: 'config.json maxTokens', value: hasOwn(file, 'maxTokens') ? file.maxTokens : undefined },
|
|
143
|
+
];
|
|
144
|
+
for (const layer of layers) {
|
|
145
|
+
if (layer.value === undefined)
|
|
146
|
+
continue; // this layer said nothing
|
|
147
|
+
if (layer.value === null || isAutoToken(layer.value))
|
|
148
|
+
return undefined; // AUTO
|
|
149
|
+
const n = clampMaxTokens(layer.value);
|
|
150
|
+
if (n !== undefined)
|
|
151
|
+
return n;
|
|
152
|
+
getLogger().warn('config', 'max_tokens_unusable', {
|
|
153
|
+
source: layer.name,
|
|
154
|
+
value: String(layer.value),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return DEFAULT_CONFIG.maxTokens ?? undefined;
|
|
158
|
+
}
|
|
121
159
|
/**
|
|
122
160
|
* Resolve the effective config by merging all layers, then derive the runtime
|
|
123
161
|
* (non-persisted) fields and enforce the timeout invariant.
|
|
@@ -142,7 +180,7 @@ export function loadConfig(flags = {}) {
|
|
|
142
180
|
const model = pick(flags.model, env.partial.model, file.model) ?? DEFAULT_CONFIG.model;
|
|
143
181
|
const baseUrl = pick(flags.baseUrl, env.partial.baseUrl ?? undefined, file.baseUrl ?? undefined);
|
|
144
182
|
const thinkingLevel = clampThinkingLevel(pick(flags.thinking, env.partial.thinkingLevel, file.thinkingLevel), DEFAULT_CONFIG.thinkingLevel);
|
|
145
|
-
const maxTokens =
|
|
183
|
+
const maxTokens = resolveMaxTokens(flags, env.partial, file);
|
|
146
184
|
// `clampTheme` is also where the v0.3.0 `dark` name is migrated to `cool`,
|
|
147
185
|
// so reading a legacy config file needs no extra step here (§4.5 / R-4).
|
|
148
186
|
const theme = clampTheme(pick(flags.theme, env.partial.theme, file.theme), DEFAULT_CONFIG.theme);
|
|
@@ -274,6 +312,9 @@ function recordResolvedConfig(config) {
|
|
|
274
312
|
model: config.model,
|
|
275
313
|
baseUrl: config.baseUrl,
|
|
276
314
|
thinkingLevel: config.thinkingLevel,
|
|
315
|
+
// Four layers feed this one and none of them used to leave a trace, so
|
|
316
|
+
// "why is my output truncated" was unanswerable after the fact.
|
|
317
|
+
maxTokens: config.maxTokens ?? 'auto',
|
|
277
318
|
theme: config.theme,
|
|
278
319
|
fullscreen: config.fullscreen,
|
|
279
320
|
toolTimeoutMs: config.toolTimeoutMs,
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* See spec §3.5 (resolution pipeline), §6.1 (config file), and §3.8 (timeout
|
|
6
6
|
* invariant).
|
|
7
7
|
*/
|
|
8
|
-
import type
|
|
8
|
+
import { type ThinkingLevel } from '@aragon-agent/core';
|
|
9
9
|
import { type LogLevelName } from '../logging/levels.js';
|
|
10
10
|
/**
|
|
11
11
|
* `ModelRegistry` seeds builtin lists for five providers, but `initProviders()`
|
|
@@ -245,6 +245,18 @@ export interface PersistedConfig {
|
|
|
245
245
|
model: string;
|
|
246
246
|
baseUrl: string | null;
|
|
247
247
|
thinkingLevel: ThinkingLevel;
|
|
248
|
+
/**
|
|
249
|
+
* Output token cap.
|
|
250
|
+
*
|
|
251
|
+
* a number — an explicit cap, clamped to `[256, 200000]` on the way in AND
|
|
252
|
+
* on the way out, then clamped again at request time down to the
|
|
253
|
+
* target model's real ceiling when that ceiling is lower.
|
|
254
|
+
* `null` — AUTO: use each model's own ceiling, never above 64000.
|
|
255
|
+
* absent — the product default, 64000.
|
|
256
|
+
*
|
|
257
|
+
* `null` MEANS AUTO. It used to resolve to 64000 by accident (`pick()` skips
|
|
258
|
+
* `null`), which left the user no way to express "let the model decide".
|
|
259
|
+
*/
|
|
248
260
|
maxTokens: number | null;
|
|
249
261
|
theme: ThemeName;
|
|
250
262
|
/** Replace spinners with a static glyph for calmer, low-motion output. */
|
|
@@ -324,6 +336,7 @@ export interface CliConfig {
|
|
|
324
336
|
model: string;
|
|
325
337
|
baseUrl?: string;
|
|
326
338
|
thinkingLevel: ThinkingLevel;
|
|
339
|
+
/** The resolved cap, or `undefined` for AUTO (per-model ceiling, ≤ 64000). */
|
|
327
340
|
maxTokens?: number;
|
|
328
341
|
theme: ThemeName;
|
|
329
342
|
reducedMotion: boolean;
|
|
@@ -406,8 +419,57 @@ export interface CliConfig {
|
|
|
406
419
|
/** One-shot `--api-key` override; applies to the active provider only. */
|
|
407
420
|
apiKeyOverride?: string;
|
|
408
421
|
}
|
|
409
|
-
/**
|
|
422
|
+
/**
|
|
423
|
+
* Coerce an arbitrary value into a positive integer, or `undefined`.
|
|
424
|
+
*
|
|
425
|
+
* Kept because callers outside this feature still use it, and deliberately
|
|
426
|
+
* WITHOUT the range clamp so its behaviour for in-range values is byte-identical
|
|
427
|
+
* to what it always was. New code wants `clampMaxTokens`.
|
|
428
|
+
*/
|
|
410
429
|
export declare function coerceMaxTokens(v: unknown): number | undefined;
|
|
430
|
+
/**
|
|
431
|
+
* The accepted range for an explicit output cap.
|
|
432
|
+
*
|
|
433
|
+
* The floor is the engine's: below it a turn cannot produce a usable tool call.
|
|
434
|
+
* The ceiling is a sanity bound on a hand-edited file, not a model limit — the
|
|
435
|
+
* real per-model ceiling is applied later, by `resolveOutputTokens`.
|
|
436
|
+
*/
|
|
437
|
+
export declare const MAX_TOKENS_RANGE: {
|
|
438
|
+
min: number;
|
|
439
|
+
max: number;
|
|
440
|
+
};
|
|
441
|
+
/** `auto`, an empty string and `0` all mean AUTO — "let the model decide". */
|
|
442
|
+
export declare function isAutoToken(v: unknown): boolean;
|
|
443
|
+
/**
|
|
444
|
+
* THE single gate for every `maxTokens` read AND write, in the shape
|
|
445
|
+
* `clampSkillsConfig` / `clampLogConfig` / `clampTeamConfig` already use.
|
|
446
|
+
*
|
|
447
|
+
* Both directions on purpose: hardening only the read path leaves a bad value on
|
|
448
|
+
* disk that reverts to a default on every launch, which presents to the user as
|
|
449
|
+
* "my setting won't stick".
|
|
450
|
+
*
|
|
451
|
+
* Returns `undefined` for AUTO and for anything unusable — the caller decides
|
|
452
|
+
* which of the two it was, because they mean different things.
|
|
453
|
+
*/
|
|
454
|
+
export declare function clampMaxTokens(v: unknown): number | undefined;
|
|
455
|
+
/**
|
|
456
|
+
* Parse text typed into the settings screen, a slash command, or `config set`.
|
|
457
|
+
*
|
|
458
|
+
* Distinguishes AUTO from "the user typed nonsense", which `clampMaxTokens`
|
|
459
|
+
* cannot: both come back as `undefined` there, and treating a typo as AUTO would
|
|
460
|
+
* silently discard the number the user meant to set.
|
|
461
|
+
*/
|
|
462
|
+
export declare function parseMaxTokensInput(raw: string): {
|
|
463
|
+
kind: 'auto';
|
|
464
|
+
} | {
|
|
465
|
+
kind: 'value';
|
|
466
|
+
value: number;
|
|
467
|
+
clamped: boolean;
|
|
468
|
+
} | {
|
|
469
|
+
kind: 'invalid';
|
|
470
|
+
};
|
|
471
|
+
/** The product default, re-exported so the CLI never spells the number itself. */
|
|
472
|
+
export declare const DEFAULT_MAX_TOKENS = 64000;
|
|
411
473
|
/** Coerce a value into a positive integer with a fallback (for timeouts). */
|
|
412
474
|
export declare function coercePositiveInt(v: unknown, fallback: number): number;
|
|
413
475
|
/**
|
package/dist/config/schema.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* invariant).
|
|
7
7
|
*/
|
|
8
8
|
import { isAbsolute } from 'node:path';
|
|
9
|
+
import { ABSOLUTE_MAX_OUTPUT_TOKENS, DEFAULT_MAX_OUTPUT_TOKENS, MIN_MAX_OUTPUT_TOKENS, } from '@aragon-agent/core';
|
|
9
10
|
import { clampLogLevel } from '../logging/levels.js';
|
|
10
11
|
import { TEAM_LIMITS } from '../team/limits.js';
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
@@ -317,7 +318,9 @@ export const DEFAULT_CONFIG = {
|
|
|
317
318
|
model: DEFAULT_MODEL,
|
|
318
319
|
baseUrl: null,
|
|
319
320
|
thinkingLevel: 'off',
|
|
320
|
-
|
|
321
|
+
// The engine owns this number; spelling it here again is how the CLI and the
|
|
322
|
+
// provider adapters end up disagreeing about what "the default" is.
|
|
323
|
+
maxTokens: DEFAULT_MAX_OUTPUT_TOKENS,
|
|
321
324
|
theme: 'auto',
|
|
322
325
|
reducedMotion: false,
|
|
323
326
|
fullscreen: true,
|
|
@@ -376,7 +379,7 @@ export function stripLegacyStateKeys(raw) {
|
|
|
376
379
|
// Coercion / validation helpers
|
|
377
380
|
// ---------------------------------------------------------------------------
|
|
378
381
|
/** Coerce an arbitrary value into a positive integer, or `undefined`. */
|
|
379
|
-
|
|
382
|
+
function coerceTokenCount(v) {
|
|
380
383
|
if (v === null || v === undefined || v === '')
|
|
381
384
|
return undefined;
|
|
382
385
|
const n = typeof v === 'number' ? v : Number.parseInt(String(v), 10);
|
|
@@ -384,6 +387,74 @@ export function coerceMaxTokens(v) {
|
|
|
384
387
|
return undefined;
|
|
385
388
|
return Math.floor(n);
|
|
386
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* Coerce an arbitrary value into a positive integer, or `undefined`.
|
|
392
|
+
*
|
|
393
|
+
* Kept because callers outside this feature still use it, and deliberately
|
|
394
|
+
* WITHOUT the range clamp so its behaviour for in-range values is byte-identical
|
|
395
|
+
* to what it always was. New code wants `clampMaxTokens`.
|
|
396
|
+
*/
|
|
397
|
+
export function coerceMaxTokens(v) {
|
|
398
|
+
return coerceTokenCount(v);
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* The accepted range for an explicit output cap.
|
|
402
|
+
*
|
|
403
|
+
* The floor is the engine's: below it a turn cannot produce a usable tool call.
|
|
404
|
+
* The ceiling is a sanity bound on a hand-edited file, not a model limit — the
|
|
405
|
+
* real per-model ceiling is applied later, by `resolveOutputTokens`.
|
|
406
|
+
*/
|
|
407
|
+
export const MAX_TOKENS_RANGE = {
|
|
408
|
+
min: MIN_MAX_OUTPUT_TOKENS,
|
|
409
|
+
max: ABSOLUTE_MAX_OUTPUT_TOKENS,
|
|
410
|
+
};
|
|
411
|
+
/** `auto`, an empty string and `0` all mean AUTO — "let the model decide". */
|
|
412
|
+
export function isAutoToken(v) {
|
|
413
|
+
if (v === 0)
|
|
414
|
+
return true;
|
|
415
|
+
if (typeof v !== 'string')
|
|
416
|
+
return false;
|
|
417
|
+
const trimmed = v.trim().toLowerCase();
|
|
418
|
+
return trimmed === '' || trimmed === 'auto' || trimmed === '0';
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* THE single gate for every `maxTokens` read AND write, in the shape
|
|
422
|
+
* `clampSkillsConfig` / `clampLogConfig` / `clampTeamConfig` already use.
|
|
423
|
+
*
|
|
424
|
+
* Both directions on purpose: hardening only the read path leaves a bad value on
|
|
425
|
+
* disk that reverts to a default on every launch, which presents to the user as
|
|
426
|
+
* "my setting won't stick".
|
|
427
|
+
*
|
|
428
|
+
* Returns `undefined` for AUTO and for anything unusable — the caller decides
|
|
429
|
+
* which of the two it was, because they mean different things.
|
|
430
|
+
*/
|
|
431
|
+
export function clampMaxTokens(v) {
|
|
432
|
+
const n = coerceTokenCount(v);
|
|
433
|
+
if (n === undefined)
|
|
434
|
+
return undefined;
|
|
435
|
+
return Math.min(MAX_TOKENS_RANGE.max, Math.max(MAX_TOKENS_RANGE.min, n));
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Parse text typed into the settings screen, a slash command, or `config set`.
|
|
439
|
+
*
|
|
440
|
+
* Distinguishes AUTO from "the user typed nonsense", which `clampMaxTokens`
|
|
441
|
+
* cannot: both come back as `undefined` there, and treating a typo as AUTO would
|
|
442
|
+
* silently discard the number the user meant to set.
|
|
443
|
+
*/
|
|
444
|
+
export function parseMaxTokensInput(raw) {
|
|
445
|
+
const trimmed = String(raw ?? '').trim();
|
|
446
|
+
if (isAutoToken(trimmed))
|
|
447
|
+
return { kind: 'auto' };
|
|
448
|
+
if (!/^\d+$/.test(trimmed))
|
|
449
|
+
return { kind: 'invalid' };
|
|
450
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
451
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
452
|
+
return { kind: 'invalid' };
|
|
453
|
+
const value = Math.min(MAX_TOKENS_RANGE.max, Math.max(MAX_TOKENS_RANGE.min, parsed));
|
|
454
|
+
return { kind: 'value', value, clamped: value !== parsed };
|
|
455
|
+
}
|
|
456
|
+
/** The product default, re-exported so the CLI never spells the number itself. */
|
|
457
|
+
export const DEFAULT_MAX_TOKENS = DEFAULT_MAX_OUTPUT_TOKENS;
|
|
387
458
|
/** Coerce a value into a positive integer with a fallback (for timeouts). */
|
|
388
459
|
export function coercePositiveInt(v, fallback) {
|
|
389
460
|
if (v === null || v === undefined || v === '')
|
package/dist/config/store.js
CHANGED
|
@@ -14,7 +14,23 @@ import { dirname } from 'node:path';
|
|
|
14
14
|
import process from 'node:process';
|
|
15
15
|
import { getConfigPath, getSessionsDir } from './app-paths.js';
|
|
16
16
|
import { registerSecretsFrom } from '../logging/secret-registry.js';
|
|
17
|
-
import { CONFIG_VERSION, DEFAULT_CONFIG, clampLogConfig, clampSkillsConfig, clampTeamConfig, stripLegacyStateKeys, } from './schema.js';
|
|
17
|
+
import { CONFIG_VERSION, DEFAULT_CONFIG, clampLogConfig, clampMaxTokens, clampSkillsConfig, clampTeamConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
|
|
18
|
+
/**
|
|
19
|
+
* Normalize `maxTokens` for both directions of the config file.
|
|
20
|
+
*
|
|
21
|
+
* `null` MEANS AUTO and must survive untouched; a number is clamped to the
|
|
22
|
+
* accepted range; anything else on a hand-edited file (a string, a float, a
|
|
23
|
+
* negative) becomes AUTO rather than a silent default. Same discipline as
|
|
24
|
+
* `clampSkillsConfig` — read AND write pass through one gate, or a bad value on
|
|
25
|
+
* disk reverts on every launch and presents as "my setting won't stick".
|
|
26
|
+
*/
|
|
27
|
+
function normalizeMaxTokens(v) {
|
|
28
|
+
if (v === undefined)
|
|
29
|
+
return DEFAULT_CONFIG.maxTokens;
|
|
30
|
+
if (v === null || isAutoToken(v))
|
|
31
|
+
return null;
|
|
32
|
+
return clampMaxTokens(v) ?? null;
|
|
33
|
+
}
|
|
18
34
|
// Re-exported so the many existing importers of `store.js` keep working; the
|
|
19
35
|
// definitions live in `app-paths.ts` because they are pure path arithmetic.
|
|
20
36
|
export { getConfigPath, getSessionsDir };
|
|
@@ -92,6 +108,12 @@ export function loadPersistedConfig() {
|
|
|
92
108
|
...DEFAULT_CONFIG,
|
|
93
109
|
...partial,
|
|
94
110
|
version: CONFIG_VERSION,
|
|
111
|
+
// Own-property aware, because `null` is the value we need to preserve: a
|
|
112
|
+
// spread cannot tell "the key is absent" (⇒ the default) from "the key is
|
|
113
|
+
// null" (⇒ AUTO), and those are different settings.
|
|
114
|
+
maxTokens: Object.prototype.hasOwnProperty.call(partial, 'maxTokens')
|
|
115
|
+
? normalizeMaxTokens(partial.maxTokens)
|
|
116
|
+
: DEFAULT_CONFIG.maxTokens,
|
|
95
117
|
apiKeys: { ...DEFAULT_CONFIG.apiKeys, ...(partial.apiKeys ?? {}) },
|
|
96
118
|
// `skills` is the second nested object in this file and needs the same
|
|
97
119
|
// treatment `apiKeys` gets — see the note on updatePersistedConfig below.
|
|
@@ -131,6 +153,11 @@ export function updatePersistedConfig(patch) {
|
|
|
131
153
|
...current,
|
|
132
154
|
...patch,
|
|
133
155
|
version: CONFIG_VERSION,
|
|
156
|
+
// The write half of the gate: without it `config set maxTokens 900000` puts
|
|
157
|
+
// a number on disk that the read path silently rewrites on every launch.
|
|
158
|
+
maxTokens: Object.prototype.hasOwnProperty.call(patch, 'maxTokens')
|
|
159
|
+
? normalizeMaxTokens(patch.maxTokens)
|
|
160
|
+
: current.maxTokens,
|
|
134
161
|
apiKeys: { ...current.apiKeys, ...(patch.apiKeys ?? {}) },
|
|
135
162
|
skills: clampSkillsConfig({ ...current.skills, ...(patch.skills ?? {}) }),
|
|
136
163
|
// Without this line, changing the log level from the settings screen would
|
package/dist/ui/App.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import React from 'react';
|
|
8
8
|
import type { AgentController } from '../agent/controller.js';
|
|
9
9
|
import type { ConfirmRequest } from '../tools/index.js';
|
|
10
|
-
import { type Overlay } from '../agent/reducer.js';
|
|
10
|
+
import { type Overlay, type ToastLevel } from '../agent/reducer.js';
|
|
11
11
|
import { type RenderMode } from './layout/frame.js';
|
|
12
12
|
import { type FrameGeometry } from './layout/regions.js';
|
|
13
13
|
import type { MouseSource } from '../input/stdin-mouse-filter.js';
|
|
@@ -15,6 +15,27 @@ import type { HumanInputBridge } from '../tools/human-input.js';
|
|
|
15
15
|
export interface ConfirmBridge {
|
|
16
16
|
handler: ((req: ConfirmRequest) => Promise<boolean>) | null;
|
|
17
17
|
}
|
|
18
|
+
/** What the settings screen's `Max tokens` field resolved to. */
|
|
19
|
+
export interface SettingsMaxTokens {
|
|
20
|
+
/** Pushed into the live controller. `undefined` is AUTO. */
|
|
21
|
+
applied: number | undefined;
|
|
22
|
+
/** Written to the config file, or `'skip'` to leave the stored value alone. */
|
|
23
|
+
persist: number | null | 'skip';
|
|
24
|
+
/** Replaces the generic "Settings saved." acknowledgement when present. */
|
|
25
|
+
toast?: [ToastLevel, string];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Decide what the `Max tokens` field means, WITHOUT letting a bad value in it
|
|
29
|
+
* discard the rest of the form.
|
|
30
|
+
*
|
|
31
|
+
* The three outcomes are genuinely different and must stay distinguishable:
|
|
32
|
+
* `auto` is a setting, a clamped number is a setting the user should be told
|
|
33
|
+
* about, and nonsense is not a setting at all — it keeps the previous value and
|
|
34
|
+
* says so, rather than silently resolving to a default the user never chose.
|
|
35
|
+
*
|
|
36
|
+
* Exported for the tests; there is no other caller.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveSettingsMaxTokens(raw: string, previous: number | undefined): SettingsMaxTokens;
|
|
18
39
|
export interface AppProps {
|
|
19
40
|
controller: AgentController;
|
|
20
41
|
version: string;
|
package/dist/ui/App.js
CHANGED
|
@@ -11,7 +11,7 @@ import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
|
11
11
|
import { initialViewState, reduceEvent, viewReducer, } from '../agent/reducer.js';
|
|
12
12
|
import { mergeDeltas } from '../agent/coalesce.js';
|
|
13
13
|
import { computeCost } from '../agent/usage.js';
|
|
14
|
-
import {
|
|
14
|
+
import { parseMaxTokensInput, DEFAULT_TRANSCRIPT_WINDOW, } from '../config/schema.js';
|
|
15
15
|
import { updatePersistedConfig } from '../config/store.js';
|
|
16
16
|
import { appendPrompt, loadPromptHistory } from '../config/prompt-history.js';
|
|
17
17
|
import { bumpSubmitCount, setMouseNoticeSeen } from '../config/ui-state.js';
|
|
@@ -50,6 +50,41 @@ import { CommandRegistry, runSlashInput } from '../commands/registry.js';
|
|
|
50
50
|
import { registerBuiltinCommands } from '../commands/builtins.js';
|
|
51
51
|
import { makeSkillsCommand, registerSkillCommands } from '../commands/skills.js';
|
|
52
52
|
import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
|
|
53
|
+
/**
|
|
54
|
+
* Decide what the `Max tokens` field means, WITHOUT letting a bad value in it
|
|
55
|
+
* discard the rest of the form.
|
|
56
|
+
*
|
|
57
|
+
* The three outcomes are genuinely different and must stay distinguishable:
|
|
58
|
+
* `auto` is a setting, a clamped number is a setting the user should be told
|
|
59
|
+
* about, and nonsense is not a setting at all — it keeps the previous value and
|
|
60
|
+
* says so, rather than silently resolving to a default the user never chose.
|
|
61
|
+
*
|
|
62
|
+
* Exported for the tests; there is no other caller.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveSettingsMaxTokens(raw, previous) {
|
|
65
|
+
const parsed = parseMaxTokensInput(raw);
|
|
66
|
+
if (parsed.kind === 'auto') {
|
|
67
|
+
return {
|
|
68
|
+
applied: undefined,
|
|
69
|
+
persist: null,
|
|
70
|
+
toast: ['info', 'Max tokens: auto (per-model ceiling).'],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (parsed.kind === 'value') {
|
|
74
|
+
return {
|
|
75
|
+
applied: parsed.value,
|
|
76
|
+
persist: parsed.value,
|
|
77
|
+
...(parsed.clamped
|
|
78
|
+
? { toast: ['warn', `Max tokens clamped to ${parsed.value}.`] }
|
|
79
|
+
: {}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
applied: previous,
|
|
84
|
+
persist: 'skip',
|
|
85
|
+
toast: ['warn', 'Max tokens must be a number or "auto".'],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
53
88
|
/** Merge pending delta actions ~33 ms and flush the coalescer buffer. */
|
|
54
89
|
const COALESCE_MS = 33;
|
|
55
90
|
export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, geometryOverride, }) {
|
|
@@ -518,10 +553,13 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
518
553
|
submitMessage(raw.startsWith('//') ? raw.slice(1) : raw);
|
|
519
554
|
};
|
|
520
555
|
const handleSettingsSave = (values) => {
|
|
521
|
-
|
|
556
|
+
// An unusable cap must NOT discard the rest of the form: the same Enter
|
|
557
|
+
// press is often carrying a freshly pasted API key, and losing that to a
|
|
558
|
+
// typo in an unrelated field is the worst outcome available here.
|
|
559
|
+
const cap = resolveSettingsMaxTokens(values.maxTokens, controller.getConfig().maxTokens);
|
|
522
560
|
controller.setModel(values.provider, values.model, values.baseUrl || undefined);
|
|
523
561
|
controller.setThinkingLevel(values.thinkingLevel);
|
|
524
|
-
controller.setMaxTokens(
|
|
562
|
+
controller.setMaxTokens(cap.applied);
|
|
525
563
|
const key = values.apiKey.trim();
|
|
526
564
|
if (key.length > 0) {
|
|
527
565
|
// Registration site 4 (§4.4.3), and the one that matters most: this is how
|
|
@@ -536,7 +574,9 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
536
574
|
model: values.model,
|
|
537
575
|
baseUrl: values.baseUrl.trim() ? values.baseUrl.trim() : null,
|
|
538
576
|
thinkingLevel: values.thinkingLevel,
|
|
539
|
-
|
|
577
|
+
// Omitted entirely when the field was unusable, so the previous value
|
|
578
|
+
// survives instead of being overwritten with a guess.
|
|
579
|
+
...(cap.persist !== 'skip' ? { maxTokens: cap.persist } : {}),
|
|
540
580
|
};
|
|
541
581
|
if (key.length > 0)
|
|
542
582
|
patch.apiKeys = { [values.provider]: key };
|
|
@@ -547,7 +587,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
547
587
|
persistConfig(patch);
|
|
548
588
|
getLogger().reconfigure({ ...controller.getConfig().log, level: values.logLevel });
|
|
549
589
|
dispatch({ type: 'setOverlay', overlay: null });
|
|
550
|
-
toast('success', 'Settings saved.');
|
|
590
|
+
toast(cap.toast?.[0] ?? 'success', cap.toast?.[1] ?? 'Settings saved.');
|
|
551
591
|
};
|
|
552
592
|
const handleModelSelect = (provider, model) => {
|
|
553
593
|
controller.setModel(provider, model);
|
|
@@ -767,7 +807,9 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
767
807
|
model: cfg.model,
|
|
768
808
|
baseUrl: cfg.baseUrl ?? '',
|
|
769
809
|
thinkingLevel: cfg.thinkingLevel,
|
|
770
|
-
|
|
810
|
+
// `auto`, not an empty string: an empty field reads as "unset", which
|
|
811
|
+
// is indistinguishable from AUTO and is not a state this can be in.
|
|
812
|
+
maxTokens: cfg.maxTokens ? String(cfg.maxTokens) : 'auto',
|
|
771
813
|
apiKey: '',
|
|
772
814
|
logLevel: cfg.log.level,
|
|
773
815
|
}, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onSave: handleSettingsSave })) : overlay === 'confirm' && confirmState ? (_jsx(ConfirmDialog, { state: confirmState, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onClose: closeConfirm })) : overlay === 'question' && humanRequest?.kind === 'questions' ? (_jsx(QuestionOverlay, { questions: humanRequest.questions, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onSubmit: (answers) => resolveHuman({ kind: 'answers', answers, cancelled: false }) })) : overlay === 'plan' && humanRequest?.kind === 'plan' ? (_jsx(PlanReviewOverlay, { plan: humanRequest.plan, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onVerdict: handlePlanVerdict })) : null;
|
|
@@ -25,6 +25,7 @@ const COMMANDS = [
|
|
|
25
25
|
['/plan [on|off|status]', 'Toggle plan mode (same as Shift+Tab)'],
|
|
26
26
|
['/theme <name>', 'auto | warm | cool | light'],
|
|
27
27
|
['/thinking <level>', 'Set thinking level'],
|
|
28
|
+
['/max-tokens [n|auto]', 'Output token cap; no argument reports the effective one'],
|
|
28
29
|
['/team [on|off|max n]', 'Team subagents: status, switch, fan-out width'],
|
|
29
30
|
['/tools', 'List active tools'],
|
|
30
31
|
['/clear', 'Clear the visible transcript'],
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* otherwise.
|
|
9
9
|
*/
|
|
10
10
|
import React from 'react';
|
|
11
|
-
import type
|
|
11
|
+
import { type ThinkingLevel } from '@aragon-agent/core';
|
|
12
12
|
import { type LogLevelName } from '../../logging/levels.js';
|
|
13
13
|
import type { Theme } from '../theme.js';
|
|
14
14
|
import type { TermCapabilities } from '../capabilities.js';
|
|
@@ -40,5 +40,16 @@ interface SettingsScreenProps {
|
|
|
40
40
|
caps: TermCapabilities;
|
|
41
41
|
onSave: (values: SettingsValues) => void;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* The cap this setting will ACTUALLY produce against the selected model.
|
|
45
|
+
*
|
|
46
|
+
* Exported for the tests, and computed on every render rather than in an effect:
|
|
47
|
+
* `resolveOutputTokens` is pure, allocation-light and does no I/O — the ceiling
|
|
48
|
+
* lookup is a `Map.get` plus at most seven regex tests.
|
|
49
|
+
*
|
|
50
|
+
* Without this row the settings screen can only show what the user typed, and
|
|
51
|
+
* "I set 64000 but answers still stop early" has no answer anywhere in the UI.
|
|
52
|
+
*/
|
|
53
|
+
export declare function effectiveCapLine(values: Pick<SettingsValues, 'provider' | 'model' | 'maxTokens'>): string;
|
|
43
54
|
export declare function SettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onSave, }: SettingsScreenProps): React.ReactElement;
|
|
44
55
|
export {};
|
|
@@ -10,7 +10,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
10
|
*/
|
|
11
11
|
import { useState } from 'react';
|
|
12
12
|
import { Text, useInput } from 'ink';
|
|
13
|
-
import {
|
|
13
|
+
import { DEFAULT_MAX_OUTPUT_TOKENS, resolveOutputTokens, } from '@aragon-agent/core';
|
|
14
|
+
import { ADAPTER_PROVIDERS, THINKING_LEVELS, maskSecret, parseMaxTokensInput, } from '../../config/schema.js';
|
|
14
15
|
import { LOG_LEVEL_NAMES } from '../../logging/levels.js';
|
|
15
16
|
import { pickGlyphs } from '../glyphs.js';
|
|
16
17
|
import { OverlayFrame } from '../layout/OverlayFrame.js';
|
|
@@ -23,6 +24,34 @@ const FIELDS = [
|
|
|
23
24
|
{ key: 'apiKey', label: 'API key', kind: 'secret' },
|
|
24
25
|
{ key: 'logLevel', label: 'Log level', kind: 'enum', options: LOG_LEVEL_NAMES },
|
|
25
26
|
];
|
|
27
|
+
/**
|
|
28
|
+
* The cap this setting will ACTUALLY produce against the selected model.
|
|
29
|
+
*
|
|
30
|
+
* Exported for the tests, and computed on every render rather than in an effect:
|
|
31
|
+
* `resolveOutputTokens` is pure, allocation-light and does no I/O — the ceiling
|
|
32
|
+
* lookup is a `Map.get` plus at most seven regex tests.
|
|
33
|
+
*
|
|
34
|
+
* Without this row the settings screen can only show what the user typed, and
|
|
35
|
+
* "I set 64000 but answers still stop early" has no answer anywhere in the UI.
|
|
36
|
+
*/
|
|
37
|
+
export function effectiveCapLine(values) {
|
|
38
|
+
const parsed = parseMaxTokensInput(values.maxTokens);
|
|
39
|
+
if (parsed.kind === 'invalid')
|
|
40
|
+
return 'Effective: - (not a number; use a value or "auto")';
|
|
41
|
+
const requested = parsed.kind === 'value' ? parsed.value : undefined;
|
|
42
|
+
const resolution = resolveOutputTokens({
|
|
43
|
+
providerId: values.provider,
|
|
44
|
+
modelId: values.model,
|
|
45
|
+
...(requested !== undefined ? { requested } : {}),
|
|
46
|
+
});
|
|
47
|
+
if (resolution.clampedBy === 'ceiling') {
|
|
48
|
+
const ambition = requested ?? DEFAULT_MAX_OUTPUT_TOKENS;
|
|
49
|
+
return `Effective: ${resolution.value} (clamped from ${ambition} by the ${values.model} ceiling)`;
|
|
50
|
+
}
|
|
51
|
+
if (requested === undefined)
|
|
52
|
+
return `Effective: ${resolution.value} (auto)`;
|
|
53
|
+
return `Effective: ${resolution.value}`;
|
|
54
|
+
}
|
|
26
55
|
export function SettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onSave, }) {
|
|
27
56
|
const [values, setValues] = useState(initial);
|
|
28
57
|
const [index, setIndex] = useState(0);
|
|
@@ -99,6 +128,10 @@ export function SettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset,
|
|
|
99
128
|
}
|
|
100
129
|
if (f.kind === 'enum')
|
|
101
130
|
return raw;
|
|
131
|
+
// An empty cap field is AUTO, not "unset". `(none)` would read as a missing
|
|
132
|
+
// setting, which is the one thing this field can never be.
|
|
133
|
+
if (f.key === 'maxTokens' && raw.length === 0)
|
|
134
|
+
return 'auto';
|
|
102
135
|
return raw.length > 0 ? raw : selected ? '(type to set)' : '(none)';
|
|
103
136
|
};
|
|
104
137
|
// One element per row, each `wrap="truncate"` — the contract for OverlayFrame's
|
|
@@ -108,6 +141,10 @@ export function SettingsScreen({ initial, apiKeys, maxRows, cols, scrollOffset,
|
|
|
108
141
|
const selected = i === index;
|
|
109
142
|
return (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: selected ? theme.accent : theme.muted, children: selected ? `${glyphs.caret} ` : ' ' }), _jsx(Text, { color: selected ? theme.assistant : theme.muted, children: (f.key === 'apiKey' ? `API key (${values.provider})` : f.label).padEnd(18) }), _jsx(Text, { color: selected ? theme.primary : theme.muted, children: displayValue(f, selected) }), f.kind === 'enum' && selected && (_jsxs(Text, { color: theme.muted, children: [' (', glyphs.arrowLeft, "/", glyphs.arrowRight, ')'] }))] }, f.key));
|
|
110
143
|
});
|
|
144
|
+
// One element, `wrap="truncate"`, pushed like every other row — OverlayFrame's
|
|
145
|
+
// controlled mode slices by ELEMENT, so a row that wraps desynchronises the
|
|
146
|
+
// position indicator.
|
|
147
|
+
rows.push(_jsx(Text, { wrap: "truncate", color: theme.muted, children: effectiveCapLine(values) }, "__effective"));
|
|
111
148
|
rows.push(_jsx(Text, { wrap: "truncate", color: theme.muted, children: "Enter saves. Keys persist to the config file (0600)." }, "__save"));
|
|
112
149
|
return (_jsx(OverlayFrame, { title: "Settings", hint: `${glyphs.arrowUp}${glyphs.arrowDown} field ${glyphs.midDot} ${glyphs.arrowLeft}${glyphs.arrowRight} change ${glyphs.midDot} Enter save ${glyphs.midDot} Esc close`, maxRows: maxRows, cols: cols, rows: rows, scrollOffset: scrollOffset, onScrollClamp: onScrollClamp, theme: theme, caps: caps }));
|
|
113
150
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aragon-agent/cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "AragonAgent CLI — a Claude-Code / Codex-style interactive terminal UI (TUI) on top of the @aragon-agent/core engine. Installable and runnable via npm / npx.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AragonAgent contributors",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"prepublishOnly": "npm run build"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@aragon-agent/core": "^0.2.
|
|
54
|
+
"@aragon-agent/core": "^0.2.4",
|
|
55
55
|
"chalk": "^5.6.2",
|
|
56
56
|
"cli-highlight": "^2.0.0",
|
|
57
57
|
"commander": "^12.1.0",
|