@aragon-agent/cli 0.6.11 → 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 (115) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/README.md +97 -20
  3. package/dist/agent/controller.d.ts +38 -3
  4. package/dist/agent/controller.js +151 -60
  5. package/dist/agent/model-profile-settings.d.ts +31 -0
  6. package/dist/agent/model-profile-settings.js +199 -0
  7. package/dist/agent/queued-messages.d.ts +20 -0
  8. package/dist/agent/queued-messages.js +46 -0
  9. package/dist/agent/reducer.d.ts +21 -2
  10. package/dist/agent/reducer.js +65 -37
  11. package/dist/cli.js +15 -1
  12. package/dist/commands/builtins.js +83 -42
  13. package/dist/commands/queue-command.d.ts +3 -0
  14. package/dist/commands/queue-command.js +5 -0
  15. package/dist/commands/registry.d.ts +3 -0
  16. package/dist/compaction/child.d.ts +4 -2
  17. package/dist/compaction/child.js +4 -1
  18. package/dist/compaction/compactor.d.ts +7 -2
  19. package/dist/compaction/compactor.js +13 -11
  20. package/dist/compaction/context-command.js +5 -0
  21. package/dist/compaction/wiring.d.ts +4 -3
  22. package/dist/compaction/wiring.js +6 -1
  23. package/dist/config/cli-commands.d.ts +2 -0
  24. package/dist/config/cli-commands.js +16 -4
  25. package/dist/config/load.d.ts +2 -1
  26. package/dist/config/load.js +15 -8
  27. package/dist/config/model-profile-resolution.d.ts +20 -0
  28. package/dist/config/model-profile-resolution.js +81 -0
  29. package/dist/config/model-profile-store.d.ts +67 -0
  30. package/dist/config/model-profile-store.js +153 -0
  31. package/dist/config/model-profiles.d.ts +44 -0
  32. package/dist/config/model-profiles.js +121 -0
  33. package/dist/config/schema.d.ts +9 -0
  34. package/dist/config/store.d.ts +11 -1
  35. package/dist/config/store.js +43 -27
  36. package/dist/fast/resolve.d.ts +2 -1
  37. package/dist/fast/resolve.js +6 -4
  38. package/dist/fast/wiring.d.ts +5 -3
  39. package/dist/fast/wiring.js +8 -4
  40. package/dist/input/enter-sequences.d.ts +11 -10
  41. package/dist/input/enter-sequences.js +29 -27
  42. package/dist/input/stdin-filter.js +93 -28
  43. package/dist/logging/install.d.ts +3 -0
  44. package/dist/logging/install.js +4 -2
  45. package/dist/logging/secret-registry.d.ts +2 -0
  46. package/dist/logging/secret-registry.js +18 -0
  47. package/dist/session/persist.d.ts +6 -7
  48. package/dist/session/persist.js +6 -9
  49. package/dist/session/store.d.ts +4 -1
  50. package/dist/session/validate-session.d.ts +3 -0
  51. package/dist/session/validate-session.js +207 -0
  52. package/dist/team/runtime.d.ts +2 -1
  53. package/dist/team/subagent.d.ts +3 -1
  54. package/dist/team/subagent.js +3 -1
  55. package/dist/ui/ActivityLine.d.ts +6 -70
  56. package/dist/ui/ActivityLine.js +12 -34
  57. package/dist/ui/App.d.ts +1 -0
  58. package/dist/ui/App.js +161 -226
  59. package/dist/ui/BottomStatusRow.d.ts +6 -86
  60. package/dist/ui/BottomStatusRow.js +11 -26
  61. package/dist/ui/Composer.d.ts +18 -84
  62. package/dist/ui/Composer.js +9 -100
  63. package/dist/ui/PromptInput.d.ts +6 -2
  64. package/dist/ui/PromptInput.js +91 -105
  65. package/dist/ui/QueuePanel.d.ts +8 -0
  66. package/dist/ui/QueuePanel.js +9 -0
  67. package/dist/ui/QueueStatusRow.d.ts +24 -0
  68. package/dist/ui/QueueStatusRow.js +51 -0
  69. package/dist/ui/StatusBar.d.ts +15 -149
  70. package/dist/ui/StatusBar.js +27 -112
  71. package/dist/ui/clipboard-task.d.ts +29 -0
  72. package/dist/ui/clipboard-task.js +66 -0
  73. package/dist/ui/clipboard.d.ts +29 -50
  74. package/dist/ui/clipboard.js +141 -75
  75. package/dist/ui/composer-input.d.ts +28 -0
  76. package/dist/ui/composer-input.js +46 -0
  77. package/dist/ui/composer-limits.d.ts +3 -4
  78. package/dist/ui/composer-limits.js +4 -9
  79. package/dist/ui/composer-rows.d.ts +1 -1
  80. package/dist/ui/composer-rows.js +24 -137
  81. package/dist/ui/editor-navigation.d.ts +45 -0
  82. package/dist/ui/editor-navigation.js +85 -0
  83. package/dist/ui/editor-reducer.d.ts +9 -0
  84. package/dist/ui/editor-reducer.js +31 -15
  85. package/dist/ui/enter-frames.d.ts +15 -31
  86. package/dist/ui/enter-frames.js +14 -33
  87. package/dist/ui/interaction-copy.d.ts +77 -0
  88. package/dist/ui/interaction-copy.js +40 -0
  89. package/dist/ui/interaction-hints.d.ts +18 -0
  90. package/dist/ui/interaction-hints.js +92 -0
  91. package/dist/ui/layout/budget.js +1 -1
  92. package/dist/ui/layout/queue-layout.d.ts +19 -0
  93. package/dist/ui/layout/queue-layout.js +26 -0
  94. package/dist/ui/layout/status-layout.d.ts +55 -0
  95. package/dist/ui/layout/status-layout.js +109 -0
  96. package/dist/ui/layout/todo-layout.d.ts +1 -0
  97. package/dist/ui/layout/todo-layout.js +1 -1
  98. package/dist/ui/overlays/HelpOverlay.js +11 -3
  99. package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
  100. package/dist/ui/overlays/ModelProfileEditor.js +103 -0
  101. package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
  102. package/dist/ui/overlays/ModelProfilePicker.js +167 -0
  103. package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
  104. package/dist/ui/overlays/QueueOverlay.js +79 -0
  105. package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
  106. package/dist/ui/overlays/SettingsScreen.js +347 -3
  107. package/dist/ui/overlays/model-profile-state.d.ts +74 -0
  108. package/dist/ui/overlays/model-profile-state.js +119 -0
  109. package/dist/ui/run-status-row.d.ts +7 -19
  110. package/dist/ui/run-status-row.js +2 -18
  111. package/dist/ui/selection/selection-controller.d.ts +2 -2
  112. package/dist/ui/selection/selection-controller.js +58 -20
  113. package/dist/ui/transcript-text.js +20 -12
  114. package/dist/ui/use-wheel-routing.js +2 -1
  115. package/package.json +2 -2
@@ -1,15 +1,8 @@
1
- /**
2
- * AgentController — builds the core `Agent`, wires the built-in toolset, and
3
- * exposes a small run/abort/steer surface plus the pre-flight validation that
4
- * turns the most common failure (no API key) into a guided action instead of a
5
- * blank screen (spec §3.3.1 / R2).
6
- *
7
- * The controller never inspects the resolved promise of `agent.prompt()` for
8
- * success — that promise resolves even on failure. Success/failure is derived
9
- * from the event stream by the reducer (TUI) and the headless writer.
10
- */
1
+ import { createModelSettingsDraft, saveModelSettings, reloadModelSettings, } from './model-profile-settings.js';
2
+ import { randomUUID } from 'node:crypto';
11
3
  import { Agent, initProviders, ModelRegistry, } from '@aragon-agent/core';
12
4
  import { clampCompactionConfig, clampFastConfig, clampRetryConfig, clampTeamConfig, clampTodoConfig, isAdapterProvider, toRetryPolicy, } from '../config/schema.js';
5
+ import { resolveModelProfileState } from '../config/model-profile-resolution.js';
13
6
  import { makeGetApiKey } from '../config/load.js';
14
7
  import { updatePersistedConfig } from '../config/store.js';
15
8
  import { createBuiltinTools } from '../tools/index.js';
@@ -203,6 +196,9 @@ export class AgentController {
203
196
  // no timer while it has no subscribers, so the extra object is a few scalars
204
197
  // and a `Set`.
205
198
  // -----------------------------------------------------------------------
199
+ settingsRevision = 0;
200
+ modelSettingsBlocked = false;
201
+ preparedSystemPrompt;
206
202
  contextMeter;
207
203
  // -----------------------------------------------------------------------
208
204
  // Background services (background-service-supervision §3.3 / §3.6)
@@ -252,8 +248,11 @@ export class AgentController {
252
248
  * Cleared at the next `agent_start`.
253
249
  */
254
250
  abortRequested = false;
255
- /** User messages queued since the last `turn_start` (§3.5.4a guard 2). */
256
- userSteerCount = 0;
251
+ /** Only exact acceptance receipts release user messages from reviewer protection. */
252
+ pendingUserSteering = new Set();
253
+ /** Session changes must not make a delayed receipt match a new message. */
254
+ steeringPrefix = randomUUID();
255
+ steeringSequence = 0;
257
256
  constructor(config, deps = {}) {
258
257
  this.config = config;
259
258
  this.cwd = config.cwd;
@@ -303,7 +302,7 @@ export class AgentController {
303
302
  // ONCE (C-2). `FastWiring` re-runs the same pure function in its own
304
303
  // constructor; the two agree by construction because they read the same
305
304
  // config through the same resolver.
306
- const fastTier = resolveFastTier(config, (id) => this.hasApiKey(id));
305
+ const fastTier = resolveFastTier(config, (id, role) => this.hasApiKey(id, role));
307
306
  const fastRegistered = fastTier.ok;
308
307
  // --- Team mode ---------------------------------------------------------
309
308
  this.teamRegistered = config.team.enabled;
@@ -327,11 +326,12 @@ export class AgentController {
327
326
  ...(this.config.baseUrl ? { baseUrl: this.config.baseUrl } : {}),
328
327
  },
329
328
  thinkingLevel: this.config.thinkingLevel,
329
+ role: 'main',
330
330
  },
331
331
  providerRegistry: this.providerRegistry,
332
332
  getCwd: () => this.cwd,
333
333
  getMode: () => this.effectiveMode,
334
- getApiKey: (id) => this.resolveKey(id),
334
+ getApiKey: (id, role) => this.resolveKey(id, role),
335
335
  // `skill_find` only, and the lead's always-on bodies. See the long
336
336
  // note in `subagent.ts` for why `skill` itself is withheld (D-16).
337
337
  ...(this.skillsEnabled
@@ -551,8 +551,8 @@ export class AgentController {
551
551
  this.compaction = config.compaction.enabled
552
552
  ? new CompactionWiring({
553
553
  getConfig: () => this.config,
554
- hasKey: (id) => this.hasApiKey(id),
555
- getApiKey: (id) => this.resolveKey(id),
554
+ hasKey: (id, role) => this.hasApiKey(id, role),
555
+ getApiKey: (id, role) => this.resolveKey(id, role),
556
556
  getModelInfoFor: (ref) => this.getModelInfoFor(ref),
557
557
  isPricedModel: (ref) => this.isPricedModel(ref),
558
558
  getMessages: () => this.agent.state.messages,
@@ -629,38 +629,31 @@ export class AgentController {
629
629
  this.fast = fastRegistered
630
630
  ? new FastWiring({
631
631
  getConfig: () => this.config,
632
- hasKey: (id) => this.hasApiKey(id),
633
- getApiKey: (id) => this.resolveKey(id),
632
+ hasKey: (id, role) => this.hasApiKey(id, role),
633
+ getApiKey: (id, role) => this.resolveKey(id, role),
634
634
  isPricedModel: (ref) => this.isPricedModel(ref),
635
635
  subscribe: (listener) => this.agent.subscribe(listener),
636
636
  complete: (providerId, request) => this.providerRegistry.complete(providerId, request),
637
- // THE REVIEWER GOES THROUGH THIS METHOD, NOT `agent.steer()` DIRECTLY,
638
- // so its own block is counted alongside the user's — which is what
639
- // `userSteerCount` would otherwise mis-report and what would make
640
- // guard 2 clear a queue it does not own (D-21).
641
- //
642
- // It is decremented back out below: the counter must answer "did the
643
- // USER queue anything", and the reviewer's own message is not that.
644
- steer: (text) => {
645
- this.steer(text);
646
- this.userSteerCount -= 1;
647
- },
637
+ // Reviewer advice shares skill activation, but cannot own a user ID.
638
+ steer: (text) => this.enqueueSteering(text, false),
648
639
  isRunning: () => this.isRunning(),
649
640
  isAbortRequested: () => this.abortRequested,
650
- userSteerCount: () => this.userSteerCount,
641
+ userSteerCount: () => this.pendingUserSteering.size,
651
642
  clearAllQueues: () => this.clearAllQueues(),
652
643
  notify: (level, text) => deps.notify?.(level, text),
653
644
  onPromptChanged: () => this.rebuildSystemPrompt(),
654
645
  })
655
646
  : null;
656
647
  // The two guards' bookkeeping, and the only reason this class subscribes to
657
- // its own agent. `turn_start` is where a user steer stops being pending;
648
+ // its own agent. A receipt is where a user steer stops being pending;
658
649
  // `agent_start` is where a requested abort stops being in force.
659
650
  this.agent.subscribe((event) => {
660
651
  if (event.type === 'agent_start')
661
652
  this.abortRequested = false;
662
- if (event.type === 'turn_start')
663
- this.userSteerCount = 0;
653
+ if (event.type === 'steering_accepted') {
654
+ for (const id of event.ids)
655
+ this.pendingUserSteering.delete(id);
656
+ }
664
657
  // The authoritative result arrives with this event, so from this instant
665
658
  // the tail is superseded and its slot must be released (§3.1.1). Clearing
666
659
  // HERE rather than on read is what keeps the store's size a function of
@@ -686,8 +679,8 @@ export class AgentController {
686
679
  if (problem)
687
680
  deps.notify?.('warn', problem);
688
681
  }
689
- resolveKey(providerId) {
690
- return makeGetApiKey(this.config)(providerId);
682
+ resolveKey(providerId, role = 'main') {
683
+ return makeGetApiKey(this.config, role)(providerId);
691
684
  }
692
685
  /**
693
686
  * The tool names registered right now — LAZY, and the laziness is required.
@@ -718,27 +711,30 @@ export class AgentController {
718
711
  // suddenly denies having any skills). Structure, not call ordering, is what
719
712
  // rules that out — so if you add a third caller, route it through here.
720
713
  // -----------------------------------------------------------------------
721
- composeSystemPrompt() {
714
+ composeSystemPrompt(config = this.config) {
715
+ const tier = resolveFastTier(config, (id, role) => !!makeGetApiKey(config, role)(id));
716
+ const fastAvailable = config === this.config ? this.fast?.available() === true
717
+ : this.fast?.isRegistered() && config.fast.enabled && tier.ok;
722
718
  return buildSystemPrompt({
723
719
  cwd: this.cwd,
724
720
  tools: this.tools,
725
721
  skillsBlock: this.skillsEnabled ? this.skills.catalogBlock() + this.skills.alwaysBlock() : '',
726
722
  agentMode: this.effectiveMode,
727
723
  planInteractive: this.planInteractive,
728
- planMaxAskRounds: this.config.planModeMaxAskRounds,
724
+ planMaxAskRounds: config.planModeMaxAskRounds,
729
725
  // BOTH flags, and both are required (§4.5). `teamRegistered` false means
730
726
  // there is no `task` tool to advertise; `teamEnabled` false means the user
731
727
  // turned it off for this session. Advertising in either case would
732
728
  // describe a capability the model does not have.
733
729
  teamBlock: this.teamRegistered && this.teamEnabled
734
730
  ? buildTeamBlock({
735
- maxSubagents: this.config.team.maxSubagents,
736
- maxConcurrent: this.config.team.maxConcurrent,
731
+ maxSubagents: config.team.maxSubagents,
732
+ maxConcurrent: config.team.maxConcurrent,
737
733
  // One cross-reference sentence, present only when the capability
738
734
  // is (§3.8). `this.fast` is null during the FIRST compose (the
739
735
  // wiring needs the Agent), which is why the constructor composes
740
736
  // again once it exists.
741
- fastDelegation: this.fast?.delegationAvailable() === true,
737
+ fastDelegation: !!fastAvailable && config.fast.delegate,
742
738
  })
743
739
  : '',
744
740
  // BOTH FLAGS, exactly as team mode uses both, and for the identical
@@ -748,18 +744,18 @@ export class AgentController {
748
744
  // model does not have.
749
745
  todoBlock: this.todoRegistered && this.todoEnabled
750
746
  ? buildTodoBlock({
751
- panelVisible: this.todoPanelCapable && this.config.todo.panel,
747
+ panelVisible: this.todoPanelCapable && config.todo.panel,
752
748
  })
753
749
  : '',
754
750
  // The SAME two flags one more time, plus the live resolution — all three
755
751
  // live inside `FastWiring.available()` (§3.3). `buildFastBlock` itself
756
752
  // returns `''` when neither capability is on, so a user who turned both
757
753
  // off pays nothing for the tier being technically live.
758
- fastBlock: this.fast && this.fast.available()
754
+ fastBlock: fastAvailable
759
755
  ? buildFastBlock({
760
- model: this.fast.fastRef()?.modelId ?? '',
761
- delegate: this.config.fast.delegate,
762
- review: this.config.fast.review,
756
+ model: tier.ok ? tier.ref.modelId : '',
757
+ delegate: config.fast.delegate,
758
+ review: config.fast.review,
763
759
  })
764
760
  : '',
765
761
  // ONE FLAG, unlike team/todo's two: there is no live on/off switch for
@@ -775,7 +771,7 @@ export class AgentController {
775
771
  });
776
772
  }
777
773
  rebuildSystemPrompt() {
778
- this.agent.setSystemPrompt(this.composeSystemPrompt());
774
+ this.agent.setSystemPrompt(this.preparedSystemPrompt ?? this.composeSystemPrompt());
779
775
  }
780
776
  /** Re-render the prompt from the current skill set and tell the UI. */
781
777
  refreshSkills() {
@@ -794,8 +790,10 @@ export class AgentController {
794
790
  * Read-only CLI-layer helper for the Header/Welcome key-status dot (P1-1);
795
791
  * does not touch `@aragon-agent/core`.
796
792
  */
797
- hasApiKey(provider) {
798
- const key = this.resolveKey(provider ?? this.config.provider);
793
+ hasApiKey(provider, role = 'main') {
794
+ const activeProvider = role === 'fast'
795
+ ? this.config.fast.provider || this.config.provider : this.config.provider;
796
+ const key = this.resolveKey(provider ?? activeProvider, role);
799
797
  return !!key && key.trim().length > 0;
800
798
  }
801
799
  /**
@@ -804,6 +802,7 @@ export class AgentController {
804
802
  * re-memoizes the palette (P1-1). Does not touch `@aragon-agent/core`.
805
803
  */
806
804
  setTheme(name) {
805
+ this.settingsRevision += 1;
807
806
  this.config = { ...this.config, theme: name };
808
807
  }
809
808
  /**
@@ -824,6 +823,9 @@ export class AgentController {
824
823
  // Pre-flight (R2): validate BEFORE entering the loop.
825
824
  // -----------------------------------------------------------------------
826
825
  preflight() {
826
+ if (this.modelSettingsBlocked)
827
+ return { ok: false, kind: 'config',
828
+ message: 'Settings were saved but not fully applied. Restart before sending requests.' };
827
829
  const provider = this.config.provider;
828
830
  if (!isAdapterProvider(provider)) {
829
831
  return {
@@ -858,6 +860,9 @@ export class AgentController {
858
860
  */
859
861
  async prompt(text, options = {}) {
860
862
  // Capture BEFORE stopping: synchronous abort listeners may cancel this request.
863
+ if (this.modelSettingsBlocked) {
864
+ return { status: 'not-started', reason: 'failed' };
865
+ }
861
866
  const request = ++this.startupSequence;
862
867
  try {
863
868
  if (this.agent.state.isRunning) {
@@ -1052,21 +1057,30 @@ export class AgentController {
1052
1057
  reapServicesSync() {
1053
1058
  this.procs.reapSync();
1054
1059
  }
1060
+ /** Queue automation/user guidance with the same protection as visible input. */
1055
1061
  steer(text) {
1056
- // Mid-run interjection: promote the queue, keep what is already in force
1057
- // (D-G3). Clearing here would make "ask a follow-up" the way around the
1058
- // ceiling; skipping the promotion would miss slash commands, which the TUI
1059
- // routes through `steer` whenever the agent happens to be running.
1062
+ this.enqueueSteering(text, true);
1063
+ }
1064
+ /** Return the opaque ID used by the view to match Core's acceptance receipt. */
1065
+ queueUserMessage(text) {
1066
+ return this.enqueueSteering(text, true);
1067
+ }
1068
+ enqueueSteering(text, user) {
1069
+ // Both reviewer and user guidance can activate queued skill frames.
1060
1070
  if (this.skillsEnabled)
1061
1071
  this.skills.absorbPendingFrames();
1062
- // COUNTED so the reviewer can prove it owns the queue before clearing it
1063
- // (§3.5.4a guard 2 / D-21). This method is the only other writer of
1064
- // `steeringQueue` in the package, and `Agent` exposes no way to remove ONE
1065
- // message — so `userSteerCount === 0` is the only sound licence for
1066
- // `clearAllQueues()`, and with a user message in there the reviewer must
1067
- // leave its own residue rather than destroy something the user typed.
1068
- this.userSteerCount += 1;
1069
- this.agent.steer(text);
1072
+ const id = user ? `${this.steeringPrefix}:${++this.steeringSequence}` : undefined;
1073
+ if (id !== undefined)
1074
+ this.pendingUserSteering.add(id);
1075
+ try {
1076
+ this.agent.steer(text, id);
1077
+ }
1078
+ catch (error) {
1079
+ if (id !== undefined)
1080
+ this.pendingUserSteering.delete(id);
1081
+ throw error;
1082
+ }
1083
+ return id;
1070
1084
  }
1071
1085
  followUp(text) {
1072
1086
  this.agent.followUp(text);
@@ -1203,6 +1217,7 @@ export class AgentController {
1203
1217
  * honestly by checking `isTeamRegistered()` first.
1204
1218
  */
1205
1219
  setTeamEnabled(enabled) {
1220
+ this.settingsRevision += 1;
1206
1221
  this.teamEnabled = enabled;
1207
1222
  this.rebuildSystemPrompt();
1208
1223
  }
@@ -1216,6 +1231,7 @@ export class AgentController {
1216
1231
  */
1217
1232
  setTeamConfig(patch) {
1218
1233
  const team = clampTeamConfig({ ...this.config.team, ...patch });
1234
+ this.settingsRevision += 1;
1219
1235
  this.config = { ...this.config, team };
1220
1236
  this.rebuildSystemPrompt();
1221
1237
  return team;
@@ -1277,6 +1293,7 @@ export class AgentController {
1277
1293
  * reports that honestly by checking `isTodoRegistered()` first.
1278
1294
  */
1279
1295
  setTodoEnabled(enabled) {
1296
+ this.settingsRevision += 1;
1280
1297
  this.todoEnabled = enabled;
1281
1298
  this.rebuildSystemPrompt();
1282
1299
  }
@@ -1299,6 +1316,7 @@ export class AgentController {
1299
1316
  */
1300
1317
  setTodoConfig(patch) {
1301
1318
  const todo = clampTodoConfig({ ...this.config.todo, ...patch });
1319
+ this.settingsRevision += 1;
1302
1320
  this.config = { ...this.config, todo };
1303
1321
  this.rebuildSystemPrompt();
1304
1322
  return todo;
@@ -1354,6 +1372,7 @@ export class AgentController {
1354
1372
  */
1355
1373
  setRetryConfig(patch) {
1356
1374
  const retry = clampRetryConfig({ ...this.config.retry, ...patch });
1375
+ this.settingsRevision += 1;
1357
1376
  this.config = { ...this.config, retry };
1358
1377
  this.providerRegistry.setRetryPolicy(toRetryPolicy(retry));
1359
1378
  return retry;
@@ -1361,8 +1380,69 @@ export class AgentController {
1361
1380
  // -----------------------------------------------------------------------
1362
1381
  // Mutators
1363
1382
  // -----------------------------------------------------------------------
1383
+ /** True while any request can still use a model or credential snapshot. */
1384
+ isModelSettingsBusy() {
1385
+ return this.isRunning() || this.isTeamBusy() || this.getCompactionSnapshot().inFlight
1386
+ || this.getFastStatus().snapshot.inFlight;
1387
+ }
1388
+ /** Monotonic revision protects an editor from all intervening settings changes. */
1389
+ getSettingsRevision() { return this.settingsRevision; }
1390
+ /** Read-only draft creation; no startup migration or credential copying. */
1391
+ createModelSettingsDraft() {
1392
+ return createModelSettingsDraft(this.config, this.settingsRevision);
1393
+ }
1394
+ /** Save an explicit draft as a single disk/live transaction. */
1395
+ saveModelSettings(draft) {
1396
+ return saveModelSettings({ draft, controller: this });
1397
+ }
1398
+ /** Legacy commands share the same lossless conversion and atomic save path. */
1399
+ saveModelSettingsPatch(patch) {
1400
+ const draft = this.createModelSettingsDraft();
1401
+ draft.profiles = undefined;
1402
+ draft.patch = patch;
1403
+ return this.saveModelSettings(draft);
1404
+ }
1405
+ /** Refresh both role connections and credentials while retaining conversation state. */
1406
+ reloadModelSettings() { return reloadModelSettings(this); }
1407
+ /** Prebuild all potentially failing model/prompt work before committing the file. */
1408
+ prepareModelSettingsSnapshot(config) {
1409
+ return { config, model: { providerId: config.provider, modelId: config.model,
1410
+ ...(config.baseUrl ? { baseUrl: config.baseUrl } : {}) },
1411
+ systemPrompt: this.composeSystemPrompt(config) };
1412
+ }
1413
+ /** Apply prepared state synchronously, without rebuilding the controller or its history. */
1414
+ applyModelSettingsSnapshot(snapshot) {
1415
+ this.preparedSystemPrompt = snapshot.systemPrompt;
1416
+ try {
1417
+ this.config = snapshot.config;
1418
+ this.settingsRevision += 1;
1419
+ this.agent.setModel(snapshot.model);
1420
+ this.agent.setThinkingLevel(snapshot.config.thinkingLevel);
1421
+ this.agent.setMaxTokens(snapshot.config.maxTokens);
1422
+ this.fast?.onConfigChanged(false, snapshot.config.fast.enabled);
1423
+ this.compaction?.onConfigChanged(snapshot.config.compaction.enabled);
1424
+ this.contextMeter.onWindowChanged();
1425
+ this.rebuildSystemPrompt();
1426
+ }
1427
+ finally {
1428
+ this.preparedSystemPrompt = undefined;
1429
+ }
1430
+ }
1431
+ /** Potentially failing ordinary-setting side effects are isolated from model application. */
1432
+ applyModelSettingsEffects() { getLogger().reconfigure(this.config.log); }
1433
+ /** A partially applied model snapshot must never reach another provider request. */
1434
+ blockModelSettingsRequests() {
1435
+ this.modelSettingsBlocked = true;
1436
+ this.config.modelSettingsRestartRequired = true;
1437
+ }
1438
+ /** The UI uses this before accepting or draining queued prompts. */
1439
+ areModelSettingsBlocked() { return this.modelSettingsBlocked; }
1364
1440
  setModel(provider, model, baseUrl) {
1441
+ if (this.isModelSettingsBusy())
1442
+ throw new Error('Model settings are busy.');
1443
+ this.settingsRevision += 1;
1365
1444
  this.config = { ...this.config, provider, model, baseUrl };
1445
+ this.config.modelProfileState = resolveModelProfileState(this.config);
1366
1446
  this.agent.setModel({ providerId: provider, modelId: model, ...(baseUrl ? { baseUrl } : {}) });
1367
1447
  // NOT NEUTRAL TO THE FAST TIER even when the user never touched a `fast.*`
1368
1448
  // key (§3.9 / RV-3): `fast.provider: ''` and `fast.baseUrl: ''` INHERIT from
@@ -1384,16 +1464,19 @@ export class AgentController {
1384
1464
  this.contextMeter.onWindowChanged();
1385
1465
  }
1386
1466
  setThinkingLevel(level) {
1467
+ this.settingsRevision += 1;
1387
1468
  this.config = { ...this.config, thinkingLevel: level };
1388
1469
  this.agent.setThinkingLevel(level);
1389
1470
  }
1390
1471
  setMaxTokens(value) {
1472
+ this.settingsRevision += 1;
1391
1473
  this.config = { ...this.config, maxTokens: value };
1392
1474
  this.agent.setMaxTokens(value);
1393
1475
  }
1394
1476
  setApiKey(provider, key) {
1395
1477
  // The Agent resolves keys via `resolveKey(this.config)` at call time, so
1396
1478
  // updating the config here is sufficient — no Agent rebuild needed.
1479
+ this.settingsRevision += 1;
1397
1480
  this.config = {
1398
1481
  ...this.config,
1399
1482
  apiKeys: { ...this.config.apiKeys, [provider]: key },
@@ -1408,6 +1491,7 @@ export class AgentController {
1408
1491
  }
1409
1492
  setCwd(cwd) {
1410
1493
  this.cwd = cwd;
1494
+ this.settingsRevision += 1;
1411
1495
  this.config = { ...this.config, cwd };
1412
1496
  // The project scope moved with the cwd, so rescan BEFORE rebuilding — then
1413
1497
  // let the single prompt entry point emit a prompt that reflects both the new
@@ -1524,6 +1608,7 @@ export class AgentController {
1524
1608
  }
1525
1609
  clearAllQueues() {
1526
1610
  this.agent.clearAllQueues();
1611
+ this.pendingUserSteering.clear();
1527
1612
  }
1528
1613
  // -----------------------------------------------------------------------
1529
1614
  // Model / provider info
@@ -1591,6 +1676,7 @@ export class AgentController {
1591
1676
  * `/fast` reports that honestly by checking `isFastRegistered()` first.
1592
1677
  */
1593
1678
  setFastEnabled(enabled) {
1679
+ this.settingsRevision += 1;
1594
1680
  this.fast?.setEnabled(enabled);
1595
1681
  }
1596
1682
  getFastConfig() {
@@ -1606,6 +1692,7 @@ export class AgentController {
1606
1692
  */
1607
1693
  setFastConfig(patch) {
1608
1694
  const fast = clampFastConfig({ ...this.config.fast, ...patch });
1695
+ this.settingsRevision += 1;
1609
1696
  this.config = { ...this.config, fast };
1610
1697
  // `onConfigChanged` rebuilds the prompt; when there is no wiring (the tier
1611
1698
  // was never registered) nothing has to be rebuilt, because no block is
@@ -1705,6 +1792,7 @@ export class AgentController {
1705
1792
  * on purpose (§3.2).
1706
1793
  */
1707
1794
  setCompactionEnabled(enabled) {
1795
+ this.settingsRevision += 1;
1708
1796
  this.compaction?.setEnabled(enabled);
1709
1797
  }
1710
1798
  getCompactionConfig() {
@@ -1719,6 +1807,7 @@ export class AgentController {
1719
1807
  */
1720
1808
  setCompactionConfig(patch) {
1721
1809
  const compaction = clampCompactionConfig({ ...this.config.compaction, ...patch });
1810
+ this.settingsRevision += 1;
1722
1811
  this.config = { ...this.config, compaction };
1723
1812
  this.compaction?.onConfigChanged();
1724
1813
  return compaction;
@@ -1767,6 +1856,8 @@ export class AgentController {
1767
1856
  * unbypassable. `replaceMessages` validates NOTHING.
1768
1857
  */
1769
1858
  async compactNow(instructions) {
1859
+ if (this.modelSettingsBlocked)
1860
+ return { ok: false, reason: 'settings_restart_required' };
1770
1861
  if (!this.compaction)
1771
1862
  return { ok: false, reason: 'not_registered' };
1772
1863
  const controller = new AbortController();
@@ -0,0 +1,31 @@
1
+ import type { ModelRef } from '@aragon-agent/core';
2
+ import { type CliConfig } from '../config/schema.js';
3
+ import { type ModelSettingsDraft, type ModelSettingsSaveResult } from '../config/model-profile-store.js';
4
+ /** Fully prepared live state; applying it performs no input parsing or disk access. */
5
+ export interface ModelSettingsSnapshot {
6
+ config: CliConfig;
7
+ model: ModelRef;
8
+ systemPrompt: string;
9
+ }
10
+ /** Narrow controller port keeps persistence independent from the agent implementation. */
11
+ export interface ModelSettingsController {
12
+ getConfig(): CliConfig;
13
+ getSettingsRevision(): number;
14
+ isModelSettingsBusy(): boolean;
15
+ isFastRegistered(): boolean;
16
+ prepareModelSettingsSnapshot(config: CliConfig): ModelSettingsSnapshot;
17
+ applyModelSettingsSnapshot(snapshot: ModelSettingsSnapshot): void;
18
+ applyModelSettingsEffects(): void;
19
+ blockModelSettingsRequests(): void;
20
+ }
21
+ /** Capture both baselines when the editor opens, without migrating or writing anything. */
22
+ export declare function createModelSettingsDraft(live: CliConfig, liveRevision: number): ModelSettingsDraft;
23
+ /** Map stable failure codes to safe UI text; never include parser or SDK error messages. */
24
+ export declare function modelSettingsResultMessage(result: ModelSettingsSaveResult): string;
25
+ /** Validate and prepare everything before the one synchronous, atomic disk commit. */
26
+ export declare function saveModelSettings(input: {
27
+ draft: ModelSettingsDraft;
28
+ controller: ModelSettingsController;
29
+ }): ModelSettingsSaveResult;
30
+ /** Reload the complete model snapshot; invalid disk content never replaces a live session. */
31
+ export declare function reloadModelSettings(controller: ModelSettingsController): ModelSettingsSaveResult;