Merge pull request #118 from bnema/fix/subagent-reasoning-field
fix(subagents): expose reasoning as the spawn override
3 files · +15 -23
M docs/subagents.md +2 -2
| @@ -44,7 +44,7 @@ Supported metadata: | |||
| 44 | 44 | | `tools` | Comma-separated or list-form tool names. zut enforces its built-in `read`, `write`, `edit`, `bash`, `create_worktree`, `lsp`, and `web_search` registry; the conditional `skill` tool is available when skills are enabled. Unknown names do not grant capabilities. | | |
| 45 | 45 | | `model` | Optional model ID. A qualified value such as `openai-codex/gpt-5.6-luna` selects both provider and model. | | |
| 46 | 46 | | `provider` | Optional separate provider ID for a model without a provider prefix. | | |
| 47 | - | | `thinking` / `reasoning` | Optional reasoning level: `off`, `minimum`, `low`, `medium`, `high`, `xhigh`, or `max`. | | |
| 47 | + | | `thinking` | Optional reasoning level: `off`, `minimum`, `low`, `medium`, `high`, `xhigh`, or `max`. This is applied as the child's `reasoning` setting. | | |
| 48 | 48 | | `systemPromptMode` | `append` (default) or `replace`. | | |
| 49 | 49 | | `inheritProjectContext` | Set to `false` to omit `AGENTS.md` context from the child. | | |
| 50 | 50 | | `inheritSkills` | Set to `false` to omit skill discovery and the conditional `skill` loader from the child; `--no-skill` has the same effect for a run. | | |
| @@ -122,7 +122,7 @@ Per-spawn reasoning and fast-mode overrides are also accepted: | |||
| 122 | 122 | } | |
| 123 | 123 | ``` | |
| 124 | 124 | ||
| 125 | - | `thinking` is accepted as an alias for `reasoning`. If neither is supplied, the child inherits the host reasoning level for an unnamed spawn, or the profile's `thinking` value for a named spawn. An explicit `fast_mode` value takes precedence over the selected profile and host setting; omit it to inherit them. | |
| 125 | + | If `reasoning` is omitted from `subagent_spawn`, the child inherits the host reasoning level for an unnamed spawn, or the selected profile's `thinking` value for a named spawn. An explicit `fast_mode` value takes precedence over the selected profile and host setting; omit it to inherit them. | |
| 126 | 126 | ||
| 127 | 127 | The interactive command also supports the same selection explicitly: | |
| 128 | 128 | ||
| @@ -44,7 +44,7 @@ Supported metadata: | |||||
| 44 | | `tools` | Comma-separated or list-form tool names. zut enforces its built-in `read`, `write`, `edit`, `bash`, `create_worktree`, `lsp`, and `web_search` registry; the conditional `skill` tool is available when skills are enabled. Unknown names do not grant capabilities. | | 44 | | `tools` | Comma-separated or list-form tool names. zut enforces its built-in `read`, `write`, `edit`, `bash`, `create_worktree`, `lsp`, and `web_search` registry; the conditional `skill` tool is available when skills are enabled. Unknown names do not grant capabilities. | | ||
| 45 | | `model` | Optional model ID. A qualified value such as `openai-codex/gpt-5.6-luna` selects both provider and model. | | 45 | | `model` | Optional model ID. A qualified value such as `openai-codex/gpt-5.6-luna` selects both provider and model. | | ||
| 46 | | `provider` | Optional separate provider ID for a model without a provider prefix. | | 46 | | `provider` | Optional separate provider ID for a model without a provider prefix. | | ||
| 47 | - | | `thinking` / `reasoning` | Optional reasoning level: `off`, `minimum`, `low`, `medium`, `high`, `xhigh`, or `max`. | | 47 | + | | `thinking` | Optional reasoning level: `off`, `minimum`, `low`, `medium`, `high`, `xhigh`, or `max`. This is applied as the child's `reasoning` setting. | |
| 48 | | `systemPromptMode` | `append` (default) or `replace`. | | 48 | | `systemPromptMode` | `append` (default) or `replace`. | | ||
| 49 | | `inheritProjectContext` | Set to `false` to omit `AGENTS.md` context from the child. | | 49 | | `inheritProjectContext` | Set to `false` to omit `AGENTS.md` context from the child. | | ||
| 50 | | `inheritSkills` | Set to `false` to omit skill discovery and the conditional `skill` loader from the child; `--no-skill` has the same effect for a run. | | 50 | | `inheritSkills` | Set to `false` to omit skill discovery and the conditional `skill` loader from the child; `--no-skill` has the same effect for a run. | | ||
| @@ -122,7 +122,7 @@ Per-spawn reasoning and fast-mode overrides are also accepted: | |||||
| 122 | } | 122 | } | ||
| 123 | ``` | 123 | ``` | ||
| 124 | 124 | ||||
| 125 | - | `thinking` is accepted as an alias for `reasoning`. If neither is supplied, the child inherits the host reasoning level for an unnamed spawn, or the profile's `thinking` value for a named spawn. An explicit `fast_mode` value takes precedence over the selected profile and host setting; omit it to inherit them. | 125 | + | If `reasoning` is omitted from `subagent_spawn`, the child inherits the host reasoning level for an unnamed spawn, or the selected profile's `thinking` value for a named spawn. An explicit `fast_mode` value takes precedence over the selected profile and host setting; omit it to inherit them. |
| 126 | 126 | ||||
| 127 | The interactive command also supports the same selection explicitly: | 127 | The interactive command also supports the same selection explicitly: | ||
| 128 | 128 | ||||
M packages/agent/tools/subagent_spawn.go +6 -20
| @@ -59,7 +59,6 @@ type subagentSpawnArgs struct { | |||
| 59 | 59 | Model string `json:"model,omitempty"` | |
| 60 | 60 | Provider string `json:"provider,omitempty"` | |
| 61 | 61 | Reasoning string `json:"reasoning,omitempty"` | |
| 62 | - | Thinking string `json:"thinking,omitempty"` | |
| 63 | 62 | FastMode *bool `json:"fast_mode,omitempty"` | |
| 64 | 63 | Required bool `json:"required,omitempty"` | |
| 65 | 64 | Isolation string `json:"isolation,omitempty"` | |
| @@ -89,12 +88,7 @@ const subagentSpawnSchemaTemplate = `{ | |||
| 89 | 88 | "reasoning": { | |
| 90 | 89 | "type": "string", | |
| 91 | 90 | "enum": ["off", "minimum", "low", "medium", "high", "xhigh", "max"], | |
| 92 | - | "description": "Optional reasoning/thinking level for the child. Overrides the selected profile's thinking level when provided." | |
| 93 | - | }, | |
| 94 | - | "thinking": { | |
| 95 | - | "type": "string", | |
| 96 | - | "enum": ["off", "minimum", "low", "medium", "high", "xhigh", "max"], | |
| 97 | - | "description": "Alias for reasoning, accepted for compatibility with common agent profile terminology. Prefer reasoning when both are available." | |
| 91 | + | "description": "Optional reasoning level for the child. Overrides the selected profile's thinking level when provided." | |
| 98 | 92 | }, | |
| 99 | 93 | "fast_mode": { | |
| 100 | 94 | "type": "boolean", | |
| @@ -231,18 +225,18 @@ func (t *SubagentSpawnTool) Execute(ctx context.Context, raw json.RawMessage, _ | |||
| 231 | 225 | providerID = strings.TrimSpace(t.DefaultProvider()) | |
| 232 | 226 | } | |
| 233 | 227 | ||
| 234 | - | reasoning, err := reasoningOverride(a.Reasoning, a.Thinking) | |
| 228 | + | reasoning, err := normalizeReasoning(a.Reasoning) | |
| 235 | 229 | if err != nil { | |
| 236 | 230 | return protocolToolError(prefix + ": " + err.Error()) | |
| 237 | 231 | } | |
| 238 | 232 | if reasoning == "" && profile != nil && strings.TrimSpace(profile.Thinking) != "" { | |
| 239 | - | reasoning, err = reasoningOverride(profile.Thinking, "") | |
| 233 | + | reasoning, err = normalizeReasoning(profile.Thinking) | |
| 240 | 234 | if err != nil { | |
| 241 | 235 | return protocolToolError(prefix + ": profile " + profile.Name + ": " + err.Error()) | |
| 242 | 236 | } | |
| 243 | 237 | } | |
| 244 | 238 | if reasoning == "" && t.DefaultReasoning != nil { | |
| 245 | - | reasoning, err = reasoningOverride(t.DefaultReasoning(), "") | |
| 239 | + | reasoning, err = normalizeReasoning(t.DefaultReasoning()) | |
| 246 | 240 | if err != nil { | |
| 247 | 241 | return protocolToolError(prefix + ": host " + err.Error()) | |
| 248 | 242 | } | |
| @@ -326,16 +320,8 @@ func (t *SubagentSpawnTool) Execute(ctx context.Context, raw json.RawMessage, _ | |||
| 326 | 320 | }, nil | |
| 327 | 321 | } | |
| 328 | 322 | ||
| 329 | - | func reasoningOverride(reasoning, thinking string) (string, error) { | |
| 330 | - | reasoning = strings.TrimSpace(reasoning) | |
| 331 | - | thinking = strings.TrimSpace(thinking) | |
| 332 | - | if reasoning != "" && thinking != "" && provider.NormalizeReasoning(reasoning) != provider.NormalizeReasoning(thinking) { | |
| 333 | - | return "", fmt.Errorf("reasoning and thinking disagree; provide only one") | |
| 334 | - | } | |
| 335 | - | value := reasoning | |
| 336 | - | if value == "" { | |
| 337 | - | value = thinking | |
| 338 | - | } | |
| 323 | + | func normalizeReasoning(value string) (string, error) { | |
| 324 | + | value = strings.TrimSpace(value) | |
| 339 | 325 | if value == "" { | |
| 340 | 326 | return "", nil | |
| 341 | 327 | } | |
| @@ -59,7 +59,6 @@ type subagentSpawnArgs struct { | |||||
| 59 | Model string `json:"model,omitempty"` | 59 | Model string `json:"model,omitempty"` | ||
| 60 | Provider string `json:"provider,omitempty"` | 60 | Provider string `json:"provider,omitempty"` | ||
| 61 | Reasoning string `json:"reasoning,omitempty"` | 61 | Reasoning string `json:"reasoning,omitempty"` | ||
| 62 | - | Thinking string `json:"thinking,omitempty"` | |||
| 63 | FastMode *bool `json:"fast_mode,omitempty"` | 62 | FastMode *bool `json:"fast_mode,omitempty"` | ||
| 64 | Required bool `json:"required,omitempty"` | 63 | Required bool `json:"required,omitempty"` | ||
| 65 | Isolation string `json:"isolation,omitempty"` | 64 | Isolation string `json:"isolation,omitempty"` | ||
| @@ -89,12 +88,7 @@ const subagentSpawnSchemaTemplate = `{ | |||||
| 89 | "reasoning": { | 88 | "reasoning": { | ||
| 90 | "type": "string", | 89 | "type": "string", | ||
| 91 | "enum": ["off", "minimum", "low", "medium", "high", "xhigh", "max"], | 90 | "enum": ["off", "minimum", "low", "medium", "high", "xhigh", "max"], | ||
| 92 | - | "description": "Optional reasoning/thinking level for the child. Overrides the selected profile's thinking level when provided." | 91 | + | "description": "Optional reasoning level for the child. Overrides the selected profile's thinking level when provided." |
| 93 | - | }, | |||
| 94 | - | "thinking": { | |||
| 95 | - | "type": "string", | |||
| 96 | - | "enum": ["off", "minimum", "low", "medium", "high", "xhigh", "max"], | |||
| 97 | - | "description": "Alias for reasoning, accepted for compatibility with common agent profile terminology. Prefer reasoning when both are available." | |||
| 98 | }, | 92 | }, | ||
| 99 | "fast_mode": { | 93 | "fast_mode": { | ||
| 100 | "type": "boolean", | 94 | "type": "boolean", | ||
| @@ -231,18 +225,18 @@ func (t *SubagentSpawnTool) Execute(ctx context.Context, raw json.RawMessage, _ | |||||
| 231 | providerID = strings.TrimSpace(t.DefaultProvider()) | 225 | providerID = strings.TrimSpace(t.DefaultProvider()) | ||
| 232 | } | 226 | } | ||
| 233 | 227 | ||||
| 234 | - | reasoning, err := reasoningOverride(a.Reasoning, a.Thinking) | 228 | + | reasoning, err := normalizeReasoning(a.Reasoning) |
| 235 | if err != nil { | 229 | if err != nil { | ||
| 236 | return protocolToolError(prefix + ": " + err.Error()) | 230 | return protocolToolError(prefix + ": " + err.Error()) | ||
| 237 | } | 231 | } | ||
| 238 | if reasoning == "" && profile != nil && strings.TrimSpace(profile.Thinking) != "" { | 232 | if reasoning == "" && profile != nil && strings.TrimSpace(profile.Thinking) != "" { | ||
| 239 | - | reasoning, err = reasoningOverride(profile.Thinking, "") | 233 | + | reasoning, err = normalizeReasoning(profile.Thinking) |
| 240 | if err != nil { | 234 | if err != nil { | ||
| 241 | return protocolToolError(prefix + ": profile " + profile.Name + ": " + err.Error()) | 235 | return protocolToolError(prefix + ": profile " + profile.Name + ": " + err.Error()) | ||
| 242 | } | 236 | } | ||
| 243 | } | 237 | } | ||
| 244 | if reasoning == "" && t.DefaultReasoning != nil { | 238 | if reasoning == "" && t.DefaultReasoning != nil { | ||
| 245 | - | reasoning, err = reasoningOverride(t.DefaultReasoning(), "") | 239 | + | reasoning, err = normalizeReasoning(t.DefaultReasoning()) |
| 246 | if err != nil { | 240 | if err != nil { | ||
| 247 | return protocolToolError(prefix + ": host " + err.Error()) | 241 | return protocolToolError(prefix + ": host " + err.Error()) | ||
| 248 | } | 242 | } | ||
| @@ -326,16 +320,8 @@ func (t *SubagentSpawnTool) Execute(ctx context.Context, raw json.RawMessage, _ | |||||
| 326 | }, nil | 320 | }, nil | ||
| 327 | } | 321 | } | ||
| 328 | 322 | ||||
| 329 | - | func reasoningOverride(reasoning, thinking string) (string, error) { | 323 | + | func normalizeReasoning(value string) (string, error) { |
| 330 | - | reasoning = strings.TrimSpace(reasoning) | 324 | + | value = strings.TrimSpace(value) |
| 331 | - | thinking = strings.TrimSpace(thinking) | |||
| 332 | - | if reasoning != "" && thinking != "" && provider.NormalizeReasoning(reasoning) != provider.NormalizeReasoning(thinking) { | |||
| 333 | - | return "", fmt.Errorf("reasoning and thinking disagree; provide only one") | |||
| 334 | - | } | |||
| 335 | - | value := reasoning | |||
| 336 | - | if value == "" { | |||
| 337 | - | value = thinking | |||
| 338 | - | } | |||
| 339 | if value == "" { | 325 | if value == "" { | ||
| 340 | return "", nil | 326 | return "", nil | ||
| 341 | } | 327 | } | ||
M packages/agent/tools/subagent_spawn_test.go +7 -1
| @@ -350,7 +350,7 @@ func TestSubagentSpawnAppliesProfileFastModeRestriction(t *testing.T) { | |||
| 350 | 350 | } | |
| 351 | 351 | } | |
| 352 | 352 | ||
| 353 | - | func TestSubagentSpawnSchemaAdvertisesFastModeOverride(t *testing.T) { | |
| 353 | + | func TestSubagentSpawnSchemaUsesReasoningAndKeepsProfileThinkingInternal(t *testing.T) { | |
| 354 | 354 | var schema struct { | |
| 355 | 355 | Properties map[string]struct { | |
| 356 | 356 | Type string `json:"type"` | |
| @@ -360,6 +360,12 @@ func TestSubagentSpawnSchemaAdvertisesFastModeOverride(t *testing.T) { | |||
| 360 | 360 | if err := json.Unmarshal((&SubagentSpawnTool{}).Schema(), &schema); err != nil { | |
| 361 | 361 | t.Fatal(err) | |
| 362 | 362 | } | |
| 363 | + | if _, ok := schema.Properties["reasoning"]; !ok { | |
| 364 | + | t.Fatal("subagent_spawn schema does not advertise reasoning") | |
| 365 | + | } | |
| 366 | + | if _, ok := schema.Properties["thinking"]; ok { | |
| 367 | + | t.Fatal("subagent_spawn schema exposes profile-only thinking") | |
| 368 | + | } | |
| 363 | 369 | fastMode, ok := schema.Properties["fast_mode"] | |
| 364 | 370 | if !ok { | |
| 365 | 371 | t.Fatal("subagent_spawn schema does not advertise fast_mode") | |
| @@ -350,7 +350,7 @@ func TestSubagentSpawnAppliesProfileFastModeRestriction(t *testing.T) { | |||||
| 350 | } | 350 | } | ||
| 351 | } | 351 | } | ||
| 352 | 352 | ||||
| 353 | - | func TestSubagentSpawnSchemaAdvertisesFastModeOverride(t *testing.T) { | 353 | + | func TestSubagentSpawnSchemaUsesReasoningAndKeepsProfileThinkingInternal(t *testing.T) { |
| 354 | var schema struct { | 354 | var schema struct { | ||
| 355 | Properties map[string]struct { | 355 | Properties map[string]struct { | ||
| 356 | Type string `json:"type"` | 356 | Type string `json:"type"` | ||
| @@ -360,6 +360,12 @@ func TestSubagentSpawnSchemaAdvertisesFastModeOverride(t *testing.T) { | |||||
| 360 | if err := json.Unmarshal((&SubagentSpawnTool{}).Schema(), &schema); err != nil { | 360 | if err := json.Unmarshal((&SubagentSpawnTool{}).Schema(), &schema); err != nil { | ||
| 361 | t.Fatal(err) | 361 | t.Fatal(err) | ||
| 362 | } | 362 | } | ||
| 363 | + | if _, ok := schema.Properties["reasoning"]; !ok { | |||
| 364 | + | t.Fatal("subagent_spawn schema does not advertise reasoning") | |||
| 365 | + | } | |||
| 366 | + | if _, ok := schema.Properties["thinking"]; ok { | |||
| 367 | + | t.Fatal("subagent_spawn schema exposes profile-only thinking") | |||
| 368 | + | } | |||
| 363 | fastMode, ok := schema.Properties["fast_mode"] | 369 | fastMode, ok := schema.Properties["fast_mode"] | ||
| 364 | if !ok { | 370 | if !ok { | ||
| 365 | t.Fatal("subagent_spawn schema does not advertise fast_mode") | 371 | t.Fatal("subagent_spawn schema does not advertise fast_mode") | ||