@sublang/playbook 17.2.0 → 17.3.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, fast mode, and
206
- subagent model in a `--with` overlay, invoke the effective `/command`, and run from the working directory
205
+ under `playbooks.<id>.roles`, tune compatible model, effort, fast mode,
206
+ subagent model, and subagent effort 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,9 +280,10 @@ 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` 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
283
+ current `model`, `effort`, and optional `fastMode`, `subagentModel`, and
284
+ `subagentEffort` settings apply to the next call. Boolean `false` explicitly
285
+ resets model, effort, subagent model, or subagent effort to the provider
286
+ default, but `fastMode: false` is a literal
286
287
  disabled request; omission selects the provider default or inherits the player
287
288
  value at a role binding.
288
289
  Provider conversations are local hints. Missing hints start fresh from stored
@@ -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`, `subagentModel`, `instruction`, and `permissions`.
52
- Settings are inline per stable agent
51
+ `effort`, `fastMode`, `subagentModel`, `subagentEffort`, `instruction`, and
52
+ `permissions`. 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
@@ -157,7 +157,7 @@ roles:
157
157
  ```
158
158
 
159
159
  Use a block to override only that role invocation's model, effort, fast mode,
160
- or subagent model:
160
+ subagent model, or subagent effort:
161
161
 
162
162
  ```yaml
163
163
  roles:
@@ -168,25 +168,36 @@ roles:
168
168
  fastMode: false # literal disabled request, not a default sentinel
169
169
  reviewer:
170
170
  player: dev.reviewer
171
- subagentModel: claude-sonnet-5-5 # every subagent of this role runs on it
171
+ subagentModel: inherit # inherit | <model> | false
172
+ subagentEffort: medium # <effort> | false
172
173
  ```
173
174
 
174
175
  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,
176
+ `effort`, `subagentModel`, and `subagentEffort`, boolean `false` selects the
177
+ provider default explicitly, so a resumed conversation cannot accidentally
178
+ retain an earlier selection. For `fastMode`, `false` is a literal request to
179
+ disable fast mode; omitting the top-level setting selects the provider default.
180
+ A present fast-mode boolean, subagent model, or subagent effort is accepted
181
+ only for adapters Cligent reports as supporting it. A role binding cannot override adapter, instruction,
181
182
  permissions, workspace, or tool posture; those define the stable player
182
183
  envelope, so an overriding model must be one that player's adapter serves.
183
184
 
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
185
+ `subagentModel` makes every subagent the agent starts run on one model —
186
+ `inherit` names the agent's own, a model name pins another, and `false` on a
187
+ `captain` or `players.<player-id>` block switches delegation off — and tells
188
+ the agent to hand well-defined, fine-grained tasks to its subagents while
186
189
  keeping the deep thinking, reasoning, and design work itself, without lowering
187
190
  the quality of what it delivers
188
191
  ([DR-075](https://github.com/sublang-ai/playbook/blob/main/specs/decisions/075-subagent-model-is-tuning.md)).
189
- Nothing sets one by default.
192
+ `subagentEffort` pins the effort every subagent runs at, any effort the
193
+ adapter accepts other than `ultracode`; left unset, the agent chooses an effort
194
+ per task, so its own effort never silently governs its subagents.
195
+ An agent whose adapter serves a subagent model (Claude today) and whose block
196
+ leaves `subagentModel` unset resolves to `inherit`: it delegates by default.
197
+ The launcher resolves this on its own copy and never rewrites your config, and
198
+ the starter config names neither field
199
+ ([DR-076](https://github.com/sublang-ai/playbook/blob/main/specs/decisions/076-subagents-delegate-by-default.md)).
200
+
190
201
 
191
202
  ## Sharing, isolation, and concurrency
192
203
 
@@ -279,6 +290,7 @@ playbooks:
279
290
  coder:
280
291
  player: dev.coder
281
292
  effort: low
293
+ subagentEffort: high # its subagents still run at high effort
282
294
  ```
283
295
 
284
296
  The global file is never modified, and `--with` is not forwarded to
@@ -286,9 +298,10 @@ The global file is never modified, and `--with` is not forwarded to
286
298
  Overlays apply when creating a fresh session and as current-config input for a
287
299
  compatible ordinary reopen. A selected session keeps its stored catalog,
288
300
  player roster, role bindings, adapter, instruction, permissions, and working
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.
301
+ directory; only model, effort, fast mode, subagent model, and subagent effort
302
+ may change. The next call reapplies both complete model and effort selections,
303
+ the optional effective fast-mode boolean, and the optional effective subagent
304
+ model and subagent effort. An uncertain retry accepts no tuning overlay and only restores and reports the recorded attempt. Later turns use the current compatible settings.
292
305
 
293
306
  ## Session storage
294
307
 
@@ -329,8 +342,8 @@ public UUID. Presentation-only fields are inert headlessly.
329
342
  An ordinary reopen reads current config and opening overlays, but first
330
343
  projects them to the stored playbooks and referenced players. An unrelated new
331
344
  entry cannot enter or invalidate the session. Structural drift fails closed;
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
345
+ compatible model, effort, fast-mode, subagent-model, or subagent-effort
346
+ changes apply on the next provider call. Legacy record, shell, runtime-snapshot, and trace schemas are rejected
334
347
  rather than having role or player identity guessed.
335
348
 
336
349
  ## External playbooks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sublang/playbook",
3
- "version": "17.2.0",
3
+ "version": "17.3.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.29.0",
246
+ "@sublang/cligent": "^0.30.0",
247
247
  "@sublang/spex": "^3.0.0",
248
248
  "p-queue": "^9.3.1",
249
249
  "xstate": "^5.19.4",
@@ -28,6 +28,7 @@ import {
28
28
  parseDocument as parseYamlDocument,
29
29
  stringify as stringifyYaml,
30
30
  } from "yaml";
31
+ import { isSubagentModelSupported } from "@sublang/cligent";
31
32
  import { loadTmuxPlayConfig } from "@sublang/cligent/tmux-play";
32
33
  import {
33
34
  defaultCaptainSessionsDir,
@@ -379,12 +380,14 @@ export async function normalizeSelectedLaunchPlanDataOnly(
379
380
  const configuredAgents = new Map();
380
381
  for (const playerId of expectedMembers.playerIds) {
381
382
  assertPlayerId(playerId, `players.${playerId}`);
382
- const agent = resolveAgent(playersCfg[playerId], `players.${playerId}`, [
383
- "id",
384
- ]);
383
+ const agent = resolveDelegationDefault(
384
+ resolveAgent(playersCfg[playerId], `players.${playerId}`, ["id"]),
385
+ );
385
386
  configuredAgents.set(playerId, agent);
386
387
  }
387
- let captain = resolveAgent(top.captain, "captain", ["from", "options"]);
388
+ let captain = resolveDelegationDefault(
389
+ resolveAgent(top.captain, "captain", ["from", "options"]),
390
+ );
388
391
 
389
392
  const playbooksCfg = requireObject(top.playbooks, "playbooks");
390
393
  const tuningChecks = [];
@@ -443,7 +446,8 @@ export async function normalizeSelectedLaunchPlanDataOnly(
443
446
  binding.model !== undefined ||
444
447
  binding.effort !== undefined ||
445
448
  binding.fastMode !== undefined ||
446
- binding.subagentModel !== undefined
449
+ binding.subagentModel !== undefined ||
450
+ binding.subagentEffort !== undefined
447
451
  ) {
448
452
  let checkId;
449
453
  do {
@@ -559,7 +563,8 @@ export async function normalizeSelectedLaunchPlanDataOnly(
559
563
  ? {}
560
564
  : { fastMode: agent.fastMode }
561
565
  : { fastMode: binding.fastMode }),
562
- ...effectiveSubagentModel(binding, agent),
566
+ ...effectiveOptionalTuning(binding, agent, "subagentModel"),
567
+ ...effectiveOptionalTuning(binding, agent, "subagentEffort"),
563
568
  },
564
569
  ];
565
570
  }),
@@ -689,9 +694,9 @@ export async function normalizeLaunchPlan(
689
694
  const configuredAgents = new Map();
690
695
  for (const playerId of allPlayerIds) {
691
696
  assertPlayerId(playerId, `players.${playerId}`);
692
- const agent = resolveAgent(playersCfg[playerId], `players.${playerId}`, [
693
- "id",
694
- ]);
697
+ const agent = resolveDelegationDefault(
698
+ resolveAgent(playersCfg[playerId], `players.${playerId}`, ["id"]),
699
+ );
695
700
  if (
696
701
  typeof agent.adapter !== "string" ||
697
702
  agent.adapter.trim().length === 0
@@ -710,7 +715,9 @@ export async function normalizeLaunchPlan(
710
715
  throw new Error("playbooks keys must be canonical trimmed nonblank ids");
711
716
  }
712
717
 
713
- let captain = resolveAgent(top.captain, "captain", ["from", "options"]);
718
+ let captain = resolveDelegationDefault(
719
+ resolveAgent(top.captain, "captain", ["from", "options"]),
720
+ );
714
721
  if (
715
722
  typeof captain.adapter !== "string" ||
716
723
  captain.adapter.trim().length === 0
@@ -790,7 +797,8 @@ export async function normalizeLaunchPlan(
790
797
  binding.model !== undefined ||
791
798
  binding.effort !== undefined ||
792
799
  binding.fastMode !== undefined ||
793
- binding.subagentModel !== undefined
800
+ binding.subagentModel !== undefined ||
801
+ binding.subagentEffort !== undefined
794
802
  ) {
795
803
  let checkId;
796
804
  do {
@@ -1002,7 +1010,8 @@ export async function normalizeLaunchPlan(
1002
1010
  ? {}
1003
1011
  : { fastMode: agent.fastMode }
1004
1012
  : { fastMode: binding.fastMode }),
1005
- ...effectiveSubagentModel(binding, agent),
1013
+ ...effectiveOptionalTuning(binding, agent, "subagentModel"),
1014
+ ...effectiveOptionalTuning(binding, agent, "subagentEffort"),
1006
1015
  },
1007
1016
  ];
1008
1017
  }),
@@ -1488,7 +1497,8 @@ function migrateUserConfigIfRetired(userConfigPath, onNotice) {
1488
1497
  throw new Error(
1489
1498
  `cannot migrate the retired profiles config at ${userConfigPath}: ` +
1490
1499
  `${errorMessage(error)} — edit it by hand: each agent takes its own ` +
1491
- "adapter, model, effort, fast mode, subagent model, and permissions",
1500
+ "adapter, model, effort, fast mode, subagent model, subagent effort, " +
1501
+ "and permissions",
1492
1502
  );
1493
1503
  }
1494
1504
  if (migrated === undefined) return;
@@ -1624,7 +1634,8 @@ function assertNoRetiredProfiles(top, configPath) {
1624
1634
  throw new Error(
1625
1635
  `top-level "profiles" was removed${where}: write each agent's settings ` +
1626
1636
  "inline under captain and each top-level players.<player-id> " +
1627
- "(adapter, model, effort, fast mode, subagent model, permissions)",
1637
+ "(adapter, model, effort, fast mode, subagent model, subagent effort, " +
1638
+ "permissions)",
1628
1639
  );
1629
1640
  }
1630
1641
  const legacyPath = findLegacyPlayersPath(top);
@@ -1640,7 +1651,7 @@ function assertNoRetiredProfiles(top, configPath) {
1640
1651
  throw new Error(
1641
1652
  `${path}.profile was removed${where}: write the agent's settings ` +
1642
1653
  "inline in that block (adapter, model, effort, fast mode, " +
1643
- "subagent model, permissions)",
1654
+ "subagent model, subagent effort, permissions)",
1644
1655
  );
1645
1656
  }
1646
1657
  }
@@ -2040,13 +2051,20 @@ function resolveRoleBinding(value, path) {
2040
2051
  const block = requireObject(value, path);
2041
2052
  const unknown = Object.keys(block).filter(
2042
2053
  (key) =>
2043
- !["player", "model", "effort", "fastMode", "subagentModel"].includes(key),
2054
+ ![
2055
+ "player",
2056
+ "model",
2057
+ "effort",
2058
+ "fastMode",
2059
+ "subagentModel",
2060
+ "subagentEffort",
2061
+ ].includes(key),
2044
2062
  );
2045
2063
  if (unknown.length > 0) {
2046
2064
  throw new Error(`${path} has unknown ${formatKeyList(unknown)}`);
2047
2065
  }
2048
2066
  assertPlayerId(block.player, `${path}.player`);
2049
- for (const field of ["model", "effort", "subagentModel"]) {
2067
+ for (const field of ["model", "effort", "subagentModel", "subagentEffort"]) {
2050
2068
  if (
2051
2069
  block[field] !== undefined &&
2052
2070
  block[field] !== false &&
@@ -2071,19 +2089,41 @@ function resolveRoleBinding(value, path) {
2071
2089
  ...(block.subagentModel === undefined
2072
2090
  ? {}
2073
2091
  : { subagentModel: block.subagentModel }),
2092
+ ...(block.subagentEffort === undefined
2093
+ ? {}
2094
+ : { subagentEffort: block.subagentEffort }),
2074
2095
  };
2075
2096
  }
2076
2097
 
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 };
2098
+ // A binding's subagent model or subagent effort overrides the player's;
2099
+ // `false` selects the provider default, which a complete call expresses by
2100
+ // omission (for the effort: the agent chooses per task).
2101
+ function effectiveOptionalTuning(binding, agent, field) {
2102
+ if (binding[field] === false) return {};
2103
+ if (binding[field] !== undefined) return { [field]: binding[field] };
2104
+ return agent[field] === undefined ? {} : { [field]: agent[field] };
2105
+ }
2106
+
2107
+ // DR-076: an agent whose adapter serves a subagent model delegates by
2108
+ // default. Resolved on the launcher's detached copy of a Captain or player
2109
+ // block — never the user's file — before cligent normalization, so cligent
2110
+ // validates a subagent effort against the resolved model, and both front
2111
+ // ends and the headless run share the result. A block's `false` opts out:
2112
+ // nothing is sent and no delegation directive is composed.
2113
+ function resolveDelegationDefault(agent) {
2114
+ if (!isObject(agent)) return agent;
2115
+ if (agent.subagentModel === false) {
2116
+ const { subagentModel: _off, ...rest } = agent;
2117
+ return rest;
2083
2118
  }
2084
- return agent.subagentModel === undefined
2085
- ? {}
2086
- : { subagentModel: agent.subagentModel };
2119
+ if (
2120
+ agent.subagentModel === undefined &&
2121
+ typeof agent.adapter === "string" &&
2122
+ isSubagentModelSupported(agent.adapter)
2123
+ ) {
2124
+ return { ...agent, subagentModel: "inherit" };
2125
+ }
2126
+ return agent;
2087
2127
  }
2088
2128
 
2089
2129
  function applyTuningOverrides(agent, binding) {
@@ -2096,9 +2136,9 @@ function applyTuningOverrides(agent, binding) {
2096
2136
  else effective.effort = binding.effort;
2097
2137
  }
2098
2138
  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;
2139
+ for (const field of ["subagentModel", "subagentEffort"]) {
2140
+ if (binding[field] === false) delete effective[field];
2141
+ else if (binding[field] !== undefined) effective[field] = binding[field];
2102
2142
  }
2103
2143
  return effective;
2104
2144
  }
@@ -2117,11 +2157,19 @@ function sessionAgentFromHostAgent(agent, path) {
2117
2157
  ...(agent.subagentModel === undefined
2118
2158
  ? {}
2119
2159
  : {
2120
- subagentModel: subagentModelSelection(
2160
+ subagentModel: nonblankSelection(
2121
2161
  agent.subagentModel,
2122
2162
  `${path}.subagentModel`,
2123
2163
  ),
2124
2164
  }),
2165
+ ...(agent.subagentEffort === undefined
2166
+ ? {}
2167
+ : {
2168
+ subagentEffort: nonblankSelection(
2169
+ agent.subagentEffort,
2170
+ `${path}.subagentEffort`,
2171
+ ),
2172
+ }),
2125
2173
  ...(agent.instruction === undefined
2126
2174
  ? {}
2127
2175
  : { instruction: agent.instruction }),
@@ -2150,6 +2198,9 @@ export function projectHostAgent(agent, path = "agent") {
2150
2198
  ...(normalized.subagentModel === undefined
2151
2199
  ? {}
2152
2200
  : { subagentModel: normalized.subagentModel }),
2201
+ ...(normalized.subagentEffort === undefined
2202
+ ? {}
2203
+ : { subagentEffort: normalized.subagentEffort }),
2153
2204
  ...(normalized.instruction === undefined
2154
2205
  ? {}
2155
2206
  : { instruction: normalized.instruction }),
@@ -2166,7 +2217,7 @@ function fastModeSelection(value, path) {
2166
2217
  return value;
2167
2218
  }
2168
2219
 
2169
- function subagentModelSelection(value, path) {
2220
+ function nonblankSelection(value, path) {
2170
2221
  if (typeof value !== "string" || value.trim().length === 0) {
2171
2222
  throw new Error(`${path} must be a nonblank string`);
2172
2223
  }
@@ -967,9 +967,11 @@ function helpText({
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
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,",
972
- " while fastMode false is a literal disabled request",
970
+ " subagentModel?, subagentEffort? } may retune one role;",
971
+ " false selects provider-default model/effort/subagentModel/",
972
+ " subagentEffort, while fastMode false is a literal disabled request",
973
+ " - a Claude agent delegates by default: subagentModel resolves to",
974
+ " inherit (its own model) unless the block sets false",
973
975
  " - reusing one id deliberately shares that provider conversation;",
974
976
  " distinct ids stay isolated even when their agent blocks are equal",
975
977
  " - the launcher injects captain.from and retains referenced player ids",
@@ -1367,11 +1367,14 @@ export async function validateFrozenExecutionConfig(
1367
1367
  return config;
1368
1368
  }
1369
1369
 
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.
1370
+ // Fast mode, the subagent model and the subagent effort are adapter-scoped
1371
+ // tuning (DR-075, DR-076): a frozen record re-validates each present value
1372
+ // against its adapter. Cligent serves the subagent effort exactly where it
1373
+ // serves a subagent model, so one capability assertion covers both.
1372
1374
  const FROZEN_ADAPTER_SCOPED_TUNING = [
1373
1375
  ["fastMode", assertFastModeSupported],
1374
1376
  ["subagentModel", assertSubagentModelSupported],
1377
+ ["subagentEffort", assertSubagentModelSupported],
1375
1378
  ];
1376
1379
 
1377
1380
  function assertFrozenTuningSupported(config) {
@@ -1887,7 +1890,7 @@ function runHelpText(userConfigPath) {
1887
1890
  "working directory and reports when it uses the global newest fallback.",
1888
1891
  "An ordinary continued run restores that stored structure and working",
1889
1892
  "directory, then reads current config and overlays for model, effort,",
1890
- "fast mode, and subagent model.",
1893
+ "fast mode, subagent model, and subagent effort.",
1891
1894
  "Uncertain retry only restores and reports; later work uses current settings.",
1892
1895
  "",
1893
1896
  "Options:",
@@ -5402,7 +5402,13 @@ function validateCaptainSessionProjection(
5402
5402
  : ['id', 'adapter', 'model', 'effort'],
5403
5403
  structural
5404
5404
  ? ['instruction', 'permissions']
5405
- : ['instruction', 'permissions', 'fastMode', 'subagentModel'],
5405
+ : [
5406
+ 'instruction',
5407
+ 'permissions',
5408
+ 'fastMode',
5409
+ 'subagentModel',
5410
+ 'subagentEffort',
5411
+ ],
5406
5412
  playerPath,
5407
5413
  );
5408
5414
  assertPlayerId(player.id, `${playerPath}.id`);
@@ -5503,16 +5509,13 @@ function validateCaptainSessionProjection(
5503
5509
  structural
5504
5510
  ? ['playerId']
5505
5511
  : ['playerId', 'model', 'effort'],
5506
- structural ? [] : ['fastMode', 'subagentModel'],
5512
+ structural ? [] : ['fastMode', 'subagentModel', 'subagentEffort'],
5507
5513
  bindingPath,
5508
5514
  );
5509
5515
  assertPlayerId(binding.playerId, `${bindingPath}.playerId`);
5510
5516
  if (!structural) {
5511
5517
  validateProjectedFastMode(binding, `${bindingPath}.fastMode`);
5512
- validateProjectedSubagentModel(
5513
- binding,
5514
- `${bindingPath}.subagentModel`,
5515
- );
5518
+ validateProjectedSubagentTuning(binding, bindingPath);
5516
5519
  validateTuningSelection(binding.model, `${bindingPath}.model`);
5517
5520
  const player = projection.players.find(
5518
5521
  (candidate) => candidate.id === binding.playerId,
@@ -5560,10 +5563,14 @@ function validateProjectedFastMode(record, path) {
5560
5563
  }
5561
5564
  }
5562
5565
 
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);
5566
+ // Like fast mode, absence is the canonical provider default (DR-075); the
5567
+ // subagent model may be the literal `inherit`, and an absent subagent effort
5568
+ // leaves each subagent's effort to the agent (DR-076).
5569
+ function validateProjectedSubagentTuning(record, path) {
5570
+ for (const field of ['subagentModel', 'subagentEffort']) {
5571
+ if (Object.hasOwn(record, field)) {
5572
+ requireNonblank(record[field], `${path}.${field}`);
5573
+ }
5567
5574
  }
5568
5575
  }
5569
5576
 
@@ -5571,7 +5578,7 @@ function validateProjectedAgent(value, path, { structural, hasId = false }) {
5571
5578
  const agent = requireRecord(value, path);
5572
5579
  if (!structural) {
5573
5580
  validateProjectedFastMode(agent, `${path}.fastMode`);
5574
- validateProjectedSubagentModel(agent, `${path}.subagentModel`);
5581
+ validateProjectedSubagentTuning(agent, path);
5575
5582
  }
5576
5583
  if (!hasId) {
5577
5584
  exactOptionalKeys(
@@ -5579,7 +5586,13 @@ function validateProjectedAgent(value, path, { structural, hasId = false }) {
5579
5586
  structural ? ['adapter'] : ['adapter', 'model', 'effort'],
5580
5587
  structural
5581
5588
  ? ['instruction', 'permissions']
5582
- : ['instruction', 'permissions', 'fastMode', 'subagentModel'],
5589
+ : [
5590
+ 'instruction',
5591
+ 'permissions',
5592
+ 'fastMode',
5593
+ 'subagentModel',
5594
+ 'subagentEffort',
5595
+ ],
5583
5596
  path,
5584
5597
  );
5585
5598
  }
@@ -10,14 +10,17 @@ 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. */
13
+ /** The model every subagent of this agent runs on (DR-075), or `inherit`
14
+ * for the agent's own model (DR-076). Absence is the provider default. */
15
15
  readonly subagentModel?: string;
16
+ /** The effort every subagent of this agent runs at (DR-076). Absence leaves
17
+ * each subagent's effort to the agent. */
18
+ readonly subagentEffort?: string;
16
19
  readonly instruction?: string;
17
20
  readonly permissions?: PermissionPolicy;
18
21
  }
19
22
  /** Per-call tuning: erased from every structural envelope and snapshot. */
20
- type TuningKey = 'model' | 'effort' | 'fastMode' | 'subagentModel';
23
+ type TuningKey = 'model' | 'effort' | 'fastMode' | 'subagentModel' | 'subagentEffort';
21
24
  interface PlayerLedgerEntry {
22
25
  readonly adapter: string;
23
26
  readonly instruction?: string;
@@ -2098,7 +2098,7 @@ function snapshotFastMode(value, path) {
2098
2098
  }
2099
2099
  return value;
2100
2100
  }
2101
- function snapshotSubagentModel(value, path) {
2101
+ function snapshotNonblankTuning(value, path) {
2102
2102
  if (value !== undefined &&
2103
2103
  (typeof value !== 'string' || value.trim().length === 0)) {
2104
2104
  throw new TypeError(`${path} must be a nonblank string`);
@@ -2113,13 +2113,15 @@ function snapshotSessionAgent(value, path) {
2113
2113
  'effort',
2114
2114
  'fastMode',
2115
2115
  'subagentModel',
2116
+ 'subagentEffort',
2116
2117
  'instruction',
2117
2118
  'permissions',
2118
2119
  ], path);
2119
2120
  const fixed = snapshotFixedAgent(Object.fromEntries(Object.entries(agent).filter(([key]) => key !== 'model' &&
2120
2121
  key !== 'effort' &&
2121
2122
  key !== 'fastMode' &&
2122
- key !== 'subagentModel')), path);
2123
+ key !== 'subagentModel' &&
2124
+ key !== 'subagentEffort')), path);
2123
2125
  return {
2124
2126
  adapter: fixed.adapter,
2125
2127
  ...(fixed.instruction === undefined
@@ -2136,7 +2138,12 @@ function snapshotSessionAgent(value, path) {
2136
2138
  ...(agent.subagentModel === undefined
2137
2139
  ? {}
2138
2140
  : {
2139
- subagentModel: snapshotSubagentModel(agent.subagentModel, `${path}.subagentModel`),
2141
+ subagentModel: snapshotNonblankTuning(agent.subagentModel, `${path}.subagentModel`),
2142
+ }),
2143
+ ...(agent.subagentEffort === undefined
2144
+ ? {}
2145
+ : {
2146
+ subagentEffort: snapshotNonblankTuning(agent.subagentEffort, `${path}.subagentEffort`),
2140
2147
  }),
2141
2148
  };
2142
2149
  }
@@ -2150,7 +2157,9 @@ function fixedAgent(agent) {
2150
2157
  function callSettings(agent, tuning = agent) {
2151
2158
  // cligent treats supplied call settings as a complete replacement, so an
2152
2159
  // omitted fastMode or subagentModel here is a request for the provider
2153
- // default, never an inheritance of whatever the previous call left behind.
2160
+ // default, and an omitted subagentEffort leaves each subagent's effort to
2161
+ // the agent — never an inheritance of whatever the previous call left
2162
+ // behind.
2154
2163
  return {
2155
2164
  model: tuning.model,
2156
2165
  effort: tuning.effort,
@@ -2158,6 +2167,9 @@ function callSettings(agent, tuning = agent) {
2158
2167
  ...(tuning.subagentModel === undefined
2159
2168
  ? {}
2160
2169
  : { subagentModel: tuning.subagentModel }),
2170
+ ...(tuning.subagentEffort === undefined
2171
+ ? {}
2172
+ : { subagentEffort: tuning.subagentEffort }),
2161
2173
  ...(agent.instruction === undefined ? {} : { instruction: agent.instruction }),
2162
2174
  ...(agent.permissions === undefined ? {} : { permissions: agent.permissions }),
2163
2175
  };
@@ -2285,7 +2297,14 @@ async function buildEnablements(options, loadModule, hostCapabilities) {
2285
2297
  for (const role of entry.requiredRoleIds) {
2286
2298
  const path = `captain.options.playbooks.${id}.roles.${role}`;
2287
2299
  const rawBinding = snapshotRecord(roleRecord[role], path);
2288
- rejectSnapshotKeys(rawBinding, ['playerId', 'model', 'effort', 'fastMode', 'subagentModel'], path);
2300
+ rejectSnapshotKeys(rawBinding, [
2301
+ 'playerId',
2302
+ 'model',
2303
+ 'effort',
2304
+ 'fastMode',
2305
+ 'subagentModel',
2306
+ 'subagentEffort',
2307
+ ], path);
2289
2308
  const playerId = snapshotString(rawBinding.playerId, `${path}.playerId`);
2290
2309
  if (!PLAYER_ID_PATTERN.test(playerId) || playerId === INTERNAL_CAPTAIN_ID) {
2291
2310
  throw new Error(`${path}.playerId is not a canonical player id`);
@@ -2306,7 +2325,12 @@ async function buildEnablements(options, loadModule, hostCapabilities) {
2306
2325
  ...(rawBinding.subagentModel === undefined
2307
2326
  ? {}
2308
2327
  : {
2309
- subagentModel: snapshotSubagentModel(rawBinding.subagentModel, `${path}.subagentModel`),
2328
+ subagentModel: snapshotNonblankTuning(rawBinding.subagentModel, `${path}.subagentModel`),
2329
+ }),
2330
+ ...(rawBinding.subagentEffort === undefined
2331
+ ? {}
2332
+ : {
2333
+ subagentEffort: snapshotNonblankTuning(rawBinding.subagentEffort, `${path}.subagentEffort`),
2310
2334
  }),
2311
2335
  agent,
2312
2336
  });
@@ -69,15 +69,23 @@ 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. */
72
+ /** The model every subagent of this agent runs on (DR-075), or `inherit`
73
+ * for the agent's own model (DR-076). Absence is the provider default. */
74
74
  readonly subagentModel?: string;
75
+ /** The effort every subagent of this agent runs at (DR-076). Absence leaves
76
+ * each subagent's effort to the agent. */
77
+ readonly subagentEffort?: string;
75
78
  readonly instruction?: string;
76
79
  readonly permissions?: PermissionPolicy;
77
80
  }
78
81
 
79
82
  /** Per-call tuning: erased from every structural envelope and snapshot. */
80
- type TuningKey = 'model' | 'effort' | 'fastMode' | 'subagentModel';
83
+ type TuningKey =
84
+ | 'model'
85
+ | 'effort'
86
+ | 'fastMode'
87
+ | 'subagentModel'
88
+ | 'subagentEffort';
81
89
 
82
90
  interface PlayerLedgerEntry {
83
91
  readonly adapter: string;
@@ -452,6 +460,7 @@ interface EffectivePlayerBinding {
452
460
  readonly effort: TuningSelection<Effort>;
453
461
  readonly fastMode?: boolean;
454
462
  readonly subagentModel?: string;
463
+ readonly subagentEffort?: string;
455
464
  readonly agent: SessionAgent;
456
465
  }
457
466
 
@@ -3727,7 +3736,7 @@ function snapshotFastMode(
3727
3736
  return value;
3728
3737
  }
3729
3738
 
3730
- function snapshotSubagentModel(
3739
+ function snapshotNonblankTuning(
3731
3740
  value: JsonValue | undefined,
3732
3741
  path: string,
3733
3742
  ): string | undefined {
@@ -3753,6 +3762,7 @@ function snapshotSessionAgent(
3753
3762
  'effort',
3754
3763
  'fastMode',
3755
3764
  'subagentModel',
3765
+ 'subagentEffort',
3756
3766
  'instruction',
3757
3767
  'permissions',
3758
3768
  ],
@@ -3765,7 +3775,8 @@ function snapshotSessionAgent(
3765
3775
  key !== 'model' &&
3766
3776
  key !== 'effort' &&
3767
3777
  key !== 'fastMode' &&
3768
- key !== 'subagentModel',
3778
+ key !== 'subagentModel' &&
3779
+ key !== 'subagentEffort',
3769
3780
  ),
3770
3781
  ) as JsonValue,
3771
3782
  path,
@@ -3786,11 +3797,19 @@ function snapshotSessionAgent(
3786
3797
  ...(agent.subagentModel === undefined
3787
3798
  ? {}
3788
3799
  : {
3789
- subagentModel: snapshotSubagentModel(
3800
+ subagentModel: snapshotNonblankTuning(
3790
3801
  agent.subagentModel,
3791
3802
  `${path}.subagentModel`,
3792
3803
  ),
3793
3804
  }),
3805
+ ...(agent.subagentEffort === undefined
3806
+ ? {}
3807
+ : {
3808
+ subagentEffort: snapshotNonblankTuning(
3809
+ agent.subagentEffort,
3810
+ `${path}.subagentEffort`,
3811
+ ),
3812
+ }),
3794
3813
  };
3795
3814
  }
3796
3815
 
@@ -3808,7 +3827,9 @@ function callSettings(
3808
3827
  ): AgentCallSettings {
3809
3828
  // cligent treats supplied call settings as a complete replacement, so an
3810
3829
  // omitted fastMode or subagentModel here is a request for the provider
3811
- // default, never an inheritance of whatever the previous call left behind.
3830
+ // default, and an omitted subagentEffort leaves each subagent's effort to
3831
+ // the agent — never an inheritance of whatever the previous call left
3832
+ // behind.
3812
3833
  return {
3813
3834
  model: tuning.model,
3814
3835
  effort: tuning.effort,
@@ -3816,6 +3837,9 @@ function callSettings(
3816
3837
  ...(tuning.subagentModel === undefined
3817
3838
  ? {}
3818
3839
  : { subagentModel: tuning.subagentModel }),
3840
+ ...(tuning.subagentEffort === undefined
3841
+ ? {}
3842
+ : { subagentEffort: tuning.subagentEffort as Effort }),
3819
3843
  ...(agent.instruction === undefined ? {} : { instruction: agent.instruction }),
3820
3844
  ...(agent.permissions === undefined ? {} : { permissions: agent.permissions }),
3821
3845
  };
@@ -4043,7 +4067,14 @@ async function buildEnablements(
4043
4067
  const rawBinding = snapshotRecord(roleRecord[role], path);
4044
4068
  rejectSnapshotKeys(
4045
4069
  rawBinding,
4046
- ['playerId', 'model', 'effort', 'fastMode', 'subagentModel'],
4070
+ [
4071
+ 'playerId',
4072
+ 'model',
4073
+ 'effort',
4074
+ 'fastMode',
4075
+ 'subagentModel',
4076
+ 'subagentEffort',
4077
+ ],
4047
4078
  path,
4048
4079
  );
4049
4080
  const playerId = snapshotString(rawBinding.playerId, `${path}.playerId`);
@@ -4071,11 +4102,19 @@ async function buildEnablements(
4071
4102
  ...(rawBinding.subagentModel === undefined
4072
4103
  ? {}
4073
4104
  : {
4074
- subagentModel: snapshotSubagentModel(
4105
+ subagentModel: snapshotNonblankTuning(
4075
4106
  rawBinding.subagentModel,
4076
4107
  `${path}.subagentModel`,
4077
4108
  ),
4078
4109
  }),
4110
+ ...(rawBinding.subagentEffort === undefined
4111
+ ? {}
4112
+ : {
4113
+ subagentEffort: snapshotNonblankTuning(
4114
+ rawBinding.subagentEffort,
4115
+ `${path}.subagentEffort`,
4116
+ ),
4117
+ }),
4079
4118
  agent,
4080
4119
  });
4081
4120
  }
@@ -8,11 +8,15 @@
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/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.
11
+ # adapter/model/effort/fastMode/subagentModel/subagentEffort/permissions. A
12
+ # role may override only model, effort, fastMode, subagentModel, or
13
+ # subagentEffort.
14
+ # Omit an override to inherit the player default. For model, effort,
15
+ # subagentModel, and subagentEffort, false selects the provider default;
16
+ # fastMode false is a literal disabled request.
17
+ # A Claude agent delegates by default: its subagents run on its own model
18
+ # (subagentModel: inherit) at efforts it chooses per task; subagentModel: false
19
+ # on its block switches delegation off.
16
20
 
17
21
  # Each adapter needs its vendor SDK installed as its own top-level
18
22
  # install root — they are optional peer dependencies, so you pay only