Merge pull request #3483 from radioflyer28/feat/agent-launch-reasoning-transport-3474
feat: transport resolved reasoning effort to agent launches
This commit is contained in:
5
.changeset/careful-model-transport.md
Normal file
5
.changeset/careful-model-transport.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 3474
|
||||
---
|
||||
**Agent launch model resolution now transports reasoning effort** — `resolve-model` JSON output includes supported runtime `reasoning_effort`, and runtime adapter guidance passes it to child-agent launches only when supported.
|
||||
@@ -2471,6 +2471,10 @@ Direct mapping:
|
||||
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.
|
||||
- \`fork_context: false\` by default — GSD agents load their own context via \`<files_to_read>\` blocks
|
||||
- \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct Codex mapping.
|
||||
Codex \`spawn_agent\` does not create or bind a git worktree automatically.
|
||||
|
||||
@@ -207,7 +207,9 @@ node gsd-tools.cjs config-set-model-profile <profile>
|
||||
```bash
|
||||
# Get model for agent based on current profile
|
||||
node gsd-tools.cjs resolve-model <agent-name>
|
||||
# Returns: opus | sonnet | haiku | inherit
|
||||
# Raw output returns the selected model ID/tier.
|
||||
# JSON output also includes profile and, when the active runtime supports it,
|
||||
# reasoning_effort.
|
||||
```
|
||||
|
||||
Agent names: `gsd-planner`, `gsd-executor`, `gsd-phase-researcher`, `gsd-project-researcher`, `gsd-research-synthesizer`, `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-roadmapper`, `gsd-debugger`, `gsd-codebase-mapper`, `gsd-nyquist-auditor`
|
||||
|
||||
@@ -1001,6 +1001,8 @@ The intent is the same as the Claude profile tiers -- use a stronger model for p
|
||||
|
||||
When `runtime` is set, profile tiers (`opus`/`sonnet`/`haiku`) resolve to runtime-native model IDs instead of Claude aliases. This lets a single shared `.planning/config.json` work cleanly across Claude and Codex.
|
||||
|
||||
`resolve-model` JSON output includes `reasoning_effort` when the runtime tier resolved for the agent (after phase-type overrides) defines a `reasoning_effort`. Runtime adapters may pass that value to child-agent launch calls that support it; runtimes without explicit support omit it.
|
||||
|
||||
**Built-in tier maps:**
|
||||
|
||||
| Runtime | `opus` | `sonnet` | `haiku` | reasoning_effort |
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execGit, platformWriteSync, platformReadSync, platformEnsureDir } = require('./shell-command-projection.cjs');
|
||||
const { loadConfig, isGitIgnored, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, extractCurrentMilestone, toPosixPath, output, error, findPhaseInternal, extractOneLinerFromBody, getRoadmapPhaseInternal } = require('./core.cjs');
|
||||
const { loadConfig, isGitIgnored, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, resolveReasoningEffortInternal, stripShippedMilestones, extractCurrentMilestone, toPosixPath, output, error, findPhaseInternal, extractOneLinerFromBody, getRoadmapPhaseInternal } = require('./core.cjs');
|
||||
const { planningDir, planningPaths } = require('./planning-workspace.cjs');
|
||||
const { extractFrontmatter } = require('./frontmatter.cjs');
|
||||
const { MODEL_PROFILES } = require('./model-profiles.cjs');
|
||||
@@ -240,11 +240,13 @@ function cmdResolveModel(cwd, agentType, raw) {
|
||||
const config = loadConfig(cwd);
|
||||
const profile = config.model_profile || 'balanced';
|
||||
const model = resolveModelInternal(cwd, agentType);
|
||||
const reasoningEffort = resolveReasoningEffortInternal(cwd, agentType);
|
||||
|
||||
const agentModels = MODEL_PROFILES[agentType];
|
||||
const result = agentModels
|
||||
? { model, profile }
|
||||
: { model, profile, unknown_agent: true };
|
||||
if (reasoningEffort) result.reasoning_effort = reasoningEffort;
|
||||
output(result, raw, model);
|
||||
}
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ From `read-only-parity.integration.test.ts` (full `toEqual` on this repo):
|
||||
|
||||
| SDK dispatch (canonical) | Notes |
|
||||
| ------------------------ | ----- |
|
||||
| `resolve-model` | Args e.g. `gsd-planner`. |
|
||||
| `resolve-model` | Args e.g. `gsd-planner`; returns `reasoning_effort` when the selected runtime tier defines one. |
|
||||
| `phase-plan-index` | Phase number arg. |
|
||||
| `roadmap.get-phase` | Phase number arg. |
|
||||
| `list.todos` | No args. |
|
||||
|
||||
@@ -208,8 +208,55 @@ describe('resolveModel', () => {
|
||||
const planner = (await resolveModel(['gsd-planner'], tmpDir)).data as Record<string, unknown>;
|
||||
const executor = (await resolveModel(['gsd-executor'], tmpDir)).data as Record<string, unknown>;
|
||||
|
||||
expect(planner).toMatchObject({ model: 'gpt-5.5', profile: 'balanced' });
|
||||
expect(executor).toMatchObject({ model: 'gpt-5.3-codex', profile: 'balanced' });
|
||||
expect(planner).toMatchObject({ model: 'gpt-5.5', profile: 'balanced', reasoning_effort: 'high' });
|
||||
expect(executor).toMatchObject({ model: 'gpt-5.3-codex', profile: 'balanced', reasoning_effort: 'medium' });
|
||||
});
|
||||
|
||||
it('returns runtime reasoning_effort from the same phase-tier source as model', async () => {
|
||||
const { resolveModel } = await import('./config-query.js');
|
||||
const { resolveRuntimeTierDefault } = await import('../model-catalog.js');
|
||||
const opusCodexTier = resolveRuntimeTierDefault('codex', 'opus');
|
||||
await writeFile(
|
||||
join(tmpDir, '.planning', 'config.json'),
|
||||
JSON.stringify({
|
||||
model_profile: 'budget',
|
||||
runtime: 'codex',
|
||||
models: { execution: 'opus' },
|
||||
}),
|
||||
);
|
||||
|
||||
const executor = (await resolveModel(['gsd-executor'], tmpDir)).data as Record<string, unknown>;
|
||||
|
||||
expect(executor).toMatchObject({
|
||||
model: 'gpt-5.4',
|
||||
profile: 'budget',
|
||||
reasoning_effort: opusCodexTier?.reasoning_effort,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not leak reasoning_effort from overrides for unsupported runtimes', async () => {
|
||||
const { resolveModel } = await import('./config-query.js');
|
||||
await writeFile(
|
||||
join(tmpDir, '.planning', 'config.json'),
|
||||
JSON.stringify({
|
||||
model_profile: 'balanced',
|
||||
runtime: 'opencode',
|
||||
models: { planning: 'opus' },
|
||||
model_profile_overrides: {
|
||||
opencode: {
|
||||
opus: { model: 'openrouter/openai/gpt-5.5', reasoning_effort: 'high' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const planner = (await resolveModel(['gsd-planner'], tmpDir)).data as Record<string, unknown>;
|
||||
|
||||
expect(planner).toMatchObject({
|
||||
model: 'openrouter/openai/gpt-5.5',
|
||||
profile: 'balanced',
|
||||
});
|
||||
expect(planner).not.toHaveProperty('reasoning_effort');
|
||||
});
|
||||
|
||||
it('resolveModel uses workstream config when --ws is specified', async () => {
|
||||
|
||||
@@ -30,8 +30,11 @@ import {
|
||||
VALID_PROFILES,
|
||||
getAgentToModelMapForProfile,
|
||||
resolveRuntimeTierDefault,
|
||||
runtimesWithReasoningEffort,
|
||||
} from '../model-catalog.js';
|
||||
|
||||
const RUNTIMES_WITH_REASONING_EFFORT = runtimesWithReasoningEffort();
|
||||
|
||||
// ─── configGet ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -151,7 +154,11 @@ function resolveRuntimeTier(config: Record<string, unknown>, tier: string): Runt
|
||||
const userEntry = normalizeRuntimeTierEntry(runtimeOverrides?.[tier]);
|
||||
|
||||
if (!builtin && !userEntry) return null;
|
||||
return { ...(builtin ?? {}), ...(userEntry ?? {}) };
|
||||
const merged = { ...(builtin ?? {}), ...(userEntry ?? {}) };
|
||||
if (!RUNTIMES_WITH_REASONING_EFFORT.has(runtime)) {
|
||||
delete merged.reasoning_effort;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -222,7 +229,11 @@ export const resolveModel: QueryHandler = async (args, projectDir, workstream) =
|
||||
const tier = typeof phaseTier === 'string' ? phaseTier : alias;
|
||||
const runtimeTier = resolveRuntimeTier(config as Record<string, unknown>, tier);
|
||||
if (runtimeTier?.model) {
|
||||
return { data: { model: runtimeTier.model, profile } };
|
||||
const result: Record<string, unknown> = { model: runtimeTier.model, profile };
|
||||
if (runtimeTier.reasoning_effort) {
|
||||
result.reasoning_effort = runtimeTier.reasoning_effort;
|
||||
}
|
||||
return { data: result };
|
||||
}
|
||||
|
||||
if (resolveModelIds === 'omit') {
|
||||
|
||||
@@ -151,6 +151,12 @@ 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');
|
||||
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/,
|
||||
'documents reasoning_effort transport',
|
||||
);
|
||||
assert.ok(result.includes('do not invent one-off effort literals'), 'keeps effort policy centralized');
|
||||
assert.ok(result.includes('fork_context'), 'documents fork_context default');
|
||||
assert.ok(result.includes('wait(ids)'), 'documents parallel wait pattern');
|
||||
assert.ok(result.includes('close_agent'), 'documents close_agent cleanup');
|
||||
|
||||
@@ -1117,6 +1117,57 @@ describe('resolve-model command', () => {
|
||||
assert.ok(output.model, 'should resolve a model');
|
||||
});
|
||||
|
||||
test('includes reasoning_effort when selected runtime supports it', () => {
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), JSON.stringify({
|
||||
model_profile: 'balanced',
|
||||
runtime: 'codex',
|
||||
models: { planning: 'opus' },
|
||||
}));
|
||||
const result = runGsdTools('resolve-model gsd-planner', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.model, 'gpt-5.4');
|
||||
assert.strictEqual(output.profile, 'balanced');
|
||||
assert.strictEqual(output.reasoning_effort, 'xhigh');
|
||||
});
|
||||
|
||||
test('does not include reasoning_effort for unsupported runtime overrides', () => {
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), JSON.stringify({
|
||||
model_profile: 'balanced',
|
||||
runtime: 'opencode',
|
||||
models: { planning: 'opus' },
|
||||
model_profile_overrides: {
|
||||
opencode: {
|
||||
opus: { model: 'openrouter/openai/gpt-5.5', reasoning_effort: 'high' },
|
||||
},
|
||||
},
|
||||
}));
|
||||
const result = runGsdTools('resolve-model gsd-planner', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.model, 'openrouter/openai/gpt-5.5');
|
||||
assert.strictEqual(output.profile, 'balanced');
|
||||
assert.ok(!Object.prototype.hasOwnProperty.call(output, 'reasoning_effort'));
|
||||
});
|
||||
|
||||
test('does not include reasoning_effort for per-agent model_overrides', () => {
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), JSON.stringify({
|
||||
model_profile: 'balanced',
|
||||
runtime: 'codex',
|
||||
models: { planning: 'opus' },
|
||||
model_overrides: { 'gsd-planner': 'gpt-5.5' },
|
||||
}));
|
||||
const result = runGsdTools('resolve-model gsd-planner', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.model, 'gpt-5.5');
|
||||
assert.strictEqual(output.profile, 'balanced');
|
||||
assert.ok(!Object.prototype.hasOwnProperty.call(output, 'reasoning_effort'));
|
||||
});
|
||||
|
||||
test('fails when no agent-type provided', () => {
|
||||
const result = runGsdTools('resolve-model', tmpDir);
|
||||
assert.ok(!result.success, 'should fail without agent-type');
|
||||
|
||||
Reference in New Issue
Block a user