@aragon-agent/cli 0.6.12 → 0.6.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +81 -15
  2. package/dist/agent/controller.d.ts +29 -1
  3. package/dist/agent/controller.js +123 -22
  4. package/dist/agent/model-profile-settings.d.ts +31 -0
  5. package/dist/agent/model-profile-settings.js +199 -0
  6. package/dist/agent/queued-messages.d.ts +12 -0
  7. package/dist/agent/queued-messages.js +18 -0
  8. package/dist/agent/reducer.d.ts +13 -1
  9. package/dist/agent/reducer.js +51 -12
  10. package/dist/cli.js +15 -1
  11. package/dist/commands/builtins.js +66 -39
  12. package/dist/commands/queue-command.d.ts +3 -0
  13. package/dist/commands/queue-command.js +5 -0
  14. package/dist/commands/registry.d.ts +3 -0
  15. package/dist/compaction/child.d.ts +4 -2
  16. package/dist/compaction/child.js +4 -1
  17. package/dist/compaction/compactor.d.ts +7 -2
  18. package/dist/compaction/compactor.js +13 -11
  19. package/dist/compaction/context-command.js +5 -0
  20. package/dist/compaction/wiring.d.ts +4 -3
  21. package/dist/compaction/wiring.js +6 -1
  22. package/dist/config/cli-commands.d.ts +2 -0
  23. package/dist/config/cli-commands.js +16 -4
  24. package/dist/config/load.d.ts +2 -1
  25. package/dist/config/load.js +15 -8
  26. package/dist/config/model-profile-resolution.d.ts +20 -0
  27. package/dist/config/model-profile-resolution.js +81 -0
  28. package/dist/config/model-profile-store.d.ts +67 -0
  29. package/dist/config/model-profile-store.js +153 -0
  30. package/dist/config/model-profiles.d.ts +44 -0
  31. package/dist/config/model-profiles.js +121 -0
  32. package/dist/config/schema.d.ts +9 -0
  33. package/dist/config/store.d.ts +11 -1
  34. package/dist/config/store.js +43 -27
  35. package/dist/fast/resolve.d.ts +2 -1
  36. package/dist/fast/resolve.js +6 -4
  37. package/dist/fast/wiring.d.ts +5 -3
  38. package/dist/fast/wiring.js +8 -4
  39. package/dist/input/stdin-filter.js +3 -2
  40. package/dist/logging/install.js +4 -2
  41. package/dist/logging/secret-registry.d.ts +2 -0
  42. package/dist/logging/secret-registry.js +18 -0
  43. package/dist/team/runtime.d.ts +2 -1
  44. package/dist/team/subagent.d.ts +3 -1
  45. package/dist/team/subagent.js +3 -1
  46. package/dist/ui/ActivityLine.d.ts +6 -70
  47. package/dist/ui/ActivityLine.js +12 -34
  48. package/dist/ui/App.d.ts +1 -0
  49. package/dist/ui/App.js +113 -205
  50. package/dist/ui/BottomStatusRow.d.ts +6 -86
  51. package/dist/ui/BottomStatusRow.js +11 -26
  52. package/dist/ui/Composer.d.ts +16 -83
  53. package/dist/ui/Composer.js +9 -100
  54. package/dist/ui/PromptInput.d.ts +4 -1
  55. package/dist/ui/PromptInput.js +41 -34
  56. package/dist/ui/QueuePanel.d.ts +8 -0
  57. package/dist/ui/QueuePanel.js +9 -0
  58. package/dist/ui/QueueStatusRow.d.ts +2 -0
  59. package/dist/ui/QueueStatusRow.js +1 -1
  60. package/dist/ui/StatusBar.d.ts +13 -149
  61. package/dist/ui/StatusBar.js +27 -142
  62. package/dist/ui/clipboard-task.d.ts +29 -0
  63. package/dist/ui/clipboard-task.js +66 -0
  64. package/dist/ui/clipboard.d.ts +29 -50
  65. package/dist/ui/clipboard.js +141 -75
  66. package/dist/ui/composer-limits.d.ts +3 -4
  67. package/dist/ui/composer-limits.js +4 -9
  68. package/dist/ui/composer-rows.d.ts +1 -1
  69. package/dist/ui/composer-rows.js +24 -137
  70. package/dist/ui/editor-navigation.d.ts +45 -0
  71. package/dist/ui/editor-navigation.js +85 -0
  72. package/dist/ui/editor-reducer.d.ts +4 -0
  73. package/dist/ui/editor-reducer.js +29 -15
  74. package/dist/ui/interaction-copy.d.ts +77 -0
  75. package/dist/ui/interaction-copy.js +40 -0
  76. package/dist/ui/interaction-hints.d.ts +18 -0
  77. package/dist/ui/interaction-hints.js +92 -0
  78. package/dist/ui/layout/budget.js +1 -1
  79. package/dist/ui/layout/queue-layout.d.ts +19 -0
  80. package/dist/ui/layout/queue-layout.js +26 -0
  81. package/dist/ui/layout/status-layout.d.ts +55 -0
  82. package/dist/ui/layout/status-layout.js +109 -0
  83. package/dist/ui/layout/todo-layout.d.ts +1 -0
  84. package/dist/ui/layout/todo-layout.js +1 -1
  85. package/dist/ui/overlays/HelpOverlay.js +8 -8
  86. package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
  87. package/dist/ui/overlays/ModelProfileEditor.js +103 -0
  88. package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
  89. package/dist/ui/overlays/ModelProfilePicker.js +167 -0
  90. package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
  91. package/dist/ui/overlays/QueueOverlay.js +79 -0
  92. package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
  93. package/dist/ui/overlays/SettingsScreen.js +347 -3
  94. package/dist/ui/overlays/model-profile-state.d.ts +74 -0
  95. package/dist/ui/overlays/model-profile-state.js +119 -0
  96. package/dist/ui/run-status-row.d.ts +7 -19
  97. package/dist/ui/run-status-row.js +2 -18
  98. package/dist/ui/selection/selection-controller.d.ts +2 -2
  99. package/dist/ui/selection/selection-controller.js +1 -0
  100. package/dist/ui/use-wheel-routing.js +2 -1
  101. package/package.json +2 -2
package/README.md CHANGED
@@ -432,8 +432,8 @@ completes, `Up` / `Down` moves the selection, `Esc` closes it.
432
432
 
433
433
  | Key | Action |
434
434
  | --- | --- |
435
- | `Enter` | Submit (idle) / queue a steering message (running) — shown as `Queue: …` until the agent picks it up |
436
- | `Ctrl+J` / `Alt+Enter` / `Shift+Enter` | Insert a newline. `Ctrl+J` and `Alt+Enter` work everywhere; a distinct `Shift+Enter` needs a one-line terminal binding — see [`/terminal-setup`](#terminal-setup) |
435
+ | `Enter` | 空闲时发送,运行时加入队列;收到 Core 已纳入会话的接收凭据后才移除 Queue 提示 |
436
+ | `Ctrl+J` / `Alt+Enter` / `Shift+Enter` | 插入换行。Ctrl+J 已通过输入链路测试;另外两种组合键需要宿主发送独立序列,Alt+Enter 可能被宿主拦截。参见 [终端设置](#terminal-setup-for-shiftenter) |
437
437
  | `Shift+Tab` | Toggle **plan mode** (`BUILD` <-> `PLAN`). Equivalent: `/plan`. On Windows this needs a console flag Node < 22.17 does not set; aragon sets it at startup — see [Requirements](#requirements) |
438
438
  | `Ctrl+P` | The same toggle, over a channel no Windows console can swallow. Use it if `Shift+Tab` does nothing |
439
439
  | `Esc` | Close an overlay or popup; otherwise ask for interruption confirmation. While idle, cancel pending auto-continuation |
@@ -452,7 +452,7 @@ completes, `Up` / `Down` moves the selection, `Esc` closes it.
452
452
  | `Alt/Ctrl+←` / `→` | Word-wise cursor jump |
453
453
  | `Ctrl+W`, `Alt+Backspace` | Delete the previous word |
454
454
  | `Ctrl+U` / `Ctrl+K` | Kill to line start / end |
455
- | `Up` / `Down` | Move between draft lines; recall prompt history at the edges |
455
+ | `Up` / `Down` | 按视觉行移动;空草稿或正在浏览历史时,才在边界召回历史,避免覆盖新草稿 |
456
456
  | Paste | Inserted as-is up to 6 lines / 400 chars; larger collapses to `[Pasted text #1 +15 lines]`, which deletes as one unit and expands in full when you send. `--no-paste` restores the pre-0.6.3 behaviour |
457
457
  | `?` | Open help (empty input) |
458
458
 
@@ -462,7 +462,7 @@ new output automatically; once you scroll away the status bar shows `↑N`, a sl
462
462
  scrollbar occupies the terminal's last column (including 40-column windows). Drag its thumb
463
463
  to move through history, or click the track to move one page. Messages, the team panel
464
464
  and the input area scroll together; typing or editing returns to the input without
465
- losing the draft. The fixed status row shows `PgDn down` while reading history.
465
+ losing the draft. The fixed status row reports the distance from the end while reading history.
466
466
  `PgDn` moves one page down; editing or submitting returns directly to the end.
467
467
 
468
468
  **The mouse wheel is a viewport gesture and nothing else.** Wherever the pointer
@@ -498,10 +498,9 @@ Shift+Enter 与 Enter 都发送为 CR,应用无法区分;请用 `/terminal-s
498
498
  让 Shift+Enter 发送 `\u001b[13;2u`。合并到已有终端配置,不要覆盖其他快捷键;
499
499
  终端拦截某个组合键时可使用另外两种方式。参考绑定如下:
500
500
 
501
- - **Windows Terminal** — `settings.json`, `actions`:
502
- `{ "keys": "shift+enter", "command": { "action": "sendInput", "input": "\u001b[13;2u" } }`
501
+ - **Windows Terminal** — `settings.json` 的 `actions` 与 `keybindings` 分别定义动作和按键;完整示例见下文“输入、待处理队列与底部状态区”。
503
502
  - **VS Code integrated terminal** — `keybindings.json`:
504
- `{ "key": "shift+enter", "command": "workbench.action.terminal.sendSequence", "args": { "text": "\u001b[13;2u" } }`
503
+ `{ "key": "shift+enter", "command": "workbench.action.terminal.sendSequence", "when": "terminalFocus", "args": { "text": "\u001b[13;2u" } }`
505
504
  - **iTerm2** — Settings > Profiles > Keys > Key Mappings, `Shift+Enter` ->
506
505
  *Send Escape Sequence* `[13;2u`
507
506
  - **kitty / WezTerm / foot** — native once keyboard-enhancement mode is on
@@ -518,9 +517,9 @@ user-side binding reaches the same result for the one key that needs it.
518
517
 
519
518
  ## Queued messages while the agent runs
520
519
 
521
- Agent 运行中发送的消息会进入队列。固定状态行显示最早待接收消息的首个非空行,
522
- 多条时显示剩余数量;即使转录滚动离开底部,提示仍可见,且没有自动消失的时间限制。
523
- 转录同时保留每条完整消息:
520
+ Agent 运行中发送的消息会进入队列。输入框附近的独立 Queue 面板按顺序显示摘要,
521
+ 限高后显示隐藏条数;固定状态栏始终显示真实待处理总数,没有自动消失的时间限制。
522
+ 使用 `/queue` 分页查看所有消息的完整原文,未接收消息不会在普通转录中重复显示:
524
523
 
525
524
  ```
526
525
  ◷ Queue: also check the tests
@@ -528,11 +527,9 @@ Agent 运行中发送的消息会进入队列。固定状态行显示最早待
528
527
 
529
528
  仅当 Core 将消息写入对话历史并发出对应消息 ID 的接收回执,Queue 状态才移除。
530
529
  这表示引擎已接管后续处理,不表示回答完成;`turn_start`、动画和提示超时都不能代替回执。
531
- 相同正文的两次发送仍是两条消息。中断或失败后尚未接收的消息显示 `Queue (paused)`。
532
-
533
- 窄屏使用 `P>B` / `B>P` 表示待切换模式,`E2` / `E!` 表示两次 Esc 中断 / 再按 Esc 强停,
534
- `S+` / `T+` 表示后台服务 / TODO 存在,`Queue(p)` 表示暂停;`(+99+)` 表示至少还有
535
- 100 条消息。完整服务和 TODO 数量分别可用 `/bg` 与 `/todo status` 查看。
530
+ 相同正文的两次发送仍是两条消息。中断或失败后尚未接收的消息保留并标记暂停。
531
+ 窄屏优先保留运行状态、待处理数量和上下文;其他字段整项隐藏,不截断关键数值。
532
+ 快捷键集中在操作提示行,并随覆盖层、补全、选区和中断确认状态变化。
536
533
 
537
534
  `/clear` 清转录和 TODO,但保留待接收队列。保存和退出会补回被清屏或保留环裁剪的
538
535
  未发送全文。恢复存档时,queued 一律成为 `Queued but never sent: ...` 警告,
@@ -2345,3 +2342,72 @@ UTF-16 编辑和 ZWJ/组合字形仍有既有限制,本次修复保证视觉
2345
2342
  检查流式输出不阻止闪烁;单 ESC 不停止任务,两击中断,卡住时第三击强停;菜单关闭不计入两击。
2346
2343
  任务结束后保留最终计划,下一条普通消息启动时旧计划消失,新有效计划出现才恢复;显式续跑保留计划。
2347
2344
  强停后立即提交,在等待中双 ESC 取消,旧引擎退出后不得自行执行已取消的消息。
2345
+
2346
+ ## 输入、待处理队列与底部状态区
2347
+
2348
+ Enter 在空闲时发送,运行时加入队列;启动阶段拒绝重复提交并保留草稿。
2349
+ Ctrl+J 换行;终端能发送独立序列时,Shift+Enter 与 Alt+Enter 也可换行。
2350
+ 光标按完整字素移动,支持中文、组合字符和 emoji;输入最多占终端高度四分之一且不超过六行。
2351
+ 括号粘贴保留换行,不触发发送。过滤器或粘贴支持关闭时,不保证未标记多行粘贴安全。
2352
+ 过滤器不可用时 Delete 保守按 Backspace 处理,避免退格误删后文。
2353
+
2354
+ 应用鼠标拖选只建立选区,Ctrl+C 才复制;复制中再次按 Ctrl+C 不会退出或停止服务。
2355
+ 原生剪贴板工具完成后显示确认,OSC 52 只表示请求已发送,终端可能拒绝。
2356
+ Shift+拖动或 `/mouse off` 后的选区由宿主管理,应用无法关闭宿主的自动复制。
2357
+
2358
+ Queue 面板逐条显示待处理消息,数量始终留在状态栏。只有 Core 已把消息纳入会话并发回接收凭据,
2359
+ 该条提示才会消失。中断时未接收消息显示暂停;`/queue` 可分页查看全部原文,Esc 关闭。
2360
+ `/clear` 不丢待处理消息;`/reset`、切换会话会明确报告取消数量;恢复保存的未发送历史不会自动重发。
2361
+
2362
+ 底部依次为输入框、固定操作提示/短反馈行、全局状态栏。状态字段按显示宽度完整保留或整项隐藏。
2363
+ 上下文表示当前主 Agent 的已用/上限 tokens;`~` 表示含估算,`?` 表示上限未知。
2364
+ 费用是含子代理、快速模型及压缩的会话估算 USD,恢复会话沿用累计,缺价格按零计,零值不保证免费。
2365
+ 本轮均速是本轮期间入账输出 token 增量除以耗时,可能包含上述辅助调用,并非瞬时生成速度。
2366
+ `/context` 提供统计范围和完整值。
2367
+
2368
+ Windows Terminal:在设置 JSON **根级**添加 `"copyOnSelect": false`,把以下条目合并到已有数组:
2369
+
2370
+ ```json
2371
+ {
2372
+ "actions": [{ "id": "User.AragonNewline", "command": {
2373
+ "action": "sendInput", "input": "\u001b[13;2u"
2374
+ } }],
2375
+ "keybindings": [{ "keys": "shift+enter", "id": "User.AragonNewline" }]
2376
+ }
2377
+ ```
2378
+
2379
+ 依据:[Windows Terminal 交互设置](https://learn.microsoft.com/en-us/windows/terminal/customize-settings/interaction)、
2380
+ [按键动作](https://learn.microsoft.com/en-us/windows/terminal/customize-settings/actions)。PowerShell 是 shell,不能在其 profile 中设置终端按键映射。
2381
+
2382
+ VS Code 用户设置添加 `"terminal.integrated.copyOnSelection": false`,快捷键 JSON 添加:
2383
+
2384
+ ```json
2385
+ {
2386
+ "key": "shift+enter", "command": "workbench.action.terminal.sendSequence",
2387
+ "when": "terminalFocus", "args": { "text": "\u001b[13;2u" }
2388
+ }
2389
+ ```
2390
+
2391
+ 依据:[终端基础](https://code.visualstudio.com/docs/terminal/basics)、
2392
+ [终端高级设置](https://code.visualstudio.com/docs/terminal/advanced)。应用的 `/terminal-setup` 只输出说明,不修改宿主配置。
2393
+ 若宿主把 Shift+Enter 或 IME 候选确认同样编码为 CR,应用无法从相同字节推断用户意图。
2394
+ 可使用经输入链路测试的 Ctrl+J、独立 CSI-u 绑定及外部编辑后括号粘贴;具体宿主组合仍需实测。
2395
+ 双宿主、微软拼音及持续流式性能验收状态见 `docs/plans/tui-composer-queue-status/manual-test.md`。
2396
+
2397
+ ## 多套模型配置
2398
+
2399
+ 在 `/settings` 中使用 **Main profile** 和 **Fast profile**,可以分别为主任务与快速模型选择已保存的方案。两种角色共享配置库,但各自选择互不影响,也可以选择同一个方案。**Manage profiles** 提供新建、编辑、复制和删除;名称留空时采用模型名。同名方案通过地址主机和唯一 ID 区分。超过八条时按 `/` 搜索名称、模型或服务商。
2400
+
2401
+ 上下方向键或 Tab 移动,Enter 选择或编辑,根设置页 Ctrl+S 保存。普通文本字段的 Enter 仍保存设置。选择和子表单的修改先进入草稿;Esc 返回或确认丢弃。被主模型或快速模型引用的方案必须先解除绑定才能删除。复制包含本轮已经编辑的凭据,并产生新的 ID。
2402
+
2403
+ 方案保存服务商、模型、地址和凭据,不改变思考级别、token 上限、快速评审频率或预算。选择快速模型方案不会自动启用快速层。若本次启动时快速层未注册,首次启用会提示“已保存,待重启”;缺少模型或凭据时也需要先补齐。
2404
+
2405
+ **Current custom** 保留旧式连接,可继续编辑原连接字段。创建方案时从磁盘上的旧式配置预填,不复制环境变量或 `--api-key` 的运行时密钥。专属密钥可保持、替换或切换为共享/环境凭据;修改服务商或地址后,必须明确选择保留原密钥,或重新输入。空地址在方案中表示服务商默认地址,快速方案不会继承主网关。
2406
+
2407
+ 启动优先级为 CLI 参数、环境变量、所选方案、旧式字段、默认值。界面显示启动覆盖;明确重新选择方案可对本次会话立即生效,重启仍遵循启动参数。专属密钥仅对对应角色和目标地址生效;主模型启动密钥不会跟随切换发往另一个网关。
2408
+
2409
+ 运行、团队任务、后台评审或压缩尚未结束时禁止保存。保存采用原子文件替换,同时检查磁盘与当前会话版本;失败保留草稿,发生冲突时选择 Reload settings。写入已成功但会话应用异常时会明确提示重启并禁用重复保存。跨进程版本检查属于乐观检查,不能消除同时写入的极小竞态。
2410
+
2411
+ `/model`、`/fast model` 和 `config set` 修改连接会回到对应角色的自定义配置。若解除绑定会丢失专属账户或默认地址语义,命令会拒绝并引导到设置中编辑或复制。`/reload` 会一起刷新两个角色的连接和凭据,保留对话与用量。
2412
+
2413
+ 配置库仍位于 `ARAGON_HOME` 指向目录中的 `config.json`,不写入会话文件。密钥按现有机制明文保存:POSIX 创建权限为 0600,Windows 依赖用户目录 ACL,不提供加密。`config get/list` 对方案密钥使用固定掩码;损坏或未来版本的配置段整体隐藏。既有 `config edit` 备份也包含凭据。关闭通用日志脱敏、使用短密钥或分享第三方 SDK 错误日志时,仍需自行检查日志内容。
@@ -1,3 +1,5 @@
1
+ import { type ModelSettingsSnapshot } from './model-profile-settings.js';
2
+ import type { ModelSettingsDraft, ModelSettingsPatch, ModelSettingsSaveResult } from '../config/model-profile-store.js';
1
3
  /**
2
4
  * AgentController — builds the core `Agent`, wires the built-in toolset, and
3
5
  * exposes a small run/abort/steer surface plus the pre-flight validation that
@@ -11,6 +13,7 @@
11
13
  import type { PromptOptions, PromptOutcome } from './prompt-options.js';
12
14
  import { ModelRegistry, type AgentEvent, type AgentTool, type Message, type ModelInfo, type ModelRef, type ProviderRegistry, type ThinkingLevel } from '@aragon-agent/core';
13
15
  import type { CliConfig, CompactionConfig, FastConfig, RetryConfig, TeamConfig, ThemeName, TodoConfig } from '../config/schema.js';
16
+ import type { ModelRole } from '../config/model-profiles.js';
14
17
  import { type ConfirmRequest } from '../tools/index.js';
15
18
  import type { ToolPermission } from '../exec/permission.js';
16
19
  import { type ToolOutputListener } from '../tools/tool-output-store.js';
@@ -180,6 +183,9 @@ export declare class AgentController {
180
183
  private readonly outputListeners;
181
184
  private readonly fast;
182
185
  private readonly compaction;
186
+ private settingsRevision;
187
+ private modelSettingsBlocked;
188
+ private preparedSystemPrompt;
183
189
  private readonly contextMeter;
184
190
  private readonly procs;
185
191
  /**
@@ -249,7 +255,7 @@ export declare class AgentController {
249
255
  * Read-only CLI-layer helper for the Header/Welcome key-status dot (P1-1);
250
256
  * does not touch `@aragon-agent/core`.
251
257
  */
252
- hasApiKey(provider?: string): boolean;
258
+ hasApiKey(provider?: string, role?: ModelRole): boolean;
253
259
  /**
254
260
  * Set the color theme live. Mirrors the `setModel` / `setThinkingLevel`
255
261
  * mutator pattern — a subsequent re-render re-reads `getConfig().theme` and
@@ -524,6 +530,28 @@ export declare class AgentController {
524
530
  * the runtime nor the file.
525
531
  */
526
532
  setRetryConfig(patch: Partial<RetryConfig>): RetryConfig;
533
+ /** True while any request can still use a model or credential snapshot. */
534
+ isModelSettingsBusy(): boolean;
535
+ /** Monotonic revision protects an editor from all intervening settings changes. */
536
+ getSettingsRevision(): number;
537
+ /** Read-only draft creation; no startup migration or credential copying. */
538
+ createModelSettingsDraft(): ModelSettingsDraft;
539
+ /** Save an explicit draft as a single disk/live transaction. */
540
+ saveModelSettings(draft: ModelSettingsDraft): ModelSettingsSaveResult;
541
+ /** Legacy commands share the same lossless conversion and atomic save path. */
542
+ saveModelSettingsPatch(patch: ModelSettingsPatch): ModelSettingsSaveResult;
543
+ /** Refresh both role connections and credentials while retaining conversation state. */
544
+ reloadModelSettings(): ModelSettingsSaveResult;
545
+ /** Prebuild all potentially failing model/prompt work before committing the file. */
546
+ prepareModelSettingsSnapshot(config: CliConfig): ModelSettingsSnapshot;
547
+ /** Apply prepared state synchronously, without rebuilding the controller or its history. */
548
+ applyModelSettingsSnapshot(snapshot: ModelSettingsSnapshot): void;
549
+ /** Potentially failing ordinary-setting side effects are isolated from model application. */
550
+ applyModelSettingsEffects(): void;
551
+ /** A partially applied model snapshot must never reach another provider request. */
552
+ blockModelSettingsRequests(): void;
553
+ /** The UI uses this before accepting or draining queued prompts. */
554
+ areModelSettingsBlocked(): boolean;
527
555
  setModel(provider: string, model: string, baseUrl?: string): void;
528
556
  setThinkingLevel(level: ThinkingLevel): void;
529
557
  setMaxTokens(value: number | undefined): void;
@@ -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';
@@ -194,6 +196,9 @@ export class AgentController {
194
196
  // no timer while it has no subscribers, so the extra object is a few scalars
195
197
  // and a `Set`.
196
198
  // -----------------------------------------------------------------------
199
+ settingsRevision = 0;
200
+ modelSettingsBlocked = false;
201
+ preparedSystemPrompt;
197
202
  contextMeter;
198
203
  // -----------------------------------------------------------------------
199
204
  // Background services (background-service-supervision §3.3 / §3.6)
@@ -297,7 +302,7 @@ export class AgentController {
297
302
  // ONCE (C-2). `FastWiring` re-runs the same pure function in its own
298
303
  // constructor; the two agree by construction because they read the same
299
304
  // config through the same resolver.
300
- const fastTier = resolveFastTier(config, (id) => this.hasApiKey(id));
305
+ const fastTier = resolveFastTier(config, (id, role) => this.hasApiKey(id, role));
301
306
  const fastRegistered = fastTier.ok;
302
307
  // --- Team mode ---------------------------------------------------------
303
308
  this.teamRegistered = config.team.enabled;
@@ -321,11 +326,12 @@ export class AgentController {
321
326
  ...(this.config.baseUrl ? { baseUrl: this.config.baseUrl } : {}),
322
327
  },
323
328
  thinkingLevel: this.config.thinkingLevel,
329
+ role: 'main',
324
330
  },
325
331
  providerRegistry: this.providerRegistry,
326
332
  getCwd: () => this.cwd,
327
333
  getMode: () => this.effectiveMode,
328
- getApiKey: (id) => this.resolveKey(id),
334
+ getApiKey: (id, role) => this.resolveKey(id, role),
329
335
  // `skill_find` only, and the lead's always-on bodies. See the long
330
336
  // note in `subagent.ts` for why `skill` itself is withheld (D-16).
331
337
  ...(this.skillsEnabled
@@ -545,8 +551,8 @@ export class AgentController {
545
551
  this.compaction = config.compaction.enabled
546
552
  ? new CompactionWiring({
547
553
  getConfig: () => this.config,
548
- hasKey: (id) => this.hasApiKey(id),
549
- getApiKey: (id) => this.resolveKey(id),
554
+ hasKey: (id, role) => this.hasApiKey(id, role),
555
+ getApiKey: (id, role) => this.resolveKey(id, role),
550
556
  getModelInfoFor: (ref) => this.getModelInfoFor(ref),
551
557
  isPricedModel: (ref) => this.isPricedModel(ref),
552
558
  getMessages: () => this.agent.state.messages,
@@ -623,8 +629,8 @@ export class AgentController {
623
629
  this.fast = fastRegistered
624
630
  ? new FastWiring({
625
631
  getConfig: () => this.config,
626
- hasKey: (id) => this.hasApiKey(id),
627
- getApiKey: (id) => this.resolveKey(id),
632
+ hasKey: (id, role) => this.hasApiKey(id, role),
633
+ getApiKey: (id, role) => this.resolveKey(id, role),
628
634
  isPricedModel: (ref) => this.isPricedModel(ref),
629
635
  subscribe: (listener) => this.agent.subscribe(listener),
630
636
  complete: (providerId, request) => this.providerRegistry.complete(providerId, request),
@@ -673,8 +679,8 @@ export class AgentController {
673
679
  if (problem)
674
680
  deps.notify?.('warn', problem);
675
681
  }
676
- resolveKey(providerId) {
677
- return makeGetApiKey(this.config)(providerId);
682
+ resolveKey(providerId, role = 'main') {
683
+ return makeGetApiKey(this.config, role)(providerId);
678
684
  }
679
685
  /**
680
686
  * The tool names registered right now — LAZY, and the laziness is required.
@@ -705,27 +711,30 @@ export class AgentController {
705
711
  // suddenly denies having any skills). Structure, not call ordering, is what
706
712
  // rules that out — so if you add a third caller, route it through here.
707
713
  // -----------------------------------------------------------------------
708
- composeSystemPrompt() {
714
+ composeSystemPrompt(config = this.config) {
715
+ const tier = resolveFastTier(config, (id, role) => !!makeGetApiKey(config, role)(id));
716
+ const fastAvailable = config === this.config ? this.fast?.available() === true
717
+ : this.fast?.isRegistered() && config.fast.enabled && tier.ok;
709
718
  return buildSystemPrompt({
710
719
  cwd: this.cwd,
711
720
  tools: this.tools,
712
721
  skillsBlock: this.skillsEnabled ? this.skills.catalogBlock() + this.skills.alwaysBlock() : '',
713
722
  agentMode: this.effectiveMode,
714
723
  planInteractive: this.planInteractive,
715
- planMaxAskRounds: this.config.planModeMaxAskRounds,
724
+ planMaxAskRounds: config.planModeMaxAskRounds,
716
725
  // BOTH flags, and both are required (§4.5). `teamRegistered` false means
717
726
  // there is no `task` tool to advertise; `teamEnabled` false means the user
718
727
  // turned it off for this session. Advertising in either case would
719
728
  // describe a capability the model does not have.
720
729
  teamBlock: this.teamRegistered && this.teamEnabled
721
730
  ? buildTeamBlock({
722
- maxSubagents: this.config.team.maxSubagents,
723
- maxConcurrent: this.config.team.maxConcurrent,
731
+ maxSubagents: config.team.maxSubagents,
732
+ maxConcurrent: config.team.maxConcurrent,
724
733
  // One cross-reference sentence, present only when the capability
725
734
  // is (§3.8). `this.fast` is null during the FIRST compose (the
726
735
  // wiring needs the Agent), which is why the constructor composes
727
736
  // again once it exists.
728
- fastDelegation: this.fast?.delegationAvailable() === true,
737
+ fastDelegation: !!fastAvailable && config.fast.delegate,
729
738
  })
730
739
  : '',
731
740
  // BOTH FLAGS, exactly as team mode uses both, and for the identical
@@ -735,18 +744,18 @@ export class AgentController {
735
744
  // model does not have.
736
745
  todoBlock: this.todoRegistered && this.todoEnabled
737
746
  ? buildTodoBlock({
738
- panelVisible: this.todoPanelCapable && this.config.todo.panel,
747
+ panelVisible: this.todoPanelCapable && config.todo.panel,
739
748
  })
740
749
  : '',
741
750
  // The SAME two flags one more time, plus the live resolution — all three
742
751
  // live inside `FastWiring.available()` (§3.3). `buildFastBlock` itself
743
752
  // returns `''` when neither capability is on, so a user who turned both
744
753
  // off pays nothing for the tier being technically live.
745
- fastBlock: this.fast && this.fast.available()
754
+ fastBlock: fastAvailable
746
755
  ? buildFastBlock({
747
- model: this.fast.fastRef()?.modelId ?? '',
748
- delegate: this.config.fast.delegate,
749
- review: this.config.fast.review,
756
+ model: tier.ok ? tier.ref.modelId : '',
757
+ delegate: config.fast.delegate,
758
+ review: config.fast.review,
750
759
  })
751
760
  : '',
752
761
  // ONE FLAG, unlike team/todo's two: there is no live on/off switch for
@@ -762,7 +771,7 @@ export class AgentController {
762
771
  });
763
772
  }
764
773
  rebuildSystemPrompt() {
765
- this.agent.setSystemPrompt(this.composeSystemPrompt());
774
+ this.agent.setSystemPrompt(this.preparedSystemPrompt ?? this.composeSystemPrompt());
766
775
  }
767
776
  /** Re-render the prompt from the current skill set and tell the UI. */
768
777
  refreshSkills() {
@@ -781,8 +790,10 @@ export class AgentController {
781
790
  * Read-only CLI-layer helper for the Header/Welcome key-status dot (P1-1);
782
791
  * does not touch `@aragon-agent/core`.
783
792
  */
784
- hasApiKey(provider) {
785
- const key = this.resolveKey(provider ?? this.config.provider);
793
+ hasApiKey(provider, role = 'main') {
794
+ const activeProvider = role === 'fast'
795
+ ? this.config.fast.provider || this.config.provider : this.config.provider;
796
+ const key = this.resolveKey(provider ?? activeProvider, role);
786
797
  return !!key && key.trim().length > 0;
787
798
  }
788
799
  /**
@@ -791,6 +802,7 @@ export class AgentController {
791
802
  * re-memoizes the palette (P1-1). Does not touch `@aragon-agent/core`.
792
803
  */
793
804
  setTheme(name) {
805
+ this.settingsRevision += 1;
794
806
  this.config = { ...this.config, theme: name };
795
807
  }
796
808
  /**
@@ -811,6 +823,9 @@ export class AgentController {
811
823
  // Pre-flight (R2): validate BEFORE entering the loop.
812
824
  // -----------------------------------------------------------------------
813
825
  preflight() {
826
+ if (this.modelSettingsBlocked)
827
+ return { ok: false, kind: 'config',
828
+ message: 'Settings were saved but not fully applied. Restart before sending requests.' };
814
829
  const provider = this.config.provider;
815
830
  if (!isAdapterProvider(provider)) {
816
831
  return {
@@ -845,6 +860,9 @@ export class AgentController {
845
860
  */
846
861
  async prompt(text, options = {}) {
847
862
  // Capture BEFORE stopping: synchronous abort listeners may cancel this request.
863
+ if (this.modelSettingsBlocked) {
864
+ return { status: 'not-started', reason: 'failed' };
865
+ }
848
866
  const request = ++this.startupSequence;
849
867
  try {
850
868
  if (this.agent.state.isRunning) {
@@ -1054,7 +1072,14 @@ export class AgentController {
1054
1072
  const id = user ? `${this.steeringPrefix}:${++this.steeringSequence}` : undefined;
1055
1073
  if (id !== undefined)
1056
1074
  this.pendingUserSteering.add(id);
1057
- this.agent.steer(text, id);
1075
+ try {
1076
+ this.agent.steer(text, id);
1077
+ }
1078
+ catch (error) {
1079
+ if (id !== undefined)
1080
+ this.pendingUserSteering.delete(id);
1081
+ throw error;
1082
+ }
1058
1083
  return id;
1059
1084
  }
1060
1085
  followUp(text) {
@@ -1192,6 +1217,7 @@ export class AgentController {
1192
1217
  * honestly by checking `isTeamRegistered()` first.
1193
1218
  */
1194
1219
  setTeamEnabled(enabled) {
1220
+ this.settingsRevision += 1;
1195
1221
  this.teamEnabled = enabled;
1196
1222
  this.rebuildSystemPrompt();
1197
1223
  }
@@ -1205,6 +1231,7 @@ export class AgentController {
1205
1231
  */
1206
1232
  setTeamConfig(patch) {
1207
1233
  const team = clampTeamConfig({ ...this.config.team, ...patch });
1234
+ this.settingsRevision += 1;
1208
1235
  this.config = { ...this.config, team };
1209
1236
  this.rebuildSystemPrompt();
1210
1237
  return team;
@@ -1266,6 +1293,7 @@ export class AgentController {
1266
1293
  * reports that honestly by checking `isTodoRegistered()` first.
1267
1294
  */
1268
1295
  setTodoEnabled(enabled) {
1296
+ this.settingsRevision += 1;
1269
1297
  this.todoEnabled = enabled;
1270
1298
  this.rebuildSystemPrompt();
1271
1299
  }
@@ -1288,6 +1316,7 @@ export class AgentController {
1288
1316
  */
1289
1317
  setTodoConfig(patch) {
1290
1318
  const todo = clampTodoConfig({ ...this.config.todo, ...patch });
1319
+ this.settingsRevision += 1;
1291
1320
  this.config = { ...this.config, todo };
1292
1321
  this.rebuildSystemPrompt();
1293
1322
  return todo;
@@ -1343,6 +1372,7 @@ export class AgentController {
1343
1372
  */
1344
1373
  setRetryConfig(patch) {
1345
1374
  const retry = clampRetryConfig({ ...this.config.retry, ...patch });
1375
+ this.settingsRevision += 1;
1346
1376
  this.config = { ...this.config, retry };
1347
1377
  this.providerRegistry.setRetryPolicy(toRetryPolicy(retry));
1348
1378
  return retry;
@@ -1350,8 +1380,69 @@ export class AgentController {
1350
1380
  // -----------------------------------------------------------------------
1351
1381
  // Mutators
1352
1382
  // -----------------------------------------------------------------------
1383
+ /** True while any request can still use a model or credential snapshot. */
1384
+ isModelSettingsBusy() {
1385
+ return this.isRunning() || this.isTeamBusy() || this.getCompactionSnapshot().inFlight
1386
+ || this.getFastStatus().snapshot.inFlight;
1387
+ }
1388
+ /** Monotonic revision protects an editor from all intervening settings changes. */
1389
+ getSettingsRevision() { return this.settingsRevision; }
1390
+ /** Read-only draft creation; no startup migration or credential copying. */
1391
+ createModelSettingsDraft() {
1392
+ return createModelSettingsDraft(this.config, this.settingsRevision);
1393
+ }
1394
+ /** Save an explicit draft as a single disk/live transaction. */
1395
+ saveModelSettings(draft) {
1396
+ return saveModelSettings({ draft, controller: this });
1397
+ }
1398
+ /** Legacy commands share the same lossless conversion and atomic save path. */
1399
+ saveModelSettingsPatch(patch) {
1400
+ const draft = this.createModelSettingsDraft();
1401
+ draft.profiles = undefined;
1402
+ draft.patch = patch;
1403
+ return this.saveModelSettings(draft);
1404
+ }
1405
+ /** Refresh both role connections and credentials while retaining conversation state. */
1406
+ reloadModelSettings() { return reloadModelSettings(this); }
1407
+ /** Prebuild all potentially failing model/prompt work before committing the file. */
1408
+ prepareModelSettingsSnapshot(config) {
1409
+ return { config, model: { providerId: config.provider, modelId: config.model,
1410
+ ...(config.baseUrl ? { baseUrl: config.baseUrl } : {}) },
1411
+ systemPrompt: this.composeSystemPrompt(config) };
1412
+ }
1413
+ /** Apply prepared state synchronously, without rebuilding the controller or its history. */
1414
+ applyModelSettingsSnapshot(snapshot) {
1415
+ this.preparedSystemPrompt = snapshot.systemPrompt;
1416
+ try {
1417
+ this.config = snapshot.config;
1418
+ this.settingsRevision += 1;
1419
+ this.agent.setModel(snapshot.model);
1420
+ this.agent.setThinkingLevel(snapshot.config.thinkingLevel);
1421
+ this.agent.setMaxTokens(snapshot.config.maxTokens);
1422
+ this.fast?.onConfigChanged(false, snapshot.config.fast.enabled);
1423
+ this.compaction?.onConfigChanged(snapshot.config.compaction.enabled);
1424
+ this.contextMeter.onWindowChanged();
1425
+ this.rebuildSystemPrompt();
1426
+ }
1427
+ finally {
1428
+ this.preparedSystemPrompt = undefined;
1429
+ }
1430
+ }
1431
+ /** Potentially failing ordinary-setting side effects are isolated from model application. */
1432
+ applyModelSettingsEffects() { getLogger().reconfigure(this.config.log); }
1433
+ /** A partially applied model snapshot must never reach another provider request. */
1434
+ blockModelSettingsRequests() {
1435
+ this.modelSettingsBlocked = true;
1436
+ this.config.modelSettingsRestartRequired = true;
1437
+ }
1438
+ /** The UI uses this before accepting or draining queued prompts. */
1439
+ areModelSettingsBlocked() { return this.modelSettingsBlocked; }
1353
1440
  setModel(provider, model, baseUrl) {
1441
+ if (this.isModelSettingsBusy())
1442
+ throw new Error('Model settings are busy.');
1443
+ this.settingsRevision += 1;
1354
1444
  this.config = { ...this.config, provider, model, baseUrl };
1445
+ this.config.modelProfileState = resolveModelProfileState(this.config);
1355
1446
  this.agent.setModel({ providerId: provider, modelId: model, ...(baseUrl ? { baseUrl } : {}) });
1356
1447
  // NOT NEUTRAL TO THE FAST TIER even when the user never touched a `fast.*`
1357
1448
  // key (§3.9 / RV-3): `fast.provider: ''` and `fast.baseUrl: ''` INHERIT from
@@ -1373,16 +1464,19 @@ export class AgentController {
1373
1464
  this.contextMeter.onWindowChanged();
1374
1465
  }
1375
1466
  setThinkingLevel(level) {
1467
+ this.settingsRevision += 1;
1376
1468
  this.config = { ...this.config, thinkingLevel: level };
1377
1469
  this.agent.setThinkingLevel(level);
1378
1470
  }
1379
1471
  setMaxTokens(value) {
1472
+ this.settingsRevision += 1;
1380
1473
  this.config = { ...this.config, maxTokens: value };
1381
1474
  this.agent.setMaxTokens(value);
1382
1475
  }
1383
1476
  setApiKey(provider, key) {
1384
1477
  // The Agent resolves keys via `resolveKey(this.config)` at call time, so
1385
1478
  // updating the config here is sufficient — no Agent rebuild needed.
1479
+ this.settingsRevision += 1;
1386
1480
  this.config = {
1387
1481
  ...this.config,
1388
1482
  apiKeys: { ...this.config.apiKeys, [provider]: key },
@@ -1397,6 +1491,7 @@ export class AgentController {
1397
1491
  }
1398
1492
  setCwd(cwd) {
1399
1493
  this.cwd = cwd;
1494
+ this.settingsRevision += 1;
1400
1495
  this.config = { ...this.config, cwd };
1401
1496
  // The project scope moved with the cwd, so rescan BEFORE rebuilding — then
1402
1497
  // let the single prompt entry point emit a prompt that reflects both the new
@@ -1581,6 +1676,7 @@ export class AgentController {
1581
1676
  * `/fast` reports that honestly by checking `isFastRegistered()` first.
1582
1677
  */
1583
1678
  setFastEnabled(enabled) {
1679
+ this.settingsRevision += 1;
1584
1680
  this.fast?.setEnabled(enabled);
1585
1681
  }
1586
1682
  getFastConfig() {
@@ -1596,6 +1692,7 @@ export class AgentController {
1596
1692
  */
1597
1693
  setFastConfig(patch) {
1598
1694
  const fast = clampFastConfig({ ...this.config.fast, ...patch });
1695
+ this.settingsRevision += 1;
1599
1696
  this.config = { ...this.config, fast };
1600
1697
  // `onConfigChanged` rebuilds the prompt; when there is no wiring (the tier
1601
1698
  // was never registered) nothing has to be rebuilt, because no block is
@@ -1695,6 +1792,7 @@ export class AgentController {
1695
1792
  * on purpose (§3.2).
1696
1793
  */
1697
1794
  setCompactionEnabled(enabled) {
1795
+ this.settingsRevision += 1;
1698
1796
  this.compaction?.setEnabled(enabled);
1699
1797
  }
1700
1798
  getCompactionConfig() {
@@ -1709,6 +1807,7 @@ export class AgentController {
1709
1807
  */
1710
1808
  setCompactionConfig(patch) {
1711
1809
  const compaction = clampCompactionConfig({ ...this.config.compaction, ...patch });
1810
+ this.settingsRevision += 1;
1712
1811
  this.config = { ...this.config, compaction };
1713
1812
  this.compaction?.onConfigChanged();
1714
1813
  return compaction;
@@ -1757,6 +1856,8 @@ export class AgentController {
1757
1856
  * unbypassable. `replaceMessages` validates NOTHING.
1758
1857
  */
1759
1858
  async compactNow(instructions) {
1859
+ if (this.modelSettingsBlocked)
1860
+ return { ok: false, reason: 'settings_restart_required' };
1760
1861
  if (!this.compaction)
1761
1862
  return { ok: false, reason: 'not_registered' };
1762
1863
  const controller = new AbortController();
@@ -0,0 +1,31 @@
1
+ import type { ModelRef } from '@aragon-agent/core';
2
+ import { type CliConfig } from '../config/schema.js';
3
+ import { type ModelSettingsDraft, type ModelSettingsSaveResult } from '../config/model-profile-store.js';
4
+ /** Fully prepared live state; applying it performs no input parsing or disk access. */
5
+ export interface ModelSettingsSnapshot {
6
+ config: CliConfig;
7
+ model: ModelRef;
8
+ systemPrompt: string;
9
+ }
10
+ /** Narrow controller port keeps persistence independent from the agent implementation. */
11
+ export interface ModelSettingsController {
12
+ getConfig(): CliConfig;
13
+ getSettingsRevision(): number;
14
+ isModelSettingsBusy(): boolean;
15
+ isFastRegistered(): boolean;
16
+ prepareModelSettingsSnapshot(config: CliConfig): ModelSettingsSnapshot;
17
+ applyModelSettingsSnapshot(snapshot: ModelSettingsSnapshot): void;
18
+ applyModelSettingsEffects(): void;
19
+ blockModelSettingsRequests(): void;
20
+ }
21
+ /** Capture both baselines when the editor opens, without migrating or writing anything. */
22
+ export declare function createModelSettingsDraft(live: CliConfig, liveRevision: number): ModelSettingsDraft;
23
+ /** Map stable failure codes to safe UI text; never include parser or SDK error messages. */
24
+ export declare function modelSettingsResultMessage(result: ModelSettingsSaveResult): string;
25
+ /** Validate and prepare everything before the one synchronous, atomic disk commit. */
26
+ export declare function saveModelSettings(input: {
27
+ draft: ModelSettingsDraft;
28
+ controller: ModelSettingsController;
29
+ }): ModelSettingsSaveResult;
30
+ /** Reload the complete model snapshot; invalid disk content never replaces a live session. */
31
+ export declare function reloadModelSettings(controller: ModelSettingsController): ModelSettingsSaveResult;