@aragon-agent/cli 0.6.34 → 0.6.36
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 +21 -0
- package/README.md +173 -7
- package/dist/agent/agent-mode.d.ts +20 -4
- package/dist/agent/agent-mode.js +20 -4
- package/dist/agent/controller.d.ts +67 -3
- package/dist/agent/controller.js +106 -5
- package/dist/agent/headless.js +31 -0
- 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 +13 -2
- package/dist/agent/reducer.js +1 -0
- package/dist/cli.js +12 -0
- package/dist/commands/agents-command.d.ts +15 -0
- package/dist/commands/agents-command.js +27 -0
- package/dist/commands/builtins.js +135 -4
- package/dist/commands/kit-command.d.ts +20 -0
- package/dist/commands/kit-command.js +131 -0
- package/dist/commands/perf.d.ts +11 -0
- package/dist/commands/perf.js +2 -1
- package/dist/commands/registry.d.ts +14 -0
- package/dist/config/app-paths.d.ts +15 -0
- package/dist/config/app-paths.js +33 -2
- package/dist/config/env.d.ts +4 -1
- package/dist/config/env.js +28 -2
- package/dist/config/kit-resolution.d.ts +93 -0
- package/dist/config/kit-resolution.js +226 -0
- package/dist/config/load.d.ts +10 -0
- package/dist/config/load.js +8 -1
- 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 +84 -0
- package/dist/config/schema.js +39 -0
- package/dist/config/store.js +12 -1
- package/dist/config/ui-state.d.ts +18 -0
- package/dist/config/ui-state.js +9 -0
- package/dist/exec/events.d.ts +25 -0
- package/dist/exec/events.js +4 -0
- package/dist/exec/index.js +16 -1
- package/dist/exec/runner.js +10 -0
- package/dist/input/keymap.js +18 -4
- package/dist/proc/limits.d.ts +16 -0
- package/dist/proc/limits.js +16 -0
- package/dist/proc/supervisor.d.ts +20 -1
- package/dist/proc/supervisor.js +42 -14
- package/dist/prompts/command.d.ts +34 -0
- package/dist/prompts/command.js +237 -0
- package/dist/prompts/copy.d.ts +89 -0
- package/dist/prompts/copy.js +131 -0
- package/dist/prompts/fill.d.ts +51 -0
- package/dist/prompts/fill.js +46 -0
- package/dist/prompts/limits.d.ts +53 -0
- package/dist/prompts/limits.js +53 -0
- package/dist/prompts/normalize.d.ts +53 -0
- package/dist/prompts/normalize.js +190 -0
- package/dist/prompts/resolve.d.ts +31 -0
- package/dist/prompts/resolve.js +60 -0
- package/dist/prompts/search.d.ts +58 -0
- package/dist/prompts/search.js +146 -0
- package/dist/prompts/store.d.ts +86 -0
- package/dist/prompts/store.js +453 -0
- package/dist/prompts/types.d.ts +204 -0
- package/dist/prompts/types.js +13 -0
- package/dist/session/checkpoint.d.ts +8 -1
- package/dist/session/checkpoint.js +6 -0
- package/dist/session/persist.d.ts +19 -1
- package/dist/session/persist.js +13 -1
- package/dist/session/startup-session.d.ts +7 -2
- package/dist/session/startup-session.js +5 -2
- package/dist/session/store.js +3 -0
- package/dist/session/validate-session.js +64 -1
- package/dist/team/limits.d.ts +25 -0
- package/dist/team/limits.js +26 -0
- package/dist/team/runtime.js +27 -2
- package/dist/team/settled.d.ts +57 -0
- package/dist/team/settled.js +87 -0
- package/dist/team/subagent.js +48 -2
- package/dist/team/timeline.d.ts +74 -0
- package/dist/team/timeline.js +101 -0
- package/dist/team/types.d.ts +24 -1
- package/dist/todo/acceptance.d.ts +120 -0
- package/dist/todo/acceptance.js +399 -0
- package/dist/todo/follow-through.d.ts +48 -23
- package/dist/todo/follow-through.js +110 -44
- package/dist/todo/limits.d.ts +62 -1
- package/dist/todo/limits.js +62 -1
- package/dist/todo/panel-rows.d.ts +29 -39
- package/dist/todo/panel-rows.js +37 -83
- package/dist/todo/plan-sections.d.ts +60 -0
- package/dist/todo/plan-sections.js +108 -0
- package/dist/todo/prompt.d.ts +17 -12
- package/dist/todo/prompt.js +59 -40
- package/dist/todo/row-window.d.ts +51 -0
- package/dist/todo/row-window.js +107 -0
- package/dist/todo/store.d.ts +139 -31
- package/dist/todo/store.js +271 -73
- package/dist/todo/todo-tool.d.ts +33 -8
- package/dist/todo/todo-tool.js +186 -30
- package/dist/todo/types.d.ts +58 -0
- package/dist/ui/App.js +414 -54
- package/dist/ui/Composer.d.ts +6 -2
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/Header.d.ts +17 -1
- package/dist/ui/Header.js +8 -2
- package/dist/ui/PromptInput.d.ts +21 -1
- package/dist/ui/PromptInput.js +22 -2
- package/dist/ui/ScrollbackShell.d.ts +22 -7
- package/dist/ui/ScrollbackShell.js +8 -1
- package/dist/ui/StatusBar.d.ts +18 -0
- package/dist/ui/StatusBar.js +11 -3
- package/dist/ui/TeamPanel.d.ts +35 -41
- package/dist/ui/TeamPanel.js +53 -163
- package/dist/ui/TodoPanel.d.ts +17 -14
- package/dist/ui/TodoPanel.js +146 -38
- package/dist/ui/Transcript.js +23 -3
- package/dist/ui/composer-input.d.ts +10 -0
- package/dist/ui/composer-input.js +57 -24
- package/dist/ui/entries/TeamCard.d.ts +18 -2
- package/dist/ui/entries/TeamCard.js +47 -13
- package/dist/ui/entries/TodoCard.d.ts +26 -11
- package/dist/ui/entries/TodoCard.js +56 -15
- package/dist/ui/glyphs.d.ts +19 -0
- package/dist/ui/glyphs.js +4 -0
- package/dist/ui/layout/agents-board.d.ts +102 -0
- package/dist/ui/layout/agents-board.js +389 -0
- package/dist/ui/layout/cells.d.ts +61 -0
- package/dist/ui/layout/cells.js +156 -0
- package/dist/ui/layout/header-hints.d.ts +23 -3
- package/dist/ui/layout/header-hints.js +73 -7
- package/dist/ui/layout/kit-chip.d.ts +50 -0
- package/dist/ui/layout/kit-chip.js +81 -0
- package/dist/ui/layout/status-detail-layout.js +9 -0
- package/dist/ui/layout/status-layout.d.ts +13 -0
- package/dist/ui/layout/team-row.d.ts +139 -0
- package/dist/ui/layout/team-row.js +246 -0
- package/dist/ui/layout/todo-layout.d.ts +38 -0
- package/dist/ui/layout/todo-layout.js +75 -0
- package/dist/ui/layout/virtual-window.js +32 -3
- package/dist/ui/overlays/AgentsBoardOverlay.d.ts +57 -0
- package/dist/ui/overlays/AgentsBoardOverlay.js +183 -0
- package/dist/ui/overlays/HelpOverlay.js +24 -2
- 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/PromptEditorPage.d.ts +48 -0
- package/dist/ui/overlays/PromptEditorPage.js +227 -0
- package/dist/ui/overlays/PromptLibraryOverlay.d.ts +37 -0
- package/dist/ui/overlays/PromptLibraryOverlay.js +191 -0
- package/dist/ui/overlays/PromptPicker.d.ts +36 -0
- package/dist/ui/overlays/PromptPicker.js +129 -0
- package/dist/ui/overlays/PromptTextArea.d.ts +86 -0
- package/dist/ui/overlays/PromptTextArea.js +141 -0
- 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/overlays/prompt-library-state.d.ts +162 -0
- package/dist/ui/overlays/prompt-library-state.js +335 -0
- package/dist/ui/overlays/prompt-picker-state.d.ts +71 -0
- package/dist/ui/overlays/prompt-picker-state.js +98 -0
- package/dist/ui/scrollback/LiveTail.d.ts +2 -2
- package/dist/ui/scrollback/LiveTail.js +7 -2
- package/dist/ui/scrollback/band.js +14 -1
- 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 +166 -0
- package/dist/ui/team-status.js +252 -0
- package/dist/ui/transcript-text.js +13 -4
- package/dist/ui/use-startup-notices.d.ts +35 -1
- package/dist/ui/use-startup-notices.js +29 -2
- package/dist/ui/use-wheel-routing.js +10 -2
- package/package.json +2 -2
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';
|
|
@@ -344,7 +346,9 @@ export class AgentController {
|
|
|
344
346
|
this.todoRegistered = config.todo.enabled;
|
|
345
347
|
this.todoEnabled = config.todo.enabled;
|
|
346
348
|
this.todoPanelCapable = deps.todoPanelCapable === true;
|
|
347
|
-
this.todos = this.todoRegistered
|
|
349
|
+
this.todos = this.todoRegistered
|
|
350
|
+
? new TodoStore(Date.now, () => this.isTodoAcceptanceEnabled())
|
|
351
|
+
: null;
|
|
348
352
|
// --- Fast model tier ---------------------------------------------------
|
|
349
353
|
//
|
|
350
354
|
// RESOLVED BEFORE THE AGENT, because `fastRegistered` decides whether the
|
|
@@ -556,6 +560,12 @@ export class AgentController {
|
|
|
556
560
|
store: this.todos,
|
|
557
561
|
// Read LIVE: `/todo off` flips the flag, it cannot unregister.
|
|
558
562
|
isEnabled: () => this.todoEnabled,
|
|
563
|
+
// LIVE for the same reason (D-11): `/todo acceptance off` must take
|
|
564
|
+
// effect in a session already running, because the tool array
|
|
565
|
+
// itself cannot change. THE SAME PREDICATE the store was given, on
|
|
566
|
+
// purpose: two switches for one capability that could disagree is
|
|
567
|
+
// the shape of a session that cannot exist (IF-7).
|
|
568
|
+
hasAcceptance: () => this.isTodoAcceptanceEnabled(),
|
|
559
569
|
}),
|
|
560
570
|
]
|
|
561
571
|
: [],
|
|
@@ -910,6 +920,9 @@ export class AgentController {
|
|
|
910
920
|
todoBlock: this.todoRegistered && this.todoEnabled
|
|
911
921
|
? buildTodoBlock({
|
|
912
922
|
panelVisible: this.todoPanelCapable && config.todo.panel,
|
|
923
|
+
// LIVE, never snapshotted (D-11): the paragraph is part of the
|
|
924
|
+
// block, and `/todo acceptance off` rebuilds the prompt.
|
|
925
|
+
acceptance: config.todo.acceptance,
|
|
913
926
|
})
|
|
914
927
|
: '',
|
|
915
928
|
// The SAME two flags one more time, plus the live resolution — all three
|
|
@@ -1592,6 +1605,51 @@ export class AgentController {
|
|
|
1592
1605
|
getTodoSnapshot() {
|
|
1593
1606
|
return this.todos?.snapshot() ?? null;
|
|
1594
1607
|
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Whether the acceptance table is live for this session: the config key AND the
|
|
1610
|
+
* session switch. Two flags, exactly as `isTodoEnabled` needs `isTodoRegistered`
|
|
1611
|
+
* first - a capability that is on while `todo_write` refuses is not a
|
|
1612
|
+
* capability.
|
|
1613
|
+
*/
|
|
1614
|
+
isTodoAcceptanceEnabled() {
|
|
1615
|
+
return this.config.todo.acceptance && this.todoEnabled;
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Flip the acceptance capability and RE-RENDER THE PROMPT, byte for byte the
|
|
1619
|
+
* contract `setTodoEnabled` records above: the paragraph is part of
|
|
1620
|
+
* `<todo_planning>`, and the tool reads the same live value through its
|
|
1621
|
+
* `hasAcceptance` closure (D-11).
|
|
1622
|
+
*/
|
|
1623
|
+
setTodoAcceptanceEnabled(enabled) {
|
|
1624
|
+
this.setTodoConfig({ acceptance: enabled });
|
|
1625
|
+
// THE RAIL READS THE PROJECTION, NOT THE FLAG, so the flip has to re-project or
|
|
1626
|
+
// `/todo acceptance off` would leave the second section on screen until the
|
|
1627
|
+
// model's next write (P0-2's complaint, one path further in). The verdicts
|
|
1628
|
+
// themselves are kept: `getAcceptanceItems()` still returns them for `/save`,
|
|
1629
|
+
// the checkpoint and `exec`.
|
|
1630
|
+
this.todos?.refreshAcceptanceProjection();
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* The acceptance table as DATA, independent of whether it is being drawn.
|
|
1634
|
+
*
|
|
1635
|
+
* The persistence surfaces must use this and not `getTodoSnapshot()?.acceptance`:
|
|
1636
|
+
* the projection is `null` whenever the capability is off, and a checkpoint or a
|
|
1637
|
+
* `/save` that silently dropped the user's verdicts because a display switch was
|
|
1638
|
+
* off would be a data loss dressed as a setting.
|
|
1639
|
+
*/
|
|
1640
|
+
getAcceptanceItems() {
|
|
1641
|
+
return this.todos?.acceptanceItems() ?? [];
|
|
1642
|
+
}
|
|
1643
|
+
/**
|
|
1644
|
+
* `/todo acceptance reset`: every verdict back to `pending`, evidence dropped.
|
|
1645
|
+
*
|
|
1646
|
+
* The count comes BACK from the store rather than being recomputed by the
|
|
1647
|
+
* command, so the notice and the state cannot disagree about how many criteria
|
|
1648
|
+
* moved.
|
|
1649
|
+
*/
|
|
1650
|
+
reopenAcceptance() {
|
|
1651
|
+
return this.todos?.reopenAll() ?? 0;
|
|
1652
|
+
}
|
|
1595
1653
|
/**
|
|
1596
1654
|
* Subscribe to the CLI-local todo event stream.
|
|
1597
1655
|
*
|
|
@@ -1603,9 +1661,13 @@ export class AgentController {
|
|
|
1603
1661
|
return () => { };
|
|
1604
1662
|
return this.todos.subscribe(listener);
|
|
1605
1663
|
}
|
|
1606
|
-
/**
|
|
1607
|
-
|
|
1608
|
-
|
|
1664
|
+
/**
|
|
1665
|
+
* `/resume`. An absent or empty todo list CLEARS both tables — see
|
|
1666
|
+
* `TodoStore.restore` (P0-2). `acceptance` is optional so every existing caller
|
|
1667
|
+
* keeps compiling, and an omitted table reads as "this file has none".
|
|
1668
|
+
*/
|
|
1669
|
+
restoreTodos(items, acceptance = undefined) {
|
|
1670
|
+
this.todos?.restore(items, acceptance);
|
|
1609
1671
|
}
|
|
1610
1672
|
/**
|
|
1611
1673
|
* `/todo clear` and `/clear screen`. A user override of a projection (I-2's
|
|
@@ -1680,6 +1742,45 @@ export class AgentController {
|
|
|
1680
1742
|
}
|
|
1681
1743
|
/** Refresh both role connections and credentials while retaining conversation state. */
|
|
1682
1744
|
reloadModelSettings() { return reloadModelSettings(this); }
|
|
1745
|
+
/**
|
|
1746
|
+
* The narrow port `agent/kit-apply.ts` runs against (agent-model-kits §6.3).
|
|
1747
|
+
*
|
|
1748
|
+
* A LITERAL ADAPTER RATHER THAN `this`: the port's member names (`isBusy`,
|
|
1749
|
+
* `hasKey`, `preview`) are deliberately NOT the controller's names, because a
|
|
1750
|
+
* port that shadows a public method is one signature change away from silently
|
|
1751
|
+
* calling itself. Three of the five members are one-liners onto existing
|
|
1752
|
+
* methods; nothing here is new state.
|
|
1753
|
+
*/
|
|
1754
|
+
kitApplyPort() {
|
|
1755
|
+
return {
|
|
1756
|
+
getConfig: () => this.config,
|
|
1757
|
+
isBusy: () => this.isModelSettingsBusy(),
|
|
1758
|
+
hasKey: (providerId, role) => this.hasApiKey(providerId, role),
|
|
1759
|
+
createDraft: () => this.createModelSettingsDraft(),
|
|
1760
|
+
preview: (draft) => previewModelSettings(draft, this),
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Apply a kit for THIS SESSION ONLY (D-7 / I-5): no commit, ever.
|
|
1765
|
+
*
|
|
1766
|
+
* Failed applies leave the live config and the file untouched (I-10), because
|
|
1767
|
+
* the whole operation is one prepare-and-apply with no commit in between.
|
|
1768
|
+
*/
|
|
1769
|
+
applySessionKit(kitId) {
|
|
1770
|
+
return applySessionKitPort({ kitId, port: this.kitApplyPort() });
|
|
1771
|
+
}
|
|
1772
|
+
/** Release the session kit and return to the config file's bindings (RV-P1-6). */
|
|
1773
|
+
clearSessionKit() {
|
|
1774
|
+
return clearSessionKitPort(this.kitApplyPort());
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* Which kit is in effect right now (session > default > disk bindings).
|
|
1778
|
+
*
|
|
1779
|
+
* DERIVED, never stored (D-5): the header chip, the settings root row and
|
|
1780
|
+
* `/kit` all read this, so there is exactly one answer to "which preset am I
|
|
1781
|
+
* running" and no copy of it that can drift.
|
|
1782
|
+
*/
|
|
1783
|
+
getActiveKit() { return resolveActiveKit(this.config); }
|
|
1683
1784
|
/** Prebuild all potentially failing model/prompt work before committing the file. */
|
|
1684
1785
|
prepareModelSettingsSnapshot(config) {
|
|
1685
1786
|
return { config, model: { providerId: config.provider, modelId: config.model,
|
package/dist/agent/headless.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { formatCost, formatTokens, formatDuration, computeCost } from './usage.js';
|
|
8
8
|
import { formatStreamError } from './reducer.js';
|
|
9
|
+
import { derivePlanPhase } from '../todo/acceptance.js';
|
|
9
10
|
import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow-through.js';
|
|
10
11
|
/** Run a single prompt headlessly and resolve with the process exit code. */
|
|
11
12
|
export async function runHeadless(controller, prompt, options = {}) {
|
|
@@ -139,6 +140,11 @@ export async function runHeadless(controller, prompt, options = {}) {
|
|
|
139
140
|
// stream that is already `--quiet`-able in full.
|
|
140
141
|
let announcedTotal = 0;
|
|
141
142
|
let lastActive = -1;
|
|
143
|
+
// THE ACCEPTANCE SECTION'S DEDUPE KEY (todo-acceptance-criteria §6.13.1 / P2-9).
|
|
144
|
+
// `phase` is DERIVED, never cached (I-3), so the key carries a freshly computed
|
|
145
|
+
// phase plus the two counters that can only move when a verdict lands: a
|
|
146
|
+
// constant-size string is cheaper than a field that could drift from the truth.
|
|
147
|
+
let lastAcceptance = '';
|
|
142
148
|
const unsubscribeTodos = controller.subscribeTodos?.((event) => {
|
|
143
149
|
if (quiet || event.type !== 'updated')
|
|
144
150
|
return;
|
|
@@ -147,6 +153,20 @@ export async function runHeadless(controller, prompt, options = {}) {
|
|
|
147
153
|
announcedTotal = total;
|
|
148
154
|
err.write(`\n[todo] ${total} ${total === 1 ? 'step' : 'steps'} planned\n`);
|
|
149
155
|
lastActive = -1;
|
|
156
|
+
// A NEW PLAN GETS ITS OWN FIRST `[accept]` LINE, for the reason `lastActive`
|
|
157
|
+
// is reset one line up: the key below is (phase, checked, failed) and two
|
|
158
|
+
// different plans can share all three. Without this reset the second plan's
|
|
159
|
+
// 0/3 in a `-p` run that already printed 0/3 would go unreported.
|
|
160
|
+
lastAcceptance = '';
|
|
161
|
+
}
|
|
162
|
+
const acceptance = event.snapshot.acceptance;
|
|
163
|
+
if (acceptance && acceptance.total > 0) {
|
|
164
|
+
const key = `${derivePlanPhase(items, acceptance.items)}|` +
|
|
165
|
+
`${acceptance.checkedCount}|${acceptance.failedCount}`;
|
|
166
|
+
if (key !== lastAcceptance) {
|
|
167
|
+
lastAcceptance = key;
|
|
168
|
+
err.write(`[accept] ${acceptanceLine(acceptance)}\n`);
|
|
169
|
+
}
|
|
150
170
|
}
|
|
151
171
|
if (activeIndex < 0) {
|
|
152
172
|
if (lastActive !== -2) {
|
|
@@ -220,6 +240,17 @@ export async function runHeadless(controller, prompt, options = {}) {
|
|
|
220
240
|
`(${formatTokens(r.tailRelief.charsRemoved)} chars)\n`);
|
|
221
241
|
}
|
|
222
242
|
});
|
|
243
|
+
/**
|
|
244
|
+
* `[accept] <n>/<total> checked`, plus the failure count when there is one.
|
|
245
|
+
*
|
|
246
|
+
* ONE LINE PER TRANSITION, and the transitions are the ones a `-p` reader can
|
|
247
|
+
* act on: a verdict landed, or a criterion failed. `evidence` is NEVER printed
|
|
248
|
+
* (I-8): it belongs to the human-facing surfaces, and a machine log is not one.
|
|
249
|
+
*/
|
|
250
|
+
function acceptanceLine(acceptance) {
|
|
251
|
+
const base = `${acceptance.checkedCount}/${acceptance.total} checked`;
|
|
252
|
+
return acceptance.failedCount > 0 ? `${base}, ${acceptance.failedCount} failed` : base;
|
|
253
|
+
}
|
|
223
254
|
// --- The bounded continuation loop (todo-plan-followthrough §3.6). --------
|
|
224
255
|
//
|
|
225
256
|
// THE THREE `unsubscribe` CALLS STAY OUTSIDE IT (rule 4). The loop wraps the
|
|
@@ -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;
|