@aragon-agent/cli 0.5.3 → 0.5.6
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 +670 -540
- package/README.md +1105 -932
- package/dist/agent/controller.d.ts +71 -1
- package/dist/agent/controller.js +135 -1
- package/dist/agent/headless.d.ts +29 -0
- package/dist/agent/headless.js +86 -1
- package/dist/agent/reducer.d.ts +45 -0
- package/dist/agent/reducer.js +80 -1
- package/dist/agent/system-prompt.d.ts +8 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +72 -6
- package/dist/commands/builtins.d.ts +2 -2
- package/dist/commands/builtins.js +217 -3
- package/dist/commands/registry.d.ts +27 -1
- package/dist/config/cli-commands.js +20 -2
- package/dist/config/env.d.ts +2 -1
- package/dist/config/env.js +60 -5
- package/dist/config/load.d.ts +27 -0
- package/dist/config/load.js +67 -2
- package/dist/config/schema.d.ts +111 -2
- package/dist/config/schema.js +111 -2
- package/dist/config/store.js +37 -1
- package/dist/session/persist.d.ts +19 -1
- package/dist/session/persist.js +21 -2
- package/dist/team/subagent.js +11 -0
- package/dist/todo/follow-through.d.ts +117 -0
- package/dist/todo/follow-through.js +186 -0
- package/dist/todo/limits.d.ts +114 -0
- package/dist/todo/limits.js +114 -0
- package/dist/todo/normalize.d.ts +33 -0
- package/dist/todo/normalize.js +117 -0
- package/dist/todo/panel-rows.d.ts +53 -0
- package/dist/todo/panel-rows.js +103 -0
- package/dist/todo/prompt.d.ts +54 -0
- package/dist/todo/prompt.js +76 -0
- package/dist/todo/store.d.ts +86 -0
- package/dist/todo/store.js +183 -0
- package/dist/todo/todo-tool.d.ts +38 -0
- package/dist/todo/todo-tool.js +134 -0
- package/dist/todo/types.d.ts +62 -0
- package/dist/todo/types.js +12 -0
- package/dist/tools/index.d.ts +16 -2
- package/dist/tools/index.js +13 -0
- package/dist/ui/App.d.ts +22 -1
- package/dist/ui/App.js +292 -13
- package/dist/ui/StatusBar.d.ts +18 -0
- package/dist/ui/StatusBar.js +5 -2
- package/dist/ui/TodoPanel.d.ts +48 -0
- package/dist/ui/TodoPanel.js +74 -0
- package/dist/ui/TodoStrip.d.ts +36 -0
- package/dist/ui/TodoStrip.js +32 -0
- package/dist/ui/Transcript.js +11 -1
- package/dist/ui/entries/TodoCard.d.ts +31 -0
- package/dist/ui/entries/TodoCard.js +35 -0
- package/dist/ui/glyphs.d.ts +12 -0
- package/dist/ui/glyphs.js +8 -0
- package/dist/ui/layout/AppShell.d.ts +34 -1
- package/dist/ui/layout/AppShell.js +3 -3
- package/dist/ui/layout/ScrollViewport.d.ts +13 -1
- package/dist/ui/layout/ScrollViewport.js +2 -2
- package/dist/ui/layout/rail.d.ts +56 -0
- package/dist/ui/layout/rail.js +68 -0
- package/dist/ui/overlays/HelpOverlay.js +5 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +12 -1
- package/dist/ui/overlays/SettingsScreen.js +38 -1
- package/package.json +3 -2
package/dist/config/load.d.ts
CHANGED
|
@@ -52,6 +52,33 @@ export interface CliFlags {
|
|
|
52
52
|
team?: boolean;
|
|
53
53
|
/** `--team-max <n>` — override `team.maxSubagents` for this run (still clamped). */
|
|
54
54
|
teamMax?: string | number;
|
|
55
|
+
/**
|
|
56
|
+
* `--todo` ⇒ true, `--no-todo` ⇒ false, absent ⇒ undefined
|
|
57
|
+
* (todo-plan-execution §4.3).
|
|
58
|
+
*/
|
|
59
|
+
todo?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* `--todo-panel` ⇒ true, `--no-todo-panel` ⇒ false, absent ⇒ undefined.
|
|
62
|
+
*
|
|
63
|
+
* DECLARED AS A PAIR, like `--todo` above, and unlike `--no-skills` (P1-8).
|
|
64
|
+
* `panel` is a PERSISTED setting, so "not passed" has to stay distinguishable
|
|
65
|
+
* from `true` — otherwise commander's lone-`--no-x` default silently
|
|
66
|
+
* overwrites a config-file `panel: false` on every run that passes no flag at
|
|
67
|
+
* all. `--no-skills` escapes the rule only because nothing needs to
|
|
68
|
+
* distinguish `--skills` from silence.
|
|
69
|
+
*/
|
|
70
|
+
todoPanel?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* `--todo-follow <mode>` — `notify` | `auto` | `off`
|
|
73
|
+
* (todo-plan-followthrough §4.2).
|
|
74
|
+
*
|
|
75
|
+
* A VALUE FLAG, NOT A PAIR, so commander materializes it only when passed and
|
|
76
|
+
* the `!== undefined` rule above applies to its presence rather than to a
|
|
77
|
+
* synthesized `true`. Typed `string` and not `FollowThroughMode`: an
|
|
78
|
+
* unrecognized value is CLAMPED to the default by `clampTodoConfig`, never
|
|
79
|
+
* rejected, which is what every other key in this file does.
|
|
80
|
+
*/
|
|
81
|
+
todoFollow?: string;
|
|
55
82
|
/** `--log-level <lv>` — this run only, never persisted. */
|
|
56
83
|
logLevel?: string;
|
|
57
84
|
/** `--log-file` ⇒ true, `--no-log-file` ⇒ false, absent ⇒ undefined. */
|
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, clampTodoConfig, 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';
|
|
@@ -97,6 +97,29 @@ function resolveTeamConfig(flags, env, file) {
|
|
|
97
97
|
: {}),
|
|
98
98
|
});
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Merge the `todo` section across all four layers (defaults › file › env ›
|
|
102
|
+
* flags), todo-plan-execution §4.3.
|
|
103
|
+
*
|
|
104
|
+
* `!== undefined` ON BOTH FLAGS, and that is the whole point (P1-8). `commander`
|
|
105
|
+
* materializes a lone `--no-x` as `opts.x = true` when the flag is absent, so a
|
|
106
|
+
* truthiness check cannot tell `--no-todo-panel` from "not passed" — and both
|
|
107
|
+
* keys here are persisted, so both need the distinction. This is the same rule
|
|
108
|
+
* `resolveTeamConfig` above states, one feature later.
|
|
109
|
+
*/
|
|
110
|
+
function resolveTodoConfig(flags, env, file) {
|
|
111
|
+
return clampTodoConfig({
|
|
112
|
+
...DEFAULT_CONFIG.todo,
|
|
113
|
+
...(file.todo ?? {}),
|
|
114
|
+
...(env.todo ?? {}),
|
|
115
|
+
...(flags.todo !== undefined ? { enabled: flags.todo } : {}),
|
|
116
|
+
...(flags.todoPanel !== undefined ? { panel: flags.todoPanel } : {}),
|
|
117
|
+
// `!== undefined` for the third time, though for a value flag the risk is
|
|
118
|
+
// milder: commander leaves `opts.todoFollow` absent unless it was passed.
|
|
119
|
+
// Written this way anyway so the three lines read as one rule.
|
|
120
|
+
...(flags.todoFollow !== undefined ? { followThrough: flags.todoFollow } : {}),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
100
123
|
/** Merge the `log` section across all four layers (defaults › file › env › flags). */
|
|
101
124
|
function resolveLogConfig(flags, env, file) {
|
|
102
125
|
const merged = clampLogConfig({
|
|
@@ -118,6 +141,44 @@ function pick(...vals) {
|
|
|
118
141
|
}
|
|
119
142
|
return undefined;
|
|
120
143
|
}
|
|
144
|
+
function hasOwn(obj, key) {
|
|
145
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Resolve the output cap: flag › env › file › default, where an EXPLICIT `null`
|
|
149
|
+
* at any layer means AUTO (`undefined` — let the resolver pick per model).
|
|
150
|
+
*
|
|
151
|
+
* THIS CANNOT USE `pick()`, and that is the whole point. `pick` skips `null`, so
|
|
152
|
+
* a config file holding `"maxTokens": null` silently resolves to 64000 and AUTO
|
|
153
|
+
* is unreachable through every supported write path. Detecting it needs an
|
|
154
|
+
* own-property test, not a truthiness test — `null` is exactly the value we are
|
|
155
|
+
* looking for.
|
|
156
|
+
*
|
|
157
|
+
* An unparseable layer falls through to the next one rather than becoming AUTO:
|
|
158
|
+
* a typo in `ARAGON_MAX_TOKENS` must not silently discard the number in the
|
|
159
|
+
* config file underneath it.
|
|
160
|
+
*/
|
|
161
|
+
function resolveMaxTokens(flags, env, file) {
|
|
162
|
+
const layers = [
|
|
163
|
+
{ name: '--max-tokens', value: flags.maxTokens },
|
|
164
|
+
{ name: 'ARAGON_MAX_TOKENS', value: hasOwn(env, 'maxTokens') ? env.maxTokens : undefined },
|
|
165
|
+
{ name: 'config.json maxTokens', value: hasOwn(file, 'maxTokens') ? file.maxTokens : undefined },
|
|
166
|
+
];
|
|
167
|
+
for (const layer of layers) {
|
|
168
|
+
if (layer.value === undefined)
|
|
169
|
+
continue; // this layer said nothing
|
|
170
|
+
if (layer.value === null || isAutoToken(layer.value))
|
|
171
|
+
return undefined; // AUTO
|
|
172
|
+
const n = clampMaxTokens(layer.value);
|
|
173
|
+
if (n !== undefined)
|
|
174
|
+
return n;
|
|
175
|
+
getLogger().warn('config', 'max_tokens_unusable', {
|
|
176
|
+
source: layer.name,
|
|
177
|
+
value: String(layer.value),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return DEFAULT_CONFIG.maxTokens ?? undefined;
|
|
181
|
+
}
|
|
121
182
|
/**
|
|
122
183
|
* Resolve the effective config by merging all layers, then derive the runtime
|
|
123
184
|
* (non-persisted) fields and enforce the timeout invariant.
|
|
@@ -142,7 +203,7 @@ export function loadConfig(flags = {}) {
|
|
|
142
203
|
const model = pick(flags.model, env.partial.model, file.model) ?? DEFAULT_CONFIG.model;
|
|
143
204
|
const baseUrl = pick(flags.baseUrl, env.partial.baseUrl ?? undefined, file.baseUrl ?? undefined);
|
|
144
205
|
const thinkingLevel = clampThinkingLevel(pick(flags.thinking, env.partial.thinkingLevel, file.thinkingLevel), DEFAULT_CONFIG.thinkingLevel);
|
|
145
|
-
const maxTokens =
|
|
206
|
+
const maxTokens = resolveMaxTokens(flags, env.partial, file);
|
|
146
207
|
// `clampTheme` is also where the v0.3.0 `dark` name is migrated to `cool`,
|
|
147
208
|
// so reading a legacy config file needs no extra step here (§4.5 / R-4).
|
|
148
209
|
const theme = clampTheme(pick(flags.theme, env.partial.theme, file.theme), DEFAULT_CONFIG.theme);
|
|
@@ -247,6 +308,7 @@ export function loadConfig(flags = {}) {
|
|
|
247
308
|
},
|
|
248
309
|
log: resolveLogConfig(flags, env.partial, file),
|
|
249
310
|
team: resolveTeamConfig(flags, env.partial, file),
|
|
311
|
+
todo: resolveTodoConfig(flags, env.partial, file),
|
|
250
312
|
cwd,
|
|
251
313
|
color,
|
|
252
314
|
colorLevel,
|
|
@@ -274,6 +336,9 @@ function recordResolvedConfig(config) {
|
|
|
274
336
|
model: config.model,
|
|
275
337
|
baseUrl: config.baseUrl,
|
|
276
338
|
thinkingLevel: config.thinkingLevel,
|
|
339
|
+
// Four layers feed this one and none of them used to leave a trace, so
|
|
340
|
+
// "why is my output truncated" was unanswerable after the fact.
|
|
341
|
+
maxTokens: config.maxTokens ?? 'auto',
|
|
277
342
|
theme: config.theme,
|
|
278
343
|
fullscreen: config.fullscreen,
|
|
279
344
|
toolTimeoutMs: config.toolTimeoutMs,
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
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
|
+
import type { FollowThroughMode } from '../todo/types.js';
|
|
10
11
|
/**
|
|
11
12
|
* `ModelRegistry` seeds builtin lists for five providers, but `initProviders()`
|
|
12
13
|
* registers only three adapters. Running a model whose provider has no adapter
|
|
@@ -239,12 +240,58 @@ export declare const DEFAULT_TEAM_CONFIG: TeamConfig;
|
|
|
239
240
|
* the two disagree would make the panel's `n/m` readout confusing for no gain.
|
|
240
241
|
*/
|
|
241
242
|
export declare function clampTeamConfig(raw: unknown): TeamConfig;
|
|
243
|
+
export interface TodoConfig {
|
|
244
|
+
/** Register `todo_write` at all. Default TRUE. */
|
|
245
|
+
enabled: boolean;
|
|
246
|
+
/**
|
|
247
|
+
* Render the right rail.
|
|
248
|
+
*
|
|
249
|
+
* INDEPENDENT OF `enabled` ON PURPOSE (D-16): a screen-reader user wants the
|
|
250
|
+
* planning discipline without the column, and the prompt block varies one
|
|
251
|
+
* sentence rather than disappearing (§3.7).
|
|
252
|
+
*/
|
|
253
|
+
panel: boolean;
|
|
254
|
+
/**
|
|
255
|
+
* What happens when a run ends with unfinished steps
|
|
256
|
+
* (todo-plan-followthrough §4.1). Default `'notify'`, which reproduces round
|
|
257
|
+
* 1's behaviour byte for byte.
|
|
258
|
+
*
|
|
259
|
+
* THE THIRD SCALAR, and it stays a scalar for the reason the section header
|
|
260
|
+
* above gives: `store.ts` merges this section BY HAND, and both of its merges
|
|
261
|
+
* spread the whole object (`{ ...DEFAULT_CONFIG.todo, ...(partial.todo ?? {}) }`
|
|
262
|
+
* and `{ ...current.todo, ...(patch.todo ?? {}) }`), so a third scalar key is
|
|
263
|
+
* carried automatically and needs no change there. A NESTED field would not
|
|
264
|
+
* be, and would mean replacing both merges first.
|
|
265
|
+
*/
|
|
266
|
+
followThrough: FollowThroughMode;
|
|
267
|
+
}
|
|
268
|
+
export declare const DEFAULT_TODO_CONFIG: TodoConfig;
|
|
269
|
+
/**
|
|
270
|
+
* THE single gate for every todo-config read AND write, mirroring
|
|
271
|
+
* `clampSkillsConfig` / `clampLogConfig` / `clampTeamConfig` for the reason
|
|
272
|
+
* those record: hardening only the read path leaves a bad value on disk that
|
|
273
|
+
* reverts to the default on every launch, which presents to the user as "my
|
|
274
|
+
* setting won't stick".
|
|
275
|
+
*/
|
|
276
|
+
export declare function clampTodoConfig(raw: unknown): TodoConfig;
|
|
242
277
|
export interface PersistedConfig {
|
|
243
278
|
version: number;
|
|
244
279
|
provider: string;
|
|
245
280
|
model: string;
|
|
246
281
|
baseUrl: string | null;
|
|
247
282
|
thinkingLevel: ThinkingLevel;
|
|
283
|
+
/**
|
|
284
|
+
* Output token cap.
|
|
285
|
+
*
|
|
286
|
+
* a number — an explicit cap, clamped to `[256, 200000]` on the way in AND
|
|
287
|
+
* on the way out, then clamped again at request time down to the
|
|
288
|
+
* target model's real ceiling when that ceiling is lower.
|
|
289
|
+
* `null` — AUTO: use each model's own ceiling, never above 64000.
|
|
290
|
+
* absent — the product default, 64000.
|
|
291
|
+
*
|
|
292
|
+
* `null` MEANS AUTO. It used to resolve to 64000 by accident (`pick()` skips
|
|
293
|
+
* `null`), which left the user no way to express "let the model decide".
|
|
294
|
+
*/
|
|
248
295
|
maxTokens: number | null;
|
|
249
296
|
theme: ThemeName;
|
|
250
297
|
/** Replace spinners with a static glyph for calmer, low-motion output. */
|
|
@@ -293,6 +340,8 @@ export interface PersistedConfig {
|
|
|
293
340
|
log: LogConfig;
|
|
294
341
|
/** Team mode. The FOURTH nested object; scalars only (see TeamConfig). */
|
|
295
342
|
team: TeamConfig;
|
|
343
|
+
/** Todo planning. The FIFTH nested object; scalars only (see TodoConfig). */
|
|
344
|
+
todo: TodoConfig;
|
|
296
345
|
}
|
|
297
346
|
export declare const DEFAULT_CONFIG: PersistedConfig;
|
|
298
347
|
/**
|
|
@@ -324,6 +373,7 @@ export interface CliConfig {
|
|
|
324
373
|
model: string;
|
|
325
374
|
baseUrl?: string;
|
|
326
375
|
thinkingLevel: ThinkingLevel;
|
|
376
|
+
/** The resolved cap, or `undefined` for AUTO (per-model ceiling, ≤ 64000). */
|
|
327
377
|
maxTokens?: number;
|
|
328
378
|
theme: ThemeName;
|
|
329
379
|
reducedMotion: boolean;
|
|
@@ -386,6 +436,16 @@ export interface CliConfig {
|
|
|
386
436
|
* `/team` flips is a separate flag on the controller.
|
|
387
437
|
*/
|
|
388
438
|
team: TeamConfig;
|
|
439
|
+
/**
|
|
440
|
+
* Resolved todo-planning settings (todo-plan-execution §4.2).
|
|
441
|
+
*
|
|
442
|
+
* `enabled` here decides whether `todo_write` is REGISTERED, which happens
|
|
443
|
+
* once at construction and can never be undone (C-1). The live on/off switch
|
|
444
|
+
* `/todo` flips is a separate flag on the controller; `panel` is read at
|
|
445
|
+
* RENDER time through `controller.getConfig()`, which is why `/todo panel`
|
|
446
|
+
* must call `setTodoConfig` as well as persisting (P1-2).
|
|
447
|
+
*/
|
|
448
|
+
todo: TodoConfig;
|
|
389
449
|
/**
|
|
390
450
|
* Lifetime submit count driving the composer hint fade (`HINT_FADE_AFTER`).
|
|
391
451
|
*
|
|
@@ -406,8 +466,57 @@ export interface CliConfig {
|
|
|
406
466
|
/** One-shot `--api-key` override; applies to the active provider only. */
|
|
407
467
|
apiKeyOverride?: string;
|
|
408
468
|
}
|
|
409
|
-
/**
|
|
469
|
+
/**
|
|
470
|
+
* Coerce an arbitrary value into a positive integer, or `undefined`.
|
|
471
|
+
*
|
|
472
|
+
* Kept because callers outside this feature still use it, and deliberately
|
|
473
|
+
* WITHOUT the range clamp so its behaviour for in-range values is byte-identical
|
|
474
|
+
* to what it always was. New code wants `clampMaxTokens`.
|
|
475
|
+
*/
|
|
410
476
|
export declare function coerceMaxTokens(v: unknown): number | undefined;
|
|
477
|
+
/**
|
|
478
|
+
* The accepted range for an explicit output cap.
|
|
479
|
+
*
|
|
480
|
+
* The floor is the engine's: below it a turn cannot produce a usable tool call.
|
|
481
|
+
* The ceiling is a sanity bound on a hand-edited file, not a model limit — the
|
|
482
|
+
* real per-model ceiling is applied later, by `resolveOutputTokens`.
|
|
483
|
+
*/
|
|
484
|
+
export declare const MAX_TOKENS_RANGE: {
|
|
485
|
+
min: number;
|
|
486
|
+
max: number;
|
|
487
|
+
};
|
|
488
|
+
/** `auto`, an empty string and `0` all mean AUTO — "let the model decide". */
|
|
489
|
+
export declare function isAutoToken(v: unknown): boolean;
|
|
490
|
+
/**
|
|
491
|
+
* THE single gate for every `maxTokens` read AND write, in the shape
|
|
492
|
+
* `clampSkillsConfig` / `clampLogConfig` / `clampTeamConfig` already use.
|
|
493
|
+
*
|
|
494
|
+
* Both directions on purpose: hardening only the read path leaves a bad value on
|
|
495
|
+
* disk that reverts to a default on every launch, which presents to the user as
|
|
496
|
+
* "my setting won't stick".
|
|
497
|
+
*
|
|
498
|
+
* Returns `undefined` for AUTO and for anything unusable — the caller decides
|
|
499
|
+
* which of the two it was, because they mean different things.
|
|
500
|
+
*/
|
|
501
|
+
export declare function clampMaxTokens(v: unknown): number | undefined;
|
|
502
|
+
/**
|
|
503
|
+
* Parse text typed into the settings screen, a slash command, or `config set`.
|
|
504
|
+
*
|
|
505
|
+
* Distinguishes AUTO from "the user typed nonsense", which `clampMaxTokens`
|
|
506
|
+
* cannot: both come back as `undefined` there, and treating a typo as AUTO would
|
|
507
|
+
* silently discard the number the user meant to set.
|
|
508
|
+
*/
|
|
509
|
+
export declare function parseMaxTokensInput(raw: string): {
|
|
510
|
+
kind: 'auto';
|
|
511
|
+
} | {
|
|
512
|
+
kind: 'value';
|
|
513
|
+
value: number;
|
|
514
|
+
clamped: boolean;
|
|
515
|
+
} | {
|
|
516
|
+
kind: 'invalid';
|
|
517
|
+
};
|
|
518
|
+
/** The product default, re-exported so the CLI never spells the number itself. */
|
|
519
|
+
export declare const DEFAULT_MAX_TOKENS = 64000;
|
|
411
520
|
/** Coerce a value into a positive integer with a fallback (for timeouts). */
|
|
412
521
|
export declare function coercePositiveInt(v: unknown, fallback: number): number;
|
|
413
522
|
/**
|
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
|
// ---------------------------------------------------------------------------
|
|
@@ -311,13 +312,49 @@ export function clampTeamConfig(raw) {
|
|
|
311
312
|
maxTurnsPerSubagent: clampInt(src.maxTurnsPerSubagent, DEFAULT_TEAM_CONFIG.maxTurnsPerSubagent, TURNS_RANGE),
|
|
312
313
|
};
|
|
313
314
|
}
|
|
315
|
+
export const DEFAULT_TODO_CONFIG = {
|
|
316
|
+
enabled: true,
|
|
317
|
+
panel: true,
|
|
318
|
+
followThrough: 'notify',
|
|
319
|
+
};
|
|
320
|
+
/**
|
|
321
|
+
* There is no enum helper in this file, so this follows the `isThinkingLevel`
|
|
322
|
+
* shape already here rather than inventing a second idiom.
|
|
323
|
+
*/
|
|
324
|
+
const FOLLOW_THROUGH_MODES = ['notify', 'auto', 'off'];
|
|
325
|
+
function followMode(value, fallback) {
|
|
326
|
+
return typeof value === 'string' && FOLLOW_THROUGH_MODES.includes(value)
|
|
327
|
+
? value
|
|
328
|
+
: fallback;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* THE single gate for every todo-config read AND write, mirroring
|
|
332
|
+
* `clampSkillsConfig` / `clampLogConfig` / `clampTeamConfig` for the reason
|
|
333
|
+
* those record: hardening only the read path leaves a bad value on disk that
|
|
334
|
+
* reverts to the default on every launch, which presents to the user as "my
|
|
335
|
+
* setting won't stick".
|
|
336
|
+
*/
|
|
337
|
+
export function clampTodoConfig(raw) {
|
|
338
|
+
const src = (raw && typeof raw === 'object' ? raw : {});
|
|
339
|
+
return {
|
|
340
|
+
enabled: bool(src.enabled, DEFAULT_TODO_CONFIG.enabled),
|
|
341
|
+
panel: bool(src.panel, DEFAULT_TODO_CONFIG.panel),
|
|
342
|
+
// CLAMPED TO THE DEFAULT, NEVER REJECTED — the discipline every other key in
|
|
343
|
+
// this file follows, and the trade round 1 already made for `--thinking`.
|
|
344
|
+
// `ARAGON_TODO_FOLLOW=garbage` therefore resolves to `'notify'` and throws
|
|
345
|
+
// nothing (AC-5).
|
|
346
|
+
followThrough: followMode(src.followThrough, DEFAULT_TODO_CONFIG.followThrough),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
314
349
|
export const DEFAULT_CONFIG = {
|
|
315
350
|
version: CONFIG_VERSION,
|
|
316
351
|
provider: 'anthropic',
|
|
317
352
|
model: DEFAULT_MODEL,
|
|
318
353
|
baseUrl: null,
|
|
319
354
|
thinkingLevel: 'off',
|
|
320
|
-
|
|
355
|
+
// The engine owns this number; spelling it here again is how the CLI and the
|
|
356
|
+
// provider adapters end up disagreeing about what "the default" is.
|
|
357
|
+
maxTokens: DEFAULT_MAX_OUTPUT_TOKENS,
|
|
321
358
|
theme: 'auto',
|
|
322
359
|
reducedMotion: false,
|
|
323
360
|
fullscreen: true,
|
|
@@ -337,6 +374,10 @@ export const DEFAULT_CONFIG = {
|
|
|
337
374
|
skills: DEFAULT_SKILLS_CONFIG,
|
|
338
375
|
log: DEFAULT_LOG_CONFIG,
|
|
339
376
|
team: DEFAULT_TEAM_CONFIG,
|
|
377
|
+
// The load path's first spread is what supplies the section to a user with no
|
|
378
|
+
// `todo` key at all, so this line is as required as the two merges in
|
|
379
|
+
// `store.ts` (P0-1).
|
|
380
|
+
todo: DEFAULT_TODO_CONFIG,
|
|
340
381
|
};
|
|
341
382
|
/**
|
|
342
383
|
* Runtime state that used to be stored in `config.json` by mistake
|
|
@@ -376,7 +417,7 @@ export function stripLegacyStateKeys(raw) {
|
|
|
376
417
|
// Coercion / validation helpers
|
|
377
418
|
// ---------------------------------------------------------------------------
|
|
378
419
|
/** Coerce an arbitrary value into a positive integer, or `undefined`. */
|
|
379
|
-
|
|
420
|
+
function coerceTokenCount(v) {
|
|
380
421
|
if (v === null || v === undefined || v === '')
|
|
381
422
|
return undefined;
|
|
382
423
|
const n = typeof v === 'number' ? v : Number.parseInt(String(v), 10);
|
|
@@ -384,6 +425,74 @@ export function coerceMaxTokens(v) {
|
|
|
384
425
|
return undefined;
|
|
385
426
|
return Math.floor(n);
|
|
386
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* Coerce an arbitrary value into a positive integer, or `undefined`.
|
|
430
|
+
*
|
|
431
|
+
* Kept because callers outside this feature still use it, and deliberately
|
|
432
|
+
* WITHOUT the range clamp so its behaviour for in-range values is byte-identical
|
|
433
|
+
* to what it always was. New code wants `clampMaxTokens`.
|
|
434
|
+
*/
|
|
435
|
+
export function coerceMaxTokens(v) {
|
|
436
|
+
return coerceTokenCount(v);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* The accepted range for an explicit output cap.
|
|
440
|
+
*
|
|
441
|
+
* The floor is the engine's: below it a turn cannot produce a usable tool call.
|
|
442
|
+
* The ceiling is a sanity bound on a hand-edited file, not a model limit — the
|
|
443
|
+
* real per-model ceiling is applied later, by `resolveOutputTokens`.
|
|
444
|
+
*/
|
|
445
|
+
export const MAX_TOKENS_RANGE = {
|
|
446
|
+
min: MIN_MAX_OUTPUT_TOKENS,
|
|
447
|
+
max: ABSOLUTE_MAX_OUTPUT_TOKENS,
|
|
448
|
+
};
|
|
449
|
+
/** `auto`, an empty string and `0` all mean AUTO — "let the model decide". */
|
|
450
|
+
export function isAutoToken(v) {
|
|
451
|
+
if (v === 0)
|
|
452
|
+
return true;
|
|
453
|
+
if (typeof v !== 'string')
|
|
454
|
+
return false;
|
|
455
|
+
const trimmed = v.trim().toLowerCase();
|
|
456
|
+
return trimmed === '' || trimmed === 'auto' || trimmed === '0';
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* THE single gate for every `maxTokens` read AND write, in the shape
|
|
460
|
+
* `clampSkillsConfig` / `clampLogConfig` / `clampTeamConfig` already use.
|
|
461
|
+
*
|
|
462
|
+
* Both directions on purpose: hardening only the read path leaves a bad value on
|
|
463
|
+
* disk that reverts to a default on every launch, which presents to the user as
|
|
464
|
+
* "my setting won't stick".
|
|
465
|
+
*
|
|
466
|
+
* Returns `undefined` for AUTO and for anything unusable — the caller decides
|
|
467
|
+
* which of the two it was, because they mean different things.
|
|
468
|
+
*/
|
|
469
|
+
export function clampMaxTokens(v) {
|
|
470
|
+
const n = coerceTokenCount(v);
|
|
471
|
+
if (n === undefined)
|
|
472
|
+
return undefined;
|
|
473
|
+
return Math.min(MAX_TOKENS_RANGE.max, Math.max(MAX_TOKENS_RANGE.min, n));
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Parse text typed into the settings screen, a slash command, or `config set`.
|
|
477
|
+
*
|
|
478
|
+
* Distinguishes AUTO from "the user typed nonsense", which `clampMaxTokens`
|
|
479
|
+
* cannot: both come back as `undefined` there, and treating a typo as AUTO would
|
|
480
|
+
* silently discard the number the user meant to set.
|
|
481
|
+
*/
|
|
482
|
+
export function parseMaxTokensInput(raw) {
|
|
483
|
+
const trimmed = String(raw ?? '').trim();
|
|
484
|
+
if (isAutoToken(trimmed))
|
|
485
|
+
return { kind: 'auto' };
|
|
486
|
+
if (!/^\d+$/.test(trimmed))
|
|
487
|
+
return { kind: 'invalid' };
|
|
488
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
489
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
490
|
+
return { kind: 'invalid' };
|
|
491
|
+
const value = Math.min(MAX_TOKENS_RANGE.max, Math.max(MAX_TOKENS_RANGE.min, parsed));
|
|
492
|
+
return { kind: 'value', value, clamped: value !== parsed };
|
|
493
|
+
}
|
|
494
|
+
/** The product default, re-exported so the CLI never spells the number itself. */
|
|
495
|
+
export const DEFAULT_MAX_TOKENS = DEFAULT_MAX_OUTPUT_TOKENS;
|
|
387
496
|
/** Coerce a value into a positive integer with a fallback (for timeouts). */
|
|
388
497
|
export function coercePositiveInt(v, fallback) {
|
|
389
498
|
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, clampTodoConfig, 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.
|
|
@@ -100,6 +122,10 @@ export function loadPersistedConfig() {
|
|
|
100
122
|
log: clampLogConfig({ ...DEFAULT_CONFIG.log, ...(partial.log ?? {}) }),
|
|
101
123
|
// `team` is the fourth. Same one-level rule, same merge.
|
|
102
124
|
team: clampTeamConfig({ ...DEFAULT_CONFIG.team, ...(partial.team ?? {}) }),
|
|
125
|
+
// `todo` is the fifth. Same one-level rule, same merge — and this line is
|
|
126
|
+
// the READ half of the pair that keeps `/todo panel off` from silently
|
|
127
|
+
// unregistering the tool on the next launch (P0-1 / AC-35).
|
|
128
|
+
todo: clampTodoConfig({ ...DEFAULT_CONFIG.todo, ...(partial.todo ?? {}) }),
|
|
103
129
|
};
|
|
104
130
|
}
|
|
105
131
|
/**
|
|
@@ -131,6 +157,11 @@ export function updatePersistedConfig(patch) {
|
|
|
131
157
|
...current,
|
|
132
158
|
...patch,
|
|
133
159
|
version: CONFIG_VERSION,
|
|
160
|
+
// The write half of the gate: without it `config set maxTokens 900000` puts
|
|
161
|
+
// a number on disk that the read path silently rewrites on every launch.
|
|
162
|
+
maxTokens: Object.prototype.hasOwnProperty.call(patch, 'maxTokens')
|
|
163
|
+
? normalizeMaxTokens(patch.maxTokens)
|
|
164
|
+
: current.maxTokens,
|
|
134
165
|
apiKeys: { ...current.apiKeys, ...(patch.apiKeys ?? {}) },
|
|
135
166
|
skills: clampSkillsConfig({ ...current.skills, ...(patch.skills ?? {}) }),
|
|
136
167
|
// Without this line, changing the log level from the settings screen would
|
|
@@ -141,6 +172,11 @@ export function updatePersistedConfig(patch) {
|
|
|
141
172
|
// 4` sends `{ team: { maxSubagents: 4 } }`, which a shallow spread would
|
|
142
173
|
// turn into "changing the fan-out width silently reverted every timeout".
|
|
143
174
|
team: clampTeamConfig({ ...current.team, ...(patch.team ?? {}) }),
|
|
175
|
+
// The fifth section, and its omission is the worst of the five (P0-1):
|
|
176
|
+
// `/todo panel off` sends `{ todo: { panel: false } }`, which a shallow
|
|
177
|
+
// spread turns into "turning the panel off silently unregisters the tool",
|
|
178
|
+
// permanently, from the next launch onward, with nothing anywhere saying so.
|
|
179
|
+
todo: clampTodoConfig({ ...current.todo, ...(patch.todo ?? {}) }),
|
|
144
180
|
};
|
|
145
181
|
writeConfigFile(merged);
|
|
146
182
|
return merged;
|
|
@@ -7,22 +7,40 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { Message, ModelRef } from '@aragon-agent/core';
|
|
9
9
|
import type { Entry } from '../agent/reducer.js';
|
|
10
|
+
import type { TodoItem } from '../todo/types.js';
|
|
10
11
|
export interface SavedSession {
|
|
11
12
|
version: number;
|
|
12
13
|
savedAt: number;
|
|
13
14
|
model: ModelRef;
|
|
14
15
|
messages: Message[];
|
|
15
16
|
entries: Entry[];
|
|
17
|
+
/**
|
|
18
|
+
* The live todo list at save time (todo-plan-execution §3.13).
|
|
19
|
+
*
|
|
20
|
+
* `SESSION_VERSION` STAYS 1: the field is additive and optional, `loadSession`
|
|
21
|
+
* validates only that `messages` and `entries` are arrays, and an older file
|
|
22
|
+
* simply yields `undefined` — which `restoreTodos` treats as "clear", the
|
|
23
|
+
* right answer for a session that predates the feature (D-20 / D-22).
|
|
24
|
+
*/
|
|
25
|
+
todos?: TodoItem[];
|
|
16
26
|
}
|
|
17
27
|
/** Resolve a `/save` target: absolute/relative path, or a name in the data dir. */
|
|
18
28
|
export declare function resolveSessionPath(nameOrPath: string | undefined, cwd: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* `data` GAINS `todos`, AND WIDENING `SavedSession` ALONE IS NOT ENOUGH (P0-2):
|
|
31
|
+
* the writer below builds its payload from THIS parameter, not from the type, so
|
|
32
|
+
* a `todos?` field on the interface with no field here would ship a key nothing
|
|
33
|
+
* ever writes.
|
|
34
|
+
*/
|
|
19
35
|
export declare function saveSession(filePath: string, data: {
|
|
20
36
|
model: ModelRef;
|
|
21
37
|
messages: Message[];
|
|
22
38
|
entries: Entry[];
|
|
39
|
+
todos: TodoItem[];
|
|
23
40
|
}): void;
|
|
24
41
|
/**
|
|
25
|
-
* Settle any team entry that was saved mid-dispatch (team-subagents §5.3 / P1-5)
|
|
42
|
+
* Settle any team entry that was saved mid-dispatch (team-subagents §5.3 / P1-5),
|
|
43
|
+
* and any todo card that was saved mid-run (todo-plan-execution §3.8 / C-5).
|
|
26
44
|
*
|
|
27
45
|
* `saveSession` writes `entries` verbatim and `loadSession` validates only
|
|
28
46
|
* array-ness, so a session saved while a dispatch was in flight resumes with a
|
package/dist/session/persist.js
CHANGED
|
@@ -28,6 +28,12 @@ function defaultStamp() {
|
|
|
28
28
|
// Filesystem-safe timestamp without punctuation that breaks on Windows.
|
|
29
29
|
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* `data` GAINS `todos`, AND WIDENING `SavedSession` ALONE IS NOT ENOUGH (P0-2):
|
|
33
|
+
* the writer below builds its payload from THIS parameter, not from the type, so
|
|
34
|
+
* a `todos?` field on the interface with no field here would ship a key nothing
|
|
35
|
+
* ever writes.
|
|
36
|
+
*/
|
|
31
37
|
export function saveSession(filePath, data) {
|
|
32
38
|
const dir = dirname(filePath);
|
|
33
39
|
if (!existsSync(dir))
|
|
@@ -38,11 +44,13 @@ export function saveSession(filePath, data) {
|
|
|
38
44
|
model: data.model,
|
|
39
45
|
messages: data.messages,
|
|
40
46
|
entries: data.entries,
|
|
47
|
+
todos: data.todos,
|
|
41
48
|
};
|
|
42
49
|
writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
|
|
43
50
|
}
|
|
44
51
|
/**
|
|
45
|
-
* Settle any team entry that was saved mid-dispatch (team-subagents §5.3 / P1-5)
|
|
52
|
+
* Settle any team entry that was saved mid-dispatch (team-subagents §5.3 / P1-5),
|
|
53
|
+
* and any todo card that was saved mid-run (todo-plan-execution §3.8 / C-5).
|
|
46
54
|
*
|
|
47
55
|
* `saveSession` writes `entries` verbatim and `loadSession` validates only
|
|
48
56
|
* array-ness, so a session saved while a dispatch was in flight resumes with a
|
|
@@ -57,7 +65,18 @@ export function saveSession(filePath, data) {
|
|
|
57
65
|
* never legitimately sees a stale-active entry — only the file does.
|
|
58
66
|
*/
|
|
59
67
|
export function normalizeLoadedEntries(entries) {
|
|
60
|
-
return entries.map((entry) =>
|
|
68
|
+
return entries.map((entry) => {
|
|
69
|
+
if (entry.kind === 'team' && entry.active) {
|
|
70
|
+
return { ...entry, active: false, aborted: true };
|
|
71
|
+
}
|
|
72
|
+
// `interrupted: true` is the todo analogue of the team entry's `aborted`,
|
|
73
|
+
// and true for the same reason: the run behind the card died with the
|
|
74
|
+
// process (P2-6). Without it a resumed `2/7` reads as a run still in flight.
|
|
75
|
+
if (entry.kind === 'todo' && entry.live) {
|
|
76
|
+
return { ...entry, live: false, interrupted: true };
|
|
77
|
+
}
|
|
78
|
+
return entry;
|
|
79
|
+
});
|
|
61
80
|
}
|
|
62
81
|
export function loadSession(filePath) {
|
|
63
82
|
const raw = readFileSync(filePath, 'utf-8');
|
package/dist/team/subagent.js
CHANGED
|
@@ -89,6 +89,17 @@ export function buildSubagentTools(spec, deps, getAgent) {
|
|
|
89
89
|
// registering tools it could never use burns a turn — the same argument that
|
|
90
90
|
// keeps them out of headless mode.
|
|
91
91
|
planTools: [],
|
|
92
|
+
// NO `todo_write` FOR CHILDREN (todo-plan-execution §3.11 / D-14 / AC-34).
|
|
93
|
+
// Children run concurrently against ONE list under a full-replacement
|
|
94
|
+
// protocol: the last writer would win and the lead's plan would be destroyed
|
|
95
|
+
// by a subagent's private checklist. A list PER child would be a second
|
|
96
|
+
// panel and a second scroll model for work the lead already tracks as one
|
|
97
|
+
// item.
|
|
98
|
+
//
|
|
99
|
+
// EXPLICIT, WITH THIS COMMENT, even though the option's absence already
|
|
100
|
+
// yields an empty array: a silent default is not a decision anyone can find
|
|
101
|
+
// later.
|
|
102
|
+
todoTools: [],
|
|
92
103
|
teamTools: commTools,
|
|
93
104
|
// `spec.readOnly` forces the plan gate on for this child even in a BUILD
|
|
94
105
|
// session. TIGHTENING ONLY (D-13): there is no field anywhere that gives a
|