@aragon-agent/cli 0.6.12 → 0.6.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +108 -31
- package/dist/agent/controller.d.ts +38 -2
- package/dist/agent/controller.js +185 -34
- package/dist/agent/model-profile-settings.d.ts +31 -0
- package/dist/agent/model-profile-settings.js +208 -0
- package/dist/agent/queued-messages.d.ts +12 -0
- package/dist/agent/queued-messages.js +18 -0
- package/dist/agent/reducer.d.ts +13 -1
- package/dist/agent/reducer.js +52 -12
- package/dist/boot/guard.d.ts +3 -3
- package/dist/boot/guard.js +3 -3
- package/dist/cli.js +48 -16
- package/dist/commands/builtins.js +66 -39
- package/dist/commands/queue-command.d.ts +3 -0
- package/dist/commands/queue-command.js +5 -0
- package/dist/commands/registry.d.ts +3 -0
- package/dist/compaction/child.d.ts +4 -2
- package/dist/compaction/child.js +8 -1
- package/dist/compaction/compactor.d.ts +7 -2
- package/dist/compaction/compactor.js +21 -16
- package/dist/compaction/context-command.js +10 -3
- package/dist/compaction/meter.js +2 -0
- package/dist/compaction/pressure.d.ts +1 -0
- package/dist/compaction/pressure.js +1 -0
- package/dist/compaction/types.d.ts +2 -0
- package/dist/compaction/wiring.d.ts +4 -3
- package/dist/compaction/wiring.js +10 -2
- package/dist/config/cli-commands.d.ts +2 -0
- package/dist/config/cli-commands.js +16 -4
- package/dist/config/load.d.ts +5 -2
- package/dist/config/load.js +23 -12
- package/dist/config/model-profile-resolution.d.ts +20 -0
- package/dist/config/model-profile-resolution.js +81 -0
- package/dist/config/model-profile-store.d.ts +67 -0
- package/dist/config/model-profile-store.js +153 -0
- package/dist/config/model-profiles.d.ts +44 -0
- package/dist/config/model-profiles.js +121 -0
- package/dist/config/schema.d.ts +11 -2
- package/dist/config/store.d.ts +11 -1
- package/dist/config/store.js +43 -27
- package/dist/fast/resolve.d.ts +2 -1
- package/dist/fast/resolve.js +6 -4
- package/dist/fast/wiring.d.ts +5 -3
- package/dist/fast/wiring.js +8 -4
- package/dist/input/stdin-filter.js +3 -2
- package/dist/logging/install.js +4 -2
- package/dist/logging/secret-registry.d.ts +2 -0
- package/dist/logging/secret-registry.js +18 -0
- package/dist/team/runtime.d.ts +2 -1
- package/dist/team/subagent.d.ts +3 -1
- package/dist/team/subagent.js +3 -1
- package/dist/ui/ActivityLine.d.ts +6 -70
- package/dist/ui/ActivityLine.js +12 -34
- package/dist/ui/App.d.ts +1 -0
- package/dist/ui/App.js +116 -215
- package/dist/ui/BottomStatusRow.d.ts +6 -86
- package/dist/ui/BottomStatusRow.js +11 -26
- package/dist/ui/Composer.d.ts +16 -83
- package/dist/ui/Composer.js +9 -100
- package/dist/ui/Logo.d.ts +3 -5
- package/dist/ui/Logo.js +4 -8
- package/dist/ui/PromptInput.d.ts +4 -1
- package/dist/ui/PromptInput.js +44 -35
- package/dist/ui/QueuePanel.d.ts +8 -0
- package/dist/ui/QueuePanel.js +9 -0
- package/dist/ui/QueueStatusRow.d.ts +2 -0
- package/dist/ui/QueueStatusRow.js +1 -1
- package/dist/ui/StatusBar.d.ts +13 -149
- package/dist/ui/StatusBar.js +27 -142
- package/dist/ui/capabilities.d.ts +2 -0
- package/dist/ui/capabilities.js +6 -0
- package/dist/ui/clipboard-task.d.ts +29 -0
- package/dist/ui/clipboard-task.js +66 -0
- package/dist/ui/clipboard.d.ts +29 -50
- package/dist/ui/clipboard.js +141 -75
- package/dist/ui/color-runtime.d.ts +5 -0
- package/dist/ui/color-runtime.js +29 -0
- package/dist/ui/composer-limits.d.ts +3 -4
- package/dist/ui/composer-limits.js +4 -9
- package/dist/ui/composer-rows.d.ts +1 -1
- package/dist/ui/composer-rows.js +24 -137
- package/dist/ui/editor-navigation.d.ts +45 -0
- package/dist/ui/editor-navigation.js +85 -0
- package/dist/ui/editor-reducer.d.ts +4 -0
- package/dist/ui/editor-reducer.js +29 -15
- package/dist/ui/glyphs.js +10 -8
- package/dist/ui/ink-runtime.d.ts +3 -0
- package/dist/ui/ink-runtime.js +30 -0
- package/dist/ui/interaction-copy.d.ts +77 -0
- package/dist/ui/interaction-copy.js +40 -0
- package/dist/ui/interaction-hints.d.ts +18 -0
- package/dist/ui/interaction-hints.js +92 -0
- package/dist/ui/layout/budget.js +1 -1
- package/dist/ui/layout/queue-layout.d.ts +19 -0
- package/dist/ui/layout/queue-layout.js +26 -0
- package/dist/ui/layout/status-layout.d.ts +55 -0
- package/dist/ui/layout/status-layout.js +110 -0
- package/dist/ui/layout/todo-layout.d.ts +1 -0
- package/dist/ui/layout/todo-layout.js +1 -1
- package/dist/ui/overlays/HelpOverlay.js +8 -8
- package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
- package/dist/ui/overlays/ModelProfileEditor.js +103 -0
- package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
- package/dist/ui/overlays/ModelProfilePicker.js +167 -0
- package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
- package/dist/ui/overlays/QueueOverlay.js +79 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
- package/dist/ui/overlays/SettingsScreen.js +347 -3
- package/dist/ui/overlays/model-profile-state.d.ts +74 -0
- package/dist/ui/overlays/model-profile-state.js +119 -0
- package/dist/ui/render-cache.d.ts +2 -2
- package/dist/ui/render-cache.js +4 -3
- package/dist/ui/run-status-row.d.ts +7 -19
- package/dist/ui/run-status-row.js +2 -18
- package/dist/ui/selection/selection-controller.d.ts +2 -2
- package/dist/ui/selection/selection-controller.js +1 -0
- package/dist/ui/use-wheel-routing.js +2 -1
- package/package.json +2 -2
package/dist/agent/controller.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { createModelSettingsDraft, saveModelSettings, reloadModelSettings, } from './model-profile-settings.js';
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { Agent, initProviders, ModelRegistry, } from '@aragon-agent/core';
|
|
3
4
|
import { clampCompactionConfig, clampFastConfig, clampRetryConfig, clampTeamConfig, clampTodoConfig, isAdapterProvider, toRetryPolicy, } from '../config/schema.js';
|
|
5
|
+
import { resolveModelProfileState } from '../config/model-profile-resolution.js';
|
|
4
6
|
import { makeGetApiKey } from '../config/load.js';
|
|
5
7
|
import { updatePersistedConfig } from '../config/store.js';
|
|
6
8
|
import { createBuiltinTools } from '../tools/index.js';
|
|
@@ -49,6 +51,10 @@ export class AgentController {
|
|
|
49
51
|
agent;
|
|
50
52
|
providerRegistry;
|
|
51
53
|
modelRegistry;
|
|
54
|
+
modelMetadataEnabled = false;
|
|
55
|
+
modelMetadataGeneration = 0;
|
|
56
|
+
modelMetadataAbort = new AbortController();
|
|
57
|
+
disposed = false;
|
|
52
58
|
tools;
|
|
53
59
|
skills;
|
|
54
60
|
skillsEnabled;
|
|
@@ -194,6 +200,9 @@ export class AgentController {
|
|
|
194
200
|
// no timer while it has no subscribers, so the extra object is a few scalars
|
|
195
201
|
// and a `Set`.
|
|
196
202
|
// -----------------------------------------------------------------------
|
|
203
|
+
settingsRevision = 0;
|
|
204
|
+
modelSettingsBlocked = false;
|
|
205
|
+
preparedSystemPrompt;
|
|
197
206
|
contextMeter;
|
|
198
207
|
// -----------------------------------------------------------------------
|
|
199
208
|
// Background services (background-service-supervision §3.3 / §3.6)
|
|
@@ -297,7 +306,7 @@ export class AgentController {
|
|
|
297
306
|
// ONCE (C-2). `FastWiring` re-runs the same pure function in its own
|
|
298
307
|
// constructor; the two agree by construction because they read the same
|
|
299
308
|
// config through the same resolver.
|
|
300
|
-
const fastTier = resolveFastTier(config, (id) => this.hasApiKey(id));
|
|
309
|
+
const fastTier = resolveFastTier(config, (id, role) => this.hasApiKey(id, role));
|
|
301
310
|
const fastRegistered = fastTier.ok;
|
|
302
311
|
// --- Team mode ---------------------------------------------------------
|
|
303
312
|
this.teamRegistered = config.team.enabled;
|
|
@@ -321,11 +330,12 @@ export class AgentController {
|
|
|
321
330
|
...(this.config.baseUrl ? { baseUrl: this.config.baseUrl } : {}),
|
|
322
331
|
},
|
|
323
332
|
thinkingLevel: this.config.thinkingLevel,
|
|
333
|
+
role: 'main',
|
|
324
334
|
},
|
|
325
335
|
providerRegistry: this.providerRegistry,
|
|
326
336
|
getCwd: () => this.cwd,
|
|
327
337
|
getMode: () => this.effectiveMode,
|
|
328
|
-
getApiKey: (id) => this.resolveKey(id),
|
|
338
|
+
getApiKey: (id, role) => this.resolveKey(id, role),
|
|
329
339
|
// `skill_find` only, and the lead's always-on bodies. See the long
|
|
330
340
|
// note in `subagent.ts` for why `skill` itself is withheld (D-16).
|
|
331
341
|
...(this.skillsEnabled
|
|
@@ -523,15 +533,8 @@ export class AgentController {
|
|
|
523
533
|
getMessages: () => this.agent.state.messages,
|
|
524
534
|
getSystemPrompt: () => this.agent.state.systemPrompt,
|
|
525
535
|
getModelInfo: () => this.getModelInfo(),
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
// back to `buildRuntimeModel` - a 128k placeholder AND a zero cost table -
|
|
529
|
-
// for exactly the models it answers `false` for. The two are the same
|
|
530
|
-
// question about the same static table, not a coincidence.
|
|
531
|
-
isWindowKnown: () => this.isPricedModel({
|
|
532
|
-
providerId: this.config.provider,
|
|
533
|
-
modelId: this.config.model,
|
|
534
|
-
}),
|
|
536
|
+
// Context discovery does not imply that the model's price is known.
|
|
537
|
+
isWindowKnown: () => this.getModelInfo().contextWindowSource !== 'fallback',
|
|
535
538
|
// LIVE, so a settings-screen edit moves the denominator without a relaunch.
|
|
536
539
|
getWindowOverride: () => this.config.contextWindow,
|
|
537
540
|
});
|
|
@@ -545,8 +548,8 @@ export class AgentController {
|
|
|
545
548
|
this.compaction = config.compaction.enabled
|
|
546
549
|
? new CompactionWiring({
|
|
547
550
|
getConfig: () => this.config,
|
|
548
|
-
hasKey: (id) => this.hasApiKey(id),
|
|
549
|
-
getApiKey: (id) => this.resolveKey(id),
|
|
551
|
+
hasKey: (id, role) => this.hasApiKey(id, role),
|
|
552
|
+
getApiKey: (id, role) => this.resolveKey(id, role),
|
|
550
553
|
getModelInfoFor: (ref) => this.getModelInfoFor(ref),
|
|
551
554
|
isPricedModel: (ref) => this.isPricedModel(ref),
|
|
552
555
|
getMessages: () => this.agent.state.messages,
|
|
@@ -623,8 +626,8 @@ export class AgentController {
|
|
|
623
626
|
this.fast = fastRegistered
|
|
624
627
|
? new FastWiring({
|
|
625
628
|
getConfig: () => this.config,
|
|
626
|
-
hasKey: (id) => this.hasApiKey(id),
|
|
627
|
-
getApiKey: (id) => this.resolveKey(id),
|
|
629
|
+
hasKey: (id, role) => this.hasApiKey(id, role),
|
|
630
|
+
getApiKey: (id, role) => this.resolveKey(id, role),
|
|
628
631
|
isPricedModel: (ref) => this.isPricedModel(ref),
|
|
629
632
|
subscribe: (listener) => this.agent.subscribe(listener),
|
|
630
633
|
complete: (providerId, request) => this.providerRegistry.complete(providerId, request),
|
|
@@ -673,8 +676,8 @@ export class AgentController {
|
|
|
673
676
|
if (problem)
|
|
674
677
|
deps.notify?.('warn', problem);
|
|
675
678
|
}
|
|
676
|
-
resolveKey(providerId) {
|
|
677
|
-
return makeGetApiKey(this.config)(providerId);
|
|
679
|
+
resolveKey(providerId, role = 'main') {
|
|
680
|
+
return makeGetApiKey(this.config, role)(providerId);
|
|
678
681
|
}
|
|
679
682
|
/**
|
|
680
683
|
* The tool names registered right now — LAZY, and the laziness is required.
|
|
@@ -705,27 +708,30 @@ export class AgentController {
|
|
|
705
708
|
// suddenly denies having any skills). Structure, not call ordering, is what
|
|
706
709
|
// rules that out — so if you add a third caller, route it through here.
|
|
707
710
|
// -----------------------------------------------------------------------
|
|
708
|
-
composeSystemPrompt() {
|
|
711
|
+
composeSystemPrompt(config = this.config) {
|
|
712
|
+
const tier = resolveFastTier(config, (id, role) => !!makeGetApiKey(config, role)(id));
|
|
713
|
+
const fastAvailable = config === this.config ? this.fast?.available() === true
|
|
714
|
+
: this.fast?.isRegistered() && config.fast.enabled && tier.ok;
|
|
709
715
|
return buildSystemPrompt({
|
|
710
716
|
cwd: this.cwd,
|
|
711
717
|
tools: this.tools,
|
|
712
718
|
skillsBlock: this.skillsEnabled ? this.skills.catalogBlock() + this.skills.alwaysBlock() : '',
|
|
713
719
|
agentMode: this.effectiveMode,
|
|
714
720
|
planInteractive: this.planInteractive,
|
|
715
|
-
planMaxAskRounds:
|
|
721
|
+
planMaxAskRounds: config.planModeMaxAskRounds,
|
|
716
722
|
// BOTH flags, and both are required (§4.5). `teamRegistered` false means
|
|
717
723
|
// there is no `task` tool to advertise; `teamEnabled` false means the user
|
|
718
724
|
// turned it off for this session. Advertising in either case would
|
|
719
725
|
// describe a capability the model does not have.
|
|
720
726
|
teamBlock: this.teamRegistered && this.teamEnabled
|
|
721
727
|
? buildTeamBlock({
|
|
722
|
-
maxSubagents:
|
|
723
|
-
maxConcurrent:
|
|
728
|
+
maxSubagents: config.team.maxSubagents,
|
|
729
|
+
maxConcurrent: config.team.maxConcurrent,
|
|
724
730
|
// One cross-reference sentence, present only when the capability
|
|
725
731
|
// is (§3.8). `this.fast` is null during the FIRST compose (the
|
|
726
732
|
// wiring needs the Agent), which is why the constructor composes
|
|
727
733
|
// again once it exists.
|
|
728
|
-
fastDelegation:
|
|
734
|
+
fastDelegation: !!fastAvailable && config.fast.delegate,
|
|
729
735
|
})
|
|
730
736
|
: '',
|
|
731
737
|
// BOTH FLAGS, exactly as team mode uses both, and for the identical
|
|
@@ -735,18 +741,18 @@ export class AgentController {
|
|
|
735
741
|
// model does not have.
|
|
736
742
|
todoBlock: this.todoRegistered && this.todoEnabled
|
|
737
743
|
? buildTodoBlock({
|
|
738
|
-
panelVisible: this.todoPanelCapable &&
|
|
744
|
+
panelVisible: this.todoPanelCapable && config.todo.panel,
|
|
739
745
|
})
|
|
740
746
|
: '',
|
|
741
747
|
// The SAME two flags one more time, plus the live resolution — all three
|
|
742
748
|
// live inside `FastWiring.available()` (§3.3). `buildFastBlock` itself
|
|
743
749
|
// returns `''` when neither capability is on, so a user who turned both
|
|
744
750
|
// off pays nothing for the tier being technically live.
|
|
745
|
-
fastBlock:
|
|
751
|
+
fastBlock: fastAvailable
|
|
746
752
|
? buildFastBlock({
|
|
747
|
-
model:
|
|
748
|
-
delegate:
|
|
749
|
-
review:
|
|
753
|
+
model: tier.ok ? tier.ref.modelId : '',
|
|
754
|
+
delegate: config.fast.delegate,
|
|
755
|
+
review: config.fast.review,
|
|
750
756
|
})
|
|
751
757
|
: '',
|
|
752
758
|
// ONE FLAG, unlike team/todo's two: there is no live on/off switch for
|
|
@@ -762,7 +768,7 @@ export class AgentController {
|
|
|
762
768
|
});
|
|
763
769
|
}
|
|
764
770
|
rebuildSystemPrompt() {
|
|
765
|
-
this.agent.setSystemPrompt(this.composeSystemPrompt());
|
|
771
|
+
this.agent.setSystemPrompt(this.preparedSystemPrompt ?? this.composeSystemPrompt());
|
|
766
772
|
}
|
|
767
773
|
/** Re-render the prompt from the current skill set and tell the UI. */
|
|
768
774
|
refreshSkills() {
|
|
@@ -781,8 +787,10 @@ export class AgentController {
|
|
|
781
787
|
* Read-only CLI-layer helper for the Header/Welcome key-status dot (P1-1);
|
|
782
788
|
* does not touch `@aragon-agent/core`.
|
|
783
789
|
*/
|
|
784
|
-
hasApiKey(provider) {
|
|
785
|
-
const
|
|
790
|
+
hasApiKey(provider, role = 'main') {
|
|
791
|
+
const activeProvider = role === 'fast'
|
|
792
|
+
? this.config.fast.provider || this.config.provider : this.config.provider;
|
|
793
|
+
const key = this.resolveKey(provider ?? activeProvider, role);
|
|
786
794
|
return !!key && key.trim().length > 0;
|
|
787
795
|
}
|
|
788
796
|
/**
|
|
@@ -791,6 +799,7 @@ export class AgentController {
|
|
|
791
799
|
* re-memoizes the palette (P1-1). Does not touch `@aragon-agent/core`.
|
|
792
800
|
*/
|
|
793
801
|
setTheme(name) {
|
|
802
|
+
this.settingsRevision += 1;
|
|
794
803
|
this.config = { ...this.config, theme: name };
|
|
795
804
|
}
|
|
796
805
|
/**
|
|
@@ -811,6 +820,9 @@ export class AgentController {
|
|
|
811
820
|
// Pre-flight (R2): validate BEFORE entering the loop.
|
|
812
821
|
// -----------------------------------------------------------------------
|
|
813
822
|
preflight() {
|
|
823
|
+
if (this.modelSettingsBlocked)
|
|
824
|
+
return { ok: false, kind: 'config',
|
|
825
|
+
message: 'Settings were saved but not fully applied. Restart before sending requests.' };
|
|
814
826
|
const provider = this.config.provider;
|
|
815
827
|
if (!isAdapterProvider(provider)) {
|
|
816
828
|
return {
|
|
@@ -845,6 +857,11 @@ export class AgentController {
|
|
|
845
857
|
*/
|
|
846
858
|
async prompt(text, options = {}) {
|
|
847
859
|
// Capture BEFORE stopping: synchronous abort listeners may cancel this request.
|
|
860
|
+
if (this.modelSettingsBlocked) {
|
|
861
|
+
return { status: 'not-started', reason: 'failed' };
|
|
862
|
+
}
|
|
863
|
+
if (this.modelMetadataEnabled)
|
|
864
|
+
void this.refreshModelMetadata();
|
|
848
865
|
const request = ++this.startupSequence;
|
|
849
866
|
try {
|
|
850
867
|
if (this.agent.state.isRunning) {
|
|
@@ -1054,7 +1071,14 @@ export class AgentController {
|
|
|
1054
1071
|
const id = user ? `${this.steeringPrefix}:${++this.steeringSequence}` : undefined;
|
|
1055
1072
|
if (id !== undefined)
|
|
1056
1073
|
this.pendingUserSteering.add(id);
|
|
1057
|
-
|
|
1074
|
+
try {
|
|
1075
|
+
this.agent.steer(text, id);
|
|
1076
|
+
}
|
|
1077
|
+
catch (error) {
|
|
1078
|
+
if (id !== undefined)
|
|
1079
|
+
this.pendingUserSteering.delete(id);
|
|
1080
|
+
throw error;
|
|
1081
|
+
}
|
|
1058
1082
|
return id;
|
|
1059
1083
|
}
|
|
1060
1084
|
followUp(text) {
|
|
@@ -1192,6 +1216,7 @@ export class AgentController {
|
|
|
1192
1216
|
* honestly by checking `isTeamRegistered()` first.
|
|
1193
1217
|
*/
|
|
1194
1218
|
setTeamEnabled(enabled) {
|
|
1219
|
+
this.settingsRevision += 1;
|
|
1195
1220
|
this.teamEnabled = enabled;
|
|
1196
1221
|
this.rebuildSystemPrompt();
|
|
1197
1222
|
}
|
|
@@ -1205,6 +1230,7 @@ export class AgentController {
|
|
|
1205
1230
|
*/
|
|
1206
1231
|
setTeamConfig(patch) {
|
|
1207
1232
|
const team = clampTeamConfig({ ...this.config.team, ...patch });
|
|
1233
|
+
this.settingsRevision += 1;
|
|
1208
1234
|
this.config = { ...this.config, team };
|
|
1209
1235
|
this.rebuildSystemPrompt();
|
|
1210
1236
|
return team;
|
|
@@ -1230,6 +1256,9 @@ export class AgentController {
|
|
|
1230
1256
|
}
|
|
1231
1257
|
/** Abort every child and clear timers. Idempotent; safe to call twice. */
|
|
1232
1258
|
dispose() {
|
|
1259
|
+
this.disposed = true;
|
|
1260
|
+
this.modelMetadataGeneration += 1;
|
|
1261
|
+
this.modelMetadataAbort.abort();
|
|
1233
1262
|
this.teamRuntime?.dispose();
|
|
1234
1263
|
this.fast?.dispose();
|
|
1235
1264
|
this.compaction?.dispose();
|
|
@@ -1266,6 +1295,7 @@ export class AgentController {
|
|
|
1266
1295
|
* reports that honestly by checking `isTodoRegistered()` first.
|
|
1267
1296
|
*/
|
|
1268
1297
|
setTodoEnabled(enabled) {
|
|
1298
|
+
this.settingsRevision += 1;
|
|
1269
1299
|
this.todoEnabled = enabled;
|
|
1270
1300
|
this.rebuildSystemPrompt();
|
|
1271
1301
|
}
|
|
@@ -1288,6 +1318,7 @@ export class AgentController {
|
|
|
1288
1318
|
*/
|
|
1289
1319
|
setTodoConfig(patch) {
|
|
1290
1320
|
const todo = clampTodoConfig({ ...this.config.todo, ...patch });
|
|
1321
|
+
this.settingsRevision += 1;
|
|
1291
1322
|
this.config = { ...this.config, todo };
|
|
1292
1323
|
this.rebuildSystemPrompt();
|
|
1293
1324
|
return todo;
|
|
@@ -1343,6 +1374,7 @@ export class AgentController {
|
|
|
1343
1374
|
*/
|
|
1344
1375
|
setRetryConfig(patch) {
|
|
1345
1376
|
const retry = clampRetryConfig({ ...this.config.retry, ...patch });
|
|
1377
|
+
this.settingsRevision += 1;
|
|
1346
1378
|
this.config = { ...this.config, retry };
|
|
1347
1379
|
this.providerRegistry.setRetryPolicy(toRetryPolicy(retry));
|
|
1348
1380
|
return retry;
|
|
@@ -1350,8 +1382,80 @@ export class AgentController {
|
|
|
1350
1382
|
// -----------------------------------------------------------------------
|
|
1351
1383
|
// Mutators
|
|
1352
1384
|
// -----------------------------------------------------------------------
|
|
1385
|
+
/** True while any request can still use a model or credential snapshot. */
|
|
1386
|
+
isModelSettingsBusy() {
|
|
1387
|
+
return this.isRunning() || this.isTeamBusy() || this.getCompactionSnapshot().inFlight
|
|
1388
|
+
|| this.getFastStatus().snapshot.inFlight;
|
|
1389
|
+
}
|
|
1390
|
+
/** Monotonic revision protects an editor from all intervening settings changes. */
|
|
1391
|
+
getSettingsRevision() { return this.settingsRevision; }
|
|
1392
|
+
/** Read-only draft creation; no startup migration or credential copying. */
|
|
1393
|
+
createModelSettingsDraft() {
|
|
1394
|
+
return createModelSettingsDraft(this.config, this.settingsRevision);
|
|
1395
|
+
}
|
|
1396
|
+
/** Save an explicit draft as a single disk/live transaction. */
|
|
1397
|
+
saveModelSettings(draft) {
|
|
1398
|
+
return saveModelSettings({ draft, controller: this });
|
|
1399
|
+
}
|
|
1400
|
+
/** Legacy commands share the same lossless conversion and atomic save path. */
|
|
1401
|
+
saveModelSettingsPatch(patch) {
|
|
1402
|
+
const draft = this.createModelSettingsDraft();
|
|
1403
|
+
draft.profiles = undefined;
|
|
1404
|
+
draft.patch = patch;
|
|
1405
|
+
return this.saveModelSettings(draft);
|
|
1406
|
+
}
|
|
1407
|
+
/** Refresh both role connections and credentials while retaining conversation state. */
|
|
1408
|
+
reloadModelSettings() { return reloadModelSettings(this); }
|
|
1409
|
+
/** Prebuild all potentially failing model/prompt work before committing the file. */
|
|
1410
|
+
prepareModelSettingsSnapshot(config) {
|
|
1411
|
+
return { config, model: { providerId: config.provider, modelId: config.model,
|
|
1412
|
+
...(config.baseUrl ? { baseUrl: config.baseUrl } : {}) },
|
|
1413
|
+
systemPrompt: this.composeSystemPrompt(config) };
|
|
1414
|
+
}
|
|
1415
|
+
/** Apply prepared state synchronously, without rebuilding the controller or its history. */
|
|
1416
|
+
applyModelSettingsSnapshot(snapshot) {
|
|
1417
|
+
this.preparedSystemPrompt = snapshot.systemPrompt;
|
|
1418
|
+
try {
|
|
1419
|
+
this.config = snapshot.config;
|
|
1420
|
+
this.settingsRevision += 1;
|
|
1421
|
+
this.agent.setModel(snapshot.model);
|
|
1422
|
+
this.agent.setThinkingLevel(snapshot.config.thinkingLevel);
|
|
1423
|
+
this.agent.setMaxTokens(snapshot.config.maxTokens);
|
|
1424
|
+
// Credentials may change even when endpoint/model do not. Invalidate
|
|
1425
|
+
// before publishing a window, including when the new profile has no key.
|
|
1426
|
+
if (this.modelMetadataEnabled) {
|
|
1427
|
+
this.resetModelMetadataRequest();
|
|
1428
|
+
this.modelRegistry.clearCache();
|
|
1429
|
+
}
|
|
1430
|
+
this.fast?.onConfigChanged(false, snapshot.config.fast.enabled);
|
|
1431
|
+
this.compaction?.onConfigChanged(snapshot.config.compaction.enabled);
|
|
1432
|
+
this.contextMeter.onWindowChanged();
|
|
1433
|
+
this.rebuildSystemPrompt();
|
|
1434
|
+
}
|
|
1435
|
+
finally {
|
|
1436
|
+
this.preparedSystemPrompt = undefined;
|
|
1437
|
+
}
|
|
1438
|
+
if (this.modelMetadataEnabled)
|
|
1439
|
+
void this.refreshModelMetadata();
|
|
1440
|
+
}
|
|
1441
|
+
/** Potentially failing ordinary-setting side effects are isolated from model application. */
|
|
1442
|
+
applyModelSettingsEffects() { getLogger().reconfigure(this.config.log); }
|
|
1443
|
+
/** A partially applied model snapshot must never reach another provider request. */
|
|
1444
|
+
blockModelSettingsRequests() {
|
|
1445
|
+
this.modelSettingsBlocked = true;
|
|
1446
|
+
this.config.modelSettingsRestartRequired = true;
|
|
1447
|
+
this.resetModelMetadataRequest();
|
|
1448
|
+
}
|
|
1449
|
+
/** The UI uses this before accepting or draining queued prompts. */
|
|
1450
|
+
areModelSettingsBlocked() { return this.modelSettingsBlocked; }
|
|
1353
1451
|
setModel(provider, model, baseUrl) {
|
|
1452
|
+
if (this.isModelSettingsBusy())
|
|
1453
|
+
throw new Error('Model settings are busy.');
|
|
1454
|
+
if (this.modelMetadataEnabled)
|
|
1455
|
+
this.resetModelMetadataRequest();
|
|
1456
|
+
this.settingsRevision += 1;
|
|
1354
1457
|
this.config = { ...this.config, provider, model, baseUrl };
|
|
1458
|
+
this.config.modelProfileState = resolveModelProfileState(this.config);
|
|
1355
1459
|
this.agent.setModel({ providerId: provider, modelId: model, ...(baseUrl ? { baseUrl } : {}) });
|
|
1356
1460
|
// NOT NEUTRAL TO THE FAST TIER even when the user never touched a `fast.*`
|
|
1357
1461
|
// key (§3.9 / RV-3): `fast.provider: ''` and `fast.baseUrl: ''` INHERIT from
|
|
@@ -1371,22 +1475,33 @@ export class AgentController {
|
|
|
1371
1475
|
// numerator's calibration is one turn stale until the next `turn_end`
|
|
1372
1476
|
// (RV-13), which is the trade this call names.
|
|
1373
1477
|
this.contextMeter.onWindowChanged();
|
|
1478
|
+
if (this.modelMetadataEnabled)
|
|
1479
|
+
void this.refreshModelMetadata();
|
|
1374
1480
|
}
|
|
1375
1481
|
setThinkingLevel(level) {
|
|
1482
|
+
this.settingsRevision += 1;
|
|
1376
1483
|
this.config = { ...this.config, thinkingLevel: level };
|
|
1377
1484
|
this.agent.setThinkingLevel(level);
|
|
1378
1485
|
}
|
|
1379
1486
|
setMaxTokens(value) {
|
|
1487
|
+
this.settingsRevision += 1;
|
|
1380
1488
|
this.config = { ...this.config, maxTokens: value };
|
|
1381
1489
|
this.agent.setMaxTokens(value);
|
|
1382
1490
|
}
|
|
1383
1491
|
setApiKey(provider, key) {
|
|
1384
1492
|
// The Agent resolves keys via `resolveKey(this.config)` at call time, so
|
|
1385
1493
|
// updating the config here is sufficient — no Agent rebuild needed.
|
|
1494
|
+
this.settingsRevision += 1;
|
|
1386
1495
|
this.config = {
|
|
1387
1496
|
...this.config,
|
|
1388
1497
|
apiKeys: { ...this.config.apiKeys, [provider]: key },
|
|
1389
1498
|
};
|
|
1499
|
+
if (this.modelMetadataEnabled) {
|
|
1500
|
+
this.resetModelMetadataRequest();
|
|
1501
|
+
this.modelRegistry.clearCache(provider);
|
|
1502
|
+
this.contextMeter.onWindowChanged();
|
|
1503
|
+
void this.refreshModelMetadata();
|
|
1504
|
+
}
|
|
1390
1505
|
// Rule 5 of §3.2 reads `apiKeys`, so a key edit is a tier event: this is the
|
|
1391
1506
|
// path that turns `no_key` back into a working tier without a relaunch.
|
|
1392
1507
|
this.fast?.onConfigChanged();
|
|
@@ -1397,6 +1512,7 @@ export class AgentController {
|
|
|
1397
1512
|
}
|
|
1398
1513
|
setCwd(cwd) {
|
|
1399
1514
|
this.cwd = cwd;
|
|
1515
|
+
this.settingsRevision += 1;
|
|
1400
1516
|
this.config = { ...this.config, cwd };
|
|
1401
1517
|
// The project scope moved with the cwd, so rescan BEFORE rebuilding — then
|
|
1402
1518
|
// let the single prompt entry point emit a prompt that reflects both the new
|
|
@@ -1544,9 +1660,38 @@ export class AgentController {
|
|
|
1544
1660
|
*/
|
|
1545
1661
|
getModelInfoFor(ref) {
|
|
1546
1662
|
const found = this.modelRegistry.getModel(ref.providerId, ref.modelId);
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
return
|
|
1663
|
+
const baseUrl = ref.baseUrl ?? (ref.providerId === this.config.provider && ref.modelId === this.config.model
|
|
1664
|
+
? this.config.baseUrl : undefined);
|
|
1665
|
+
return {
|
|
1666
|
+
...(found ?? this.modelRegistry.buildRuntimeModel(ref.providerId, ref.modelId)),
|
|
1667
|
+
...this.modelRegistry.getContextWindow(ref.providerId, ref.modelId, baseUrl),
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
/** Best-effort metadata lookup; never block typing or a model request. */
|
|
1671
|
+
async refreshModelMetadata() {
|
|
1672
|
+
if (this.disposed || this.modelSettingsBlocked)
|
|
1673
|
+
return;
|
|
1674
|
+
this.modelMetadataEnabled = true;
|
|
1675
|
+
const generation = ++this.modelMetadataGeneration;
|
|
1676
|
+
const { provider, baseUrl } = this.config;
|
|
1677
|
+
try {
|
|
1678
|
+
const key = this.resolveKey(provider);
|
|
1679
|
+
if (!key)
|
|
1680
|
+
return;
|
|
1681
|
+
await this.modelRegistry.discoverModels(provider, key, baseUrl, this.modelMetadataAbort.signal);
|
|
1682
|
+
if (this.disposed || generation !== this.modelMetadataGeneration)
|
|
1683
|
+
return;
|
|
1684
|
+
this.contextMeter.onWindowChanged();
|
|
1685
|
+
}
|
|
1686
|
+
catch {
|
|
1687
|
+
// Offline or unsupported discovery leaves catalog/unknown metadata intact.
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
/** Invalidate an old connection without restarting a lookup on every prompt. */
|
|
1691
|
+
resetModelMetadataRequest() {
|
|
1692
|
+
this.modelMetadataGeneration += 1;
|
|
1693
|
+
this.modelMetadataAbort.abort();
|
|
1694
|
+
this.modelMetadataAbort = new AbortController();
|
|
1550
1695
|
}
|
|
1551
1696
|
/**
|
|
1552
1697
|
* Whether the STATIC table knows this model's price (C-11 / RV-4).
|
|
@@ -1581,6 +1726,7 @@ export class AgentController {
|
|
|
1581
1726
|
* `/fast` reports that honestly by checking `isFastRegistered()` first.
|
|
1582
1727
|
*/
|
|
1583
1728
|
setFastEnabled(enabled) {
|
|
1729
|
+
this.settingsRevision += 1;
|
|
1584
1730
|
this.fast?.setEnabled(enabled);
|
|
1585
1731
|
}
|
|
1586
1732
|
getFastConfig() {
|
|
@@ -1596,6 +1742,7 @@ export class AgentController {
|
|
|
1596
1742
|
*/
|
|
1597
1743
|
setFastConfig(patch) {
|
|
1598
1744
|
const fast = clampFastConfig({ ...this.config.fast, ...patch });
|
|
1745
|
+
this.settingsRevision += 1;
|
|
1599
1746
|
this.config = { ...this.config, fast };
|
|
1600
1747
|
// `onConfigChanged` rebuilds the prompt; when there is no wiring (the tier
|
|
1601
1748
|
// was never registered) nothing has to be rebuilt, because no block is
|
|
@@ -1695,6 +1842,7 @@ export class AgentController {
|
|
|
1695
1842
|
* on purpose (§3.2).
|
|
1696
1843
|
*/
|
|
1697
1844
|
setCompactionEnabled(enabled) {
|
|
1845
|
+
this.settingsRevision += 1;
|
|
1698
1846
|
this.compaction?.setEnabled(enabled);
|
|
1699
1847
|
}
|
|
1700
1848
|
getCompactionConfig() {
|
|
@@ -1709,6 +1857,7 @@ export class AgentController {
|
|
|
1709
1857
|
*/
|
|
1710
1858
|
setCompactionConfig(patch) {
|
|
1711
1859
|
const compaction = clampCompactionConfig({ ...this.config.compaction, ...patch });
|
|
1860
|
+
this.settingsRevision += 1;
|
|
1712
1861
|
this.config = { ...this.config, compaction };
|
|
1713
1862
|
this.compaction?.onConfigChanged();
|
|
1714
1863
|
return compaction;
|
|
@@ -1757,6 +1906,8 @@ export class AgentController {
|
|
|
1757
1906
|
* unbypassable. `replaceMessages` validates NOTHING.
|
|
1758
1907
|
*/
|
|
1759
1908
|
async compactNow(instructions) {
|
|
1909
|
+
if (this.modelSettingsBlocked)
|
|
1910
|
+
return { ok: false, reason: 'settings_restart_required' };
|
|
1760
1911
|
if (!this.compaction)
|
|
1761
1912
|
return { ok: false, reason: 'not_registered' };
|
|
1762
1913
|
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;
|