@aragon-agent/cli 0.6.22 → 0.6.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -2
- package/dist/agent/controller.d.ts +11 -0
- package/dist/agent/controller.js +42 -4
- package/dist/agent/model-profile-settings.js +29 -2
- package/dist/agent/reducer.js +8 -6
- package/dist/commands/builtins.js +87 -3
- package/dist/compaction/child.d.ts +15 -0
- package/dist/compaction/child.js +14 -3
- package/dist/compaction/compactor.js +1 -0
- package/dist/compaction/context-command.js +10 -1
- package/dist/compaction/meter.d.ts +9 -0
- package/dist/compaction/meter.js +16 -1
- package/dist/compaction/pressure.d.ts +7 -0
- package/dist/compaction/pressure.js +5 -0
- package/dist/compaction/types.d.ts +11 -0
- package/dist/compaction/wiring.js +1 -0
- package/dist/config/cli-commands.d.ts +1 -1
- package/dist/config/cli-commands.js +12 -0
- package/dist/config/load.js +66 -9
- package/dist/config/model-profile-resolution.d.ts +8 -0
- package/dist/config/model-profile-resolution.js +13 -6
- package/dist/config/model-profile-store.d.ts +4 -2
- package/dist/config/model-profiles.d.ts +13 -0
- package/dist/config/model-profiles.js +18 -2
- package/dist/config/schema.d.ts +86 -0
- package/dist/config/schema.js +74 -0
- package/dist/team/model.d.ts +88 -0
- package/dist/team/model.js +136 -0
- package/dist/team/overseer.d.ts +5 -3
- package/dist/team/overseer.js +15 -4
- package/dist/team/report.d.ts +11 -0
- package/dist/team/report.js +24 -1
- package/dist/team/runtime.js +20 -3
- package/dist/team/subagent.d.ts +10 -0
- package/dist/team/subagent.js +57 -27
- package/dist/team/task-tool.d.ts +25 -6
- package/dist/team/task-tool.js +15 -9
- package/dist/team/types.d.ts +4 -3
- package/dist/ui/App.js +5 -0
- package/dist/ui/overlays/ModelProfileEditor.js +18 -4
- package/dist/ui/overlays/ModelProfilePicker.d.ts +14 -2
- package/dist/ui/overlays/ModelProfilePicker.js +51 -10
- package/dist/ui/overlays/QuestionOverlay.js +47 -6
- package/dist/ui/overlays/SettingsScreen.d.ts +9 -0
- package/dist/ui/overlays/SettingsScreen.js +119 -17
- package/dist/ui/overlays/model-profile-state.d.ts +53 -4
- package/dist/ui/overlays/model-profile-state.js +111 -14
- package/package.json +2 -2
|
@@ -45,6 +45,8 @@ export const TEAM_CONFIG_SET_KEYS = [
|
|
|
45
45
|
'team.maxTurnsPerSubagent',
|
|
46
46
|
'team.overseer',
|
|
47
47
|
'team.overseerIntervalMs',
|
|
48
|
+
'team.model',
|
|
49
|
+
'team.modelProfileId',
|
|
48
50
|
];
|
|
49
51
|
/**
|
|
50
52
|
* Config keys under `retry.` that `aragon config set` accepts
|
|
@@ -205,6 +207,16 @@ export function applyTeamConfigSet(key, value) {
|
|
|
205
207
|
return patch({ overseer: isTrue(value) });
|
|
206
208
|
case 'team.overseerIntervalMs':
|
|
207
209
|
return patch(clampOne('overseerIntervalMs'));
|
|
210
|
+
// The subagent-model keys (subagent-model-and-question-controls V-3):
|
|
211
|
+
// without these two cases `aragon config set team.model fast` is refused
|
|
212
|
+
// with "Unknown config key" while `config list` shows the key one command
|
|
213
|
+
// over - the exact gap COMPACTION_CONFIG_SET_KEYS records at length.
|
|
214
|
+
// Both route through `clampTeamConfig` like every other key here: a typo'd
|
|
215
|
+
// mode resolves to 'main' and a bad id is nulled, never rejected.
|
|
216
|
+
case 'team.model':
|
|
217
|
+
return patch(clampOne('model'));
|
|
218
|
+
case 'team.modelProfileId':
|
|
219
|
+
return patch(clampOne('modelProfileId'));
|
|
208
220
|
default:
|
|
209
221
|
return null;
|
|
210
222
|
}
|
package/dist/config/load.js
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
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, clampCompactionConfig, clampLogConfig, clampFastConfig, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampBashConfig, clampUpdateConfig, clampTheme, clampThinkingLevel, clampContextWindow, clampMaxTokens, clampMaxRenderInterval, clampScrollResumeMs, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAutoToken, isSkillsToolPolicyMode, parseThresholdInput, } from './schema.js';
|
|
9
|
+
import { DEFAULT_CONFIG, IDLE_TIMEOUT_MARGIN_MS, clampAskRounds, clampDensity, clampHumanTimeout, clampCompactionConfig, clampLogConfig, clampFastConfig, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampBashConfig, clampUpdateConfig, clampTheme, clampThinkingLevel, clampContextWindow, clampMaxTokens, clampMaxRenderInterval, clampScrollResumeMs, clampTranscriptRetain, clampTranscriptWindow, coercePositiveInt, isAutoToken, isSkillsToolPolicyMode, legacyTeamCeilingPatch, parseThresholdInput, } from './schema.js';
|
|
10
10
|
import { loadDotenv, readEnvConfig, readEnvDisabledSkills } from './env.js';
|
|
11
|
-
import { readConfigFile } from './store.js';
|
|
12
|
-
import { projectModelProfiles, resolveModelProfileState, resolveModelRoleKey } from './model-profile-resolution.js';
|
|
11
|
+
import { readConfigFile, updatePersistedConfig } from './store.js';
|
|
12
|
+
import { projectModelProfiles, resolveModelProfileState, resolveModelRoleKey, } from './model-profile-resolution.js';
|
|
13
13
|
import { getConfigPath } from './app-paths.js';
|
|
14
14
|
import { setHistoryEnabled } from './prompt-history.js';
|
|
15
15
|
import { setEntryRetain } from '../agent/entry-limits.js';
|
|
@@ -411,33 +411,82 @@ function resolveMaxTokens(flags, env, file) {
|
|
|
411
411
|
* `null`, and `null` is exactly the value being looked for. NO CLI FLAG feeds
|
|
412
412
|
* this - it is a per-installation correction for a model table entry, not a
|
|
413
413
|
* per-invocation choice, so `config set` and the environment variable are the
|
|
414
|
-
* two surfaces the design names.
|
|
414
|
+
* two surfaces the design names. The model profile bound as Main is the third
|
|
415
|
+
* (profile-context-window): its window arrives already merged into the `file`
|
|
416
|
+
* layer by `projectModelProfiles`, and `profileOwned` names it.
|
|
417
|
+
*
|
|
418
|
+
* THE ORIGIN CONTRACT (profile-context-window §6.1, shared verbatim with the
|
|
419
|
+
* save transaction): `origin !== undefined` IF AND ONLY IF `value !== null`.
|
|
420
|
+
* An explicit AUTO - profile set to auto, `config set contextWindow auto`,
|
|
421
|
+
* `ARAGON_CONTEXT_WINDOW=auto` - returns `{ value: null }` and NO origin,
|
|
422
|
+
* because "the user asserted this denominator" and "the user declared AUTO"
|
|
423
|
+
* are different facts and the meter's override branch only sees the first.
|
|
415
424
|
*/
|
|
416
|
-
function resolveContextWindow(env, file) {
|
|
425
|
+
function resolveContextWindow(env, file, profileOwned) {
|
|
417
426
|
const layers = [
|
|
418
427
|
{
|
|
419
428
|
name: 'ARAGON_CONTEXT_WINDOW',
|
|
420
429
|
value: hasOwn(env, 'contextWindow') ? env.contextWindow : undefined,
|
|
430
|
+
origin: 'env',
|
|
421
431
|
},
|
|
422
432
|
{
|
|
423
433
|
name: 'config.json contextWindow',
|
|
424
434
|
value: hasOwn(file, 'contextWindow') ? file.contextWindow : undefined,
|
|
435
|
+
origin: profileOwned ? 'profile' : 'config',
|
|
425
436
|
},
|
|
426
437
|
];
|
|
427
438
|
for (const layer of layers) {
|
|
428
439
|
if (layer.value === undefined)
|
|
429
440
|
continue; // this layer said nothing
|
|
430
441
|
if (layer.value === null || isAutoToken(layer.value))
|
|
431
|
-
return null; // AUTO
|
|
442
|
+
return { value: null }; // AUTO
|
|
432
443
|
const n = clampContextWindow(layer.value, null);
|
|
433
444
|
if (n !== null)
|
|
434
|
-
return n;
|
|
445
|
+
return { value: n, origin: layer.origin };
|
|
435
446
|
getLogger().warn('config', 'context_window_unusable', {
|
|
436
447
|
source: layer.name,
|
|
437
448
|
value: String(layer.value),
|
|
438
449
|
});
|
|
439
450
|
}
|
|
440
|
-
return DEFAULT_CONFIG.contextWindow;
|
|
451
|
+
return { value: DEFAULT_CONFIG.contextWindow };
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Rewrite the cli<=0.6.16 persisted team ceiling defaults to 0 and persist the
|
|
455
|
+
* change (F-8 A'4). ONE-SHOT and IDEMPOTENT: after the first rewrite the file
|
|
456
|
+
* carries 0, which matches nothing, so nothing is written again.
|
|
457
|
+
*
|
|
458
|
+
* THE FILE LAYER ONLY, deliberately: env and flags are this-launch intent and
|
|
459
|
+
* survive untouched, and with the D-4 semantics a positive surviving value is
|
|
460
|
+
* a supervisor cadence hint, never a kill. The raw file object is patched IN
|
|
461
|
+
* PLACE (the same precedent `readConfigFile` sets with `fullscreen`) so the
|
|
462
|
+
* `file` layer this function's caller resolves from already reads 0; the
|
|
463
|
+
* write goes through `updatePersistedConfig`, the one atomic write path every
|
|
464
|
+
* other settings change uses.
|
|
465
|
+
*
|
|
466
|
+
* A FAILED WRITE MUST NOT BLOCK STARTUP: the values are already neutralized
|
|
467
|
+
* in memory, and the next launch retries — which is why the catch is empty
|
|
468
|
+
* and the migration is logged instead of surfaced.
|
|
469
|
+
*/
|
|
470
|
+
function migrateLegacyTeamCeilings(raw) {
|
|
471
|
+
const patch = legacyTeamCeilingPatch(raw?.team);
|
|
472
|
+
if (patch === null)
|
|
473
|
+
return;
|
|
474
|
+
// THE FULL SECTION, not the bare patch: the write port types `team` as a
|
|
475
|
+
// whole `TeamConfig` (its merge then deep-merges over what is on disk), and
|
|
476
|
+
// the section the file already carries plus the zeroed keys IS the section
|
|
477
|
+
// we want on disk.
|
|
478
|
+
const migratedTeam = { ...raw.team, ...patch };
|
|
479
|
+
Object.assign(raw.team, patch);
|
|
480
|
+
try {
|
|
481
|
+
updatePersistedConfig({ team: migratedTeam });
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
// Unwritable config dir: in-memory values are already migrated; retry
|
|
485
|
+
// next launch (idempotent).
|
|
486
|
+
}
|
|
487
|
+
getLogger().warn('config', 'team_legacy_ceilings_migrated', {
|
|
488
|
+
keys: Object.keys(patch).join(','),
|
|
489
|
+
});
|
|
441
490
|
}
|
|
442
491
|
/**
|
|
443
492
|
* Resolve the effective config by merging all layers, then derive the runtime
|
|
@@ -448,6 +497,7 @@ export function loadConfig(flags = {}, options = {}) {
|
|
|
448
497
|
// `.env` must be loaded before reading env overrides.
|
|
449
498
|
loadDotenv(cwd);
|
|
450
499
|
const read = readConfigFile();
|
|
500
|
+
migrateLegacyTeamCeilings(read.config);
|
|
451
501
|
const projection = projectModelProfiles(read.config ?? {});
|
|
452
502
|
const file = projection.file;
|
|
453
503
|
const env = readEnvConfig();
|
|
@@ -474,7 +524,8 @@ export function loadConfig(flags = {}, options = {}) {
|
|
|
474
524
|
const liveToolOutput = pick(flags.liveToolOutput, env.partial.liveToolOutput, file.liveToolOutput) ??
|
|
475
525
|
DEFAULT_CONFIG.liveToolOutput;
|
|
476
526
|
const maxTokens = resolveMaxTokens(flags, env.partial, file);
|
|
477
|
-
const
|
|
527
|
+
const resolvedWindow = resolveContextWindow(env.partial, file, projection.mainOwnsContextWindow);
|
|
528
|
+
const contextWindow = resolvedWindow.value;
|
|
478
529
|
// `clampTheme` is also where the v0.3.0 `dark` name is migrated to `cool`,
|
|
479
530
|
// so reading a legacy config file needs no extra step here (§4.5 / R-4).
|
|
480
531
|
const theme = clampTheme(pick(flags.theme, env.partial.theme, file.theme), DEFAULT_CONFIG.theme);
|
|
@@ -570,6 +621,12 @@ export function loadConfig(flags = {}, options = {}) {
|
|
|
570
621
|
liveToolOutput,
|
|
571
622
|
maxTokens,
|
|
572
623
|
contextWindow,
|
|
624
|
+
// CONDITIONAL SPREAD so a feature-off config object stays byte-identical
|
|
625
|
+
// (invariant 6): with no override there is no origin KEY, not an undefined
|
|
626
|
+
// one, and every `toEqual` on a default config keeps passing untouched.
|
|
627
|
+
...(resolvedWindow.origin !== undefined
|
|
628
|
+
? { contextWindowOrigin: resolvedWindow.origin }
|
|
629
|
+
: {}),
|
|
573
630
|
theme,
|
|
574
631
|
reducedMotion,
|
|
575
632
|
exitTranscript: flags.exitTranscript !== undefined
|
|
@@ -4,6 +4,14 @@ export interface ProfileProjection {
|
|
|
4
4
|
file: Partial<PersistedConfig>;
|
|
5
5
|
state: ModelProfilesRuntime;
|
|
6
6
|
issues: ProfileValidationIssue[];
|
|
7
|
+
/**
|
|
8
|
+
* True when a valid library has Main bound, so the bound profile OWNS the
|
|
9
|
+
* context-window denominator (profile-context-window tradeoff B). THE ONE
|
|
10
|
+
* AUTHORITY for "does the profile decide the window" - load.ts and the save
|
|
11
|
+
* transaction both read it rather than comparing numbers, which would lie on
|
|
12
|
+
* a coincidental match.
|
|
13
|
+
*/
|
|
14
|
+
mainOwnsContextWindow: boolean;
|
|
7
15
|
}
|
|
8
16
|
export interface RoleKeyInput {
|
|
9
17
|
config: CliConfig;
|
|
@@ -9,8 +9,9 @@ export function projectModelProfiles(file) {
|
|
|
9
9
|
const issues = validateModelProfiles(file.modelProfiles);
|
|
10
10
|
const state = emptyModelProfileState(issues.length > 0);
|
|
11
11
|
const projected = { ...file };
|
|
12
|
-
if (issues.length || !file.modelProfiles)
|
|
13
|
-
return { file: projected, state, issues };
|
|
12
|
+
if (issues.length || !file.modelProfiles) {
|
|
13
|
+
return { file: projected, state, issues, mainOwnsContextWindow: false };
|
|
14
|
+
}
|
|
14
15
|
const library = normalizeModelProfiles(file.modelProfiles);
|
|
15
16
|
projected.modelProfiles = library;
|
|
16
17
|
for (const role of ['main', 'fast']) {
|
|
@@ -20,13 +21,19 @@ export function projectModelProfiles(file) {
|
|
|
20
21
|
state[role] = { selectedId: entry.id, appliedId: entry.id,
|
|
21
22
|
overriddenFields: [], invalid: false };
|
|
22
23
|
const connection = { provider: entry.provider, model: entry.model, baseUrl: entry.baseUrl };
|
|
23
|
-
if (role === 'main')
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
if (role === 'main') {
|
|
25
|
+
// The profile owns the denominator: project BOTH a number and null
|
|
26
|
+
// (null = AUTO), so "profile set auto" stays distinguishable from
|
|
27
|
+
// "no profile bound" (profile-context-window tradeoff B).
|
|
28
|
+
Object.assign(projected, connection, { contextWindow: entry.contextWindow });
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
26
31
|
projected.fast = { ...DEFAULT_CONFIG.fast, ...file.fast, ...connection,
|
|
27
32
|
baseUrl: entry.baseUrl ?? '' };
|
|
33
|
+
}
|
|
28
34
|
}
|
|
29
|
-
return { file: projected, state, issues
|
|
35
|
+
return { file: projected, state, issues,
|
|
36
|
+
mainOwnsContextWindow: state.main.appliedId !== null };
|
|
30
37
|
}
|
|
31
38
|
/** Derive binding status from current connections, including session/model overrides. */
|
|
32
39
|
export function resolveModelProfileState(config, profiles = config.modelProfiles, invalid = false) {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { type ModelProfilesConfig, type ModelRole, type ProfileValidationIssue } from './model-profiles.js';
|
|
2
|
-
import type { CliConfig, CompactionConfig, FastConfig, LogConfig, PersistedConfig } from './schema.js';
|
|
3
|
-
export interface ModelSettingsPatch extends Partial<Omit<PersistedConfig, 'modelProfiles' | 'fast' | 'compaction' | 'log'>> {
|
|
2
|
+
import type { CliConfig, CompactionConfig, FastConfig, LogConfig, PersistedConfig, TeamConfig } from './schema.js';
|
|
3
|
+
export interface ModelSettingsPatch extends Partial<Omit<PersistedConfig, 'modelProfiles' | 'fast' | 'compaction' | 'log' | 'team'>> {
|
|
4
4
|
fast?: Partial<FastConfig>;
|
|
5
5
|
compaction?: Partial<CompactionConfig>;
|
|
6
6
|
log?: Partial<LogConfig>;
|
|
7
|
+
/** The subagent-model picker writes here (subagent-model D-5/I-5): draft-only until Save. */
|
|
8
|
+
team?: Partial<TeamConfig>;
|
|
7
9
|
}
|
|
8
10
|
export interface ModelSettingsDraft {
|
|
9
11
|
profiles?: ModelProfilesConfig;
|
|
@@ -6,6 +6,8 @@ export interface ModelProfile {
|
|
|
6
6
|
model: string;
|
|
7
7
|
baseUrl: string | null;
|
|
8
8
|
apiKey: string | null;
|
|
9
|
+
/** The context window of this model+endpoint; null = AUTO (profile-context-window 5.1). */
|
|
10
|
+
contextWindow: number | null;
|
|
9
11
|
}
|
|
10
12
|
export interface ModelProfilesConfig {
|
|
11
13
|
version: 1;
|
|
@@ -34,6 +36,17 @@ export declare function emptyModelProfiles(): ModelProfilesConfig;
|
|
|
34
36
|
export declare function emptyModelProfileState(invalid?: boolean): ModelProfilesRuntime;
|
|
35
37
|
/** Validate untrusted input; diagnostics never include supplied values. */
|
|
36
38
|
export declare function validateModelProfiles(value: unknown): ProfileValidationIssue[];
|
|
39
|
+
/**
|
|
40
|
+
* Disk-tolerance gate for a profile-level context window (profile-context-window 5.3).
|
|
41
|
+
*
|
|
42
|
+
* THE SAME TRI-STATE DISCIPLINE as store.ts normalizeContextWindow, kept out of
|
|
43
|
+
* store.ts on purpose: this file already imports schema.js and store's sibling
|
|
44
|
+
* model-profile-store.ts already imports this one, so placing it here avoids
|
|
45
|
+
* widening the existing store <-> model-profile-store cycle. A garbage value on
|
|
46
|
+
* a hand-edited file degrades to AUTO; it NEVER invalidates the whole library
|
|
47
|
+
* (invariant 4 - one bad number must not drop the user's credentials).
|
|
48
|
+
*/
|
|
49
|
+
export declare function normalizeProfileContextWindow(value: unknown): number | null;
|
|
37
50
|
/** Normalize a valid library into a safe field whitelist; throws a fixed error if invalid. */
|
|
38
51
|
export declare function normalizeModelProfiles(value: unknown): ModelProfilesConfig;
|
|
39
52
|
/** Create an entry with a stable UUID and a frozen default name. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { ADAPTER_PROVIDERS } from './schema.js';
|
|
2
|
+
import { ADAPTER_PROVIDERS, clampContextWindow, isAutoToken } from './schema.js';
|
|
3
3
|
const CONTROL = /[\u0000-\u001f\u007f-\u009f]/;
|
|
4
4
|
/** Return a fresh empty library without initiating a disk migration. */
|
|
5
5
|
export function emptyModelProfiles() {
|
|
@@ -91,6 +91,21 @@ export function validateModelProfiles(value) {
|
|
|
91
91
|
}
|
|
92
92
|
return issues;
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Disk-tolerance gate for a profile-level context window (profile-context-window 5.3).
|
|
96
|
+
*
|
|
97
|
+
* THE SAME TRI-STATE DISCIPLINE as store.ts normalizeContextWindow, kept out of
|
|
98
|
+
* store.ts on purpose: this file already imports schema.js and store's sibling
|
|
99
|
+
* model-profile-store.ts already imports this one, so placing it here avoids
|
|
100
|
+
* widening the existing store <-> model-profile-store cycle. A garbage value on
|
|
101
|
+
* a hand-edited file degrades to AUTO; it NEVER invalidates the whole library
|
|
102
|
+
* (invariant 4 - one bad number must not drop the user's credentials).
|
|
103
|
+
*/
|
|
104
|
+
export function normalizeProfileContextWindow(value) {
|
|
105
|
+
if (value === undefined || value === null || isAutoToken(value))
|
|
106
|
+
return null;
|
|
107
|
+
return clampContextWindow(value, null);
|
|
108
|
+
}
|
|
94
109
|
/** Normalize a valid library into a safe field whitelist; throws a fixed error if invalid. */
|
|
95
110
|
export function normalizeModelProfiles(value) {
|
|
96
111
|
if (validateModelProfiles(value).length)
|
|
@@ -102,7 +117,8 @@ export function normalizeModelProfiles(value) {
|
|
|
102
117
|
entries: library.entries.map((entry) => ({ id: entry.id, name: entry.name.trim()
|
|
103
118
|
|| [...entry.model.trim()].slice(0, 80).join(''), provider: entry.provider,
|
|
104
119
|
model: entry.model.trim(), baseUrl: entry.baseUrl?.trim() || null,
|
|
105
|
-
apiKey: entry.apiKey?.trim() || null
|
|
120
|
+
apiKey: entry.apiKey?.trim() || null,
|
|
121
|
+
contextWindow: normalizeProfileContextWindow(entry.contextWindow) })) };
|
|
106
122
|
}
|
|
107
123
|
/** Create an entry with a stable UUID and a frozen default name. */
|
|
108
124
|
export function createModelProfile(input) {
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -99,6 +99,35 @@ export declare const MAX_CONTEXT_WINDOW = 5000000;
|
|
|
99
99
|
* this deliberately copies its shape rather than inventing a second convention.
|
|
100
100
|
*/
|
|
101
101
|
export declare function clampContextWindow(v: unknown, fallback: number | null): number | null;
|
|
102
|
+
/**
|
|
103
|
+
* Where the effective `contextWindow` override came from
|
|
104
|
+
* (profile-context-window §5.2).
|
|
105
|
+
*
|
|
106
|
+
* A CLI-LOCAL RUNTIME FACT: never persisted, never promoted into Core's event
|
|
107
|
+
* shapes. It exists so `/context` can name WHICH surface supplied a
|
|
108
|
+
* denominator the user asserted - the environment, the legacy top-level key,
|
|
109
|
+
* or the model profile bound as Main.
|
|
110
|
+
*/
|
|
111
|
+
export type ContextWindowOrigin = 'env' | 'config' | 'profile';
|
|
112
|
+
/**
|
|
113
|
+
* Parse text typed into the settings screen or the profile editor for a
|
|
114
|
+
* context window (profile-context-window §5.3).
|
|
115
|
+
*
|
|
116
|
+
* THE MIRROR OF `parseMaxTokensInput` BELOW, down to the tri-state shape: it
|
|
117
|
+
* distinguishes AUTO from "the user typed nonsense", which `clampContextWindow`
|
|
118
|
+
* cannot - a typo must change NOTHING rather than silently discard the number
|
|
119
|
+
* the user meant to set. `''` / `'auto'` / `'0'` are AUTO (`isAutoToken`);
|
|
120
|
+
* a digit run is clamped through THE single gate; anything else is invalid.
|
|
121
|
+
*/
|
|
122
|
+
export declare function parseContextWindowInput(raw: string): {
|
|
123
|
+
kind: 'auto';
|
|
124
|
+
} | {
|
|
125
|
+
kind: 'value';
|
|
126
|
+
value: number;
|
|
127
|
+
clamped: boolean;
|
|
128
|
+
} | {
|
|
129
|
+
kind: 'invalid';
|
|
130
|
+
};
|
|
102
131
|
/**
|
|
103
132
|
* Entries kept in `ViewState` at all (tui-render-performance L1 / R5).
|
|
104
133
|
*
|
|
@@ -356,6 +385,26 @@ export interface TeamConfig {
|
|
|
356
385
|
* meaningful while `team.overseer` is true.
|
|
357
386
|
*/
|
|
358
387
|
overseerIntervalMs: number;
|
|
388
|
+
/**
|
|
389
|
+
* Which model a team child runs on (subagent-model-and-question-controls
|
|
390
|
+
* D-2). `'main'` = the lead's own connection - the default and the
|
|
391
|
+
* historical parity behavior; `'fast'` = the fast tier, resolved per
|
|
392
|
+
* dispatch through `resolveFastTier`; `'profile'` = the entry named by
|
|
393
|
+
* `team.modelProfileId`.
|
|
394
|
+
*
|
|
395
|
+
* A USER-OWNED SETTING, NEVER A TOOL PARAMETER (D-1): `normalizeSubagentSpecs`
|
|
396
|
+
* keeps silently dropping a stale `model` key, so the MODEL cannot downgrade
|
|
397
|
+
* its own delegation. This key is the one explicit, visible, reversible
|
|
398
|
+
* switch the human owns.
|
|
399
|
+
*/
|
|
400
|
+
model: 'main' | 'fast' | 'profile';
|
|
401
|
+
/**
|
|
402
|
+
* The profile entry a `model: 'profile'` dispatch binds to; `null` = not
|
|
403
|
+
* bound. IGNORED by the other modes. Existence is NOT validated here - a
|
|
404
|
+
* dangling id degrades to the main model with a disclosed reason at
|
|
405
|
+
* resolution time (D-4), so deleting a profile can never break a dispatch.
|
|
406
|
+
*/
|
|
407
|
+
modelProfileId: string | null;
|
|
359
408
|
}
|
|
360
409
|
/**
|
|
361
410
|
* The requirement's ceiling on the fan-out (R-g).
|
|
@@ -380,6 +429,31 @@ export declare const DEFAULT_TEAM_CONFIG: TeamConfig;
|
|
|
380
429
|
* the two disagree would make the panel's `n/m` readout confusing for no gain.
|
|
381
430
|
*/
|
|
382
431
|
export declare function clampTeamConfig(raw: unknown): TeamConfig;
|
|
432
|
+
/**
|
|
433
|
+
* The team ceilings cli <= 0.6.16 (7a6bf5630) persisted as its OWN defaults:
|
|
434
|
+
* every user who touched team mode back then still carries this exact triple
|
|
435
|
+
* in config.json, because `resolveTeamConfig` merges file values verbatim and
|
|
436
|
+
* no migration ever rewrote them. They are the kill ceilings behind F-8's
|
|
437
|
+
* "first block all failed" dispatches, so loading rewrites them to 0 (= no
|
|
438
|
+
* ceiling, the shipped default since 2ff15a9c3).
|
|
439
|
+
*
|
|
440
|
+
* EXACT-EQUALITY ONLY: a value that differs was a deliberate user choice and
|
|
441
|
+
* survives untouched. The price of that test is that a user who hand-types
|
|
442
|
+
* 300000 today is migrated to 0 too - the accepted trade, and the safe
|
|
443
|
+
* direction (no ceiling, not a cliff).
|
|
444
|
+
*/
|
|
445
|
+
export declare const LEGACY_TEAM_CEILING_DEFAULTS: Readonly<{
|
|
446
|
+
subagentTimeoutMs: number;
|
|
447
|
+
dispatchTimeoutMs: number;
|
|
448
|
+
maxTurnsPerSubagent: number;
|
|
449
|
+
}>;
|
|
450
|
+
/**
|
|
451
|
+
* The `{ key: 0 }` patch for legacy-default-equal ceilings, or `null` when
|
|
452
|
+
* nothing matches. Pure on purpose: `loadConfig` decides WHERE the patch is
|
|
453
|
+
* applied and persisted; this function only answers WHICH keys match, so the
|
|
454
|
+
* equality test lives beside the constants it reads.
|
|
455
|
+
*/
|
|
456
|
+
export declare function legacyTeamCeilingPatch(raw: unknown): Partial<TeamConfig> | null;
|
|
383
457
|
export interface TodoConfig {
|
|
384
458
|
/** Register `todo_write` at all. Default TRUE. */
|
|
385
459
|
enabled: boolean;
|
|
@@ -942,6 +1016,18 @@ export interface CliConfig {
|
|
|
942
1016
|
* would parse and be discarded, with nothing failing.
|
|
943
1017
|
*/
|
|
944
1018
|
contextWindow: number | null;
|
|
1019
|
+
/**
|
|
1020
|
+
* WHERE the override above came from, or `undefined` when there is none
|
|
1021
|
+
* (profile-context-window §5.2). RUNTIME-ONLY, never persisted.
|
|
1022
|
+
*
|
|
1023
|
+
* THE CONTRACT IS AN IFF: `contextWindowOrigin !== undefined` exactly when
|
|
1024
|
+
* `contextWindow !== null`. An explicit AUTO (profile set to auto,
|
|
1025
|
+
* `config set contextWindow auto`, `ARAGON_CONTEXT_WINDOW=auto`) has value
|
|
1026
|
+
* null, so its origin is undefined - "the user asserted this denominator"
|
|
1027
|
+
* and "the user declared AUTO" are different facts, and the meter's
|
|
1028
|
+
* override branch only ever sees the first.
|
|
1029
|
+
*/
|
|
1030
|
+
contextWindowOrigin?: ContextWindowOrigin;
|
|
945
1031
|
theme: ThemeName;
|
|
946
1032
|
reducedMotion: boolean;
|
|
947
1033
|
/** Replay a plain-text summary on exit (full-screen only — see §4.4). */
|
package/dist/config/schema.js
CHANGED
|
@@ -148,6 +148,27 @@ export function clampContextWindow(v, fallback) {
|
|
|
148
148
|
return fallback;
|
|
149
149
|
return Math.min(MAX_CONTEXT_WINDOW, Math.max(MIN_CONTEXT_WINDOW, Math.floor(n)));
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Parse text typed into the settings screen or the profile editor for a
|
|
153
|
+
* context window (profile-context-window §5.3).
|
|
154
|
+
*
|
|
155
|
+
* THE MIRROR OF `parseMaxTokensInput` BELOW, down to the tri-state shape: it
|
|
156
|
+
* distinguishes AUTO from "the user typed nonsense", which `clampContextWindow`
|
|
157
|
+
* cannot - a typo must change NOTHING rather than silently discard the number
|
|
158
|
+
* the user meant to set. `''` / `'auto'` / `'0'` are AUTO (`isAutoToken`);
|
|
159
|
+
* a digit run is clamped through THE single gate; anything else is invalid.
|
|
160
|
+
*/
|
|
161
|
+
export function parseContextWindowInput(raw) {
|
|
162
|
+
const trimmed = String(raw ?? '').trim();
|
|
163
|
+
if (isAutoToken(trimmed))
|
|
164
|
+
return { kind: 'auto' };
|
|
165
|
+
if (!/^\d+$/.test(trimmed))
|
|
166
|
+
return { kind: 'invalid' };
|
|
167
|
+
const value = clampContextWindow(trimmed, null);
|
|
168
|
+
if (value === null)
|
|
169
|
+
return { kind: 'invalid' };
|
|
170
|
+
return { kind: 'value', value, clamped: value !== Number.parseInt(trimmed, 10) };
|
|
171
|
+
}
|
|
151
172
|
/**
|
|
152
173
|
* Entries kept in `ViewState` at all (tui-render-performance L1 / R5).
|
|
153
174
|
*
|
|
@@ -457,6 +478,10 @@ export const DEFAULT_TEAM_CONFIG = {
|
|
|
457
478
|
overseer: true,
|
|
458
479
|
// 0 = the structural cadence default (300 s). See the field docstring.
|
|
459
480
|
overseerIntervalMs: 0,
|
|
481
|
+
// The parity default: children run the lead's own model until the user
|
|
482
|
+
// says otherwise (AC-1 - an upgraded config behaves exactly as before).
|
|
483
|
+
model: 'main',
|
|
484
|
+
modelProfileId: null,
|
|
460
485
|
};
|
|
461
486
|
const SUBAGENTS_RANGE = { min: 1, max: HARD_MAX_SUBAGENTS };
|
|
462
487
|
const CONCURRENT_RANGE = { min: 1, max: HARD_MAX_SUBAGENTS };
|
|
@@ -490,8 +515,57 @@ export function clampTeamConfig(raw) {
|
|
|
490
515
|
maxTurnsPerSubagent: clampIntAllowingZero(src.maxTurnsPerSubagent, DEFAULT_TEAM_CONFIG.maxTurnsPerSubagent, TURNS_RANGE),
|
|
491
516
|
overseer: bool(src.overseer, DEFAULT_TEAM_CONFIG.overseer),
|
|
492
517
|
overseerIntervalMs: clampIntAllowingZero(src.overseerIntervalMs, DEFAULT_TEAM_CONFIG.overseerIntervalMs, OVERSEER_INTERVAL_RANGE),
|
|
518
|
+
// typo-changes-nothing, the discipline every enum clamp here follows: a
|
|
519
|
+
// hand-edited `model: 'fats'` resolves to 'main' (the parity default)
|
|
520
|
+
// rather than invalidating the file or refusing to launch. A bad
|
|
521
|
+
// `modelProfileId` (wrong type, or over the 128-codepoint budget
|
|
522
|
+
// `model-profiles.ts::validText` defines) is nulled the same way;
|
|
523
|
+
// reference EXISTENCE stays a resolution-time concern (D-4).
|
|
524
|
+
model: src.model === 'fast' || src.model === 'profile' ? src.model : 'main',
|
|
525
|
+
modelProfileId: typeof src.modelProfileId === 'string' && src.modelProfileId.length > 0
|
|
526
|
+
&& [...src.modelProfileId].length <= 128
|
|
527
|
+
? src.modelProfileId
|
|
528
|
+
: null,
|
|
493
529
|
};
|
|
494
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* The team ceilings cli <= 0.6.16 (7a6bf5630) persisted as its OWN defaults:
|
|
533
|
+
* every user who touched team mode back then still carries this exact triple
|
|
534
|
+
* in config.json, because `resolveTeamConfig` merges file values verbatim and
|
|
535
|
+
* no migration ever rewrote them. They are the kill ceilings behind F-8's
|
|
536
|
+
* "first block all failed" dispatches, so loading rewrites them to 0 (= no
|
|
537
|
+
* ceiling, the shipped default since 2ff15a9c3).
|
|
538
|
+
*
|
|
539
|
+
* EXACT-EQUALITY ONLY: a value that differs was a deliberate user choice and
|
|
540
|
+
* survives untouched. The price of that test is that a user who hand-types
|
|
541
|
+
* 300000 today is migrated to 0 too - the accepted trade, and the safe
|
|
542
|
+
* direction (no ceiling, not a cliff).
|
|
543
|
+
*/
|
|
544
|
+
export const LEGACY_TEAM_CEILING_DEFAULTS = {
|
|
545
|
+
subagentTimeoutMs: 300_000,
|
|
546
|
+
dispatchTimeoutMs: 900_000,
|
|
547
|
+
maxTurnsPerSubagent: 24,
|
|
548
|
+
};
|
|
549
|
+
/**
|
|
550
|
+
* The `{ key: 0 }` patch for legacy-default-equal ceilings, or `null` when
|
|
551
|
+
* nothing matches. Pure on purpose: `loadConfig` decides WHERE the patch is
|
|
552
|
+
* applied and persisted; this function only answers WHICH keys match, so the
|
|
553
|
+
* equality test lives beside the constants it reads.
|
|
554
|
+
*/
|
|
555
|
+
export function legacyTeamCeilingPatch(raw) {
|
|
556
|
+
if (!raw || typeof raw !== 'object')
|
|
557
|
+
return null;
|
|
558
|
+
const src = raw;
|
|
559
|
+
const patch = {};
|
|
560
|
+
let matched = false;
|
|
561
|
+
for (const key of Object.keys(LEGACY_TEAM_CEILING_DEFAULTS)) {
|
|
562
|
+
if (src[key] === LEGACY_TEAM_CEILING_DEFAULTS[key]) {
|
|
563
|
+
patch[key] = 0;
|
|
564
|
+
matched = true;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return matched ? patch : null;
|
|
568
|
+
}
|
|
495
569
|
export const DEFAULT_TODO_CONFIG = {
|
|
496
570
|
enabled: true,
|
|
497
571
|
panel: true,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `resolveSubagentModel` — THE single authority on which model a team child
|
|
3
|
+
* runs (subagent-model-and-question-controls §3.3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* EVERY CONSUMER READS THE CHILD'S MODEL THROUGH THIS FUNCTION AND NOWHERE
|
|
8
|
+
* ELSE — `createSubagent`, the `task` report header, the settings root row and
|
|
9
|
+
* `/team model` — following the `resolveFastTier` precedent one directory over:
|
|
10
|
+
* four call sites each recomputing "which model do children run on?" from raw
|
|
11
|
+
* config keys is exactly how the report ends up naming a model the children
|
|
12
|
+
* silently fell back from.
|
|
13
|
+
*
|
|
14
|
+
* IT IS RE-RUN ON EVERY DISPATCH AND EVERY RENDER, NOT ONCE (rule 8 / RV-3 of
|
|
15
|
+
* the fast tier, inherited here): `team.model`, the fast tier and the profile
|
|
16
|
+
* library are all LIVE keys a settings save, `/fast off` or `/team model` can
|
|
17
|
+
* rewrite mid-session. A resolution captured at construction would be a
|
|
18
|
+
* resolution describing a session that no longer exists.
|
|
19
|
+
*
|
|
20
|
+
* A CHILD IS NEVER LEFT WITHOUT A MODEL (D-4). Every failure mode — fast tier
|
|
21
|
+
* off, profile deleted, profile key missing — degrades to the MAIN connection
|
|
22
|
+
* with a `fallbackReason`, so a dispatch cannot fail because its model setting
|
|
23
|
+
* went stale. The reason is disclosed, never swallowed: the report header and
|
|
24
|
+
* the settings summary line both render it.
|
|
25
|
+
*/
|
|
26
|
+
import type { ModelRef, ThinkingLevel } from '@aragon-agent/core';
|
|
27
|
+
import type { ModelProfile, ModelRole } from '../config/model-profiles.js';
|
|
28
|
+
import type { CliConfig } from '../config/schema.js';
|
|
29
|
+
/** Which model children run on. `main` is the default and the historical behavior. */
|
|
30
|
+
export type SubagentModelMode = 'main' | 'fast' | 'profile';
|
|
31
|
+
/** Why a non-main request degraded to the main model (D-4 disclosure). */
|
|
32
|
+
export type SubagentFallbackReason =
|
|
33
|
+
/** `team.model='fast'` but `resolveFastTier` is not ok (off, unconfigured, no key). */
|
|
34
|
+
'fast_unavailable'
|
|
35
|
+
/** `modelProfileId` is absent from the library (deleted, or never existed). */
|
|
36
|
+
| 'profile_missing'
|
|
37
|
+
/** The entry exists but carries no key of its own and the shared layer has none. */
|
|
38
|
+
| 'profile_no_key';
|
|
39
|
+
/** In-memory resolution; recomputed per dispatch/render, never persisted (§6.3). */
|
|
40
|
+
export interface SubagentModelResolution {
|
|
41
|
+
/** The user's requested mode, verbatim from config (UI highlight reads this). */
|
|
42
|
+
mode: SubagentModelMode;
|
|
43
|
+
/** The mode actually in effect; `'main'` whenever a fallback fired. */
|
|
44
|
+
effective: SubagentModelMode;
|
|
45
|
+
/** The connection children will use. On fallback, exactly the main connection. */
|
|
46
|
+
ref: ModelRef;
|
|
47
|
+
/** main/profile take `config.thinkingLevel`; fast takes `config.fast.thinkingLevel`. */
|
|
48
|
+
thinkingLevel: ThinkingLevel;
|
|
49
|
+
/** The role controller key resolution uses: main/profile `'main'`, fast `'fast'`. */
|
|
50
|
+
keyRole: ModelRole;
|
|
51
|
+
/** The profile entry a `profile` request hit, dedicated key included; else null. */
|
|
52
|
+
profile: ModelProfile | null;
|
|
53
|
+
/** Present only when `effective !== mode`. */
|
|
54
|
+
fallbackReason?: SubagentFallbackReason;
|
|
55
|
+
}
|
|
56
|
+
/** The `AgentController.hasApiKey` shape, injected so there is one key resolver. */
|
|
57
|
+
export type HasApiKey = (providerId: string, role?: ModelRole) => boolean;
|
|
58
|
+
/** The lead-layer key resolver `resolveSubagentKey` falls back to. */
|
|
59
|
+
export type SharedKeyResolver = (providerId: string, role: ModelRole) => string | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the model every child of the NEXT dispatch will run on.
|
|
62
|
+
*
|
|
63
|
+
* Pure, synchronous, no IO — testable as a matrix over config fixtures.
|
|
64
|
+
*
|
|
65
|
+
* @param config The live effective config. Read fresh; never snapshot it.
|
|
66
|
+
* @param hasKey `AgentController.hasApiKey`, which already resolves the
|
|
67
|
+
* config-file key, the provider environment variable and the one-shot
|
|
68
|
+
* `--api-key` override. Injected rather than reimplemented for the reason
|
|
69
|
+
* `resolveFastTier` records: a second key resolver is a second answer to
|
|
70
|
+
* "why does it say no key?".
|
|
71
|
+
*/
|
|
72
|
+
export declare function resolveSubagentModel(config: CliConfig, hasKey: HasApiKey): SubagentModelResolution;
|
|
73
|
+
/**
|
|
74
|
+
* The key a CHILD resolves for a provider, profile-aware (§3.3 / I-4).
|
|
75
|
+
*
|
|
76
|
+
* A profile's dedicated key is visible ONLY here: it never reaches env,
|
|
77
|
+
* `config.apiKeys` or a log line. While the resolution's profile is bound and
|
|
78
|
+
* the provider matches it, the entry key takes priority over the shared layer;
|
|
79
|
+
* every other provider (and every non-profile mode) resolves through the
|
|
80
|
+
* lead's own resolver at `resolution.keyRole` — which is what keeps a `fast`
|
|
81
|
+
* child entitled to the fast-role key and a `main` child to the main one.
|
|
82
|
+
*
|
|
83
|
+
* @param config The live config (carried for parity with the resolver pair).
|
|
84
|
+
* @param resolution A fresh `resolveSubagentModel` output.
|
|
85
|
+
* @param providerId The provider the child needs a key for.
|
|
86
|
+
* @param shared The lead-layer resolver (`controller.resolveKey`).
|
|
87
|
+
*/
|
|
88
|
+
export declare function resolveSubagentKey(config: CliConfig, resolution: SubagentModelResolution, providerId: string, shared: SharedKeyResolver): string | undefined;
|