@sublang/playbook 17.1.0 → 17.2.0

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/docs/cli.md CHANGED
@@ -202,8 +202,8 @@ The former positional `<from>`, `resume`, `--player`, `--captain`,
202
202
  `--option`, `--cwd`, `--last`, run-only `--config`, and top-level `run:`
203
203
  config are removed from `playbook run`. Enable a registry under `playbooks`,
204
204
  declare provider agents once under top-level `players`, bind every local role
205
- under `playbooks.<id>.roles`, tune compatible model, effort, and fast mode in a `--with`
206
- overlay, invoke the effective `/command`, and run from the working directory
205
+ under `playbooks.<id>.roles`, tune compatible model, effort, fast mode, and
206
+ subagent model in a `--with` overlay, invoke the effective `/command`, and run from the working directory
207
207
  you want agents to use. Legacy `playbooks.<id>.players` blocks are rejected and
208
208
  are not auto-migrated because choosing equal or distinct new player IDs chooses
209
209
  conversation sharing or isolation; see [Migrating per-playbook
@@ -280,10 +280,11 @@ competing front end fails closed instead of forking the history.
280
280
  An ordinary reopen reads current config and any opening `--with` fragments,
281
281
  projects them to the stored catalog and player roster, and requires the stored
282
282
  role bindings plus every structural setting to remain exact. Compatible
283
- current `model`, `effort`, and optional `fastMode` settings apply to the next
284
- call. Boolean `false` explicitly resets model or effort to the provider
285
- default, but `fastMode: false` is a literal disabled request; omission selects
286
- the fast-mode provider default or inherits the player value at a role binding.
283
+ current `model`, `effort`, and optional `fastMode` and `subagentModel` settings
284
+ apply to the next call. Boolean `false` explicitly resets model, effort, or
285
+ subagent model to the provider default, but `fastMode: false` is a literal
286
+ disabled request; omission selects the provider default or inherits the player
287
+ value at a role binding.
287
288
  Provider conversations are local hints. Missing hints start fresh from stored
288
289
  context; a definite pre-execution rejection permits one fresh attempt.
289
290
  Unsupported settings and ambiguous failures do not trigger that fallback
@@ -48,8 +48,8 @@ names, nesting, and ancestry never infer a binding.
48
48
 
49
49
  Each `captain` or `players.<player-id>` value is either an adapter shorthand
50
50
  (`claude`, `codex`) or a block carrying that agent's own `adapter`, `model`,
51
- `effort`, `fastMode`, `instruction`, and `permissions`. Settings are inline per
52
- stable agent
51
+ `effort`, `fastMode`, `subagentModel`, `instruction`, and `permissions`.
52
+ Settings are inline per stable agent
53
53
  ([DR-021](https://github.com/sublang-ai/playbook/blob/main/specs/decisions/021-inline-agent-settings.md)).
54
54
  Dots in a player ID are literal characters, not YAML hierarchy. Other adapter
55
55
  IDs pass through to `tmux-play` with a warning because `playbook` cannot
@@ -156,8 +156,8 @@ roles:
156
156
  coder: dev.coder
157
157
  ```
158
158
 
159
- Use a block to override only that role invocation's model, effort, or fast
160
- mode:
159
+ Use a block to override only that role invocation's model, effort, fast mode,
160
+ or subagent model:
161
161
 
162
162
  ```yaml
163
163
  roles:
@@ -166,17 +166,27 @@ roles:
166
166
  model: claude-sonnet-5-5
167
167
  effort: false # explicitly reset to this provider's default
168
168
  fastMode: false # literal disabled request, not a default sentinel
169
+ reviewer:
170
+ player: dev.reviewer
171
+ subagentModel: claude-sonnet-5-5 # every subagent of this role runs on it
169
172
  ```
170
173
 
171
- Omitting any override inherits that player's top-level default. For `model`
172
- and `effort`, boolean `false` selects the provider default explicitly, so a
173
- resumed conversation cannot accidentally retain an earlier selection. For
174
- `fastMode`, `false` is a literal request to disable fast mode; omitting the
175
- top-level setting selects the provider default. A present fast-mode boolean is
176
- accepted only for adapters Cligent reports as supporting it. A role binding
177
- cannot override adapter, instruction, permissions, workspace, or tool posture;
178
- those define the stable player envelope, so an overriding model must be one
179
- that player's adapter serves.
174
+ Omitting any override inherits that player's top-level default. For `model`,
175
+ `effort`, and `subagentModel`, boolean `false` selects the provider default
176
+ explicitly, so a resumed conversation cannot accidentally retain an earlier
177
+ selection. For `fastMode`, `false` is a literal request to disable fast mode;
178
+ omitting the top-level setting selects the provider default. A present
179
+ fast-mode boolean or subagent model is accepted only for adapters Cligent
180
+ reports as supporting it. A role binding cannot override adapter, instruction,
181
+ permissions, workspace, or tool posture; those define the stable player
182
+ envelope, so an overriding model must be one that player's adapter serves.
183
+
184
+ A subagent model makes every subagent the agent starts run on that model, and
185
+ tells the agent to hand well-defined, fine-grained tasks to its subagents while
186
+ keeping the deep thinking, reasoning, and design work itself, without lowering
187
+ the quality of what it delivers
188
+ ([DR-075](https://github.com/sublang-ai/playbook/blob/main/specs/decisions/075-subagent-model-is-tuning.md)).
189
+ Nothing sets one by default.
180
190
 
181
191
  ## Sharing, isolation, and concurrency
182
192
 
@@ -276,9 +286,9 @@ The global file is never modified, and `--with` is not forwarded to
276
286
  Overlays apply when creating a fresh session and as current-config input for a
277
287
  compatible ordinary reopen. A selected session keeps its stored catalog,
278
288
  player roster, role bindings, adapter, instruction, permissions, and working
279
- directory; only model, effort, and fast mode may change. The next call reapplies
280
- both complete model and effort selections and the optional effective fast-mode
281
- boolean. An uncertain retry accepts no tuning overlay and only restores and reports the recorded attempt. Later turns use the current compatible settings.
289
+ directory; only model, effort, fast mode, and subagent model may change. The
290
+ next call reapplies both complete model and effort selections, the optional
291
+ effective fast-mode boolean, and the optional effective subagent model. An uncertain retry accepts no tuning overlay and only restores and reports the recorded attempt. Later turns use the current compatible settings.
282
292
 
283
293
  ## Session storage
284
294
 
@@ -319,8 +329,8 @@ public UUID. Presentation-only fields are inert headlessly.
319
329
  An ordinary reopen reads current config and opening overlays, but first
320
330
  projects them to the stored playbooks and referenced players. An unrelated new
321
331
  entry cannot enter or invalidate the session. Structural drift fails closed;
322
- compatible model, effort, or fast-mode changes apply on the next provider
323
- call. Legacy record, shell, runtime-snapshot, and trace schemas are rejected
332
+ compatible model, effort, fast-mode, or subagent-model changes apply on the
333
+ next provider call. Legacy record, shell, runtime-snapshot, and trace schemas are rejected
324
334
  rather than having role or player identity guessed.
325
335
 
326
336
  ## External playbooks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sublang/playbook",
3
- "version": "17.1.0",
3
+ "version": "17.2.0",
4
4
  "type": "module",
5
5
  "description": "Composable XState v5 playbook runtime with compiled Captain, CODE, REVIEW, DECIDE, DEV, BRANCH, and PR workflows driven by GEARS specs.",
6
6
  "license": "Apache-2.0",
@@ -243,7 +243,7 @@
243
243
  "provenance": true
244
244
  },
245
245
  "dependencies": {
246
- "@sublang/cligent": "^0.28.0",
246
+ "@sublang/cligent": "^0.29.0",
247
247
  "@sublang/spex": "^3.0.0",
248
248
  "p-queue": "^9.3.1",
249
249
  "xstate": "^5.19.4",
@@ -442,7 +442,8 @@ export async function normalizeSelectedLaunchPlanDataOnly(
442
442
  if (
443
443
  binding.model !== undefined ||
444
444
  binding.effort !== undefined ||
445
- binding.fastMode !== undefined
445
+ binding.fastMode !== undefined ||
446
+ binding.subagentModel !== undefined
446
447
  ) {
447
448
  let checkId;
448
449
  do {
@@ -558,6 +559,7 @@ export async function normalizeSelectedLaunchPlanDataOnly(
558
559
  ? {}
559
560
  : { fastMode: agent.fastMode }
560
561
  : { fastMode: binding.fastMode }),
562
+ ...effectiveSubagentModel(binding, agent),
561
563
  },
562
564
  ];
563
565
  }),
@@ -787,7 +789,8 @@ export async function normalizeLaunchPlan(
787
789
  if (
788
790
  binding.model !== undefined ||
789
791
  binding.effort !== undefined ||
790
- binding.fastMode !== undefined
792
+ binding.fastMode !== undefined ||
793
+ binding.subagentModel !== undefined
791
794
  ) {
792
795
  let checkId;
793
796
  do {
@@ -999,6 +1002,7 @@ export async function normalizeLaunchPlan(
999
1002
  ? {}
1000
1003
  : { fastMode: agent.fastMode }
1001
1004
  : { fastMode: binding.fastMode }),
1005
+ ...effectiveSubagentModel(binding, agent),
1002
1006
  },
1003
1007
  ];
1004
1008
  }),
@@ -1484,7 +1488,7 @@ function migrateUserConfigIfRetired(userConfigPath, onNotice) {
1484
1488
  throw new Error(
1485
1489
  `cannot migrate the retired profiles config at ${userConfigPath}: ` +
1486
1490
  `${errorMessage(error)} — edit it by hand: each agent takes its own ` +
1487
- "adapter, model, effort, fast mode, and permissions",
1491
+ "adapter, model, effort, fast mode, subagent model, and permissions",
1488
1492
  );
1489
1493
  }
1490
1494
  if (migrated === undefined) return;
@@ -1620,7 +1624,7 @@ function assertNoRetiredProfiles(top, configPath) {
1620
1624
  throw new Error(
1621
1625
  `top-level "profiles" was removed${where}: write each agent's settings ` +
1622
1626
  "inline under captain and each top-level players.<player-id> " +
1623
- "(adapter, model, effort, fast mode, permissions)",
1627
+ "(adapter, model, effort, fast mode, subagent model, permissions)",
1624
1628
  );
1625
1629
  }
1626
1630
  const legacyPath = findLegacyPlayersPath(top);
@@ -1635,7 +1639,8 @@ function assertNoRetiredProfiles(top, configPath) {
1635
1639
  if (isObject(block) && block.profile !== undefined) {
1636
1640
  throw new Error(
1637
1641
  `${path}.profile was removed${where}: write the agent's settings ` +
1638
- "inline in that block (adapter, model, effort, fast mode, permissions)",
1642
+ "inline in that block (adapter, model, effort, fast mode, " +
1643
+ "subagent model, permissions)",
1639
1644
  );
1640
1645
  }
1641
1646
  }
@@ -2034,13 +2039,14 @@ function resolveRoleBinding(value, path) {
2034
2039
  }
2035
2040
  const block = requireObject(value, path);
2036
2041
  const unknown = Object.keys(block).filter(
2037
- (key) => !["player", "model", "effort", "fastMode"].includes(key),
2042
+ (key) =>
2043
+ !["player", "model", "effort", "fastMode", "subagentModel"].includes(key),
2038
2044
  );
2039
2045
  if (unknown.length > 0) {
2040
2046
  throw new Error(`${path} has unknown ${formatKeyList(unknown)}`);
2041
2047
  }
2042
2048
  assertPlayerId(block.player, `${path}.player`);
2043
- for (const field of ["model", "effort"]) {
2049
+ for (const field of ["model", "effort", "subagentModel"]) {
2044
2050
  if (
2045
2051
  block[field] !== undefined &&
2046
2052
  block[field] !== false &&
@@ -2062,9 +2068,24 @@ function resolveRoleBinding(value, path) {
2062
2068
  ...(block.model === undefined ? {} : { model: block.model }),
2063
2069
  ...(block.effort === undefined ? {} : { effort: block.effort }),
2064
2070
  ...(block.fastMode === undefined ? {} : { fastMode: block.fastMode }),
2071
+ ...(block.subagentModel === undefined
2072
+ ? {}
2073
+ : { subagentModel: block.subagentModel }),
2065
2074
  };
2066
2075
  }
2067
2076
 
2077
+ // A binding's subagent model overrides the player's; `false` selects the
2078
+ // provider default, which a complete call expresses by omission.
2079
+ function effectiveSubagentModel(binding, agent) {
2080
+ if (binding.subagentModel === false) return {};
2081
+ if (binding.subagentModel !== undefined) {
2082
+ return { subagentModel: binding.subagentModel };
2083
+ }
2084
+ return agent.subagentModel === undefined
2085
+ ? {}
2086
+ : { subagentModel: agent.subagentModel };
2087
+ }
2088
+
2068
2089
  function applyTuningOverrides(agent, binding) {
2069
2090
  const effective = { ...agent };
2070
2091
  if (binding.model === false) delete effective.model;
@@ -2075,6 +2096,10 @@ function applyTuningOverrides(agent, binding) {
2075
2096
  else effective.effort = binding.effort;
2076
2097
  }
2077
2098
  if (binding.fastMode !== undefined) effective.fastMode = binding.fastMode;
2099
+ if (binding.subagentModel === false) delete effective.subagentModel;
2100
+ else if (binding.subagentModel !== undefined) {
2101
+ effective.subagentModel = binding.subagentModel;
2102
+ }
2078
2103
  return effective;
2079
2104
  }
2080
2105
 
@@ -2089,6 +2114,14 @@ function sessionAgentFromHostAgent(agent, path) {
2089
2114
  ...(agent.fastMode === undefined
2090
2115
  ? {}
2091
2116
  : { fastMode: fastModeSelection(agent.fastMode, `${path}.fastMode`) }),
2117
+ ...(agent.subagentModel === undefined
2118
+ ? {}
2119
+ : {
2120
+ subagentModel: subagentModelSelection(
2121
+ agent.subagentModel,
2122
+ `${path}.subagentModel`,
2123
+ ),
2124
+ }),
2092
2125
  ...(agent.instruction === undefined
2093
2126
  ? {}
2094
2127
  : { instruction: agent.instruction }),
@@ -2114,6 +2147,9 @@ export function projectHostAgent(agent, path = "agent") {
2114
2147
  ...(normalized.fastMode === undefined
2115
2148
  ? {}
2116
2149
  : { fastMode: normalized.fastMode }),
2150
+ ...(normalized.subagentModel === undefined
2151
+ ? {}
2152
+ : { subagentModel: normalized.subagentModel }),
2117
2153
  ...(normalized.instruction === undefined
2118
2154
  ? {}
2119
2155
  : { instruction: normalized.instruction }),
@@ -2130,6 +2166,13 @@ function fastModeSelection(value, path) {
2130
2166
  return value;
2131
2167
  }
2132
2168
 
2169
+ function subagentModelSelection(value, path) {
2170
+ if (typeof value !== "string" || value.trim().length === 0) {
2171
+ throw new Error(`${path} must be a nonblank string`);
2172
+ }
2173
+ return value;
2174
+ }
2175
+
2133
2176
  function tuningSelection(value, path) {
2134
2177
  if (
2135
2178
  value !== undefined &&
@@ -966,8 +966,9 @@ function helpText({
966
966
  " - set the top-level captain and each stable players.<id> to an",
967
967
  " adapter shorthand (claude, codex) or an inline agent block",
968
968
  " - bind every playbooks.<id>.roles.<role> explicitly to a player id;",
969
- " a scalar names the id, while { player, model?, effort?, fastMode? }",
970
- " may retune one role; false selects provider-default model/effort,",
969
+ " a scalar names the id, while { player, model?, effort?, fastMode?,",
970
+ " subagentModel? } may retune one role;",
971
+ " false selects provider-default model/effort/subagentModel,",
971
972
  " while fastMode false is a literal disabled request",
972
973
  " - reusing one id deliberately shares that provider conversation;",
973
974
  " distinct ids stay isolated even when their agent blocks are equal",
@@ -11,7 +11,10 @@ import { attachSessionHints, validateSessionContext } from "./portable-codec.js"
11
11
  import { homedir } from "node:os";
12
12
  import { resolve } from "node:path";
13
13
  import { isDeepStrictEqual } from "node:util";
14
- import { assertFastModeSupported } from "@sublang/cligent";
14
+ import {
15
+ assertFastModeSupported,
16
+ assertSubagentModelSupported,
17
+ } from "@sublang/cligent";
15
18
  import { createTmuxPlayRuntime } from "@sublang/cligent/tmux-play";
16
19
  import {
17
20
  assertPlaybookEffectLedger,
@@ -1307,7 +1310,7 @@ export async function validateFrozenExecutionConfig(
1307
1310
  structuralProjection,
1308
1311
  executionProjection,
1309
1312
  );
1310
- assertFrozenFastModesSupported(config);
1313
+ assertFrozenTuningSupported(config);
1311
1314
  const catalogItems = Object.entries(config.catalog);
1312
1315
 
1313
1316
  // Preserve the complete-catalog preparation transaction: prepare every
@@ -1364,28 +1367,42 @@ export async function validateFrozenExecutionConfig(
1364
1367
  return config;
1365
1368
  }
1366
1369
 
1367
- function assertFrozenFastModesSupported(config) {
1368
- if (config.captain.fastMode !== undefined) {
1369
- assertFastModeSupported(
1370
- config.captain.adapter,
1371
- "stored Captain fastMode",
1372
- );
1373
- }
1370
+ // Fast mode and the subagent model are adapter-scoped tuning (DR-075): a
1371
+ // frozen record re-validates each present value against its adapter.
1372
+ const FROZEN_ADAPTER_SCOPED_TUNING = [
1373
+ ["fastMode", assertFastModeSupported],
1374
+ ["subagentModel", assertSubagentModelSupported],
1375
+ ];
1376
+
1377
+ function assertFrozenTuningSupported(config) {
1378
+ const assertSupported = (record, adapter, path) => {
1379
+ for (const [field, assertFieldSupported] of FROZEN_ADAPTER_SCOPED_TUNING) {
1380
+ if (record[field] !== undefined) {
1381
+ assertFieldSupported(adapter, `${path} ${field}`);
1382
+ }
1383
+ }
1384
+ };
1385
+ assertSupported(config.captain, config.captain.adapter, "stored Captain");
1374
1386
 
1375
1387
  const playerAdapters = new Map();
1376
1388
  for (const player of config.players) {
1377
1389
  playerAdapters.set(player.id, player.adapter);
1378
- if (player.fastMode !== undefined) {
1379
- assertFastModeSupported(
1380
- player.adapter,
1381
- `stored player ${JSON.stringify(player.id)} fastMode`,
1382
- );
1383
- }
1390
+ assertSupported(
1391
+ player,
1392
+ player.adapter,
1393
+ `stored player ${JSON.stringify(player.id)}`,
1394
+ );
1384
1395
  }
1385
1396
 
1386
1397
  for (const [playbookId, item] of Object.entries(config.catalog)) {
1387
1398
  for (const [roleId, binding] of Object.entries(item.roles)) {
1388
- if (binding.fastMode === undefined) continue;
1399
+ if (
1400
+ FROZEN_ADAPTER_SCOPED_TUNING.every(
1401
+ ([field]) => binding[field] === undefined,
1402
+ )
1403
+ ) {
1404
+ continue;
1405
+ }
1389
1406
  const adapter = playerAdapters.get(binding.playerId);
1390
1407
  if (adapter === undefined) {
1391
1408
  throw new Error(
@@ -1393,10 +1410,11 @@ function assertFrozenFastModesSupported(config) {
1393
1410
  `${JSON.stringify(roleId)} names an unknown player`,
1394
1411
  );
1395
1412
  }
1396
- assertFastModeSupported(
1413
+ assertSupported(
1414
+ binding,
1397
1415
  adapter,
1398
1416
  `stored playbook ${JSON.stringify(playbookId)} role ` +
1399
- `${JSON.stringify(roleId)} fastMode`,
1417
+ `${JSON.stringify(roleId)}`,
1400
1418
  );
1401
1419
  }
1402
1420
  }
@@ -1869,7 +1887,7 @@ function runHelpText(userConfigPath) {
1869
1887
  "working directory and reports when it uses the global newest fallback.",
1870
1888
  "An ordinary continued run restores that stored structure and working",
1871
1889
  "directory, then reads current config and overlays for model, effort,",
1872
- "and fast mode.",
1890
+ "fast mode, and subagent model.",
1873
1891
  "Uncertain retry only restores and reports; later work uses current settings.",
1874
1892
  "",
1875
1893
  "Options:",
@@ -5402,7 +5402,7 @@ function validateCaptainSessionProjection(
5402
5402
  : ['id', 'adapter', 'model', 'effort'],
5403
5403
  structural
5404
5404
  ? ['instruction', 'permissions']
5405
- : ['instruction', 'permissions', 'fastMode'],
5405
+ : ['instruction', 'permissions', 'fastMode', 'subagentModel'],
5406
5406
  playerPath,
5407
5407
  );
5408
5408
  assertPlayerId(player.id, `${playerPath}.id`);
@@ -5503,12 +5503,16 @@ function validateCaptainSessionProjection(
5503
5503
  structural
5504
5504
  ? ['playerId']
5505
5505
  : ['playerId', 'model', 'effort'],
5506
- structural ? [] : ['fastMode'],
5506
+ structural ? [] : ['fastMode', 'subagentModel'],
5507
5507
  bindingPath,
5508
5508
  );
5509
5509
  assertPlayerId(binding.playerId, `${bindingPath}.playerId`);
5510
5510
  if (!structural) {
5511
5511
  validateProjectedFastMode(binding, `${bindingPath}.fastMode`);
5512
+ validateProjectedSubagentModel(
5513
+ binding,
5514
+ `${bindingPath}.subagentModel`,
5515
+ );
5512
5516
  validateTuningSelection(binding.model, `${bindingPath}.model`);
5513
5517
  const player = projection.players.find(
5514
5518
  (candidate) => candidate.id === binding.playerId,
@@ -5556,16 +5560,26 @@ function validateProjectedFastMode(record, path) {
5556
5560
  }
5557
5561
  }
5558
5562
 
5563
+ // Like fast mode, absence is the canonical provider default (DR-075).
5564
+ function validateProjectedSubagentModel(record, path) {
5565
+ if (Object.hasOwn(record, 'subagentModel')) {
5566
+ requireNonblank(record.subagentModel, path);
5567
+ }
5568
+ }
5569
+
5559
5570
  function validateProjectedAgent(value, path, { structural, hasId = false }) {
5560
5571
  const agent = requireRecord(value, path);
5561
- if (!structural) validateProjectedFastMode(agent, `${path}.fastMode`);
5572
+ if (!structural) {
5573
+ validateProjectedFastMode(agent, `${path}.fastMode`);
5574
+ validateProjectedSubagentModel(agent, `${path}.subagentModel`);
5575
+ }
5562
5576
  if (!hasId) {
5563
5577
  exactOptionalKeys(
5564
5578
  agent,
5565
5579
  structural ? ['adapter'] : ['adapter', 'model', 'effort'],
5566
5580
  structural
5567
5581
  ? ['instruction', 'permissions']
5568
- : ['instruction', 'permissions', 'fastMode'],
5582
+ : ['instruction', 'permissions', 'fastMode', 'subagentModel'],
5569
5583
  path,
5570
5584
  );
5571
5585
  }
@@ -10,9 +10,14 @@ interface SessionAgent {
10
10
  /** Adapter-scoped fast mode. Absence is the provider default; `false` is a
11
11
  * literal request, so this carries no provider-default sentinel. */
12
12
  readonly fastMode?: boolean;
13
+ /** The model every subagent of this agent runs on (DR-075). Absence is the
14
+ * provider default. */
15
+ readonly subagentModel?: string;
13
16
  readonly instruction?: string;
14
17
  readonly permissions?: PermissionPolicy;
15
18
  }
19
+ /** Per-call tuning: erased from every structural envelope and snapshot. */
20
+ type TuningKey = 'model' | 'effort' | 'fastMode' | 'subagentModel';
16
21
  interface PlayerLedgerEntry {
17
22
  readonly adapter: string;
18
23
  readonly instruction?: string;
@@ -32,7 +37,7 @@ interface PlaybookCaptainUnresolvedEffectSettlementInput {
32
37
  readonly rootPlaybookId: string;
33
38
  readonly unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[];
34
39
  }
35
- type SnapshotAgentEnvelope = DeepReadonly<Omit<SessionAgent, 'model' | 'effort' | 'fastMode'>>;
40
+ type SnapshotAgentEnvelope = DeepReadonly<Omit<SessionAgent, TuningKey>>;
36
41
  type PlayerLedgerSnapshotEntry = DeepReadonly<PlayerLedgerEntry>;
37
42
  export interface ProgressChange {
38
43
  snapshot?: PlaybookCaptainShellSnapshot | null;
@@ -2098,10 +2098,28 @@ function snapshotFastMode(value, path) {
2098
2098
  }
2099
2099
  return value;
2100
2100
  }
2101
+ function snapshotSubagentModel(value, path) {
2102
+ if (value !== undefined &&
2103
+ (typeof value !== 'string' || value.trim().length === 0)) {
2104
+ throw new TypeError(`${path} must be a nonblank string`);
2105
+ }
2106
+ return value;
2107
+ }
2101
2108
  function snapshotSessionAgent(value, path) {
2102
2109
  const agent = snapshotRecord(value, path);
2103
- rejectSnapshotKeys(agent, ['adapter', 'model', 'effort', 'fastMode', 'instruction', 'permissions'], path);
2104
- const fixed = snapshotFixedAgent(Object.fromEntries(Object.entries(agent).filter(([key]) => key !== 'model' && key !== 'effort' && key !== 'fastMode')), path);
2110
+ rejectSnapshotKeys(agent, [
2111
+ 'adapter',
2112
+ 'model',
2113
+ 'effort',
2114
+ 'fastMode',
2115
+ 'subagentModel',
2116
+ 'instruction',
2117
+ 'permissions',
2118
+ ], path);
2119
+ const fixed = snapshotFixedAgent(Object.fromEntries(Object.entries(agent).filter(([key]) => key !== 'model' &&
2120
+ key !== 'effort' &&
2121
+ key !== 'fastMode' &&
2122
+ key !== 'subagentModel')), path);
2105
2123
  return {
2106
2124
  adapter: fixed.adapter,
2107
2125
  ...(fixed.instruction === undefined
@@ -2115,6 +2133,11 @@ function snapshotSessionAgent(value, path) {
2115
2133
  ...(agent.fastMode === undefined
2116
2134
  ? {}
2117
2135
  : { fastMode: snapshotFastMode(agent.fastMode, `${path}.fastMode`) }),
2136
+ ...(agent.subagentModel === undefined
2137
+ ? {}
2138
+ : {
2139
+ subagentModel: snapshotSubagentModel(agent.subagentModel, `${path}.subagentModel`),
2140
+ }),
2118
2141
  };
2119
2142
  }
2120
2143
  function fixedAgent(agent) {
@@ -2126,12 +2149,15 @@ function fixedAgent(agent) {
2126
2149
  }
2127
2150
  function callSettings(agent, tuning = agent) {
2128
2151
  // cligent treats supplied call settings as a complete replacement, so an
2129
- // omitted fastMode here is a request for the provider default, never an
2130
- // inheritance of whatever the previous call left behind.
2152
+ // omitted fastMode or subagentModel here is a request for the provider
2153
+ // default, never an inheritance of whatever the previous call left behind.
2131
2154
  return {
2132
2155
  model: tuning.model,
2133
2156
  effort: tuning.effort,
2134
2157
  ...(tuning.fastMode === undefined ? {} : { fastMode: tuning.fastMode }),
2158
+ ...(tuning.subagentModel === undefined
2159
+ ? {}
2160
+ : { subagentModel: tuning.subagentModel }),
2135
2161
  ...(agent.instruction === undefined ? {} : { instruction: agent.instruction }),
2136
2162
  ...(agent.permissions === undefined ? {} : { permissions: agent.permissions }),
2137
2163
  };
@@ -2259,7 +2285,7 @@ async function buildEnablements(options, loadModule, hostCapabilities) {
2259
2285
  for (const role of entry.requiredRoleIds) {
2260
2286
  const path = `captain.options.playbooks.${id}.roles.${role}`;
2261
2287
  const rawBinding = snapshotRecord(roleRecord[role], path);
2262
- rejectSnapshotKeys(rawBinding, ['playerId', 'model', 'effort', 'fastMode'], path);
2288
+ rejectSnapshotKeys(rawBinding, ['playerId', 'model', 'effort', 'fastMode', 'subagentModel'], path);
2263
2289
  const playerId = snapshotString(rawBinding.playerId, `${path}.playerId`);
2264
2290
  if (!PLAYER_ID_PATTERN.test(playerId) || playerId === INTERNAL_CAPTAIN_ID) {
2265
2291
  throw new Error(`${path}.playerId is not a canonical player id`);
@@ -2277,6 +2303,11 @@ async function buildEnablements(options, loadModule, hostCapabilities) {
2277
2303
  : {
2278
2304
  fastMode: snapshotFastMode(rawBinding.fastMode, `${path}.fastMode`),
2279
2305
  }),
2306
+ ...(rawBinding.subagentModel === undefined
2307
+ ? {}
2308
+ : {
2309
+ subagentModel: snapshotSubagentModel(rawBinding.subagentModel, `${path}.subagentModel`),
2310
+ }),
2280
2311
  agent,
2281
2312
  });
2282
2313
  }
@@ -69,10 +69,16 @@ interface SessionAgent {
69
69
  /** Adapter-scoped fast mode. Absence is the provider default; `false` is a
70
70
  * literal request, so this carries no provider-default sentinel. */
71
71
  readonly fastMode?: boolean;
72
+ /** The model every subagent of this agent runs on (DR-075). Absence is the
73
+ * provider default. */
74
+ readonly subagentModel?: string;
72
75
  readonly instruction?: string;
73
76
  readonly permissions?: PermissionPolicy;
74
77
  }
75
78
 
79
+ /** Per-call tuning: erased from every structural envelope and snapshot. */
80
+ type TuningKey = 'model' | 'effort' | 'fastMode' | 'subagentModel';
81
+
76
82
  interface PlayerLedgerEntry {
77
83
  readonly adapter: string;
78
84
  readonly instruction?: string;
@@ -107,9 +113,7 @@ interface PlaybookCaptainUnresolvedEffectSettlementInput {
107
113
  readonly unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[];
108
114
  }
109
115
 
110
- type SnapshotAgentEnvelope = DeepReadonly<
111
- Omit<SessionAgent, 'model' | 'effort' | 'fastMode'>
112
- >;
116
+ type SnapshotAgentEnvelope = DeepReadonly<Omit<SessionAgent, TuningKey>>;
113
117
 
114
118
  type PlayerLedgerSnapshotEntry = DeepReadonly<PlayerLedgerEntry>;
115
119
 
@@ -447,6 +451,7 @@ interface EffectivePlayerBinding {
447
451
  readonly model: TuningSelection;
448
452
  readonly effort: TuningSelection<Effort>;
449
453
  readonly fastMode?: boolean;
454
+ readonly subagentModel?: string;
450
455
  readonly agent: SessionAgent;
451
456
  }
452
457
 
@@ -3722,6 +3727,19 @@ function snapshotFastMode(
3722
3727
  return value;
3723
3728
  }
3724
3729
 
3730
+ function snapshotSubagentModel(
3731
+ value: JsonValue | undefined,
3732
+ path: string,
3733
+ ): string | undefined {
3734
+ if (
3735
+ value !== undefined &&
3736
+ (typeof value !== 'string' || value.trim().length === 0)
3737
+ ) {
3738
+ throw new TypeError(`${path} must be a nonblank string`);
3739
+ }
3740
+ return value;
3741
+ }
3742
+
3725
3743
  function snapshotSessionAgent(
3726
3744
  value: JsonValue | undefined,
3727
3745
  path: string,
@@ -3729,14 +3747,25 @@ function snapshotSessionAgent(
3729
3747
  const agent = snapshotRecord(value, path);
3730
3748
  rejectSnapshotKeys(
3731
3749
  agent,
3732
- ['adapter', 'model', 'effort', 'fastMode', 'instruction', 'permissions'],
3750
+ [
3751
+ 'adapter',
3752
+ 'model',
3753
+ 'effort',
3754
+ 'fastMode',
3755
+ 'subagentModel',
3756
+ 'instruction',
3757
+ 'permissions',
3758
+ ],
3733
3759
  path,
3734
3760
  );
3735
3761
  const fixed = snapshotFixedAgent(
3736
3762
  Object.fromEntries(
3737
3763
  Object.entries(agent).filter(
3738
3764
  ([key]) =>
3739
- key !== 'model' && key !== 'effort' && key !== 'fastMode',
3765
+ key !== 'model' &&
3766
+ key !== 'effort' &&
3767
+ key !== 'fastMode' &&
3768
+ key !== 'subagentModel',
3740
3769
  ),
3741
3770
  ) as JsonValue,
3742
3771
  path,
@@ -3754,12 +3783,18 @@ function snapshotSessionAgent(
3754
3783
  ...(agent.fastMode === undefined
3755
3784
  ? {}
3756
3785
  : { fastMode: snapshotFastMode(agent.fastMode, `${path}.fastMode`) }),
3786
+ ...(agent.subagentModel === undefined
3787
+ ? {}
3788
+ : {
3789
+ subagentModel: snapshotSubagentModel(
3790
+ agent.subagentModel,
3791
+ `${path}.subagentModel`,
3792
+ ),
3793
+ }),
3757
3794
  };
3758
3795
  }
3759
3796
 
3760
- function fixedAgent(
3761
- agent: SessionAgent,
3762
- ): Omit<SessionAgent, 'model' | 'effort' | 'fastMode'> {
3797
+ function fixedAgent(agent: SessionAgent): Omit<SessionAgent, TuningKey> {
3763
3798
  return {
3764
3799
  adapter: agent.adapter,
3765
3800
  ...(agent.instruction === undefined ? {} : { instruction: agent.instruction }),
@@ -3769,15 +3804,18 @@ function fixedAgent(
3769
3804
 
3770
3805
  function callSettings(
3771
3806
  agent: SessionAgent,
3772
- tuning: Pick<SessionAgent, 'model' | 'effort' | 'fastMode'> = agent,
3807
+ tuning: Pick<SessionAgent, TuningKey> = agent,
3773
3808
  ): AgentCallSettings {
3774
3809
  // cligent treats supplied call settings as a complete replacement, so an
3775
- // omitted fastMode here is a request for the provider default, never an
3776
- // inheritance of whatever the previous call left behind.
3810
+ // omitted fastMode or subagentModel here is a request for the provider
3811
+ // default, never an inheritance of whatever the previous call left behind.
3777
3812
  return {
3778
3813
  model: tuning.model,
3779
3814
  effort: tuning.effort,
3780
3815
  ...(tuning.fastMode === undefined ? {} : { fastMode: tuning.fastMode }),
3816
+ ...(tuning.subagentModel === undefined
3817
+ ? {}
3818
+ : { subagentModel: tuning.subagentModel }),
3781
3819
  ...(agent.instruction === undefined ? {} : { instruction: agent.instruction }),
3782
3820
  ...(agent.permissions === undefined ? {} : { permissions: agent.permissions }),
3783
3821
  };
@@ -4005,7 +4043,7 @@ async function buildEnablements(
4005
4043
  const rawBinding = snapshotRecord(roleRecord[role], path);
4006
4044
  rejectSnapshotKeys(
4007
4045
  rawBinding,
4008
- ['playerId', 'model', 'effort', 'fastMode'],
4046
+ ['playerId', 'model', 'effort', 'fastMode', 'subagentModel'],
4009
4047
  path,
4010
4048
  );
4011
4049
  const playerId = snapshotString(rawBinding.playerId, `${path}.playerId`);
@@ -4030,6 +4068,14 @@ async function buildEnablements(
4030
4068
  `${path}.fastMode`,
4031
4069
  ),
4032
4070
  }),
4071
+ ...(rawBinding.subagentModel === undefined
4072
+ ? {}
4073
+ : {
4074
+ subagentModel: snapshotSubagentModel(
4075
+ rawBinding.subagentModel,
4076
+ `${path}.subagentModel`,
4077
+ ),
4078
+ }),
4033
4079
  agent,
4034
4080
  });
4035
4081
  }
@@ -8,10 +8,11 @@
8
8
 
9
9
  # The Captain and each stable session player carries its own defaults inline:
10
10
  # an adapter shorthand (claude, codex) or a block with
11
- # adapter/model/effort/fastMode/permissions. A role may override only model,
12
- # effort, or fastMode.
13
- # Omit an override to inherit the player default. For model and effort, false
14
- # selects the provider default; fastMode false is a literal disabled request.
11
+ # adapter/model/effort/fastMode/subagentModel/permissions. A role may override
12
+ # only model, effort, fastMode, or subagentModel.
13
+ # Omit an override to inherit the player default. For model, effort, and
14
+ # subagentModel, false selects the provider default; fastMode false is a
15
+ # literal disabled request.
15
16
 
16
17
  # Each adapter needs its vendor SDK installed as its own top-level
17
18
  # install root — they are optional peer dependencies, so you pay only