feat: transport resolved reasoning effort

This commit is contained in:
radioflyer28
2026-05-13 18:26:54 -04:00
parent 6ea25ec8c7
commit f70829d067
6 changed files with 51 additions and 4 deletions

View File

@@ -2360,6 +2360,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"\` → 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.

View File

@@ -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);
}

View File

@@ -208,8 +208,28 @@ 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');
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: 'xhigh',
});
});
it('resolveModel uses workstream config when --ws is specified', async () => {

View File

@@ -222,7 +222,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') {

View File

@@ -133,6 +133,8 @@ 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.ok(result.includes('reasoning_effort'), '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');

View File

@@ -1117,6 +1117,21 @@ 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('fails when no agent-type provided', () => {
const result = runGsdTools('resolve-model', tmpDir);
assert.ok(!result.success, 'should fail without agent-type');