@aragon-agent/cli 0.6.34 → 0.6.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3071 -3007
- package/dist/agent/controller.d.ts +29 -0
- package/dist/agent/controller.js +42 -1
- package/dist/agent/kit-apply.d.ts +56 -0
- package/dist/agent/kit-apply.js +209 -0
- package/dist/agent/model-profile-settings.d.ts +28 -0
- package/dist/agent/model-profile-settings.js +142 -29
- package/dist/agent/reducer.d.ts +1 -1
- package/dist/commands/builtins.js +4 -0
- package/dist/commands/kit-command.d.ts +20 -0
- package/dist/commands/kit-command.js +131 -0
- package/dist/config/kit-resolution.d.ts +93 -0
- package/dist/config/kit-resolution.js +226 -0
- package/dist/config/model-kits.d.ts +224 -0
- package/dist/config/model-kits.js +302 -0
- package/dist/config/model-profile-store.d.ts +25 -0
- package/dist/config/model-profiles.d.ts +41 -3
- package/dist/config/model-profiles.js +46 -5
- package/dist/config/schema.d.ts +16 -0
- package/dist/input/keymap.js +18 -4
- package/dist/ui/App.js +108 -12
- package/dist/ui/Header.d.ts +9 -1
- package/dist/ui/Header.js +8 -2
- package/dist/ui/PromptInput.js +1 -1
- package/dist/ui/ScrollbackShell.d.ts +18 -3
- package/dist/ui/ScrollbackShell.js +1 -1
- package/dist/ui/StatusBar.d.ts +13 -0
- package/dist/ui/StatusBar.js +10 -3
- package/dist/ui/TeamPanel.d.ts +12 -0
- package/dist/ui/TeamPanel.js +10 -31
- package/dist/ui/Transcript.js +22 -2
- package/dist/ui/entries/TeamCard.d.ts +18 -2
- package/dist/ui/entries/TeamCard.js +47 -13
- package/dist/ui/layout/header-hints.d.ts +11 -1
- package/dist/ui/layout/header-hints.js +35 -3
- package/dist/ui/layout/kit-chip.d.ts +50 -0
- package/dist/ui/layout/kit-chip.js +81 -0
- package/dist/ui/layout/virtual-window.js +9 -0
- package/dist/ui/overlays/HelpOverlay.js +7 -0
- package/dist/ui/overlays/KitEditor.d.ts +23 -0
- package/dist/ui/overlays/KitEditor.js +101 -0
- package/dist/ui/overlays/KitManager.d.ts +23 -0
- package/dist/ui/overlays/KitManager.js +104 -0
- package/dist/ui/overlays/KitPicker.d.ts +49 -0
- package/dist/ui/overlays/KitPicker.js +58 -0
- package/dist/ui/overlays/ModelProfilePicker.js +16 -6
- package/dist/ui/overlays/SettingsScreen.js +139 -16
- package/dist/ui/overlays/kit-picker-state.d.ts +87 -0
- package/dist/ui/overlays/kit-picker-state.js +136 -0
- package/dist/ui/overlays/kit-settings-state.d.ts +129 -0
- package/dist/ui/overlays/kit-settings-state.js +267 -0
- package/dist/ui/overlays/model-profile-state.d.ts +26 -3
- package/dist/ui/overlays/model-profile-state.js +59 -6
- package/dist/ui/scrollback/LiveTail.d.ts +2 -2
- package/dist/ui/scrollback/LiveTail.js +7 -2
- package/dist/ui/scrollback/live-preview.js +10 -3
- package/dist/ui/scrollback/tail-window.d.ts +12 -0
- package/dist/ui/scrollback/tail-window.js +17 -2
- package/dist/ui/scrollback/use-scrollback-view.js +7 -0
- package/dist/ui/team-status.d.ts +141 -0
- package/dist/ui/team-status.js +224 -0
- package/dist/ui/transcript-text.js +13 -4
- package/dist/ui/use-wheel-routing.js +2 -2
- package/package.json +2 -2
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type ModelSettingsSnapshot } from './model-profile-settings.js';
|
|
2
|
+
import { type ActiveKit } from '../config/kit-resolution.js';
|
|
3
|
+
import type { KitApplyResult } from '../config/model-kits.js';
|
|
2
4
|
import type { ModelSettingsDraft, ModelSettingsPatch, ModelSettingsSaveResult } from '../config/model-profile-store.js';
|
|
3
5
|
import { type CompactionIdentity } from '../compaction/memory-identity.js';
|
|
4
6
|
/**
|
|
@@ -629,6 +631,33 @@ export declare class AgentController {
|
|
|
629
631
|
saveModelSettingsPatch(patch: ModelSettingsPatch): ModelSettingsSaveResult;
|
|
630
632
|
/** Refresh both role connections and credentials while retaining conversation state. */
|
|
631
633
|
reloadModelSettings(): ModelSettingsSaveResult;
|
|
634
|
+
/**
|
|
635
|
+
* The narrow port `agent/kit-apply.ts` runs against (agent-model-kits §6.3).
|
|
636
|
+
*
|
|
637
|
+
* A LITERAL ADAPTER RATHER THAN `this`: the port's member names (`isBusy`,
|
|
638
|
+
* `hasKey`, `preview`) are deliberately NOT the controller's names, because a
|
|
639
|
+
* port that shadows a public method is one signature change away from silently
|
|
640
|
+
* calling itself. Three of the five members are one-liners onto existing
|
|
641
|
+
* methods; nothing here is new state.
|
|
642
|
+
*/
|
|
643
|
+
private kitApplyPort;
|
|
644
|
+
/**
|
|
645
|
+
* Apply a kit for THIS SESSION ONLY (D-7 / I-5): no commit, ever.
|
|
646
|
+
*
|
|
647
|
+
* Failed applies leave the live config and the file untouched (I-10), because
|
|
648
|
+
* the whole operation is one prepare-and-apply with no commit in between.
|
|
649
|
+
*/
|
|
650
|
+
applySessionKit(kitId: string): KitApplyResult;
|
|
651
|
+
/** Release the session kit and return to the config file's bindings (RV-P1-6). */
|
|
652
|
+
clearSessionKit(): KitApplyResult;
|
|
653
|
+
/**
|
|
654
|
+
* Which kit is in effect right now (session > default > disk bindings).
|
|
655
|
+
*
|
|
656
|
+
* DERIVED, never stored (D-5): the header chip, the settings root row and
|
|
657
|
+
* `/kit` all read this, so there is exactly one answer to "which preset am I
|
|
658
|
+
* running" and no copy of it that can drift.
|
|
659
|
+
*/
|
|
660
|
+
getActiveKit(): ActiveKit;
|
|
632
661
|
/** Prebuild all potentially failing model/prompt work before committing the file. */
|
|
633
662
|
prepareModelSettingsSnapshot(config: CliConfig): ModelSettingsSnapshot;
|
|
634
663
|
/** Apply prepared state synchronously, without rebuilding the controller or its history. */
|
package/dist/agent/controller.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { createModelSettingsDraft, saveModelSettings, reloadModelSettings, } from './model-profile-settings.js';
|
|
1
|
+
import { createModelSettingsDraft, previewModelSettings, saveModelSettings, reloadModelSettings, } from './model-profile-settings.js';
|
|
2
|
+
import { applySessionKit as applySessionKitPort, clearSessionKit as clearSessionKitPort } from './kit-apply.js';
|
|
3
|
+
import { resolveActiveKit } from '../config/kit-resolution.js';
|
|
2
4
|
import { randomUUID } from 'node:crypto';
|
|
3
5
|
import { join } from 'node:path';
|
|
4
6
|
import { verifyCompactionIdentity } from '../compaction/memory-identity.js';
|
|
@@ -1680,6 +1682,45 @@ export class AgentController {
|
|
|
1680
1682
|
}
|
|
1681
1683
|
/** Refresh both role connections and credentials while retaining conversation state. */
|
|
1682
1684
|
reloadModelSettings() { return reloadModelSettings(this); }
|
|
1685
|
+
/**
|
|
1686
|
+
* The narrow port `agent/kit-apply.ts` runs against (agent-model-kits §6.3).
|
|
1687
|
+
*
|
|
1688
|
+
* A LITERAL ADAPTER RATHER THAN `this`: the port's member names (`isBusy`,
|
|
1689
|
+
* `hasKey`, `preview`) are deliberately NOT the controller's names, because a
|
|
1690
|
+
* port that shadows a public method is one signature change away from silently
|
|
1691
|
+
* calling itself. Three of the five members are one-liners onto existing
|
|
1692
|
+
* methods; nothing here is new state.
|
|
1693
|
+
*/
|
|
1694
|
+
kitApplyPort() {
|
|
1695
|
+
return {
|
|
1696
|
+
getConfig: () => this.config,
|
|
1697
|
+
isBusy: () => this.isModelSettingsBusy(),
|
|
1698
|
+
hasKey: (providerId, role) => this.hasApiKey(providerId, role),
|
|
1699
|
+
createDraft: () => this.createModelSettingsDraft(),
|
|
1700
|
+
preview: (draft) => previewModelSettings(draft, this),
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Apply a kit for THIS SESSION ONLY (D-7 / I-5): no commit, ever.
|
|
1705
|
+
*
|
|
1706
|
+
* Failed applies leave the live config and the file untouched (I-10), because
|
|
1707
|
+
* the whole operation is one prepare-and-apply with no commit in between.
|
|
1708
|
+
*/
|
|
1709
|
+
applySessionKit(kitId) {
|
|
1710
|
+
return applySessionKitPort({ kitId, port: this.kitApplyPort() });
|
|
1711
|
+
}
|
|
1712
|
+
/** Release the session kit and return to the config file's bindings (RV-P1-6). */
|
|
1713
|
+
clearSessionKit() {
|
|
1714
|
+
return clearSessionKitPort(this.kitApplyPort());
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Which kit is in effect right now (session > default > disk bindings).
|
|
1718
|
+
*
|
|
1719
|
+
* DERIVED, never stored (D-5): the header chip, the settings root row and
|
|
1720
|
+
* `/kit` all read this, so there is exactly one answer to "which preset am I
|
|
1721
|
+
* running" and no copy of it that can drift.
|
|
1722
|
+
*/
|
|
1723
|
+
getActiveKit() { return resolveActiveKit(this.config); }
|
|
1683
1724
|
/** Prebuild all potentially failing model/prompt work before committing the file. */
|
|
1684
1725
|
prepareModelSettingsSnapshot(config) {
|
|
1685
1726
|
return { config, model: { providerId: config.provider, modelId: config.model,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session exit: apply a kit WITHOUT writing the config file (§4.2 exit B).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/agent/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* WHY A NARROW PORT AND NOT THE CONTROLLER: the same reason
|
|
7
|
+
* `ModelSettingsController` exists next door - a five-method port can be stubbed
|
|
8
|
+
* by one object literal in a test, so every refusal code is reachable without a
|
|
9
|
+
* controller, a provider, or a disk. The controller supplies the real one.
|
|
10
|
+
*
|
|
11
|
+
* EVERY PATH EITHER APPLIES OR CHANGES NOTHING (I-10). There is no partial state
|
|
12
|
+
* to clean up, because the whole thing is one `preview` call: the reused prepare
|
|
13
|
+
* chain builds the snapshot before it touches the live config, exactly as a save
|
|
14
|
+
* does - only the commit is missing.
|
|
15
|
+
*/
|
|
16
|
+
import type { CliConfig } from '../config/schema.js';
|
|
17
|
+
import type { ModelSettingsDraft, ModelSettingsSaveResult } from '../config/model-profile-store.js';
|
|
18
|
+
import type { HasApiKey } from '../team/model.js';
|
|
19
|
+
import { type KitApplyResult, type KitNote } from '../config/model-kits.js';
|
|
20
|
+
export interface KitApplyPort {
|
|
21
|
+
getConfig(): CliConfig;
|
|
22
|
+
/** `controller.isModelSettingsBusy` - a run, a team, a review, a compaction. */
|
|
23
|
+
isBusy(): boolean;
|
|
24
|
+
/** `controller.hasApiKey`: two-parameter form, so a role-scoped key is seen. */
|
|
25
|
+
hasKey: HasApiKey;
|
|
26
|
+
/** `controller.createModelSettingsDraft`: already seeded with `sessionKit`. */
|
|
27
|
+
createDraft(): ModelSettingsDraft;
|
|
28
|
+
/** `previewModelSettings`: validate + prepare + apply, never commit. */
|
|
29
|
+
preview(draft: ModelSettingsDraft): ModelSettingsSaveResult;
|
|
30
|
+
}
|
|
31
|
+
/** One note, as the sentence the toast shows. Exhaustive by construction. */
|
|
32
|
+
export declare function kitNoteMessage(note: KitNote): string;
|
|
33
|
+
/** Every note for a result, in the order the estimator emitted it. */
|
|
34
|
+
export declare function kitNoteMessages(notes: readonly KitNote[]): string[];
|
|
35
|
+
/** A non-empty sentence for EVERY code, including the two kit-only ones. */
|
|
36
|
+
export declare function kitApplyMessage(result: KitApplyResult): string;
|
|
37
|
+
/**
|
|
38
|
+
* Apply a kit for THIS SESSION ONLY.
|
|
39
|
+
*
|
|
40
|
+
* `kitId` is a resolved id: the `/kit use` command and the picker may accept a
|
|
41
|
+
* name or an `#id` prefix, but resolving those is the command layer's job, so
|
|
42
|
+
* an ambiguous name is reported once, at the surface that accepted it.
|
|
43
|
+
*/
|
|
44
|
+
export declare function applySessionKit(input: {
|
|
45
|
+
kitId: string;
|
|
46
|
+
port: KitApplyPort;
|
|
47
|
+
}): KitApplyResult;
|
|
48
|
+
/**
|
|
49
|
+
* Release the session kit: back to the bindings in the config file (RV-P1-6).
|
|
50
|
+
*
|
|
51
|
+
* WITHOUT THIS the feature is one-way: `ctrl+K` could enter a session kit and
|
|
52
|
+
* nothing could leave it except restarting the process. It shares the session
|
|
53
|
+
* exit's prepare chain, so it inherits every refusal code rather than defining
|
|
54
|
+
* a second set of failure semantics.
|
|
55
|
+
*/
|
|
56
|
+
export declare function clearSessionKit(port: KitApplyPort): KitApplyResult;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session exit: apply a kit WITHOUT writing the config file (§4.2 exit B).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/agent/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* WHY A NARROW PORT AND NOT THE CONTROLLER: the same reason
|
|
7
|
+
* `ModelSettingsController` exists next door - a five-method port can be stubbed
|
|
8
|
+
* by one object literal in a test, so every refusal code is reachable without a
|
|
9
|
+
* controller, a provider, or a disk. The controller supplies the real one.
|
|
10
|
+
*
|
|
11
|
+
* EVERY PATH EITHER APPLIES OR CHANGES NOTHING (I-10). There is no partial state
|
|
12
|
+
* to clean up, because the whole thing is one `preview` call: the reused prepare
|
|
13
|
+
* chain builds the snapshot before it touches the live config, exactly as a save
|
|
14
|
+
* does - only the commit is missing.
|
|
15
|
+
*/
|
|
16
|
+
import { emptyModelProfiles } from '../config/model-profiles.js';
|
|
17
|
+
import { findKit, kitCandidate, kitSlotConnections } from '../config/model-kits.js';
|
|
18
|
+
import { ALL_SLOTS } from '../config/kit-resolution.js';
|
|
19
|
+
import { modelSettingsResultMessage } from './model-profile-settings.js';
|
|
20
|
+
/**
|
|
21
|
+
* Kit-specific wording, layered over `modelSettingsResultMessage` (RV-P1-3).
|
|
22
|
+
*
|
|
23
|
+
* ONLY the codes whose save-screen text would send the user to the wrong screen
|
|
24
|
+
* are overridden. Everything else keeps the wording the settings screen already
|
|
25
|
+
* uses, so a `read_failed` reads the same wherever it appears.
|
|
26
|
+
*/
|
|
27
|
+
const KIT_CODE_MESSAGES = {
|
|
28
|
+
busy: 'Wait for the current run to finish before switching kit.',
|
|
29
|
+
conflict: 'Settings changed elsewhere. Reload before switching kit.',
|
|
30
|
+
write_failed: 'Could not write config. Your settings are unchanged.',
|
|
31
|
+
apply_failed: 'Could not apply the kit to this session. Nothing changed.',
|
|
32
|
+
kit_missing: 'That kit is not in your library. Create it in /settings (Settings > Kit pack).',
|
|
33
|
+
kit_references_missing_profile: 'That kit points at a profile that no longer exists.'
|
|
34
|
+
+ ' Fix it in /settings (Settings > Kit pack).',
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* The `KitNote` sentences, verbatim (agent-model-kits §4.2).
|
|
38
|
+
*
|
|
39
|
+
* ONE TABLE, THREE CALLERS: the picker, `/kit use`, and any future surface must
|
|
40
|
+
* say the same thing about the same consequence. `fast_review_enabled` is the
|
|
41
|
+
* one that matters most - it is a recurring charge the user did not ask for by
|
|
42
|
+
* name, only by applying a kit that switches the tier on.
|
|
43
|
+
*/
|
|
44
|
+
const KIT_NOTE_MESSAGES = {
|
|
45
|
+
fast_tier_enabled: 'Fast tier enabled by this kit.',
|
|
46
|
+
fast_review_enabled: 'Periodic review is on for this tier (/fast review off to stop it).',
|
|
47
|
+
fast_tier_off: 'Fast tier turned off by this kit.',
|
|
48
|
+
restart_required: 'Fast tier needs a restart to take effect.',
|
|
49
|
+
main_key_missing: 'Main profile has no API key; set one in /settings.',
|
|
50
|
+
subagent_key_missing: 'Subagent profile has no API key; children fall back to main.',
|
|
51
|
+
subagent_fast_unavailable: 'Subagent uses the fast tier, which this kit leaves off; children fall back to main.',
|
|
52
|
+
};
|
|
53
|
+
/** One note, as the sentence the toast shows. Exhaustive by construction. */
|
|
54
|
+
export function kitNoteMessage(note) { return KIT_NOTE_MESSAGES[note]; }
|
|
55
|
+
/** Every note for a result, in the order the estimator emitted it. */
|
|
56
|
+
export function kitNoteMessages(notes) {
|
|
57
|
+
return notes.map(kitNoteMessage);
|
|
58
|
+
}
|
|
59
|
+
/** A non-empty sentence for EVERY code, including the two kit-only ones. */
|
|
60
|
+
export function kitApplyMessage(result) {
|
|
61
|
+
if (result.ok) {
|
|
62
|
+
return result.released
|
|
63
|
+
? 'Kit released; using the config file\'s settings.'
|
|
64
|
+
: 'Kit applied for this session.';
|
|
65
|
+
}
|
|
66
|
+
const code = result.code ?? 'invalid';
|
|
67
|
+
const specific = KIT_CODE_MESSAGES[code];
|
|
68
|
+
if (specific)
|
|
69
|
+
return specific;
|
|
70
|
+
// NO `apply_failed` BRANCH ABOVE: it is in the table, because a stopped apply
|
|
71
|
+
// must say "nothing changed" rather than repeat the save screen's wording.
|
|
72
|
+
// Every remaining code falls through to the shared table, which is what makes
|
|
73
|
+
// "there is a code with no sentence" unrepresentable instead of merely unlikely.
|
|
74
|
+
return modelSettingsResultMessage({ ok: false, persisted: false, status: 'rejected',
|
|
75
|
+
code: code });
|
|
76
|
+
}
|
|
77
|
+
/** Slots whose referenced profile is gone (I-8 / AC-39). */
|
|
78
|
+
function missingSlots(kit, draft) {
|
|
79
|
+
const connections = kitSlotConnections(draft.profiles, kit);
|
|
80
|
+
const missing = [];
|
|
81
|
+
if (!connections.main)
|
|
82
|
+
missing.push('main');
|
|
83
|
+
if (kit.fastProfileId !== null && !connections.fast)
|
|
84
|
+
missing.push('fast');
|
|
85
|
+
if (kit.subagent.model === 'profile' && !connections.subagent.name)
|
|
86
|
+
missing.push('subagent');
|
|
87
|
+
return missing;
|
|
88
|
+
}
|
|
89
|
+
function missingIssue(slot, kit) {
|
|
90
|
+
const field = slot === 'subagent' ? 'subagent.modelProfileId' : `${slot}ProfileId`;
|
|
91
|
+
return { path: `modelProfiles.kits[${kit.id}].${field}`,
|
|
92
|
+
code: 'missing_reference',
|
|
93
|
+
message: `Kit "${kit.name}" references a profile that no longer exists (${slot}).` };
|
|
94
|
+
}
|
|
95
|
+
function keyMissing(port, draft, id, role) {
|
|
96
|
+
const entry = id ? draft.profiles?.entries.find((candidate) => candidate.id === id) : undefined;
|
|
97
|
+
if (!entry)
|
|
98
|
+
return false;
|
|
99
|
+
return !entry.apiKey && !port.hasKey(entry.provider, role);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Everything the user is entitled to know about the apply (§4.2 `KitNote`).
|
|
103
|
+
*
|
|
104
|
+
* Computed from the config AS IT WAS BEFORE, because half of these are about
|
|
105
|
+
* transitions ("the tier just came ON") and the other half are still true after.
|
|
106
|
+
*/
|
|
107
|
+
function kitNotes(input) {
|
|
108
|
+
const { before, draft, kit, port } = input;
|
|
109
|
+
const notes = [];
|
|
110
|
+
const wasEnabled = before.fast.enabled;
|
|
111
|
+
if (kit.fastProfileId !== null && !wasEnabled)
|
|
112
|
+
notes.push('fast_tier_enabled');
|
|
113
|
+
// The honest disclosure of a real cost (F-23): a fast tier with review on
|
|
114
|
+
// spends money on its own, every N turns, without being asked.
|
|
115
|
+
if (kit.fastProfileId !== null && !wasEnabled && before.fast.review) {
|
|
116
|
+
notes.push('fast_review_enabled');
|
|
117
|
+
}
|
|
118
|
+
if (kit.fastProfileId === null && wasEnabled)
|
|
119
|
+
notes.push('fast_tier_off');
|
|
120
|
+
if (input.restartRequired)
|
|
121
|
+
notes.push('restart_required');
|
|
122
|
+
if (keyMissing(port, draft, kit.mainProfileId, 'main'))
|
|
123
|
+
notes.push('main_key_missing');
|
|
124
|
+
if (kit.subagent.model === 'profile'
|
|
125
|
+
&& keyMissing(port, draft, kit.subagent.modelProfileId, 'main')) {
|
|
126
|
+
notes.push('subagent_key_missing');
|
|
127
|
+
}
|
|
128
|
+
if (kit.subagent.model === 'fast' && kit.fastProfileId === null) {
|
|
129
|
+
notes.push('subagent_fast_unavailable');
|
|
130
|
+
}
|
|
131
|
+
return notes;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Apply a kit for THIS SESSION ONLY.
|
|
135
|
+
*
|
|
136
|
+
* `kitId` is a resolved id: the `/kit use` command and the picker may accept a
|
|
137
|
+
* name or an `#id` prefix, but resolving those is the command layer's job, so
|
|
138
|
+
* an ambiguous name is reported once, at the surface that accepted it.
|
|
139
|
+
*/
|
|
140
|
+
export function applySessionKit(input) {
|
|
141
|
+
const { kitId, port } = input;
|
|
142
|
+
if (port.isBusy())
|
|
143
|
+
return { ok: false, code: 'busy', notes: [] };
|
|
144
|
+
const before = port.getConfig();
|
|
145
|
+
const draft = port.createDraft();
|
|
146
|
+
const library = draft.profiles ?? emptyModelProfiles();
|
|
147
|
+
const kit = findKit(library, kitId);
|
|
148
|
+
if (!kit)
|
|
149
|
+
return { ok: false, code: 'kit_missing', notes: [] };
|
|
150
|
+
const missing = missingSlots(kit, draft);
|
|
151
|
+
if (missing.length) {
|
|
152
|
+
return { ok: false, code: 'kit_references_missing_profile', notes: [],
|
|
153
|
+
issues: missing.map((slot) => missingIssue(slot, kit)) };
|
|
154
|
+
}
|
|
155
|
+
const patch = kitCandidate({ library, kit });
|
|
156
|
+
draft.profiles = patch.modelProfiles;
|
|
157
|
+
draft.patch = { ...draft.patch, team: patch.team, fast: patch.fast };
|
|
158
|
+
// I-13: a kit that turns the tier OFF must NOT project the fast role, or
|
|
159
|
+
// `projectModelProfiles`'s `continue` would copy the DISK fast endpoint into
|
|
160
|
+
// the live config and the kit would read as "restore, then switch off".
|
|
161
|
+
draft.activateRoles = kit.fastProfileId === null ? ['main'] : ['main', 'fast'];
|
|
162
|
+
draft.sessionKit = { id: kit.id, held: [...ALL_SLOTS] };
|
|
163
|
+
// THE MARKER, not a heuristic: this draft IS the kit, so `releasedSlots` must
|
|
164
|
+
// not read the apply's own writes as user decisions (§4.2 exit B).
|
|
165
|
+
draft.sessionKitFresh = true;
|
|
166
|
+
const result = port.preview(draft);
|
|
167
|
+
if (!result.ok) {
|
|
168
|
+
return { ok: false, code: result.code ?? 'invalid', notes: [],
|
|
169
|
+
...(result.issues ? { issues: result.issues } : {}) };
|
|
170
|
+
}
|
|
171
|
+
const notes = kitNotes({ before, draft, kit, port,
|
|
172
|
+
...(result.restartRequired ? { restartRequired: true } : {}) });
|
|
173
|
+
return { ok: true, notes };
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Release the session kit: back to the bindings in the config file (RV-P1-6).
|
|
177
|
+
*
|
|
178
|
+
* WITHOUT THIS the feature is one-way: `ctrl+K` could enter a session kit and
|
|
179
|
+
* nothing could leave it except restarting the process. It shares the session
|
|
180
|
+
* exit's prepare chain, so it inherits every refusal code rather than defining
|
|
181
|
+
* a second set of failure semantics.
|
|
182
|
+
*/
|
|
183
|
+
export function clearSessionKit(port) {
|
|
184
|
+
if (port.isBusy())
|
|
185
|
+
return { ok: false, code: 'busy', notes: [] };
|
|
186
|
+
const before = port.getConfig();
|
|
187
|
+
const draft = port.createDraft();
|
|
188
|
+
const file = draft.baseline;
|
|
189
|
+
draft.sessionKit = null;
|
|
190
|
+
draft.patch = {
|
|
191
|
+
...draft.patch,
|
|
192
|
+
team: { model: file.team.model, modelProfileId: file.team.modelProfileId },
|
|
193
|
+
// The disk fast section, KEY BY KEY: `enabled` restores the tier state and
|
|
194
|
+
// the other three restore the endpoints the file names, so no trace of the
|
|
195
|
+
// kit's fast choice survives in the live config.
|
|
196
|
+
fast: { enabled: file.fast.enabled, provider: file.fast.provider,
|
|
197
|
+
model: file.fast.model, baseUrl: file.fast.baseUrl },
|
|
198
|
+
};
|
|
199
|
+
draft.activateRoles = ['main', 'fast'];
|
|
200
|
+
const result = port.preview(draft);
|
|
201
|
+
if (!result.ok)
|
|
202
|
+
return { ok: false, code: result.code ?? 'invalid', notes: [] };
|
|
203
|
+
const notes = [];
|
|
204
|
+
if (result.restartRequired)
|
|
205
|
+
notes.push('restart_required');
|
|
206
|
+
if (before.fast.enabled && !file.fast.enabled)
|
|
207
|
+
notes.push('fast_tier_off');
|
|
208
|
+
return { ok: true, released: true, notes };
|
|
209
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ModelRef } from '@aragon-agent/core';
|
|
2
2
|
import { type CliConfig } from '../config/schema.js';
|
|
3
3
|
import { type ModelSettingsDraft, type ModelSettingsSaveResult } from '../config/model-profile-store.js';
|
|
4
|
+
import { type KitSlot } from '../config/model-kits.js';
|
|
4
5
|
/** Fully prepared live state; applying it performs no input parsing or disk access. */
|
|
5
6
|
export interface ModelSettingsSnapshot {
|
|
6
7
|
config: CliConfig;
|
|
@@ -22,10 +23,37 @@ export interface ModelSettingsController {
|
|
|
22
23
|
export declare function createModelSettingsDraft(live: CliConfig, liveRevision: number): ModelSettingsDraft;
|
|
23
24
|
/** Map stable failure codes to safe UI text; never include parser or SDK error messages. */
|
|
24
25
|
export declare function modelSettingsResultMessage(result: ModelSettingsSaveResult): string;
|
|
26
|
+
/**
|
|
27
|
+
* The slots THIS transaction explicitly decided, and that therefore leave the
|
|
28
|
+
* session kit (§4.3 / D-6).
|
|
29
|
+
*
|
|
30
|
+
* Released BY SLOT, never wholesale: a user who only retargeted the subagent
|
|
31
|
+
* must not lose the main and fast choices the kit made for this session, and a
|
|
32
|
+
* user who retargeted main must not have it silently ignored. Per-slot is the
|
|
33
|
+
* only rule shaped like the data (three independent slots).
|
|
34
|
+
*/
|
|
35
|
+
export declare function releasedSlots(draft: ModelSettingsDraft): KitSlot[];
|
|
25
36
|
/** Validate and prepare everything before the one synchronous, atomic disk commit. */
|
|
26
37
|
export declare function saveModelSettings(input: {
|
|
27
38
|
draft: ModelSettingsDraft;
|
|
28
39
|
controller: ModelSettingsController;
|
|
29
40
|
}): ModelSettingsSaveResult;
|
|
41
|
+
/**
|
|
42
|
+
* The SAME prepare chain, without the commit: a session kit (§4.2 exit B).
|
|
43
|
+
*
|
|
44
|
+
* NO `diskRevision` GATE: nothing is written, so a save that landed elsewhere is
|
|
45
|
+
* not this operation's business. It still refuses while work is in flight
|
|
46
|
+
* (`busy`) and still refuses a draft whose disk could not be read, so "nothing
|
|
47
|
+
* changed" is guaranteed for every other failure too (I-10).
|
|
48
|
+
*/
|
|
49
|
+
export declare function previewModelSettings(draft: ModelSettingsDraft, controller: ModelSettingsController): ModelSettingsSaveResult;
|
|
50
|
+
/**
|
|
51
|
+
* Apply a prepared snapshot.
|
|
52
|
+
*
|
|
53
|
+
* `persisted` HAS NO DEFAULT (RV-P1-4): the one attribute that distinguishes
|
|
54
|
+
* "the file now says this" from "only this process says this" is not something
|
|
55
|
+
* a call site should be able to forget.
|
|
56
|
+
*/
|
|
57
|
+
export declare function applyPreparedSnapshot(controller: ModelSettingsController, snapshot: ModelSettingsSnapshot, persisted: boolean): ModelSettingsSaveResult;
|
|
30
58
|
/** Reload the complete model snapshot; invalid disk content never replaces a live session. */
|
|
31
59
|
export declare function reloadModelSettings(controller: ModelSettingsController): ModelSettingsSaveResult;
|
|
@@ -4,7 +4,10 @@ import { mergePersistedConfig } from '../config/store.js';
|
|
|
4
4
|
import { adaptLegacyConnectionPatch, assertLegacyFastTarget, commitModelSettings, readModelSettingsDisk, } from '../config/model-profile-store.js';
|
|
5
5
|
import { projectModelProfiles, resolveModelProfileState } from '../config/model-profile-resolution.js';
|
|
6
6
|
import { validateModelProfiles } from '../config/model-profiles.js';
|
|
7
|
+
import { applyHeldToLive, carrySessionKit } from '../config/kit-resolution.js';
|
|
8
|
+
import { findKit } from '../config/model-kits.js';
|
|
7
9
|
import { registerProfileSecrets } from '../logging/secret-registry.js';
|
|
10
|
+
import { getLogger } from '../logging/logger.js';
|
|
8
11
|
/** Capture both baselines when the editor opens, without migrating or writing anything. */
|
|
9
12
|
export function createModelSettingsDraft(live, liveRevision) {
|
|
10
13
|
const disk = readModelSettingsDisk();
|
|
@@ -15,6 +18,11 @@ export function createModelSettingsDraft(live, liveRevision) {
|
|
|
15
18
|
profiles: disk.config?.modelProfiles
|
|
16
19
|
? structuredClone(disk.config.modelProfiles) : undefined,
|
|
17
20
|
activateRoles: [], patch: {},
|
|
21
|
+
// SEEDED FROM THE LIVE CONFIG (RV-P0-2). Without this line `draft.sessionKit`
|
|
22
|
+
// is always `undefined`, the `undefined` case below means "release", and any
|
|
23
|
+
// save made from the settings screen would silently drop a session kit the
|
|
24
|
+
// user just applied - the exact opposite of "this session only".
|
|
25
|
+
sessionKit: live.sessionKit ? structuredClone(live.sessionKit) : undefined,
|
|
18
26
|
...(!disk.ok ? { readError: 'Cannot read config. Repair it with aragon config edit.' } : {}),
|
|
19
27
|
};
|
|
20
28
|
}
|
|
@@ -61,6 +69,47 @@ function hasConnectionPatch(patch, role) {
|
|
|
61
69
|
const section = role === 'main' ? patch : patch.fast;
|
|
62
70
|
return !!section && ['provider', 'model', 'baseUrl'].some((key) => key in section);
|
|
63
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* The slots THIS transaction explicitly decided, and that therefore leave the
|
|
74
|
+
* session kit (§4.3 / D-6).
|
|
75
|
+
*
|
|
76
|
+
* Released BY SLOT, never wholesale: a user who only retargeted the subagent
|
|
77
|
+
* must not lose the main and fast choices the kit made for this session, and a
|
|
78
|
+
* user who retargeted main must not have it silently ignored. Per-slot is the
|
|
79
|
+
* only rule shaped like the data (three independent slots).
|
|
80
|
+
*/
|
|
81
|
+
export function releasedSlots(draft) {
|
|
82
|
+
const held = draft.sessionKit?.held ?? [];
|
|
83
|
+
if (!held.length)
|
|
84
|
+
return [];
|
|
85
|
+
// THE APPLY ITSELF RELEASES NOTHING (`kit-apply.ts` sets this marker). Without
|
|
86
|
+
// it the apply's own writes - `activateRoles`, `patch.team`, the re-bound
|
|
87
|
+
// library - read as "the user decided every slot", and the save would drop the
|
|
88
|
+
// three slots it was just handed.
|
|
89
|
+
if (draft.sessionKitFresh)
|
|
90
|
+
return [];
|
|
91
|
+
const kit = findKit(draft.profiles ?? draft.baseline.modelProfiles, draft.sessionKit?.id);
|
|
92
|
+
if (!kit)
|
|
93
|
+
return [...held];
|
|
94
|
+
const released = new Set();
|
|
95
|
+
for (const role of ['main', 'fast']) {
|
|
96
|
+
if (draft.activateRoles.includes(role) || connectionChanged(draft, role)
|
|
97
|
+
|| hasConnectionPatch(draft.patch, role))
|
|
98
|
+
released.add(role);
|
|
99
|
+
}
|
|
100
|
+
if (draft.patch.team !== undefined)
|
|
101
|
+
released.add('subagent');
|
|
102
|
+
return [...released].filter((slot) => held.includes(slot));
|
|
103
|
+
}
|
|
104
|
+
/** The session kit as it stands AFTER this transaction's explicit decisions. */
|
|
105
|
+
function narrowedSessionKit(draft) {
|
|
106
|
+
const current = draft.sessionKit;
|
|
107
|
+
if (!current)
|
|
108
|
+
return null;
|
|
109
|
+
const released = releasedSlots(draft);
|
|
110
|
+
const held = current.held.filter((slot) => !released.includes(slot));
|
|
111
|
+
return held.length ? { id: current.id, held } : null;
|
|
112
|
+
}
|
|
64
113
|
function liveCandidate(live, candidate, draft) {
|
|
65
114
|
const patch = draft.patch;
|
|
66
115
|
const normalizedPatch = {};
|
|
@@ -128,8 +177,16 @@ function liveCandidate(live, candidate, draft) {
|
|
|
128
177
|
next.apiKeyOverrideTarget = undefined;
|
|
129
178
|
}
|
|
130
179
|
else {
|
|
131
|
-
|
|
132
|
-
|
|
180
|
+
// I-13 (RV-P1-7): `projectModelProfiles` projects fast ONLY when the
|
|
181
|
+
// library binds one. With `fastId: null` the projection's `fast` is just the
|
|
182
|
+
// file's untouched section (`projected` starts as `{...file}`), so copying it
|
|
183
|
+
// here would resurrect the disk's fast endpoint underneath a kit that
|
|
184
|
+
// deliberately turned the tier off - the live config would then say "off"
|
|
185
|
+
// while naming a model nobody chose.
|
|
186
|
+
if (projected.modelProfiles?.fastId) {
|
|
187
|
+
next.fast = { ...next.fast, provider: projected.fast.provider,
|
|
188
|
+
model: projected.fast.model, baseUrl: projected.fast.baseUrl };
|
|
189
|
+
}
|
|
133
190
|
}
|
|
134
191
|
}
|
|
135
192
|
if (next.apiKeyOverride && !next.apiKeyOverrideTarget
|
|
@@ -139,49 +196,100 @@ function liveCandidate(live, candidate, draft) {
|
|
|
139
196
|
next.modelProfileState = resolveModelProfileState(next);
|
|
140
197
|
return next;
|
|
141
198
|
}
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const issues = validateModelProfiles(draft.profiles ??
|
|
199
|
+
/**
|
|
200
|
+
* Build both candidates and the snapshot.
|
|
201
|
+
*
|
|
202
|
+
* TWO CANDIDATES, NAMED DIFFERENTLY ON PURPOSE (RV-P0-1 / I-14). `diskCandidate`
|
|
203
|
+
* is the commit side and never learns about a held slot; `liveCandidateInput`
|
|
204
|
+
* is what the session sees and is never committed. Collapsing them into one
|
|
205
|
+
* variable is the defect this split exists to prevent: one unrelated save would
|
|
206
|
+
* then write the session's kit into the user's permanent defaults, and nothing
|
|
207
|
+
* visible would change.
|
|
208
|
+
*/
|
|
209
|
+
function prepareModelSettings(input) {
|
|
210
|
+
const { draft, controller, current } = input;
|
|
211
|
+
const issues = validateModelProfiles(draft.profiles ?? current.modelProfiles);
|
|
155
212
|
if (issues.length)
|
|
156
|
-
return { ...rejected('invalid'), issues };
|
|
157
|
-
let snapshot;
|
|
158
|
-
let candidate;
|
|
213
|
+
return { failure: { ...rejected('invalid'), issues } };
|
|
159
214
|
try {
|
|
160
|
-
const current = disk.config ?? structuredClone(DEFAULT_CONFIG);
|
|
161
215
|
const profilesChanged = draft.activateRoles.length > 0
|
|
162
216
|
|| JSON.stringify(draft.profiles) !== JSON.stringify(draft.baseline.modelProfiles);
|
|
163
217
|
const implicitLegacy = draft.profiles === undefined || !profilesChanged;
|
|
164
218
|
const patch = adaptLegacyConnectionPatch({ current, patch: implicitLegacy
|
|
165
219
|
? draft.patch : { ...draft.patch, modelProfiles: draft.profiles } });
|
|
166
|
-
|
|
167
|
-
registerProfileSecrets(
|
|
168
|
-
const
|
|
220
|
+
const diskCandidate = mergePersistedConfig(current, patch);
|
|
221
|
+
registerProfileSecrets(diskCandidate);
|
|
222
|
+
const sessionKit = narrowedSessionKit(draft);
|
|
223
|
+
const applied = applyHeldToLive(diskCandidate, sessionKit);
|
|
224
|
+
if (applied.dropped.length) {
|
|
225
|
+
getLogger().warn('agent', 'session_kit_slots_dropped', { slots: applied.dropped });
|
|
226
|
+
}
|
|
227
|
+
const next = liveCandidate(controller.getConfig(), applied.candidate, draft);
|
|
228
|
+
next.sessionKit = sessionKit ?? undefined;
|
|
169
229
|
// Disk preferences cannot reveal startup/session gateways retained by the live main role.
|
|
170
230
|
if (implicitLegacy && current.modelProfiles?.fastId && hasConnectionPatch(draft.patch, 'fast')) {
|
|
171
231
|
assertLegacyFastTarget(next.fast, next);
|
|
172
232
|
}
|
|
173
|
-
snapshot = controller.prepareModelSettingsSnapshot(next);
|
|
233
|
+
const snapshot = controller.prepareModelSettingsSnapshot(next);
|
|
234
|
+
return { prepared: { diskCandidate, liveCandidateInput: applied.candidate, snapshot } };
|
|
174
235
|
}
|
|
175
236
|
catch (error) {
|
|
176
237
|
const code = error.code ?? 'invalid';
|
|
177
|
-
return rejected(code);
|
|
238
|
+
return { failure: rejected(code) };
|
|
178
239
|
}
|
|
179
|
-
|
|
240
|
+
}
|
|
241
|
+
/** Validate and prepare everything before the one synchronous, atomic disk commit. */
|
|
242
|
+
export function saveModelSettings(input) {
|
|
243
|
+
const { draft, controller } = input;
|
|
244
|
+
if (controller.isModelSettingsBusy())
|
|
245
|
+
return rejected('busy');
|
|
246
|
+
if (draft.liveRevision !== controller.getSettingsRevision())
|
|
247
|
+
return rejected('conflict');
|
|
248
|
+
const disk = readModelSettingsDisk();
|
|
249
|
+
if (!disk.ok || draft.readError)
|
|
250
|
+
return rejected('read_failed');
|
|
251
|
+
if (disk.revision !== draft.diskRevision)
|
|
252
|
+
return rejected('conflict');
|
|
253
|
+
const prepared = prepareModelSettings({ draft, controller,
|
|
254
|
+
current: disk.config ?? structuredClone(DEFAULT_CONFIG) });
|
|
255
|
+
if (!prepared.prepared)
|
|
256
|
+
return prepared.failure;
|
|
257
|
+
const commit = commitModelSettings({ candidate: prepared.prepared.diskCandidate,
|
|
258
|
+
diskRevision: draft.diskRevision });
|
|
180
259
|
if (!commit.ok)
|
|
181
260
|
return rejected(commit.code ?? 'write_failed');
|
|
182
|
-
return applyPreparedSnapshot(controller, snapshot, true);
|
|
261
|
+
return applyPreparedSnapshot(controller, prepared.prepared.snapshot, true);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* The SAME prepare chain, without the commit: a session kit (§4.2 exit B).
|
|
265
|
+
*
|
|
266
|
+
* NO `diskRevision` GATE: nothing is written, so a save that landed elsewhere is
|
|
267
|
+
* not this operation's business. It still refuses while work is in flight
|
|
268
|
+
* (`busy`) and still refuses a draft whose disk could not be read, so "nothing
|
|
269
|
+
* changed" is guaranteed for every other failure too (I-10).
|
|
270
|
+
*/
|
|
271
|
+
export function previewModelSettings(draft, controller) {
|
|
272
|
+
if (controller.isModelSettingsBusy())
|
|
273
|
+
return rejected('busy');
|
|
274
|
+
if (draft.readError)
|
|
275
|
+
return rejected('read_failed');
|
|
276
|
+
const disk = readModelSettingsDisk();
|
|
277
|
+
if (!disk.ok)
|
|
278
|
+
return rejected('read_failed');
|
|
279
|
+
const prepared = prepareModelSettings({ draft, controller,
|
|
280
|
+
current: disk.config ?? structuredClone(DEFAULT_CONFIG) });
|
|
281
|
+
if (!prepared.prepared)
|
|
282
|
+
return prepared.failure;
|
|
283
|
+
return applyPreparedSnapshot(controller, prepared.prepared.snapshot, false);
|
|
183
284
|
}
|
|
184
|
-
|
|
285
|
+
/**
|
|
286
|
+
* Apply a prepared snapshot.
|
|
287
|
+
*
|
|
288
|
+
* `persisted` HAS NO DEFAULT (RV-P1-4): the one attribute that distinguishes
|
|
289
|
+
* "the file now says this" from "only this process says this" is not something
|
|
290
|
+
* a call site should be able to forget.
|
|
291
|
+
*/
|
|
292
|
+
export function applyPreparedSnapshot(controller, snapshot, persisted) {
|
|
185
293
|
try {
|
|
186
294
|
controller.applyModelSettingsSnapshot(snapshot);
|
|
187
295
|
}
|
|
@@ -220,14 +328,19 @@ export function reloadModelSettings(controller) {
|
|
|
220
328
|
if (live.unicode !== undefined)
|
|
221
329
|
resolved.unicode = live.unicode;
|
|
222
330
|
resolved.reducedMotion ||= !live.color;
|
|
331
|
+
// THE SESSION KIT SURVIVES A RELOAD (D-6): the three connections it holds
|
|
332
|
+
// must come from the kit, not from the file we just re-read, and the
|
|
333
|
+
// projection has to be recomputed so the live config stays equivalent to
|
|
334
|
+
// what its own bindings say (I-3).
|
|
335
|
+
const carried = carrySessionKit({ resolved, previous: live });
|
|
223
336
|
const after = readModelSettingsDisk();
|
|
224
337
|
if (!after.ok)
|
|
225
338
|
return rejected('read_failed');
|
|
226
339
|
if (after.revision !== disk.revision)
|
|
227
340
|
return rejected('conflict');
|
|
228
|
-
if (
|
|
341
|
+
if (carried.modelProfileState?.main.invalid)
|
|
229
342
|
return rejected('invalid');
|
|
230
|
-
return applyPreparedSnapshot(controller, controller.prepareModelSettingsSnapshot(
|
|
343
|
+
return applyPreparedSnapshot(controller, controller.prepareModelSettingsSnapshot(carried), false);
|
|
231
344
|
}
|
|
232
345
|
catch {
|
|
233
346
|
return rejected('invalid');
|
package/dist/agent/reducer.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ import type { RetryPhase } from './retry-view.js';
|
|
|
22
22
|
import type { FilePatch } from '../tools/patch.js';
|
|
23
23
|
export type NoticeLevel = 'info' | 'warn' | 'error';
|
|
24
24
|
export type ToastLevel = NoticeLevel | 'success';
|
|
25
|
-
export type Overlay = null | 'settings' | 'model' | 'help' | 'queue' | 'confirm' | 'question' | 'plan';
|
|
25
|
+
export type Overlay = null | 'settings' | 'model' | 'help' | 'queue' | 'confirm' | 'question' | 'plan' | 'kit';
|
|
26
26
|
export type ToolStatus = 'pending' | 'running' | 'done' | 'error';
|
|
27
27
|
/** Ephemeral, auto-dismissed acknowledgement (spec §3.9). */
|
|
28
28
|
export interface Toast {
|