@aragon-agent/cli 0.6.23 → 0.6.24
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 +20 -2
- package/dist/agent/controller.d.ts +11 -0
- package/dist/agent/controller.js +33 -3
- package/dist/agent/model-profile-settings.js +29 -2
- package/dist/commands/builtins.js +87 -3
- package/dist/compaction/child.d.ts +15 -0
- package/dist/compaction/child.js +14 -3
- package/dist/compaction/compactor.js +1 -0
- package/dist/compaction/context-command.js +10 -1
- package/dist/compaction/meter.d.ts +9 -0
- package/dist/compaction/meter.js +16 -1
- package/dist/compaction/pressure.d.ts +7 -0
- package/dist/compaction/pressure.js +5 -0
- package/dist/compaction/types.d.ts +11 -0
- package/dist/compaction/wiring.js +1 -0
- package/dist/config/cli-commands.d.ts +1 -1
- package/dist/config/cli-commands.js +12 -0
- package/dist/config/load.js +24 -6
- package/dist/config/model-profile-resolution.d.ts +8 -0
- package/dist/config/model-profile-resolution.js +13 -6
- package/dist/config/model-profile-store.d.ts +4 -2
- package/dist/config/model-profiles.d.ts +13 -0
- package/dist/config/model-profiles.js +18 -2
- package/dist/config/schema.d.ts +61 -0
- package/dist/config/schema.js +36 -0
- package/dist/team/model.d.ts +88 -0
- package/dist/team/model.js +136 -0
- package/dist/team/report.d.ts +11 -0
- package/dist/team/report.js +24 -1
- package/dist/team/subagent.js +37 -21
- package/dist/team/task-tool.d.ts +25 -6
- package/dist/team/task-tool.js +15 -9
- package/dist/ui/App.js +5 -0
- package/dist/ui/overlays/ModelProfileEditor.js +18 -4
- package/dist/ui/overlays/ModelProfilePicker.d.ts +14 -2
- package/dist/ui/overlays/ModelProfilePicker.js +51 -10
- package/dist/ui/overlays/QuestionOverlay.js +47 -6
- package/dist/ui/overlays/SettingsScreen.d.ts +9 -0
- package/dist/ui/overlays/SettingsScreen.js +119 -17
- package/dist/ui/overlays/model-profile-state.d.ts +53 -4
- package/dist/ui/overlays/model-profile-state.js +111 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -406,7 +406,7 @@ signal ends the run (`130` SIGINT, `143` SIGTERM, `129` SIGHUP).
|
|
|
406
406
|
| `/save [file]` | Save the session to JSON |
|
|
407
407
|
| `/resume [file]` | Load a saved session |
|
|
408
408
|
| `/plan [on\|off\|status]` | Toggle plan mode - the keyboard-free equivalent of `Shift+Tab` |
|
|
409
|
-
| `/team [on\|off\|max <n>\|overseer on\|off]` | Team subagents: status and the live roster, the on/off switch, the fan-out width, the dispatch supervisor |
|
|
409
|
+
| `/team [on\|off\|max <n>\|overseer on\|off\|model [main\|fast\|<profile-name>]]` | Team subagents: status and the live roster, the on/off switch, the fan-out width, the dispatch supervisor |
|
|
410
410
|
| `/todo [on\|off\|panel on\|off\|follow <mode>\|clear\|continue]` | Todo planning: status, the on/off switch, the rail, follow-through, and picking an unfinished plan back up |
|
|
411
411
|
| `/retry [show\|on\|off\|max <n>]` | API retry: the effective ladder, the on/off switch, the retry count. Takes effect **in the running session**, children included |
|
|
412
412
|
| `/fast [on\|off\|model <id>\|provider <id>\|same\|review <n\|off>]` | Fast model tier: status and this session's totals, the on/off switch, which model, the review cadence |
|
|
@@ -792,6 +792,11 @@ questions.
|
|
|
792
792
|
- **`ask_user`** renders a keyboard-driven wizard: 1–5 questions, 2–4 options
|
|
793
793
|
each, exactly one marked `RECOMMENDED`, plus an `Other…` free-text option.
|
|
794
794
|
`↑`/`↓` choose, `Enter` confirms and advances, `←` goes back, `Esc` cancels.
|
|
795
|
+
**`Space` selects**: on a multi-choice question it toggles the highlighted
|
|
796
|
+
option; on a single-choice question it marks the highlighted option without
|
|
797
|
+
advancing, and `Enter` then submits the marked item — while a plain `Enter`
|
|
798
|
+
without `Space` still takes the highlighted option, recommended preselect
|
|
799
|
+
and all. `Space` does nothing on the `Other…` row (`Enter` opens its editor).
|
|
795
800
|
Pressing `Enter` through the whole wizard accepts every recommendation, which
|
|
796
801
|
is a deliberate "use your judgement" path rather than an accident. A last
|
|
797
802
|
screen lets you review before submitting.
|
|
@@ -1118,6 +1123,17 @@ parallel, then read one combined report of what they each found. It decides for
|
|
|
1118
1123
|
itself whether to delegate; a session that never calls `task` is the session
|
|
1119
1124
|
that existed before this feature.
|
|
1120
1125
|
|
|
1126
|
+
**Which model a subagent runs on is your setting, not the model's.** The
|
|
1127
|
+
default (`team.model: 'main'`) keeps main-agent parity: every child runs the
|
|
1128
|
+
lead's own connection. Switch it to the **fast tier** or a **saved profile**
|
|
1129
|
+
from the settings screen's *Subagent model* row or with
|
|
1130
|
+
`/team model main|fast|<profile-name>` — useful when a fan-out of read-only
|
|
1131
|
+
scouts does not need the main model. The choice is re-resolved on every
|
|
1132
|
+
dispatch, and an unusable one (tier off, profile deleted, key missing) falls
|
|
1133
|
+
back to the main model with a disclosed line in the dispatch report instead of
|
|
1134
|
+
failing the dispatch. The model itself still cannot downgrade its own
|
|
1135
|
+
delegation: a stale `model` key in a `task` call is ignored, exactly as before.
|
|
1136
|
+
|
|
1121
1137
|
```
|
|
1122
1138
|
> read the auth middleware, map the route table, and check the migration
|
|
1123
1139
|
|
|
@@ -2025,6 +2041,8 @@ env / `.env` → CLI flags**.
|
|
|
2025
2041
|
| `team.maxTurnsPerSubagent` | `0` | Runaway-loop cap, clamped to `[0, 100]`. `0` (the default) = no cap, matching the lead. A subagent stopped at a positive cap is reported as `stopped: turn cap reached`, with whatever it had produced. |
|
|
2026
2042
|
| `team.overseer` | `true` | The dispatch supervisor: one per dispatch on the fast model, ticking on a geometric cadence over every live subagent and deciding wait / nudge / replace / abandon. With the fast tier missing a tick degrades to an announced unassisted wait — never a kill; with this `false`, children run under the legacy hard ceilings exactly. `/team overseer on\|off` flips it live, and it is read per dispatch. |
|
|
2027
2043
|
| `team.overseerIntervalMs` | `0` | Supervisor cadence base, clamped to `[0, 3600000]`. `0` (the default) = the structural 300 s; a positive `team.subagentTimeoutMs` is honoured first when this is `0` (compat). Later checks grow ×1.6, clamped to `[60 s, 900 s]`; any nudge/replace/abandon resets that child's cadence to the base. Only meaningful while `team.overseer` is true. |
|
|
2044
|
+
| `team.model` | `"main"` | Which model every subagent runs on: `'main'` (the default - the lead's own connection), `'fast'` (the fast tier, re-resolved per dispatch), or `'profile'` (the entry named by `team.modelProfileId`). A **user-owned setting** — the model itself still cannot downgrade its own delegation, and an unusable choice falls back to the main model with a disclosed line in the dispatch report. Also settable via `/team model main\|fast\|<profile-name>` and the settings screen's **Subagent model** row. |
|
|
2045
|
+
| `team.modelProfileId` | `null` | The profile entry `team.model: 'profile'` binds to. Reference existence is not validated here — a dangling id degrades to the main model at resolution time, so deleting a profile never breaks a dispatch. |
|
|
2028
2046
|
| `todo.enabled` | `true` | Master switch for todo planning. `false` means the `todo_write` tool is never registered — `/todo on` in such a session saves the setting for next launch and says so, rather than advertising a tool that is not there. |
|
|
2029
2047
|
| `todo.panel` | `true` | Render the right-hand rail. Independent of `todo.enabled` on purpose: a screen-reader user wants the planning discipline without the column, and the system prompt varies one sentence accordingly. |
|
|
2030
2048
|
| `todo.followThrough` | `"notify"` | What happens when a run ends with steps left. `notify` says so and stops; `auto` continues the plan after a 3-second grace window, bounded by two structural limits (one fruitless attempt, 25 continuations per plan) that are deliberately **not** settings; `off` is silent. An unrecognized value falls back to `notify`. |
|
|
@@ -2521,7 +2539,7 @@ VS Code 用户设置添加 `"terminal.integrated.copyOnSelection": false`,快
|
|
|
2521
2539
|
|
|
2522
2540
|
在 `/settings` 中使用 **Main profile** 和 **Fast profile**,可以分别为主任务与快速模型选择已保存的方案。两种角色共享配置库,但各自选择互不影响,也可以选择同一个方案。**Manage profiles** 提供新建、编辑、复制和删除;名称留空时采用模型名。同名方案通过地址主机和唯一 ID 区分。超过八条时按 `/` 搜索名称、模型或服务商。
|
|
2523
2541
|
|
|
2524
|
-
上下方向键或 Tab 移动,Enter 选择或编辑,根设置页 Ctrl+S 保存。普通文本字段的 Enter 仍保存设置。选择和子表单的修改先进入草稿;Esc
|
|
2542
|
+
上下方向键或 Tab 移动,Enter 选择或编辑,根设置页 Ctrl+S 保存。普通文本字段的 Enter 仍保存设置。选择和子表单的修改先进入草稿;Esc 返回或确认丢弃。列表页(角色选择页与管理页)聚焦在方案条目上时按 `e` 直接编辑、按 `d` 直接进入删除确认。删除被引用的方案(主模型 / 快速模型 / 子代理)走“解除绑定并删除”单次流程:确认页会列出引用方,确认后绑定同步清空;未被引用的方案仍显示普通的 Delete。复制包含本轮已经编辑的凭据,并产生新的 ID。
|
|
2525
2543
|
|
|
2526
2544
|
方案保存服务商、模型、地址和凭据,不改变思考级别、token 上限、快速评审频率或预算。选择快速模型方案不会自动启用快速层。若本次启动时快速层未注册,首次启用会提示“已保存,待重启”;缺少模型或凭据时也需要先补齐。
|
|
2527
2545
|
|
|
@@ -20,6 +20,7 @@ import { type ConfirmRequest } from '../tools/index.js';
|
|
|
20
20
|
import type { ToolPermission } from '../exec/permission.js';
|
|
21
21
|
import { type ToolOutputListener } from '../tools/tool-output-store.js';
|
|
22
22
|
import type { FilePatch } from '../tools/patch.js';
|
|
23
|
+
import type { SubagentModelResolution } from '../team/model.js';
|
|
23
24
|
import type { TeamEvent, TeamSnapshot } from '../team/types.js';
|
|
24
25
|
import { ProcSupervisor } from '../proc/supervisor.js';
|
|
25
26
|
import type { ProcEventListener, ServiceSnapshot } from '../proc/types.js';
|
|
@@ -478,6 +479,16 @@ export declare class AgentController {
|
|
|
478
479
|
* value can never reach the runtime.
|
|
479
480
|
*/
|
|
480
481
|
setTeamConfig(patch: Partial<TeamConfig>): TeamConfig;
|
|
482
|
+
/**
|
|
483
|
+
* The dispatch-time subagent model resolution (subagent-model §3.4).
|
|
484
|
+
*
|
|
485
|
+
* One authority for the `task` report's disclosure line and its price
|
|
486
|
+
* table: both read through here, so the header can never name one model
|
|
487
|
+
* while pricing another. Re-resolved on every call for the reason
|
|
488
|
+
* `resolveFastTier` records - `/fast off`, a settings save and `/team model`
|
|
489
|
+
* all rewrite the inputs mid-session.
|
|
490
|
+
*/
|
|
491
|
+
subagentModelResolution(): SubagentModelResolution;
|
|
481
492
|
/** Refused mid-dispatch by `/team`; half a dispatch under two ceilings is not
|
|
482
493
|
* a result anything can afterwards explain (the reasoning `/reload` uses). */
|
|
483
494
|
isTeamBusy(): boolean;
|
package/dist/agent/controller.js
CHANGED
|
@@ -16,6 +16,7 @@ import { createChildReviewer } from '../team/child-reviewer.js';
|
|
|
16
16
|
import { TeamHumanQueue } from '../team/human-queue.js';
|
|
17
17
|
import { createFastOverseerProvider } from '../team/overseer.js';
|
|
18
18
|
import { createTaskTool } from '../team/task-tool.js';
|
|
19
|
+
import { resolveSubagentModel } from '../team/model.js';
|
|
19
20
|
import { buildTeamBlock } from '../team/prompt.js';
|
|
20
21
|
import { buildFastBlock } from '../fast/prompt.js';
|
|
21
22
|
import { ProcSupervisor } from '../proc/supervisor.js';
|
|
@@ -516,9 +517,22 @@ export class AgentController {
|
|
|
516
517
|
maxSubagents: () => this.config.team.maxSubagents,
|
|
517
518
|
hasApiKey: () => this.hasApiKey(),
|
|
518
519
|
activeProvider: () => this.config.provider,
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
|
|
520
|
+
// Priced at the model children ACTUALLY run (subagent-model §3.4):
|
|
521
|
+
// under the default `main` mode this is exactly the lead's own
|
|
522
|
+
// table (I-2 byte parity); a fast or profile model the static
|
|
523
|
+
// tables do not price yields undefined, so the report says
|
|
524
|
+
// `pricing unknown` rather than fabricating $0.00 (C-11 / RV-4).
|
|
525
|
+
modelCost: () => {
|
|
526
|
+
const resolution = this.subagentModelResolution();
|
|
527
|
+
if (resolution.effective === 'main')
|
|
528
|
+
return this.getModelInfo().cost;
|
|
529
|
+
return this.isPricedModel(resolution.ref)
|
|
530
|
+
? this.getModelInfoFor(resolution.ref).cost
|
|
531
|
+
: undefined;
|
|
532
|
+
},
|
|
533
|
+
// The same resolution, re-read per dispatch, feeds the report's
|
|
534
|
+
// disclosure line - one read per consumer, one authority both.
|
|
535
|
+
subagentModel: () => this.subagentModelResolution(),
|
|
522
536
|
// The FAST table for the supervisor's own spend line
|
|
523
537
|
// (subagent-overseer-v2 D-7 / R-P1-2). Reuses the tier
|
|
524
538
|
// resolution and the unknown-pricing predicate the fast
|
|
@@ -575,6 +589,10 @@ export class AgentController {
|
|
|
575
589
|
isWindowKnown: () => this.getModelInfo().contextWindowSource !== 'fallback',
|
|
576
590
|
// LIVE, so a settings-screen edit moves the denominator without a relaunch.
|
|
577
591
|
getWindowOverride: () => this.config.contextWindow,
|
|
592
|
+
// Beside the value, LIVE too: `/context` must name the surface that
|
|
593
|
+
// supplied the denominator (profile-context-window AC-9, one of four
|
|
594
|
+
// meter sites that must stay in lockstep).
|
|
595
|
+
getWindowOverrideOrigin: () => this.config.contextWindowOrigin,
|
|
578
596
|
});
|
|
579
597
|
// --- Context compaction, part 1: BEFORE the Agent ----------------------
|
|
580
598
|
//
|
|
@@ -1307,6 +1325,18 @@ export class AgentController {
|
|
|
1307
1325
|
this.rebuildSystemPrompt();
|
|
1308
1326
|
return team;
|
|
1309
1327
|
}
|
|
1328
|
+
/**
|
|
1329
|
+
* The dispatch-time subagent model resolution (subagent-model §3.4).
|
|
1330
|
+
*
|
|
1331
|
+
* One authority for the `task` report's disclosure line and its price
|
|
1332
|
+
* table: both read through here, so the header can never name one model
|
|
1333
|
+
* while pricing another. Re-resolved on every call for the reason
|
|
1334
|
+
* `resolveFastTier` records - `/fast off`, a settings save and `/team model`
|
|
1335
|
+
* all rewrite the inputs mid-session.
|
|
1336
|
+
*/
|
|
1337
|
+
subagentModelResolution() {
|
|
1338
|
+
return resolveSubagentModel(this.config, (id, role) => this.hasApiKey(id, role));
|
|
1339
|
+
}
|
|
1310
1340
|
/** Refused mid-dispatch by `/team`; half a dispatch under two ceilings is not
|
|
1311
1341
|
* a result anything can afterwards explain (the reasoning `/reload` uses). */
|
|
1312
1342
|
isTeamBusy() {
|
|
@@ -68,7 +68,11 @@ function liveCandidate(live, candidate, draft) {
|
|
|
68
68
|
normalizedPatch[key] = candidate[key];
|
|
69
69
|
}
|
|
70
70
|
const next = { ...live, ...normalizedPatch };
|
|
71
|
-
|
|
71
|
+
// 'team' joined the loop with the subagent-model picker (D-5): its rows
|
|
72
|
+
// write `draft.patch.team`, and `mergePersistedConfig` clamps the candidate
|
|
73
|
+
// through `clampTeamConfig` on the same per-key merge `/team` uses, so the
|
|
74
|
+
// live view and the file can never disagree about which model children run.
|
|
75
|
+
for (const section of ['apiKeys', 'fast', 'compaction', 'log', 'team']) {
|
|
72
76
|
const normalized = { ...live[section] };
|
|
73
77
|
for (const key of Object.keys(patch[section] ?? {})) {
|
|
74
78
|
normalized[key] = candidate[section][key];
|
|
@@ -84,10 +88,33 @@ function liveCandidate(live, candidate, draft) {
|
|
|
84
88
|
next.maxTokens = candidate.maxTokens ?? undefined;
|
|
85
89
|
if ('baseUrl' in patch)
|
|
86
90
|
next.baseUrl = candidate.baseUrl ?? undefined;
|
|
87
|
-
const
|
|
91
|
+
const projection = projectModelProfiles(candidate);
|
|
92
|
+
// `projected` keeps its name and every existing use (`projected.provider`
|
|
93
|
+
// etc.); the FULL projection is retained because `mainOwnsContextWindow`
|
|
94
|
+
// lives on it, not on `.file` (profile-context-window §6.4).
|
|
95
|
+
const projected = projection.file;
|
|
88
96
|
if (draft.profiles !== undefined || candidate.modelProfiles !== undefined) {
|
|
89
97
|
next.modelProfiles = structuredClone(projected.modelProfiles);
|
|
90
98
|
}
|
|
99
|
+
// THE WINDOW VALUE AND ITS ORIGIN RECOMPUTE IN THE SAME BATCH (P0-1): the
|
|
100
|
+
// meter's `getWindowOverride()` reads `this.config.contextWindow`, so a save
|
|
101
|
+
// that only recomputed the origin would leave a freshly edited profile
|
|
102
|
+
// window invisible until the next launch. env is a STARTUP-layer fact and no
|
|
103
|
+
// save may displace it (AC-4 / AC-14c); an explicit root-row edit takes the
|
|
104
|
+
// candidate value; a bound Main profile owns the denominator (value or null,
|
|
105
|
+
// tradeoff B); unbinding falls back to the legacy top-level key.
|
|
106
|
+
next.contextWindow = live.contextWindowOrigin === 'env'
|
|
107
|
+
? live.contextWindow
|
|
108
|
+
: 'contextWindow' in patch ? candidate.contextWindow
|
|
109
|
+
: projection.mainOwnsContextWindow ? projected.contextWindow ?? null
|
|
110
|
+
: candidate.contextWindow;
|
|
111
|
+
// ORIGIN OBEYS THE IFF CONTRACT (§6.1): value null means AUTO, so origin
|
|
112
|
+
// MUST reset to undefined - this unified formula is what removes the stale
|
|
113
|
+
// 'profile' a per-branch expression would leave behind on unbind-to-null.
|
|
114
|
+
next.contextWindowOrigin = next.contextWindow === null ? undefined
|
|
115
|
+
: live.contextWindowOrigin === 'env' ? 'env'
|
|
116
|
+
: 'contextWindow' in patch ? 'config'
|
|
117
|
+
: projection.mainOwnsContextWindow ? 'profile' : 'config';
|
|
91
118
|
next.liveToolOutput = live.liveToolOutput;
|
|
92
119
|
for (const role of ['main', 'fast']) {
|
|
93
120
|
if (!draft.activateRoles.includes(role) && !connectionChanged(draft, role)
|
|
@@ -16,6 +16,8 @@ import { buildContinuationMessage } from '../todo/follow-through.js';
|
|
|
16
16
|
import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
|
|
17
17
|
import { loadSession, resolveSessionPath, saveSession } from '../session/persist.js';
|
|
18
18
|
import { getLogger } from '../logging/logger.js';
|
|
19
|
+
import { resolveFastTier } from '../fast/resolve.js';
|
|
20
|
+
import { subagentModelSummary } from '../ui/overlays/model-profile-state.js';
|
|
19
21
|
import { registerQueueCommand } from './queue-command.js';
|
|
20
22
|
import { perfCommand } from './perf.js';
|
|
21
23
|
import { runCompactCommand } from '../compaction/command.js';
|
|
@@ -90,6 +92,66 @@ function formatUpdateStatus(port, override) {
|
|
|
90
92
|
lines.push(` next ${next ? new Date(next).toLocaleTimeString() : 'not scheduled'}`);
|
|
91
93
|
return lines.join('\n');
|
|
92
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* `/team model [main|fast|<profile-name>]` (subagent-model §3.6 / 5.2).
|
|
97
|
+
*
|
|
98
|
+
* The user-owned switch's command-line surface. `main` and `fast` are verbs
|
|
99
|
+
* compared case-insensitively; a NAME is matched EXACTLY, case and all,
|
|
100
|
+
* against the library - the same identity the picker shows - and a miss lists
|
|
101
|
+
* the names that would work rather than failing silently. The summary reuses
|
|
102
|
+
* the exact wording the settings root row renders, so the two surfaces can
|
|
103
|
+
* never describe the same setting differently.
|
|
104
|
+
*/
|
|
105
|
+
function runTeamModelCommand(ctx, subject) {
|
|
106
|
+
const controller = ctx.controller;
|
|
107
|
+
const config = controller.getConfig();
|
|
108
|
+
const tier = resolveFastTier(config, (id, role) => controller.hasApiKey(id, role));
|
|
109
|
+
const summary = subagentModelSummary({
|
|
110
|
+
mode: config.team.model,
|
|
111
|
+
modelProfileId: config.team.modelProfileId,
|
|
112
|
+
mainConnection: `${config.provider}:${config.model}`,
|
|
113
|
+
fastConnection: tier.ok ? `${tier.ref.providerId}:${tier.ref.modelId}` : null,
|
|
114
|
+
profiles: config.modelProfiles,
|
|
115
|
+
hasKey: (id) => controller.hasApiKey(id),
|
|
116
|
+
});
|
|
117
|
+
if (!subject) {
|
|
118
|
+
// The status readout: mode + the connection actually in effect + the
|
|
119
|
+
// disclosed reason when the request had to fall back (D-4).
|
|
120
|
+
const resolution = controller.subagentModelResolution();
|
|
121
|
+
const note = resolution.fallbackReason
|
|
122
|
+
? ` Unavailable (${resolution.fallbackReason}); children run on the main model.`
|
|
123
|
+
: resolution.effective === 'main'
|
|
124
|
+
? ''
|
|
125
|
+
: ` Children run on ${resolution.ref.providerId}:${resolution.ref.modelId}.`;
|
|
126
|
+
ctx.notify('info', `Subagent model: ${summary}.${note}`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (subject.toLowerCase() === 'main' || subject.toLowerCase() === 'fast') {
|
|
130
|
+
const mode = subject.toLowerCase();
|
|
131
|
+
const applied = controller.setTeamConfig({ model: mode });
|
|
132
|
+
ctx.persistConfig({ team: { model: applied.model } });
|
|
133
|
+
if (mode === 'fast' && !tier.ok) {
|
|
134
|
+
ctx.notify('warn', 'The fast tier is not available now; subagents will fall back to the main model.');
|
|
135
|
+
}
|
|
136
|
+
ctx.toast('success', `Subagent model: ${mode === 'main' ? 'same as main' : 'fast tier'}.`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const entries = config.modelProfiles?.entries ?? [];
|
|
140
|
+
const matches = entries.filter((entry) => entry.name === subject);
|
|
141
|
+
if (matches.length === 0) {
|
|
142
|
+
const names = entries.slice(0, 8).map((entry) => entry.name);
|
|
143
|
+
ctx.notify('warn', `No profile named "${subject}".`
|
|
144
|
+
+ (names.length ? ` Available: ${names.join(', ')}` : ' Create one in /settings first.'));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const entry = matches[0];
|
|
148
|
+
const disambiguation = matches.length > 1 ? ` (#${entry.id.slice(0, 8)})` : '';
|
|
149
|
+
const applied = controller.setTeamConfig({ model: 'profile', modelProfileId: entry.id });
|
|
150
|
+
const teamPatch = { model: applied.model,
|
|
151
|
+
modelProfileId: applied.modelProfileId };
|
|
152
|
+
ctx.persistConfig({ team: teamPatch });
|
|
153
|
+
ctx.toast('success', `Subagent model: ${entry.name}${disambiguation} (${entry.provider}:${entry.model}).`);
|
|
154
|
+
}
|
|
93
155
|
function formatFastStatus(controller) {
|
|
94
156
|
const status = controller.getFastStatus();
|
|
95
157
|
const cfg = controller.getFastConfig();
|
|
@@ -254,7 +316,7 @@ const COMMANDS = [
|
|
|
254
316
|
},
|
|
255
317
|
{
|
|
256
318
|
name: 'team',
|
|
257
|
-
description: 'Team subagents: status | on | off | max <n> | overseer on|off',
|
|
319
|
+
description: 'Team subagents: status | on | off | max <n> | overseer on|off | model [main|fast|<profile-name>]',
|
|
258
320
|
/**
|
|
259
321
|
* `/team on` CANNOT turn team mode on in a session that started without it,
|
|
260
322
|
* and this command must not pretend otherwise (§4.5 / D-17 / P0-2).
|
|
@@ -275,6 +337,12 @@ const COMMANDS = [
|
|
|
275
337
|
const arg = ctx.args.trim().toLowerCase();
|
|
276
338
|
const [verb, value] = arg.split(/\s+/, 2);
|
|
277
339
|
const controller = ctx.controller;
|
|
340
|
+
// RAW TOKENS, KEPT BESIDE THE LOWERCASED PAIR (V-2): `model` matches a
|
|
341
|
+
// profile NAME case-sensitively - like the picker and the library -
|
|
342
|
+
// and the whole-args toLowerCase() above would have made
|
|
343
|
+
// `/team model MyProfile` silently unmatchable. Only the VERB token is
|
|
344
|
+
// compared through the lowercased pair.
|
|
345
|
+
const rawTokens = ctx.args.trim().split(/\s+/).filter((token) => token.length > 0);
|
|
278
346
|
// Refused mid-dispatch for the reason `/reload` already records: half a
|
|
279
347
|
// dispatch under one ceiling and half under another produces a result
|
|
280
348
|
// nothing can afterwards explain.
|
|
@@ -284,6 +352,7 @@ const COMMANDS = [
|
|
|
284
352
|
}
|
|
285
353
|
if (!verb || verb === 'status') {
|
|
286
354
|
const cfg = controller.getTeamConfig();
|
|
355
|
+
const config = controller.getConfig();
|
|
287
356
|
const state = !controller.isTeamRegistered()
|
|
288
357
|
? 'off for this session (started with --no-team)'
|
|
289
358
|
: controller.isTeamEnabled()
|
|
@@ -293,9 +362,20 @@ const COMMANDS = [
|
|
|
293
362
|
const roster = snapshot
|
|
294
363
|
? `\nRunning: ${snapshot.runs.map((r) => `${r.label} (${r.phase})`).join(', ')}`
|
|
295
364
|
: '';
|
|
365
|
+
const tier = resolveFastTier(controller.getConfig(), (id, role) => controller.hasApiKey(id, role));
|
|
366
|
+
const subagent = subagentModelSummary({
|
|
367
|
+
mode: cfg.model,
|
|
368
|
+
modelProfileId: cfg.modelProfileId,
|
|
369
|
+
mainConnection: `${config.provider}:${config.model}`,
|
|
370
|
+
fastConnection: tier.ok ? `${tier.ref.providerId}:${tier.ref.modelId}` : null,
|
|
371
|
+
profiles: config.modelProfiles,
|
|
372
|
+
hasKey: (id) => controller.hasApiKey(id),
|
|
373
|
+
});
|
|
296
374
|
ctx.notify('info', `Team mode: ${state}. Max subagents: ${cfg.maxSubagents}, ` +
|
|
297
375
|
`concurrent: ${cfg.maxConcurrent}, supervisor: ${cfg.overseer ? 'on' : 'off'}` +
|
|
298
|
-
`${cfg.overseer ? ' (never kills; checks the fast tier each tick)' : ' (legacy hard timeouts)'}
|
|
376
|
+
`${cfg.overseer ? ' (never kills; checks the fast tier each tick)' : ' (legacy hard timeouts)'}.` +
|
|
377
|
+
`\nSubagent model: ${subagent}` +
|
|
378
|
+
`${roster}`);
|
|
299
379
|
return;
|
|
300
380
|
}
|
|
301
381
|
if (verb === 'on' || verb === 'off') {
|
|
@@ -332,6 +412,10 @@ const COMMANDS = [
|
|
|
332
412
|
ctx.toast('success', `Max subagents set to ${applied.maxSubagents}.`);
|
|
333
413
|
return;
|
|
334
414
|
}
|
|
415
|
+
if (verb === 'model') {
|
|
416
|
+
runTeamModelCommand(ctx, rawTokens[1]);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
335
419
|
if (verb === 'overseer') {
|
|
336
420
|
if (value !== 'on' && value !== 'off') {
|
|
337
421
|
ctx.notify('warn', 'Usage: /team overseer on|off');
|
|
@@ -347,7 +431,7 @@ const COMMANDS = [
|
|
|
347
431
|
ctx.toast('success', `Dispatch supervisor ${applied.overseer ? 'on' : 'off'}.`);
|
|
348
432
|
return;
|
|
349
433
|
}
|
|
350
|
-
ctx.notify('warn', `Unknown argument "${verb}" - use /team [status|on|off|max <n>|overseer on|off].`);
|
|
434
|
+
ctx.notify('warn', `Unknown argument "${verb}" - use /team [status|on|off|max <n>|overseer on|off|model main|fast|<profile-name>].`);
|
|
351
435
|
},
|
|
352
436
|
},
|
|
353
437
|
{
|
|
@@ -56,6 +56,21 @@ export interface ChildCompactionDeps {
|
|
|
56
56
|
export interface ChildContextManagerRequest {
|
|
57
57
|
role?: ModelRole;
|
|
58
58
|
label: string;
|
|
59
|
+
/**
|
|
60
|
+
* REQ-LEVEL key resolvers, preferred over the lead's (V-4).
|
|
61
|
+
*
|
|
62
|
+
* `wiring.ts::childFactory` injects the LEAD layer's `getApiKey`/`hasKey`,
|
|
63
|
+
* which cannot see a fast-only key (`role='fast'` only) or a profile's
|
|
64
|
+
* dedicated key (it exists only inside the entry). `Compactor.resolveSummarizer`
|
|
65
|
+
* gates its main choice on `hasKey(provider, role)` and reads the call key
|
|
66
|
+
* through `getApiKey`, so without these a `team.model: fast/profile` child
|
|
67
|
+
* whose only key lives in one of those places would have its compaction
|
|
68
|
+
* SILENTLY DISABLED - the summarizer resolves to null and nobody says why.
|
|
69
|
+
* `subagent.ts` passes the same profile-aware closures the child's own
|
|
70
|
+
* `Agent` gets; ABSENT keeps today's behavior byte for byte (matrix 24).
|
|
71
|
+
*/
|
|
72
|
+
getApiKey?: (providerId: string, role?: ModelRole) => string | undefined;
|
|
73
|
+
hasKey?: (providerId: string, role?: ModelRole) => boolean;
|
|
59
74
|
/**
|
|
60
75
|
* The model the child actually runs on.
|
|
61
76
|
*
|
package/dist/compaction/child.js
CHANGED
|
@@ -65,6 +65,10 @@ export function createChildContextManager(req, deps) {
|
|
|
65
65
|
baseUrl: req.model.baseUrl,
|
|
66
66
|
// The lead's manual limit describes its connection, not every worker.
|
|
67
67
|
contextWindow: sameConnection ? base.contextWindow : null,
|
|
68
|
+
// THE ORIGIN FOLLOWS THE VALUE through the same gate, honoring the iff
|
|
69
|
+
// contract (value null => origin undefined): a child on a different
|
|
70
|
+
// connection inherits neither the number nor its provenance.
|
|
71
|
+
contextWindowOrigin: sameConnection ? base.contextWindowOrigin : undefined,
|
|
68
72
|
// NO POLICY OVERLAY (main-agent parity): `keepRecentTurns`, `onFailure`,
|
|
69
73
|
// `useFastTier` and `archive` are read LIVE from the base config, so the
|
|
70
74
|
// child compacts under exactly the policy the lead compacts under. A
|
|
@@ -81,15 +85,22 @@ export function createChildContextManager(req, deps) {
|
|
|
81
85
|
return source === undefined ? deps.isPricedModel(req.model) : source !== 'fallback';
|
|
82
86
|
},
|
|
83
87
|
getWindowOverride: () => childConfig().contextWindow,
|
|
88
|
+
getWindowOverrideOrigin: () => childConfig().contextWindowOrigin,
|
|
84
89
|
});
|
|
85
90
|
let overflowNotified = false;
|
|
91
|
+
// REQ-LEVEL RESOLVERS WIN (V-4), and the fallback is the lead layer - so a
|
|
92
|
+
// request built without them behaves exactly as before, while one built by
|
|
93
|
+
// `subagent.ts` under `team.model: fast/profile` lets the summarizer see
|
|
94
|
+
// the same keys the child itself authenticates with.
|
|
95
|
+
const hasKey = req.hasKey ?? deps.hasKey;
|
|
96
|
+
const getApiKey = req.getApiKey ?? deps.getApiKey;
|
|
86
97
|
const compactor = new Compactor({
|
|
87
98
|
getConfig: childConfig,
|
|
88
99
|
meter,
|
|
89
100
|
mainRole: req.role ?? 'main',
|
|
90
|
-
resolveFastCandidate: () => resolveFastTier(deps.getConfig(),
|
|
91
|
-
hasKey
|
|
92
|
-
getApiKey
|
|
101
|
+
resolveFastCandidate: () => resolveFastTier(deps.getConfig(), hasKey),
|
|
102
|
+
hasKey,
|
|
103
|
+
getApiKey,
|
|
93
104
|
getModelInfoFor: deps.getModelInfoFor,
|
|
94
105
|
isPricedModel: deps.isPricedModel,
|
|
95
106
|
getMessages: req.getMessages,
|
|
@@ -403,6 +403,7 @@ export class Compactor {
|
|
|
403
403
|
return source === undefined ? this.deps.isPricedModel(this.mainRef()) : source !== 'fallback';
|
|
404
404
|
},
|
|
405
405
|
getWindowOverride: () => this.deps.getConfig().contextWindow,
|
|
406
|
+
getWindowOverrideOrigin: () => this.deps.getConfig().contextWindowOrigin,
|
|
406
407
|
});
|
|
407
408
|
return this.ownMeter;
|
|
408
409
|
}
|
|
@@ -87,7 +87,16 @@ function occupancyLine(usage) {
|
|
|
87
87
|
*/
|
|
88
88
|
function windowLine(usage) {
|
|
89
89
|
if (usage.windowOverridden) {
|
|
90
|
-
|
|
90
|
+
// THREE NAMED SURFACES (profile-context-window §6.2). `undefined` falls
|
|
91
|
+
// into the config branch ON PURPOSE: that string is what every pre-feature
|
|
92
|
+
// test asserts, and a meter built without the origin dep must report
|
|
93
|
+
// byte-identically (invariant 6).
|
|
94
|
+
const named = usage.windowOverrideOrigin === 'profile'
|
|
95
|
+
? 'from your model profile'
|
|
96
|
+
: usage.windowOverrideOrigin === 'env'
|
|
97
|
+
? 'from ARAGON_CONTEXT_WINDOW'
|
|
98
|
+
: 'from contextWindow configuration';
|
|
99
|
+
return `${usage.window} ${named} (overrides API, model table, and model-windows.json)`;
|
|
91
100
|
}
|
|
92
101
|
if (!usage.windowKnown) {
|
|
93
102
|
return (`UNKNOWN - no verified model limit; internal fallback budget ${usage.window}; ` +
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* `current()` carries a structural belt for a missed invalidation site.
|
|
29
29
|
*/
|
|
30
30
|
import type { AgentEvent, Message, ModelInfo, TokenUsage } from '@aragon-agent/core';
|
|
31
|
+
import type { ContextWindowOrigin } from '../config/schema.js';
|
|
31
32
|
import type { ContextUsageSnapshot, Pressure } from './types.js';
|
|
32
33
|
/**
|
|
33
34
|
* How long a scheduled publish waits.
|
|
@@ -62,6 +63,14 @@ export interface ContextMeterDeps {
|
|
|
62
63
|
* measurement, so a live settings edit moves the denominator without a relaunch.
|
|
63
64
|
*/
|
|
64
65
|
getWindowOverride(): number | null;
|
|
66
|
+
/**
|
|
67
|
+
* WHERE the override above came from, read live beside it
|
|
68
|
+
* (profile-context-window §6.1). Optional so every existing construction
|
|
69
|
+
* site stays untouched: a meter built without it reports an overridden
|
|
70
|
+
* window with origin `undefined`, and `/context` falls back to its
|
|
71
|
+
* pre-feature "from contextWindow configuration" wording byte-for-byte.
|
|
72
|
+
*/
|
|
73
|
+
getWindowOverrideOrigin?(): ContextWindowOrigin | undefined;
|
|
65
74
|
getRequestVersion?(): number;
|
|
66
75
|
}
|
|
67
76
|
export type ContextUsageListener = (usage: ContextUsageSnapshot) => void;
|
package/dist/compaction/meter.js
CHANGED
|
@@ -63,6 +63,9 @@ export function toContextUsage(p) {
|
|
|
63
63
|
deltaTokens: p.deltaTokens,
|
|
64
64
|
windowKnown: p.windowKnown,
|
|
65
65
|
windowOverridden: p.windowOverridden,
|
|
66
|
+
...(p.windowOverrideOrigin !== undefined
|
|
67
|
+
? { windowOverrideOrigin: p.windowOverrideOrigin }
|
|
68
|
+
: {}),
|
|
66
69
|
};
|
|
67
70
|
}
|
|
68
71
|
/** The reading a meter with nothing measured yet reports. */
|
|
@@ -426,6 +429,10 @@ export class ContextMeter {
|
|
|
426
429
|
window.contextWindow !== last?.contextWindow ||
|
|
427
430
|
window.windowKnown !== last?.windowKnown ||
|
|
428
431
|
window.windowOverridden !== last?.windowOverridden ||
|
|
432
|
+
// ORIGIN IS PART OF THE DIRTY CHECK: a save that keeps the same number
|
|
433
|
+
// but moves it from the legacy key to the bound profile would otherwise
|
|
434
|
+
// leave `/context` naming the old source until the next measurement.
|
|
435
|
+
window.windowOverrideOrigin !== last?.windowOverrideOrigin ||
|
|
429
436
|
this.deps.getModelInfo().contextWindowSource !== last?.windowSource)
|
|
430
437
|
this.dirty = true;
|
|
431
438
|
}
|
|
@@ -451,6 +458,9 @@ export class ContextMeter {
|
|
|
451
458
|
windowSource: this.deps.getModelInfo().contextWindowSource,
|
|
452
459
|
windowKnown: window.windowKnown,
|
|
453
460
|
windowOverridden: window.windowOverridden,
|
|
461
|
+
...(window.windowOverrideOrigin !== undefined
|
|
462
|
+
? { windowOverrideOrigin: window.windowOverrideOrigin }
|
|
463
|
+
: {}),
|
|
454
464
|
...(this.estimateOffset !== undefined ? { estimateOffset: this.estimateOffset } : {}),
|
|
455
465
|
...(this.measuredPrefixLength !== undefined
|
|
456
466
|
? { measuredPrefixLength: this.measuredPrefixLength }
|
|
@@ -469,7 +479,12 @@ export class ContextMeter {
|
|
|
469
479
|
resolveWindow() {
|
|
470
480
|
const override = this.deps.getWindowOverride();
|
|
471
481
|
if (override !== null && Number.isFinite(override) && override > 0) {
|
|
472
|
-
|
|
482
|
+
// ORIGIN ONLY RIDES THE OVERRIDE BRANCH: the AUTO branch names its own
|
|
483
|
+
// source through `getModelInfo().contextWindowSource`, and the two must
|
|
484
|
+
// never both claim the denominator (profile-context-window §6.1).
|
|
485
|
+
const origin = this.deps.getWindowOverrideOrigin?.();
|
|
486
|
+
return { contextWindow: override, windowKnown: true, windowOverridden: true,
|
|
487
|
+
...(origin !== undefined ? { windowOverrideOrigin: origin } : {}) };
|
|
473
488
|
}
|
|
474
489
|
return {
|
|
475
490
|
contextWindow: this.deps.getModelInfo().contextWindow,
|
|
@@ -93,6 +93,13 @@ export interface PressureInput {
|
|
|
93
93
|
* construction site is unchanged and the arithmetic below is untouched.
|
|
94
94
|
*/
|
|
95
95
|
windowOverridden?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Which override surface supplied `contextWindow` (profile-context-window).
|
|
98
|
+
* PURE PASS-THROUGH like `windowOverridden` - the arithmetic below never
|
|
99
|
+
* reads it; it only travels with the number so `/context` can name the
|
|
100
|
+
* source honestly.
|
|
101
|
+
*/
|
|
102
|
+
windowOverrideOrigin?: Pressure['windowOverrideOrigin'];
|
|
96
103
|
/** Carried from the last `turn_end`; applied to every ESTIMATED figure. */
|
|
97
104
|
estimateOffset?: number;
|
|
98
105
|
/**
|
|
@@ -174,6 +174,11 @@ export function computePressure(input) {
|
|
|
174
174
|
// context-usage-gauge-accuracy; the flag only travels with the number so
|
|
175
175
|
// `/context` can name the denominator's source (I-6).
|
|
176
176
|
windowOverridden: input.windowOverridden ?? false,
|
|
177
|
+
// CONDITIONAL SPREAD: with no origin there is no KEY, keeping a
|
|
178
|
+
// feature-off Pressure byte-identical to the pre-feature one (I-6).
|
|
179
|
+
...(input.windowOverrideOrigin !== undefined
|
|
180
|
+
? { windowOverrideOrigin: input.windowOverrideOrigin }
|
|
181
|
+
: {}),
|
|
177
182
|
...(input.estimateOffset !== undefined ? { estimateOffset: input.estimateOffset } : {}),
|
|
178
183
|
deltaTokens,
|
|
179
184
|
};
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* possible change.
|
|
21
21
|
*/
|
|
22
22
|
import type { ModelRef, TokenUsage } from '@aragon-agent/core';
|
|
23
|
+
import type { ContextWindowOrigin } from '../config/schema.js';
|
|
23
24
|
/** Billing belongs to the actual call, including fallback and late responses. */
|
|
24
25
|
export interface CompactionBilling {
|
|
25
26
|
modelRef?: ModelRef;
|
|
@@ -92,6 +93,14 @@ export interface Pressure {
|
|
|
92
93
|
* one being wrong is exactly the case that most needs to be visible.
|
|
93
94
|
*/
|
|
94
95
|
windowOverridden: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* WHICH override surface supplied the denominator (profile-context-window
|
|
98
|
+
* §5.2): env / legacy top-level key / the profile bound as Main. Optional
|
|
99
|
+
* and PURE PASS-THROUGH - present only when `windowOverridden` is true, and
|
|
100
|
+
* `undefined` there means "config", the pre-feature answer. The arithmetic
|
|
101
|
+
* never reads it; only `/context`'s window line does.
|
|
102
|
+
*/
|
|
103
|
+
windowOverrideOrigin?: ContextWindowOrigin;
|
|
95
104
|
/**
|
|
96
105
|
* `occupiedTokens(lastUsage) - estimateOf(the history that produced it)`,
|
|
97
106
|
* clamped to `>= 0`, or `undefined` before the first `turn_end`.
|
|
@@ -150,6 +159,8 @@ export interface ContextUsageSnapshot {
|
|
|
150
159
|
windowKnown: boolean;
|
|
151
160
|
/** Whether the denominator came from the user's `contextWindow` (I-6). */
|
|
152
161
|
windowOverridden: boolean;
|
|
162
|
+
/** Which override surface named the denominator; see `Pressure` (pass-through). */
|
|
163
|
+
windowOverrideOrigin?: ContextWindowOrigin;
|
|
153
164
|
}
|
|
154
165
|
/** One compaction, in whatever state it reached. Rendered by `CompactionCard`. */
|
|
155
166
|
export interface CompactionRecord {
|
|
@@ -105,6 +105,7 @@ export class CompactionWiring {
|
|
|
105
105
|
return source === undefined ? deps.isPricedModel(this.mainRef()) : source !== 'fallback';
|
|
106
106
|
},
|
|
107
107
|
getWindowOverride: () => deps.getConfig().contextWindow,
|
|
108
|
+
getWindowOverrideOrigin: () => deps.getConfig().contextWindowOrigin,
|
|
108
109
|
});
|
|
109
110
|
this.compactor = new Compactor({
|
|
110
111
|
getConfig: deps.getConfig,
|
|
@@ -20,7 +20,7 @@ export declare const LOG_CONFIG_SET_KEYS: readonly ["log.level", "log.toFile", "
|
|
|
20
20
|
* switch falls through every case, writes nothing, and still prints
|
|
21
21
|
* `Set team.enabled = false`.
|
|
22
22
|
*/
|
|
23
|
-
export declare const TEAM_CONFIG_SET_KEYS: readonly ["team.enabled", "team.maxSubagents", "team.maxConcurrent", "team.subagentTimeoutMs", "team.dispatchTimeoutMs", "team.maxTurnsPerSubagent", "team.overseer", "team.overseerIntervalMs"];
|
|
23
|
+
export declare const TEAM_CONFIG_SET_KEYS: readonly ["team.enabled", "team.maxSubagents", "team.maxConcurrent", "team.subagentTimeoutMs", "team.dispatchTimeoutMs", "team.maxTurnsPerSubagent", "team.overseer", "team.overseerIntervalMs", "team.model", "team.modelProfileId"];
|
|
24
24
|
/**
|
|
25
25
|
* Config keys under `retry.` that `aragon config set` accepts
|
|
26
26
|
* (llm-api-retry-backoff §8.2).
|
|
@@ -45,6 +45,8 @@ export const TEAM_CONFIG_SET_KEYS = [
|
|
|
45
45
|
'team.maxTurnsPerSubagent',
|
|
46
46
|
'team.overseer',
|
|
47
47
|
'team.overseerIntervalMs',
|
|
48
|
+
'team.model',
|
|
49
|
+
'team.modelProfileId',
|
|
48
50
|
];
|
|
49
51
|
/**
|
|
50
52
|
* Config keys under `retry.` that `aragon config set` accepts
|
|
@@ -205,6 +207,16 @@ export function applyTeamConfigSet(key, value) {
|
|
|
205
207
|
return patch({ overseer: isTrue(value) });
|
|
206
208
|
case 'team.overseerIntervalMs':
|
|
207
209
|
return patch(clampOne('overseerIntervalMs'));
|
|
210
|
+
// The subagent-model keys (subagent-model-and-question-controls V-3):
|
|
211
|
+
// without these two cases `aragon config set team.model fast` is refused
|
|
212
|
+
// with "Unknown config key" while `config list` shows the key one command
|
|
213
|
+
// over - the exact gap COMPACTION_CONFIG_SET_KEYS records at length.
|
|
214
|
+
// Both route through `clampTeamConfig` like every other key here: a typo'd
|
|
215
|
+
// mode resolves to 'main' and a bad id is nulled, never rejected.
|
|
216
|
+
case 'team.model':
|
|
217
|
+
return patch(clampOne('model'));
|
|
218
|
+
case 'team.modelProfileId':
|
|
219
|
+
return patch(clampOne('modelProfileId'));
|
|
208
220
|
default:
|
|
209
221
|
return null;
|
|
210
222
|
}
|