Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
5
.changeset/silly-geese-hop.md
Normal file
5
.changeset/silly-geese-hop.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 4281
|
||||
---
|
||||
Forward Codex adaptive per-agent model and reasoning-effort routing through supported spawn_agent fields while preserving inheritance fallback for older schemas. (#4270)
|
||||
@@ -3890,26 +3890,37 @@ Execute mode fallback:
|
||||
## C. Task() → spawn_agent Mapping
|
||||
GSD workflows use \`Task(...)\` (Claude Code syntax). Translate to Codex collaboration tools:
|
||||
|
||||
**Schema detection (required first step):** Codex exposes two \`spawn_agent\` schemas:
|
||||
- **agent_type-capable schema** (e.g. \`multi_agent_v2\`): \`spawn_agent\` accepts \`agent_type\`, \`message\`, \`reasoning_effort\`, \`fork_context\`, etc. — typed GSD agent dispatch is available.
|
||||
- **Generic schema** (\`multi_agent_v1\`): \`spawn_agent\` accepts only \`message\`, \`items\`, \`fork_context\` — there is **no \`agent_type\` field**. Typed GSD agent dispatch is unavailable in this session.
|
||||
**Schema detection (required first step):** Before spawning, inspect the \`spawn_agent\`
|
||||
tool's visible parameter schema (via \`tool_search\` or the tool list). Use the presence
|
||||
of \`agent_type\` only to choose typed dispatch versus the generic-agent workaround.
|
||||
Detect optional fields independently: \`model\`, \`reasoning_effort\`, \`task_name\`,
|
||||
\`fork_turns\`, and \`fork_context\` may be added or removed without \`agent_type\` changing.
|
||||
Never infer one field from a schema/version label or from the presence of another field.
|
||||
|
||||
Before spawning, inspect the \`spawn_agent\` tool's visible parameter schema (via \`tool_search\` or the tool list) to determine which form is active.
|
||||
- **agent_type-capable schema:** \`spawn_agent\` advertises \`agent_type\` — typed GSD agent dispatch is available.
|
||||
- **Generic schema:** \`spawn_agent\` does not advertise \`agent_type\` — typed GSD agent dispatch is unavailable in this session, even if other optional fields are present.
|
||||
|
||||
Typed mapping (agent_type-capable schema only):
|
||||
- \`Task(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\`
|
||||
- \`Agent(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\`
|
||||
- \`Task(model="...")\` → omit. \`spawn_agent\` has no inline \`model\` parameter;
|
||||
GSD embeds the resolved per-agent model directly into each agent's \`.toml\`
|
||||
at install time so \`model_overrides\` from \`.planning/config.json\` and
|
||||
\`~/.gsd/defaults.json\` are honored automatically by Codex's agent router.
|
||||
- Resolved \`reasoning_effort="low|medium|high|xhigh"\` (\`xhigh\` is a GSD/Codex tier, not a generic runtime enum) → pass \`reasoning_effort\`
|
||||
to \`spawn_agent\` when the runtime/tool supports it. Omit missing, empty,
|
||||
inherited, or unsupported values; do not invent one-off effort literals in
|
||||
workflow prose.
|
||||
- \`Task(model="{resolved_model}")\` → pass \`model="{resolved_model}"\` when the
|
||||
visible \`spawn_agent\` schema advertises \`model\` and the resolved value is explicit.
|
||||
This is how \`model_profile\` tier routing, including \`adaptive\`, reaches the child agent.
|
||||
Omit \`model\` only when the schema does not advertise \`model\`, or when the value is
|
||||
missing, empty, or \`"inherit"\`; omission deliberately inherits the session/static agent
|
||||
configuration. Explicit \`model_overrides\` may also be embedded in agent \`.toml\` files,
|
||||
but ordinary profile-resolved models are not, so a TOML file is not a reason to discard
|
||||
an available inline value.
|
||||
- Before each typed spawn, obtain the paired effort for its role with
|
||||
\`gsd_run query resolve-model <subagent_type> --pick effort\` when the workflow has not
|
||||
already exposed it. The resolver's unified \`effort\` field maps to the Codex spawn argument
|
||||
\`reasoning_effort\`; do not look for a resolver field named \`reasoning_effort\`.
|
||||
Pass it when the visible \`spawn_agent\` schema advertises \`reasoning_effort\`. Omit the
|
||||
field when it is not advertised, or when the value is missing, empty, \`"inherit"\`, or
|
||||
unsupported; do not invent one-off effort literals in workflow prose.
|
||||
- \`fork_context: false\` by default — GSD agents load their own context via \`<required_reading>\` blocks
|
||||
- \`task_name\` — required by the collaboration schema; provide a descriptive name for each spawned task
|
||||
- \`fork_turns\` — optional parameter controlling turn-forking depth; coexists with \`fork_context\` (not a replacement)
|
||||
- \`task_name\` — when advertised, provide a descriptive name for each spawned task
|
||||
- \`fork_turns\` — when advertised, controls turn-forking depth; coexists with \`fork_context\` (not a replacement)
|
||||
- \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct \`spawn_agent\` mapping,
|
||||
but Codex declares \`dispatch.isolation: orchestrator-worktree\` (#2584). Codex
|
||||
\`spawn_agent\` still does not create or bind a git worktree; instead GSD itself
|
||||
|
||||
@@ -227,7 +227,7 @@ derived from the shipped agent declaration.
|
||||
| `granularity` | enum | `coarse`, `standard`, `fine` | `standard` | Controls phase count: `coarse` (2-4), `standard` (4-6), `fine` (6-10) |
|
||||
| `agent_tools.<selector>` | string[] | tool names meeting the [agent tool grant validation rules](#agent-tool-grants) | (none) | Additive install-time grants for `"*"` or a named agent. A project selector replaces the corresponding global selector; wildcard grants precede named grants. Re-run `gsd install <runtime>` after changing it. |
|
||||
| `model_profile` | enum | `quality`, `balanced`, `budget`, `adaptive`, `inherit` | `balanced` | Model tier for each agent (see [Model Profiles](#model-profiles)). `adaptive` was added per [#1713](https://github.com/open-gsd/gsd-core/issues/1713) / [#1806](https://github.com/open-gsd/gsd-core/issues/1806) and resolves the same way as the other tiers under runtime-aware profiles. |
|
||||
| `runtime` | string | `claude`, `codex`, or any string | (none) | Active runtime for [runtime-aware profile resolution](#runtime-aware-profiles-2517). When set, profile tiers (opus/sonnet/haiku) resolve to runtime-native model IDs. The resolved ID is embedded into each agent's static frontmatter at install time on `opencode` (whose `spawn_agent` interface does not accept an inline `model` parameter, so editing `model_overrides` requires re-running `gsd install <runtime>` to take effect — see [Per-Agent Overrides](#per-agent-overrides)); other runtimes consume the resolver at spawn time. **`codex` is the exception: it embeds no per-tier model at all.** Codex is a passive / session-only model host ([ADR-2313](adr/2313-codex-passive-model-posture.md)) — a ChatGPT-account session exposes only its own model, so a pinned tier model returns `400 invalid_request_error` and the agent fails to spawn. Codex agents therefore inherit the session model, and only an explicit real-Codex id in `model_overrides` (e.g. `"gpt-5.6-sol"`) is written into the `.toml`. When unset (default), model resolution is unchanged from prior versions — but the runtime GSD *reports* (`agent_runtime`) then falls through to [host detection](how-to/control-the-reported-host-runtime.md), which can resolve `codex` from Codex's own session environment. Detection affects reporting and the agent-installation check only; it never feeds tier resolution, which still reads this key alone. Added in v1.39; Codex behavior changed in v1.11; reporting-only host detection added in v1.11 |
|
||||
| `runtime` | string | `claude`, `codex`, or any string | (none) | Active runtime for [runtime-aware profile resolution](#runtime-aware-profiles-2517). When set, profile tiers (opus/sonnet/haiku) resolve to runtime-native model IDs. The resolved ID is embedded into each agent's static frontmatter at install time on `opencode` (whose `spawn_agent` interface does not accept an inline `model` parameter, so editing `model_overrides` requires re-running `gsd install <runtime>` to take effect — see [Per-Agent Overrides](#per-agent-overrides)); other runtimes consume the resolver at spawn time. **Codex keeps profile-resolved models out of static TOML and transports them conditionally at spawn time.** A Codex skill passes the resolved `model` and `reasoning_effort` only when the visible `spawn_agent` schema advertises each field; otherwise it omits that field and inherits the session/static agent configuration. Explicit real-Codex IDs in `model_overrides` (for example `"gpt-5.6-sol"`) are still written into `.toml` as a fallback. When unset (default), model resolution is unchanged from prior versions — but the runtime GSD *reports* (`agent_runtime`) then falls through to [host detection](how-to/control-the-reported-host-runtime.md), which can resolve `codex` from Codex's own session environment. Detection affects reporting and the agent-installation check only; it never feeds tier resolution, which still reads this key alone. Added in v1.39; Codex static posture changed in v1.11; reporting-only host detection added in v1.11 |
|
||||
| `model_profile_overrides.<runtime>.<tier>` | string \| object | per-runtime tier override | (none) | Override the runtime-aware tier mapping for a specific `(runtime, tier)`. Tier is one of `opus`, `sonnet`, `haiku`. Value is either a model ID string (e.g. `"gpt-5-pro"`) or `{ model, reasoning_effort }`. See [Runtime-Aware Profiles](#runtime-aware-profiles-2517). Added in v1.39 |
|
||||
| `model_policy.provider` | string | `openai`, `anthropic`, `anthropic-fable`, `google`, `qwen`, `generic` | (none) | Declares the model provider. Known providers (`openai`, `anthropic`, `anthropic-fable`, `google`, `qwen`) unlock catalog-backed presets. `generic` treats all model IDs as opaque strings — no prefix inference, no reasoning-effort defaults. `model_policy.runtime_tiers` resolves before legacy `model_profile_overrides`. See [Model Policy Presets](#model-policy-presets-model_policy--added-in-v142). Added in v1.42 ([#49](https://github.com/open-gsd/gsd-core/issues/49)) |
|
||||
| `model_policy.budget` | enum | `high`, `medium`, `low` | (none) | Selects a budget tier when using a known provider. GSD materializes the matching catalog preset into explicit tier mappings at resolve time. Ignored when `provider` is `generic` or `custom`. Added in v1.42 ([#49](https://github.com/open-gsd/gsd-core/issues/49)) |
|
||||
@@ -2007,7 +2007,7 @@ When `runtime` is set, profile tiers (`opus`/`sonnet`/`haiku`) resolve to runtim
|
||||
}
|
||||
```
|
||||
|
||||
This resolves `gsd-planner` → `gpt-5.6-sol` (xhigh), `gsd-executor` → `gpt-5.6-terra` (medium), `gsd-codebase-mapper` → `gpt-5.6-luna` (medium). The Codex installer embeds `model = "..."` and `model_reasoning_effort = "..."` in each generated agent TOML.
|
||||
This resolves `gsd-planner` → `gpt-5.6-sol` (xhigh), `gsd-executor` → `gpt-5.6-terra` (medium), `gsd-codebase-mapper` → `gpt-5.6-luna` (medium). Codex skills pass each resolved `model` and `reasoning_effort` to `spawn_agent` when its visible schema advertises the corresponding field; otherwise they omit the field and inherit the session/static agent configuration.
|
||||
|
||||
**Claude example** — explicit opt-in resolves to full Claude IDs (no `resolve_model_ids: true` needed):
|
||||
|
||||
|
||||
@@ -334,3 +334,18 @@ is silently no-pin, matching how `""` already behaved.
|
||||
|
||||
This ADR's D2 ("embed a `model` only for an explicit real-Codex pin") always implied this. The
|
||||
implementation simply did not enforce it, and no test covered the case.
|
||||
|
||||
## Amendment (2026-09-04): capability-gated invocation-time routing (#4270)
|
||||
|
||||
Codex now exposes `model` and `reasoning_effort` on some `spawn_agent` schemas. This is the
|
||||
invocation-time capability signal that did not exist when this ADR adopted a session-only posture.
|
||||
GSD therefore passes a workflow's resolved values on an individual spawn when — and only when —
|
||||
the visible schema advertises each field. The fields are detected independently from each other
|
||||
and from `agent_type`; absent fields, empty values, and `"inherit"` continue to degrade to session
|
||||
or static agent configuration.
|
||||
|
||||
This amendment does not reverse D1–D4 for the static/install-time channel. Profile-resolved values
|
||||
remain absent from generated TOML, explicit `model_overrides` pins remain the only static model
|
||||
transport, and effort remains coupled to a static pin there. It supersedes only the broader claim
|
||||
that Codex has no tier routing: capable schemas now route at invocation time, while older schemas
|
||||
retain the passive fallback.
|
||||
|
||||
@@ -185,17 +185,21 @@ quota / rate-limit failures; other failures keep the tier ladder. Leaving
|
||||
|
||||
## Using GSD on non-Anthropic runtimes
|
||||
|
||||
If you installed GSD for Codex, OpenCode, Antigravity CLI, or Kilo, the installer already set `resolve_model_ids: "omit"` in your config. This tells GSD to skip Anthropic model ID resolution and let the runtime choose its own default model. No manual setup is needed for the basic case.
|
||||
If you installed GSD for Codex, OpenCode, Antigravity CLI, or Kilo, the installer already set `resolve_model_ids: "omit"` in your config. This prevents unresolved Anthropic model IDs from leaking into those runtimes. When `runtime` is set, runtime-native profile resolution still supplies any model and effort that the runtime adapter can transport. No manual setup is needed for the basic case.
|
||||
|
||||
### Codex does not do tier routing — pin explicitly instead
|
||||
### Codex routes tiers at spawn time when supported
|
||||
|
||||
**Codex agents inherit whatever model your Codex session is using.** GSD writes no `model` line into
|
||||
`~/.codex/agents/<agent>.toml`, so setting `model_profile` has no effect on Codex.
|
||||
GSD deliberately writes no profile-resolved `model` line into
|
||||
`~/.codex/agents/<agent>.toml` ([ADR-2313](../adr/2313-codex-passive-model-posture.md)).
|
||||
Instead, each Codex skill inspects the visible `spawn_agent` schema. When that schema advertises
|
||||
`model` and `reasoning_effort`, the skill passes the model and effort resolved from
|
||||
`model_profile` — including `adaptive` — on that individual spawn. When either field is absent,
|
||||
the skill omits that field and the child inherits the session or static agent configuration.
|
||||
|
||||
This is deliberate ([ADR-2313](../adr/2313-codex-passive-model-posture.md)). A ChatGPT-account Codex
|
||||
session exposes only its own model, so a pinned tier model fails the request outright —
|
||||
`400 invalid_request_error: "The 'sonnet' model is not supported when using Codex with a ChatGPT
|
||||
account"` — and the agent never spawns.
|
||||
This keeps compatibility with older Codex schemas while allowing newer installations to route
|
||||
`gsd-planner`, `gsd-executor`, and other roles to their configured tiers. The fields are detected
|
||||
independently; support for typed `agent_type` dispatch does not imply support for either routing
|
||||
field.
|
||||
|
||||
**To pin a model on Codex, name a real Codex model id per agent:**
|
||||
|
||||
@@ -209,7 +213,8 @@ account"` — and the agent never spawns.
|
||||
}
|
||||
```
|
||||
|
||||
Then re-run the installer, as with any `model_overrides` edit on Codex (see above).
|
||||
Then re-run the installer to materialize the override in the agent TOML as a fallback for spawn
|
||||
schemas that do not advertise inline `model` (see above).
|
||||
|
||||
Two rules apply to what you can put there:
|
||||
|
||||
|
||||
@@ -1884,26 +1884,37 @@ Execute mode fallback:
|
||||
## C. Task() → spawn_agent Mapping
|
||||
GSD workflows use \`Task(...)\` (Claude Code syntax). Translate to Codex collaboration tools:
|
||||
|
||||
**Schema detection (required first step):** Codex exposes two \`spawn_agent\` schemas:
|
||||
- **agent_type-capable schema** (e.g. \`multi_agent_v2\`): \`spawn_agent\` accepts \`agent_type\`, \`message\`, \`reasoning_effort\`, \`fork_context\`, etc. — typed GSD agent dispatch is available.
|
||||
- **Generic schema** (\`multi_agent_v1\`): \`spawn_agent\` accepts only \`message\`, \`items\`, \`fork_context\` — there is **no \`agent_type\` field**. Typed GSD agent dispatch is unavailable in this session.
|
||||
**Schema detection (required first step):** Before spawning, inspect the \`spawn_agent\`
|
||||
tool's visible parameter schema (via \`tool_search\` or the tool list). Use the presence
|
||||
of \`agent_type\` only to choose typed dispatch versus the generic-agent workaround.
|
||||
Detect optional fields independently: \`model\`, \`reasoning_effort\`, \`task_name\`,
|
||||
\`fork_turns\`, and \`fork_context\` may be added or removed without \`agent_type\` changing.
|
||||
Never infer one field from a schema/version label or from the presence of another field.
|
||||
|
||||
Before spawning, inspect the \`spawn_agent\` tool's visible parameter schema (via \`tool_search\` or the tool list) to determine which form is active.
|
||||
- **agent_type-capable schema:** \`spawn_agent\` advertises \`agent_type\` — typed GSD agent dispatch is available.
|
||||
- **Generic schema:** \`spawn_agent\` does not advertise \`agent_type\` — typed GSD agent dispatch is unavailable in this session, even if other optional fields are present.
|
||||
|
||||
Typed mapping (agent_type-capable schema only):
|
||||
- \`Task(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\`
|
||||
- \`Agent(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\`
|
||||
- \`Task(model="...")\` → omit. \`spawn_agent\` has no inline \`model\` parameter;
|
||||
GSD embeds the resolved per-agent model directly into each agent's \`.toml\`
|
||||
at install time so \`model_overrides\` from \`.planning/config.json\` and
|
||||
\`~/.gsd/defaults.json\` are honored automatically by Codex's agent router.
|
||||
- Resolved \`reasoning_effort="low|medium|high|xhigh"\` (\`xhigh\` is a GSD/Codex tier, not a generic runtime enum) → pass \`reasoning_effort\`
|
||||
to \`spawn_agent\` when the runtime/tool supports it. Omit missing, empty,
|
||||
inherited, or unsupported values; do not invent one-off effort literals in
|
||||
workflow prose.
|
||||
- \`Task(model="{resolved_model}")\` → pass \`model="{resolved_model}"\` when the
|
||||
visible \`spawn_agent\` schema advertises \`model\` and the resolved value is explicit.
|
||||
This is how \`model_profile\` tier routing, including \`adaptive\`, reaches the child agent.
|
||||
Omit \`model\` only when the schema does not advertise \`model\`, or when the value is
|
||||
missing, empty, or \`"inherit"\`; omission deliberately inherits the session/static agent
|
||||
configuration. Explicit \`model_overrides\` may also be embedded in agent \`.toml\` files,
|
||||
but ordinary profile-resolved models are not, so a TOML file is not a reason to discard
|
||||
an available inline value.
|
||||
- Before each typed spawn, obtain the paired effort for its role with
|
||||
\`gsd_run query resolve-model <subagent_type> --pick effort\` when the workflow has not
|
||||
already exposed it. The resolver's unified \`effort\` field maps to the Codex spawn argument
|
||||
\`reasoning_effort\`; do not look for a resolver field named \`reasoning_effort\`.
|
||||
Pass it when the visible \`spawn_agent\` schema advertises \`reasoning_effort\`. Omit the
|
||||
field when it is not advertised, or when the value is missing, empty, \`"inherit"\`, or
|
||||
unsupported; do not invent one-off effort literals in workflow prose.
|
||||
- \`fork_context: false\` by default — GSD agents load their own context via \`<required_reading>\` blocks
|
||||
- \`task_name\` — required by the collaboration schema; provide a descriptive name for each spawned task
|
||||
- \`fork_turns\` — optional parameter controlling turn-forking depth; coexists with \`fork_context\` (not a replacement)
|
||||
- \`task_name\` — when advertised, provide a descriptive name for each spawned task
|
||||
- \`fork_turns\` — when advertised, controls turn-forking depth; coexists with \`fork_context\` (not a replacement)
|
||||
- \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct \`spawn_agent\` mapping,
|
||||
but Codex declares \`dispatch.isolation: orchestrator-worktree\` (#2584). Codex
|
||||
\`spawn_agent\` still does not create or bind a git worktree; instead GSD itself
|
||||
|
||||
@@ -220,9 +220,16 @@ describe('getCodexSkillAdapterHeader', () => {
|
||||
const result = getCodexSkillAdapterHeader('gsd-execute-phase');
|
||||
assert.ok(result.includes('spawn_agent'), 'maps to spawn_agent');
|
||||
assert.ok(result.includes('agent_type'), 'maps subagent_type to agent_type');
|
||||
// #4270: resolve-model exposes the portable field as `effort`; the Codex
|
||||
// adapter must fetch it and translate it to spawn_agent.reasoning_effort.
|
||||
assert.match(
|
||||
result,
|
||||
/Resolved `reasoning_effort="low\|medium\|high\|xhigh"` \(`xhigh` is a GSD\/Codex tier, not a generic runtime enum\) → pass `reasoning_effort`\s+to `spawn_agent` when the runtime\/tool supports it/,
|
||||
/query resolve-model <subagent_type> --pick effort/,
|
||||
'retrieves the unified effort for the dispatched role',
|
||||
);
|
||||
assert.match(
|
||||
result,
|
||||
/unified `effort` field maps to the Codex spawn argument\s+`reasoning_effort`/,
|
||||
'documents reasoning_effort transport',
|
||||
);
|
||||
assert.ok(result.includes('do not invent one-off effort literals'), 'keeps effort policy centralized');
|
||||
|
||||
@@ -2347,15 +2347,30 @@ describe('bug #2256 — OpenCode adapter embeds per-project override', () => {
|
||||
});
|
||||
|
||||
describe('bug #2256 — Codex skill adapter header documents transport', () => {
|
||||
test('Task(model=...) line no longer says "omit" without explanation', () => {
|
||||
test('documents static model_overrides as a fallback transport', () => {
|
||||
const header = getCodexSkillAdapterHeader('gsd-plan-phase');
|
||||
// Header must mention that per-agent model_overrides are embedded in agent
|
||||
// TOML so spawn_agent picks them up automatically — the old text said
|
||||
// "Codex uses per-role config, not inline model selection" which left
|
||||
// users thinking their model_overrides were silently ignored.
|
||||
assert.match(header, /model_overrides/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bug #4270 — Codex adapter forwards resolved spawn routing', () => {
|
||||
test('forwards model and reasoning_effort when spawn_agent advertises each field', () => {
|
||||
const header = getCodexSkillAdapterHeader('gsd-plan-phase');
|
||||
|
||||
assert.match(header, /Task\(model="\{resolved_model\}"\).*pass `model="\{resolved_model\}"`/s);
|
||||
assert.match(header, /This is how `model_profile`.*including `adaptive`.*reaches the child agent/s);
|
||||
assert.match(header, /query resolve-model <subagent_type> --pick effort/);
|
||||
assert.match(header, /unified `effort`.*spawn argument\s+`reasoning_effort`/s);
|
||||
});
|
||||
|
||||
test('keeps inheritance fallback for absent fields and sentinel values', () => {
|
||||
const header = getCodexSkillAdapterHeader('gsd-plan-phase');
|
||||
|
||||
assert.match(header, /Omit `model` only when.*does not advertise `model`.*empty.*`"inherit"`/s);
|
||||
assert.match(header, /Detect optional fields independently/s);
|
||||
assert.doesNotMatch(header, /spawn_agent` has no inline `model` parameter/);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user