@aragon-agent/cli 0.6.12 → 0.6.13

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.
Files changed (101) hide show
  1. package/README.md +81 -15
  2. package/dist/agent/controller.d.ts +29 -1
  3. package/dist/agent/controller.js +123 -22
  4. package/dist/agent/model-profile-settings.d.ts +31 -0
  5. package/dist/agent/model-profile-settings.js +199 -0
  6. package/dist/agent/queued-messages.d.ts +12 -0
  7. package/dist/agent/queued-messages.js +18 -0
  8. package/dist/agent/reducer.d.ts +13 -1
  9. package/dist/agent/reducer.js +51 -12
  10. package/dist/cli.js +15 -1
  11. package/dist/commands/builtins.js +66 -39
  12. package/dist/commands/queue-command.d.ts +3 -0
  13. package/dist/commands/queue-command.js +5 -0
  14. package/dist/commands/registry.d.ts +3 -0
  15. package/dist/compaction/child.d.ts +4 -2
  16. package/dist/compaction/child.js +4 -1
  17. package/dist/compaction/compactor.d.ts +7 -2
  18. package/dist/compaction/compactor.js +13 -11
  19. package/dist/compaction/context-command.js +5 -0
  20. package/dist/compaction/wiring.d.ts +4 -3
  21. package/dist/compaction/wiring.js +6 -1
  22. package/dist/config/cli-commands.d.ts +2 -0
  23. package/dist/config/cli-commands.js +16 -4
  24. package/dist/config/load.d.ts +2 -1
  25. package/dist/config/load.js +15 -8
  26. package/dist/config/model-profile-resolution.d.ts +20 -0
  27. package/dist/config/model-profile-resolution.js +81 -0
  28. package/dist/config/model-profile-store.d.ts +67 -0
  29. package/dist/config/model-profile-store.js +153 -0
  30. package/dist/config/model-profiles.d.ts +44 -0
  31. package/dist/config/model-profiles.js +121 -0
  32. package/dist/config/schema.d.ts +9 -0
  33. package/dist/config/store.d.ts +11 -1
  34. package/dist/config/store.js +43 -27
  35. package/dist/fast/resolve.d.ts +2 -1
  36. package/dist/fast/resolve.js +6 -4
  37. package/dist/fast/wiring.d.ts +5 -3
  38. package/dist/fast/wiring.js +8 -4
  39. package/dist/input/stdin-filter.js +3 -2
  40. package/dist/logging/install.js +4 -2
  41. package/dist/logging/secret-registry.d.ts +2 -0
  42. package/dist/logging/secret-registry.js +18 -0
  43. package/dist/team/runtime.d.ts +2 -1
  44. package/dist/team/subagent.d.ts +3 -1
  45. package/dist/team/subagent.js +3 -1
  46. package/dist/ui/ActivityLine.d.ts +6 -70
  47. package/dist/ui/ActivityLine.js +12 -34
  48. package/dist/ui/App.d.ts +1 -0
  49. package/dist/ui/App.js +113 -205
  50. package/dist/ui/BottomStatusRow.d.ts +6 -86
  51. package/dist/ui/BottomStatusRow.js +11 -26
  52. package/dist/ui/Composer.d.ts +16 -83
  53. package/dist/ui/Composer.js +9 -100
  54. package/dist/ui/PromptInput.d.ts +4 -1
  55. package/dist/ui/PromptInput.js +41 -34
  56. package/dist/ui/QueuePanel.d.ts +8 -0
  57. package/dist/ui/QueuePanel.js +9 -0
  58. package/dist/ui/QueueStatusRow.d.ts +2 -0
  59. package/dist/ui/QueueStatusRow.js +1 -1
  60. package/dist/ui/StatusBar.d.ts +13 -149
  61. package/dist/ui/StatusBar.js +27 -142
  62. package/dist/ui/clipboard-task.d.ts +29 -0
  63. package/dist/ui/clipboard-task.js +66 -0
  64. package/dist/ui/clipboard.d.ts +29 -50
  65. package/dist/ui/clipboard.js +141 -75
  66. package/dist/ui/composer-limits.d.ts +3 -4
  67. package/dist/ui/composer-limits.js +4 -9
  68. package/dist/ui/composer-rows.d.ts +1 -1
  69. package/dist/ui/composer-rows.js +24 -137
  70. package/dist/ui/editor-navigation.d.ts +45 -0
  71. package/dist/ui/editor-navigation.js +85 -0
  72. package/dist/ui/editor-reducer.d.ts +4 -0
  73. package/dist/ui/editor-reducer.js +29 -15
  74. package/dist/ui/interaction-copy.d.ts +77 -0
  75. package/dist/ui/interaction-copy.js +40 -0
  76. package/dist/ui/interaction-hints.d.ts +18 -0
  77. package/dist/ui/interaction-hints.js +92 -0
  78. package/dist/ui/layout/budget.js +1 -1
  79. package/dist/ui/layout/queue-layout.d.ts +19 -0
  80. package/dist/ui/layout/queue-layout.js +26 -0
  81. package/dist/ui/layout/status-layout.d.ts +55 -0
  82. package/dist/ui/layout/status-layout.js +109 -0
  83. package/dist/ui/layout/todo-layout.d.ts +1 -0
  84. package/dist/ui/layout/todo-layout.js +1 -1
  85. package/dist/ui/overlays/HelpOverlay.js +8 -8
  86. package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
  87. package/dist/ui/overlays/ModelProfileEditor.js +103 -0
  88. package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
  89. package/dist/ui/overlays/ModelProfilePicker.js +167 -0
  90. package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
  91. package/dist/ui/overlays/QueueOverlay.js +79 -0
  92. package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
  93. package/dist/ui/overlays/SettingsScreen.js +347 -3
  94. package/dist/ui/overlays/model-profile-state.d.ts +74 -0
  95. package/dist/ui/overlays/model-profile-state.js +119 -0
  96. package/dist/ui/run-status-row.d.ts +7 -19
  97. package/dist/ui/run-status-row.js +2 -18
  98. package/dist/ui/selection/selection-controller.d.ts +2 -2
  99. package/dist/ui/selection/selection-controller.js +1 -0
  100. package/dist/ui/use-wheel-routing.js +2 -1
  101. package/package.json +2 -2
@@ -4,6 +4,7 @@
4
4
  * /resume /copy /terminal-setup /exit (/quit), plus the /team, /fast and
5
5
  * /todo subsystems.
6
6
  */
7
+ import { modelSettingsResultMessage } from '../agent/model-profile-settings.js';
7
8
  import { existsSync, statSync } from 'node:fs';
8
9
  import { isAbsolute, resolve } from 'node:path';
9
10
  import { resolveOutputTokens } from '@aragon-agent/core';
@@ -13,10 +14,9 @@ import { mergePendingEntries } from '../agent/queued-messages.js';
13
14
  import { TODO_FOLLOW_LIMITS } from '../todo/limits.js';
14
15
  import { buildContinuationMessage } from '../todo/follow-through.js';
15
16
  import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
16
- import { loadConfig } from '../config/load.js';
17
17
  import { loadSession, resolveSessionPath, saveSession } from '../session/persist.js';
18
18
  import { getLogger } from '../logging/logger.js';
19
- import { copyText } from '../ui/clipboard.js';
19
+ import { registerQueueCommand } from './queue-command.js';
20
20
  import { perfCommand } from './perf.js';
21
21
  import { runCompactCommand } from '../compaction/command.js';
22
22
  import { runContextCommand } from '../compaction/context-command.js';
@@ -371,8 +371,16 @@ const COMMANDS = [
371
371
  return;
372
372
  }
373
373
  const persist = (patch) => {
374
- controller.setFastConfig(patch);
375
- ctx.persistConfig({ fast: patch });
374
+ const connection = ['provider', 'model', 'baseUrl'].some((key) => key in patch);
375
+ if (!connection) {
376
+ controller.setFastConfig(patch);
377
+ ctx.persistConfig({ fast: patch });
378
+ return true;
379
+ }
380
+ const result = controller.saveModelSettingsPatch({ fast: patch });
381
+ if (!result.ok)
382
+ ctx.notify('warn', modelSettingsResultMessage(result));
383
+ return result.ok;
376
384
  };
377
385
  if (!verb || verb === 'status') {
378
386
  ctx.notify('info', formatFastStatus(controller));
@@ -380,7 +388,8 @@ const COMMANDS = [
380
388
  }
381
389
  if (verb === 'on' || verb === 'off') {
382
390
  const enabled = verb === 'on';
383
- persist({ enabled });
391
+ if (!persist({ enabled }))
392
+ return;
384
393
  if (!controller.isFastRegistered()) {
385
394
  // The honest branch, and the whole point of splitting by what was
386
395
  // decided at construction (§3.3). It is one sentence, and it keeps
@@ -414,11 +423,13 @@ const COMMANDS = [
414
423
  ctx.notify('warn', `Unknown provider "${provider}". Choose: ${ADAPTER_PROVIDERS.join(', ')}.`);
415
424
  return;
416
425
  }
417
- persist({ provider, model });
426
+ if (!persist({ provider, model }))
427
+ return;
418
428
  ctx.toast('success', `Fast model set to ${provider}:${model}.`);
419
429
  return;
420
430
  }
421
- persist({ model: raw });
431
+ if (!persist({ model: raw }))
432
+ return;
422
433
  ctx.toast('success', `Fast model set to ${raw}.`);
423
434
  return;
424
435
  }
@@ -432,7 +443,8 @@ const COMMANDS = [
432
443
  ctx.notify('warn', `Unknown provider "${raw}". Choose: ${ADAPTER_PROVIDERS.join(', ')}.`);
433
444
  return;
434
445
  }
435
- persist({ provider: raw });
446
+ if (!persist({ provider: raw }))
447
+ return;
436
448
  ctx.toast('success', `Fast provider set to ${raw}.`);
437
449
  return;
438
450
  }
@@ -441,18 +453,20 @@ const COMMANDS = [
441
453
  // (R-13): the user is allowed to run both tiers on one model, and the
442
454
  // review still costs what it costs.
443
455
  const cfg = controller.getConfig();
444
- persist({
456
+ if (!persist({
445
457
  provider: cfg.provider,
446
458
  model: cfg.model,
447
459
  baseUrl: cfg.baseUrl ?? '',
448
- });
460
+ }))
461
+ return;
449
462
  ctx.toast('success', `Fast tier set to ${cfg.provider}:${cfg.model} (same as main).`);
450
463
  return;
451
464
  }
452
465
  if (verb === 'review') {
453
466
  const raw = (value ?? '').trim().toLowerCase();
454
467
  if (raw === 'off') {
455
- persist({ review: false });
468
+ if (!persist({ review: false }))
469
+ return;
456
470
  ctx.toast('success', 'Fast review off.');
457
471
  return;
458
472
  }
@@ -508,7 +522,8 @@ const COMMANDS = [
508
522
  ctx.notify('warn', 'Usage: /fast delegate <on|off>');
509
523
  return;
510
524
  }
511
- persist({ delegate: raw === 'on' });
525
+ if (!persist({ delegate: raw === 'on' }))
526
+ return;
512
527
  ctx.toast('success', `Fast delegation ${raw}.`);
513
528
  return;
514
529
  }
@@ -891,6 +906,8 @@ const COMMANDS = [
891
906
  ctx.controller.clearAllQueues();
892
907
  ctx.dispatch({ type: 'resetConversation' });
893
908
  ctx.toast('info', 'Started a new conversation.');
909
+ if (ctx.state.pendingSteering.length)
910
+ ctx.notify('warn', `\u5df2\u53d6\u6d88 ${ctx.state.pendingSteering.length} \u6761\u672a\u63a5\u6536\u6d88\u606f`);
894
911
  },
895
912
  },
896
913
  {
@@ -941,15 +958,15 @@ const COMMANDS = [
941
958
  name: 'resume',
942
959
  description: 'Load a saved session',
943
960
  run: (ctx) => {
944
- if (ctx.controller.isRunning()) {
945
- ctx.notify('warn', 'Cannot resume while the agent is running. Wait for it to stop.');
961
+ if (ctx.controller.isRunning() || ctx.controller.isModelSettingsBusy?.()) {
962
+ ctx.notify('warn', 'Cannot resume while running work, team, review, or compaction is busy.');
946
963
  return;
947
964
  }
948
965
  try {
949
966
  const path = resolveSessionPath(ctx.args, ctx.controller.getCwd());
950
967
  const session = loadSession(path);
951
- if (ctx.controller.isRunning()) {
952
- ctx.notify('warn', 'Cannot resume while the agent is running. Wait for it to stop.');
968
+ if (ctx.controller.isRunning() || ctx.controller.isModelSettingsBusy?.()) {
969
+ ctx.notify('warn', 'Cannot resume while running work, team, review, or compaction is busy.');
953
970
  return;
954
971
  }
955
972
  // Loading has validated and normalized every external value. Commit
@@ -963,6 +980,8 @@ const COMMANDS = [
963
980
  ctx.controller.setModel(session.model.providerId, session.model.modelId, session.model.baseUrl);
964
981
  }
965
982
  ctx.notify('info', `Resumed session from ${path}`);
983
+ if (ctx.state.pendingSteering.length)
984
+ ctx.notify('warn', `\u5df2\u53d6\u6d88 ${ctx.state.pendingSteering.length} \u6761\u672a\u63a5\u6536\u6d88\u606f`);
966
985
  }
967
986
  catch (err) {
968
987
  ctx.notify('error', `Resume failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -972,7 +991,7 @@ const COMMANDS = [
972
991
  {
973
992
  name: 'copy',
974
993
  description: 'Copy the last answer to the clipboard',
975
- run: (ctx) => {
994
+ run: async (ctx) => {
976
995
  const last = [...ctx.state.entries].reverse().find((e) => e.kind === 'assistant');
977
996
  if (!last || last.kind !== 'assistant' || last.text.trim().length === 0) {
978
997
  ctx.notify('warn', 'No assistant message to copy.');
@@ -984,15 +1003,11 @@ const COMMANDS = [
984
1003
  // which the local `pbcopy` never could.
985
1004
  //
986
1005
  // `ctx.writeForeign` and not `stdout.write` — see the field's comment.
987
- const via = copyText(last.text, ctx.writeForeign);
988
- if (via === 'none') {
989
- ctx.toast('warn', 'Clipboard not available.');
1006
+ if (!ctx.requestCopy) {
1007
+ ctx.notify('warn', 'Clipboard coordinator unavailable.');
990
1008
  return;
991
1009
  }
992
- // NAMES THE MECHANISM RATHER THAN CLAIMING SUCCESS: neither path is
993
- // detectable, and tmux without `set -g set-clipboard on` swallows OSC 52
994
- // silently (R-7).
995
- ctx.toast('success', via === 'osc52' ? 'Sent to the terminal clipboard.' : 'Sent to the system clipboard.');
1010
+ await ctx.requestCopy({ text: last.text, lines: last.text.split('\n').length });
996
1011
  },
997
1012
  },
998
1013
  {
@@ -1053,20 +1068,24 @@ const COMMANDS = [
1053
1068
  const lines = [
1054
1069
  'Shift+Enter newline - terminal setup',
1055
1070
  '',
1056
- 'Ctrl+J and Alt+Enter insert a newline in every terminal, with no',
1057
- 'setup. To get a dedicated Shift+Enter key, make the terminal send',
1071
+ 'Ctrl+J inserts a newline when delivered; Alt+Enter may be intercepted.',
1072
+ 'To get a dedicated Shift+Enter key, make the terminal send',
1058
1073
  'the CSI-u sequence \\u001b[13;2u for it:',
1059
1074
  '',
1060
- ' Windows Terminal settings.json, actions array:',
1061
- ' { "keys": "shift+enter", "command": { "action": "sendInput",',
1062
- ' "input": "\\u001b[13;2u" } }',
1075
+ ' Windows Terminal settings.json, merge root-level arrays:',
1076
+ ' "copyOnSelect": false,',
1077
+ ' "actions": [{ "id": "User.AragonNewline", "command":',
1078
+ ' { "action": "sendInput", "input": "\\u001b[13;2u" } }],',
1079
+ ' "keybindings": [{ "keys": "shift+enter", "id": "User.AragonNewline" }]',
1063
1080
  ' PowerShell runs inside the terminal; configure this binding in',
1064
1081
  ' Windows Terminal, not in the PowerShell profile. If both keys',
1065
1082
  ' send CR, the application cannot distinguish Shift+Enter from Enter.',
1066
1083
  ' VS Code terminal keybindings.json:',
1067
1084
  ' { "key": "shift+enter",',
1068
1085
  ' "command": "workbench.action.terminal.sendSequence",',
1086
+ ' "when": "terminalFocus",',
1069
1087
  ' "args": { "text": "\\u001b[13;2u" } }',
1088
+ ' User settings: "terminal.integrated.copyOnSelection": false',
1070
1089
  ' iTerm2 Settings > Profiles > Keys > Key Mappings:',
1071
1090
  ' Shift+Enter -> Send Escape Sequence: [13;2u',
1072
1091
  ' kitty/WezTerm/foot native once keyboard enhancement is on',
@@ -1077,6 +1096,13 @@ const COMMANDS = [
1077
1096
  ' or Alt+Enter',
1078
1097
  '',
1079
1098
  'This command only prints advice; it changes no terminal modes.',
1099
+ 'Host selections (Shift+drag or /mouse off) belong to the terminal.',
1100
+ 'App selections wait for Ctrl+C; only a native tool exit confirms copying.',
1101
+ 'SSH uses OSC 52: the terminal may deny it; sent does not mean confirmed.',
1102
+ 'Without the stdin filter, Delete falls back to Backspace.',
1103
+ 'Without paste support, multiline paste cannot be guaranteed safe.',
1104
+ 'IME confirmation has no composition signal in PTY bytes. If it emits CR,',
1105
+ 'edit externally and use bracketed paste; this host combination needs validation.',
1080
1106
  ];
1081
1107
  ctx.notify('info', lines.join('\n'));
1082
1108
  },
@@ -1104,12 +1130,12 @@ const COMMANDS = [
1104
1130
  ctx.notify('warn', 'Cannot reload the config while a run is in progress.');
1105
1131
  return;
1106
1132
  }
1107
- const config = loadConfig({ cwd: ctx.controller.getCwd() });
1108
- ctx.controller.setModel(config.provider, config.model, config.baseUrl);
1109
- ctx.controller.setThinkingLevel(config.thinkingLevel);
1110
- ctx.controller.setMaxTokens(config.maxTokens);
1111
- ctx.controller.setTheme(config.theme);
1112
- getLogger().reconfigure(config.log);
1133
+ const result = ctx.controller.reloadModelSettings();
1134
+ if (!result.ok) {
1135
+ ctx.notify('warn', modelSettingsResultMessage(result));
1136
+ return;
1137
+ }
1138
+ const config = ctx.controller.getConfig();
1113
1139
  getLogger().info('config', 'reload', { provider: config.provider, model: config.model });
1114
1140
  ctx.toast('success', `Reloaded config: ${config.provider}:${config.model}.`);
1115
1141
  },
@@ -1219,6 +1245,7 @@ const DEFAULT_BG_LOG_ROWS = 40;
1219
1245
  export function registerBuiltinCommands(registry) {
1220
1246
  for (const command of COMMANDS)
1221
1247
  registry.register(command);
1248
+ registerQueueCommand(registry);
1222
1249
  }
1223
1250
  /**
1224
1251
  * Every built-in command name INCLUDING aliases (`quit`), for documentation and
@@ -1228,10 +1255,10 @@ export function registerBuiltinCommands(registry) {
1228
1255
  * `registry.get(name)` instead, because that is the only check that stays
1229
1256
  * correct when commands are registered dynamically (§7.1 / P1-2).
1230
1257
  */
1231
- export const BUILTIN_COMMAND_NAMES = COMMANDS.flatMap((c) => [
1232
- c.name,
1233
- ...(c.aliases ?? []),
1234
- ]).sort();
1258
+ export const BUILTIN_COMMAND_NAMES = ['queue', ...COMMANDS.flatMap((c) => [
1259
+ c.name,
1260
+ ...(c.aliases ?? []),
1261
+ ])].sort();
1235
1262
  // The `copyToClipboard` helper that used to live here moved into
1236
1263
  // `ui/clipboard.ts` (tui-selection-and-scroll-follow §4.4.5), unchanged, so that
1237
1264
  // `/copy` and the drag-select release share ONE clipboard path instead of two
@@ -0,0 +1,3 @@
1
+ import type { CommandRegistry } from './registry.js';
2
+ /** Read-only access to the live receipt-backed queue through the standard overlay port. */
3
+ export declare function registerQueueCommand(registry: CommandRegistry): void;
@@ -0,0 +1,5 @@
1
+ /** Read-only access to the live receipt-backed queue through the standard overlay port. */
2
+ export function registerQueueCommand(registry) {
3
+ registry.register({ name: 'queue', description: '\u67e5\u770b\u5f85\u5904\u7406\u6d88\u606f\u5168\u6587',
4
+ run: (ctx) => { ctx.setOverlay('queue'); } });
5
+ }
@@ -3,6 +3,7 @@
3
3
  * input is routed here instead of to the agent; `//` is the literal-`/` escape.
4
4
  */
5
5
  import type { SubmitMessageOptions } from '../agent/prompt-options.js';
6
+ import type { CopyRequest, CopyRequestResult } from '../ui/clipboard-task.js';
6
7
  import type { AgentController } from '../agent/controller.js';
7
8
  import type { AgentMode } from '../agent/agent-mode.js';
8
9
  import type { NoticeLevel, Overlay, ToastLevel, ViewAction, ViewState } from '../agent/reducer.js';
@@ -10,6 +11,8 @@ import type { PersistedConfig } from '../config/schema.js';
10
11
  import type { FollowThroughBudget } from '../todo/follow-through.js';
11
12
  import type { UpdateCommandPort } from '../update/types.js';
12
13
  export interface CommandContext {
14
+ /** App-owned shared lock and single feedback path for command and selection copies. */
15
+ requestCopy?: (request: CopyRequest) => Promise<CopyRequestResult>;
13
16
  /** Raw argument string after the command word (may be empty). */
14
17
  args: string;
15
18
  controller: AgentController;
@@ -28,13 +28,14 @@
28
28
  * lead's own history had been compacted. The team panel row and the dispatch
29
29
  * report are where a child's internals belong.
30
30
  */
31
+ import type { ModelRole } from '../config/model-profiles.js';
31
32
  import type { AssistantMessage, ContextManager, LLMRequest, Message, ModelInfo, ModelRef, TokenUsage } from '@aragon-agent/core';
32
33
  import type { CliConfig } from '../config/schema.js';
33
34
  export interface ChildCompactionDeps {
34
35
  /** Read LIVE on every call - see `childConfig` below. */
35
36
  getConfig: () => CliConfig;
36
- hasKey: (providerId: string) => boolean;
37
- getApiKey: (providerId: string) => string | undefined;
37
+ hasKey: (providerId: string, role?: ModelRole) => boolean;
38
+ getApiKey: (providerId: string, role?: ModelRole) => string | undefined;
38
39
  getModelInfoFor: (ref: Pick<ModelRef, 'providerId' | 'modelId'>) => ModelInfo;
39
40
  isPricedModel: (ref: Pick<ModelRef, 'providerId' | 'modelId'>) => boolean;
40
41
  /** The lead wiring's own transport, reused (D-10 stays true). */
@@ -50,6 +51,7 @@ export interface ChildCompactionDeps {
50
51
  * exactly on the cap.
51
52
  */
52
53
  export interface ChildContextManagerRequest {
54
+ role?: ModelRole;
53
55
  label: string;
54
56
  /**
55
57
  * The model the child actually runs on.
@@ -28,6 +28,7 @@
28
28
  * lead's own history had been compacted. The team panel row and the dispatch
29
29
  * report are where a child's internals belong.
30
30
  */
31
+ import { resolveFastTier } from '../fast/resolve.js';
31
32
  import { getLogger } from '../logging/logger.js';
32
33
  import { Compactor } from './compactor.js';
33
34
  import { COMPACTION_LIMITS } from './limits.js';
@@ -56,7 +57,7 @@ export function createChildContextManager(req, deps) {
56
57
  ...base,
57
58
  provider: req.model.providerId,
58
59
  model: req.model.modelId,
59
- ...(req.model.baseUrl !== undefined ? { baseUrl: req.model.baseUrl } : {}),
60
+ baseUrl: req.model.baseUrl,
60
61
  compaction: {
61
62
  ...base.compaction,
62
63
  keepRecentTurns: COMPACTION_LIMITS.childKeepRecentTurns,
@@ -71,6 +72,8 @@ export function createChildContextManager(req, deps) {
71
72
  };
72
73
  const compactor = new Compactor({
73
74
  getConfig: childConfig,
75
+ mainRole: req.role ?? 'main',
76
+ resolveFastCandidate: () => resolveFastTier(deps.getConfig(), deps.hasKey),
74
77
  hasKey: deps.hasKey,
75
78
  getApiKey: deps.getApiKey,
76
79
  getModelInfoFor: deps.getModelInfoFor,
@@ -9,16 +9,20 @@
9
9
  * keep, which model summarizes, what the prompt asks for, what happens when the
10
10
  * call fails, and when to stop spending.
11
11
  */
12
+ import type { ModelRole } from '../config/model-profiles.js';
12
13
  import { type Message, type ModelInfo, type ModelRef, type TokenUsage, type AssistantMessage, type CompactionContext, type CompactionOutcome, type CompactionProbe, type ContextManager, type LLMRequest } from '@aragon-agent/core';
13
14
  import type { CliConfig } from '../config/schema.js';
14
15
  import type { NoticeLevel } from '../agent/reducer.js';
16
+ import { resolveFastTier } from '../fast/resolve.js';
15
17
  import { ContextMeter } from './meter.js';
16
18
  import type { CompactionEvent, Pressure } from './types.js';
17
19
  export interface CompactorDeps {
20
+ mainRole?: ModelRole;
21
+ resolveFastCandidate?: () => ReturnType<typeof resolveFastTier>;
18
22
  /** Read LIVE on every call — the controller replaces the object on mutation. */
19
23
  getConfig: () => CliConfig;
20
- hasKey: (providerId: string) => boolean;
21
- getApiKey: (providerId: string) => string | undefined;
24
+ hasKey: (providerId: string, role?: ModelRole) => boolean;
25
+ getApiKey: (providerId: string, role?: ModelRole) => string | undefined;
22
26
  /**
23
27
  * The engine's live history and system prompt.
24
28
  *
@@ -87,6 +91,7 @@ export interface CompactorOptions {
87
91
  }
88
92
  /** The summarizer the next compaction would use. */
89
93
  export interface SummarizerChoice {
94
+ role: ModelRole;
90
95
  ref: ModelRef;
91
96
  /** Whether the fast tier supplied it, for `/compact status` and the card. */
92
97
  fromFastTier: boolean;
@@ -486,20 +486,22 @@ export class Compactor {
486
486
  const { summarizer } = args;
487
487
  if (!summarizer)
488
488
  return { kind: 'failed', reason: 'no_summarizer_model' };
489
- const first = await this.callOnce(summarizer.ref, args);
489
+ const first = await this.callOnce(summarizer, args);
490
490
  if (first.kind === 'ok' || first.kind === 'aborted')
491
491
  return first;
492
492
  // Rung 2 — retry, on the MAIN model when rung 1 was the fast tier.
493
- const mainRef = this.mainRef();
494
- const retryRef = summarizer.fromFastTier && mainRef.modelId !== summarizer.ref.modelId ? mainRef : summarizer.ref;
495
- this.log.warn('summarize_retry', { reason: first.reason, model: retryRef.modelId });
496
- const second = await this.callOnce(retryRef, args);
493
+ const retryChoice = summarizer.fromFastTier
494
+ ? { ref: this.mainRef(), role: this.deps.mainRole ?? 'main', fromFastTier: false }
495
+ : summarizer;
496
+ this.log.warn('summarize_retry', { reason: first.reason, model: retryChoice.ref.modelId });
497
+ const second = await this.callOnce(retryChoice, args);
497
498
  if (second.kind === 'ok' || second.kind === 'aborted')
498
499
  return second;
499
500
  return { kind: 'failed', reason: second.reason };
500
501
  }
501
- async callOnce(ref, args) {
502
- const apiKey = this.deps.getApiKey(ref.providerId);
502
+ async callOnce(choice, args) {
503
+ const { ref, role } = choice;
504
+ const apiKey = this.deps.getApiKey(ref.providerId, role);
503
505
  if (!apiKey)
504
506
  return { kind: 'failed', reason: 'no_api_key' };
505
507
  if (args.ctx.signal.aborted)
@@ -764,14 +766,14 @@ export class Compactor {
764
766
  resolveSummarizer() {
765
767
  const config = this.deps.getConfig();
766
768
  if (config.compaction.useFastTier) {
767
- const tier = resolveFastTier(config, this.deps.hasKey);
769
+ const tier = this.deps.resolveFastCandidate?.() ?? resolveFastTier(config, this.deps.hasKey);
768
770
  if (tier.ok)
769
- return { ref: tier.ref, fromFastTier: true };
771
+ return { ref: tier.ref, role: 'fast', fromFastTier: true };
770
772
  }
771
773
  const ref = this.mainRef();
772
- if (!this.deps.hasKey(ref.providerId))
774
+ if (!this.deps.hasKey(ref.providerId, this.deps.mainRole ?? 'main'))
773
775
  return null;
774
- return { ref, fromFastTier: false };
776
+ return { ref, role: this.deps.mainRole ?? 'main', fromFastTier: false };
775
777
  }
776
778
  /**
777
779
  * The meter this compactor reads, injected or private (R-1).
@@ -45,6 +45,11 @@ export function formatContextReport(ctx) {
45
45
  // one line and answers the same question.
46
46
  lines.push(' includes subagent, fast-tier and compaction spend,');
47
47
  lines.push(' not just this conversation');
48
+ lines.push(' Scope lead occupancy is used / limit in tokens; session input includes caches.');
49
+ lines.push(' Estimate USD session cost, including restored usage; missing prices count as 0.');
50
+ lines.push(' Zero estimated cost does not guarantee free usage or match the bill.');
51
+ lines.push(' Run average booked output-token delta / elapsed time, not instantaneous generation.');
52
+ lines.push(' May include team, fast-tier and compaction output booked during this run.');
48
53
  return lines.join('\n');
49
54
  }
50
55
  /**
@@ -35,6 +35,7 @@
35
35
  * guard is now unreachable for declines and stays for the abort race it was
36
36
  * written for.
37
37
  */
38
+ import type { ModelRole } from '../config/model-profiles.js';
38
39
  import { type AgentEvent, type ContextManager, type Message, type ModelInfo, type ModelRef, type ProviderRegistry, type ProviderRegistryOptions, type TokenUsage } from '@aragon-agent/core';
39
40
  import type { CliConfig } from '../config/schema.js';
40
41
  import type { NoticeLevel } from '../agent/reducer.js';
@@ -44,8 +45,8 @@ import type { CompactionEventListener, CompactionSnapshot } from './types.js';
44
45
  export interface CompactionWiringDeps {
45
46
  /** Read LIVE on every call - the controller replaces the object on mutation. */
46
47
  getConfig: () => CliConfig;
47
- hasKey: (providerId: string) => boolean;
48
- getApiKey: (providerId: string) => string | undefined;
48
+ hasKey: (providerId: string, role?: ModelRole) => boolean;
49
+ getApiKey: (providerId: string, role?: ModelRole) => string | undefined;
49
50
  getModelInfoFor: (ref: Pick<ModelRef, 'providerId' | 'modelId'>) => ModelInfo;
50
51
  isPricedModel: (ref: Pick<ModelRef, 'providerId' | 'modelId'>) => boolean;
51
52
  /**
@@ -158,7 +159,7 @@ export declare class CompactionWiring {
158
159
  isEnabled(): boolean;
159
160
  setEnabled(enabled: boolean): void;
160
161
  /** Re-emit the snapshot after any config mutation, so the chip and the gauge move. */
161
- onConfigChanged(): void;
162
+ onConfigChanged(enabled?: boolean): void;
162
163
  abort(): void;
163
164
  /**
164
165
  * The host replaced or cleared the history (hardening §3.2.3, sites 3 and 4).
@@ -209,7 +209,12 @@ export class CompactionWiring {
209
209
  this.emit({ type: 'snapshot', snapshot: this.snapshot() });
210
210
  }
211
211
  /** Re-emit the snapshot after any config mutation, so the chip and the gauge move. */
212
- onConfigChanged() {
212
+ onConfigChanged(enabled) {
213
+ if (enabled !== undefined && enabled !== this.enabled) {
214
+ this.enabled = enabled;
215
+ if (enabled)
216
+ this.compactor.clearSelfDisable();
217
+ }
213
218
  this.emit({ type: 'snapshot', snapshot: this.snapshot() });
214
219
  }
215
220
  abort() {
@@ -168,6 +168,8 @@ export declare function applyCompactionConfigSet(key: string, value: string): Pa
168
168
  * later pastes a log into an issue.
169
169
  */
170
170
  export declare function warnIfRedactionDisabled(key: string, value: string): void;
171
+ /** Every secret is masked wherever it is printed, `--json` included (R-12). */
172
+ export declare function maskConfig(config: PersistedConfig): Record<string, unknown>;
171
173
  export declare function runConfigGet(key: string): void;
172
174
  export declare function runConfigList(opts?: {
173
175
  json?: boolean;
@@ -13,6 +13,7 @@ import process from 'node:process';
13
13
  import { getConfigBackupPath, getConfigPath, getHomeRoot, getLogsDir, getPromptHistoryPath, getUiStatePath, isHomeOverridden, } from './app-paths.js';
14
14
  import { getHomeMigrationBreadcrumbPath } from './migrate-home.js';
15
15
  import { loadPersistedConfig, updatePersistedConfig } from './store.js';
16
+ import { normalizeModelProfiles, validateModelProfiles } from './model-profiles.js';
16
17
  import { clampFastConfig, clampRetryConfig, clampCompactionConfig, parseThresholdInput, clampUpdateConfig, coercePositiveInt, maskSecret, DEFAULT_FAST_CONFIG, DEFAULT_RETRY_CONFIG, DEFAULT_TEAM_CONFIG, DEFAULT_COMPACTION_CONFIG, DEFAULT_UPDATE_CONFIG, } from './schema.js';
17
18
  import { clampLogLevel } from '../logging/levels.js';
18
19
  /** Config keys under `log.` that `aragon config set` accepts. */
@@ -386,12 +387,23 @@ export function warnIfRedactionDisabled(key, value) {
386
387
  // get / list
387
388
  // ---------------------------------------------------------------------------
388
389
  /** Every secret is masked wherever it is printed, `--json` included (R-12). */
389
- function maskConfig(config) {
390
+ export function maskConfig(config) {
390
391
  const apiKeys = {};
391
392
  for (const [provider, key] of Object.entries(config.apiKeys)) {
392
393
  apiKeys[provider] = maskSecret(key);
393
394
  }
394
- return { ...config, apiKeys };
395
+ const masked = { ...config, apiKeys };
396
+ if (config.modelProfiles !== undefined) {
397
+ if (validateModelProfiles(config.modelProfiles).length)
398
+ masked.modelProfiles = { invalid: true };
399
+ else {
400
+ const profiles = normalizeModelProfiles(config.modelProfiles);
401
+ masked.modelProfiles = { ...profiles, entries: profiles.entries.map((entry) => ({
402
+ ...entry, apiKey: entry.apiKey ? 'Configured' : 'Missing',
403
+ })) };
404
+ }
405
+ }
406
+ return masked;
395
407
  }
396
408
  /**
397
409
  * The human-readable label for a value whose raw form would mislead, or
@@ -503,8 +515,8 @@ export async function runConfigEdit() {
503
515
  JSON.parse(readFileSync(file, 'utf-8'));
504
516
  process.stdout.write(`${file} saved.\n`);
505
517
  }
506
- catch (err) {
507
- process.stderr.write(`${file} is not valid JSON (${err instanceof Error ? err.message : String(err)}).\n` +
518
+ catch {
519
+ process.stderr.write(`${file} is not valid JSON.\n` +
508
520
  `The previous version is still at ${backup}.\n`);
509
521
  process.exitCode = 1;
510
522
  }
@@ -6,6 +6,7 @@
6
6
  * legitimately long single tool run can never be watchdog-killed.
7
7
  */
8
8
  import { type CliConfig } from './schema.js';
9
+ import type { ModelRole } from './model-profiles.js';
9
10
  export interface CliFlags {
10
11
  provider?: string;
11
12
  model?: string;
@@ -245,4 +246,4 @@ export declare function loadConfig(flags?: CliFlags): CliConfig;
245
246
  * Build the `getApiKey(providerId)` resolver the core `Agent` consumes.
246
247
  * The one-shot `--api-key` override wins, but only for the active provider.
247
248
  */
248
- export declare function makeGetApiKey(config: CliConfig): (providerId: string) => string | undefined;
249
+ export declare function makeGetApiKey(config: CliConfig, role?: ModelRole): (providerId: string) => string | undefined;
@@ -9,6 +9,7 @@ import process from 'node:process';
9
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';
10
10
  import { loadDotenv, readEnvConfig, readEnvDisabledSkills } from './env.js';
11
11
  import { readConfigFile } from './store.js';
12
+ import { projectModelProfiles, resolveModelProfileState, resolveModelRoleKey } from './model-profile-resolution.js';
12
13
  import { getConfigPath } from './app-paths.js';
13
14
  import { setHistoryEnabled } from './prompt-history.js';
14
15
  import { setEntryRetain } from '../agent/entry-limits.js';
@@ -440,11 +441,17 @@ export function loadConfig(flags = {}) {
440
441
  // `.env` must be loaded before reading env overrides.
441
442
  loadDotenv(cwd);
442
443
  const read = readConfigFile();
443
- const file = read.config ?? {};
444
+ const projection = projectModelProfiles(read.config ?? {});
445
+ const file = projection.file;
444
446
  const env = readEnvConfig();
445
447
  if (read.parseError) {
446
448
  getLogger().error('config', 'config_parse_failed', { error: read.parseError });
447
449
  }
450
+ if (projection.issues.length) {
451
+ getLogger().warn('config', 'model_profiles_invalid', {
452
+ error: 'Invalid model profiles; using custom connections. Repair with aragon config edit.',
453
+ });
454
+ }
448
455
  const provider = pick(flags.provider, env.partial.provider, file.provider) ?? DEFAULT_CONFIG.provider;
449
456
  const model = pick(flags.model, env.partial.model, file.model) ?? DEFAULT_CONFIG.model;
450
457
  const baseUrl = pick(flags.baseUrl, env.partial.baseUrl ?? undefined, file.baseUrl ?? undefined);
@@ -543,6 +550,7 @@ export function loadConfig(flags = {}) {
543
550
  // it through the same module-singleton channel `setHistoryEnabled` uses.
544
551
  setEntryRetain(transcriptRetain);
545
552
  const config = {
553
+ modelProfiles: projection.issues.length ? undefined : file.modelProfiles,
546
554
  provider,
547
555
  model,
548
556
  baseUrl,
@@ -605,6 +613,10 @@ export function loadConfig(flags = {}) {
605
613
  unicode: caps.unicode,
606
614
  apiKeyOverride: flags.apiKey && flags.apiKey.trim().length > 0 ? flags.apiKey.trim() : undefined,
607
615
  };
616
+ config.modelProfileState = resolveModelProfileState(config, config.modelProfiles, projection.issues.length > 0);
617
+ if (config.apiKeyOverride && (config.modelProfiles?.mainId || config.modelProfiles?.fastId)) {
618
+ config.apiKeyOverrideTarget = { provider, baseUrl: baseUrl?.trim() || null };
619
+ }
608
620
  recordResolvedConfig(config);
609
621
  return config;
610
622
  }
@@ -646,11 +658,6 @@ function recordResolvedConfig(config) {
646
658
  * Build the `getApiKey(providerId)` resolver the core `Agent` consumes.
647
659
  * The one-shot `--api-key` override wins, but only for the active provider.
648
660
  */
649
- export function makeGetApiKey(config) {
650
- return (providerId) => {
651
- if (config.apiKeyOverride && providerId === config.provider) {
652
- return config.apiKeyOverride;
653
- }
654
- return config.apiKeys[providerId];
655
- };
661
+ export function makeGetApiKey(config, role = 'main') {
662
+ return (providerId) => resolveModelRoleKey({ config, role, providerId });
656
663
  }
@@ -0,0 +1,20 @@
1
+ import { type CliConfig, type PersistedConfig } from './schema.js';
2
+ import { type ModelProfilesConfig, type ModelProfilesRuntime, type ModelRole, type ProfileValidationIssue } from './model-profiles.js';
3
+ export interface ProfileProjection {
4
+ file: Partial<PersistedConfig>;
5
+ state: ModelProfilesRuntime;
6
+ issues: ProfileValidationIssue[];
7
+ }
8
+ export interface RoleKeyInput {
9
+ config: CliConfig;
10
+ role: ModelRole;
11
+ providerId: string;
12
+ }
13
+ /** Compare configured endpoint spelling strictly, with null and undefined equivalent. */
14
+ export declare function normalizeProfileBaseUrl(value: string | null | undefined): string | null;
15
+ /** Project a valid library onto a copy without rewriting legacy preferences. */
16
+ export declare function projectModelProfiles(file: Partial<PersistedConfig>): ProfileProjection;
17
+ /** Derive binding status from current connections, including session/model overrides. */
18
+ export declare function resolveModelProfileState(config: CliConfig, profiles?: ModelProfilesConfig | undefined, invalid?: boolean): ModelProfilesRuntime;
19
+ /** Resolve only credentials scoped to the requested role and actual connection target. */
20
+ export declare function resolveModelRoleKey(input: RoleKeyInput): string | undefined;