Restoring subagent roles, model and reasoning in multi_agent_v2

To help anyone else working around the recent changes. Big thanks to robertmsale and INFUSION on the Codex discord sub-thread for help debugging the last 48hrs!

Purpose

Some Codex builds reserve the provider-visible spawn_agent schema when multi_agent_v2 is enabled. Adding role, model, or reasoning fields directly to that visible schema can prevent a task from starting:

Function 'collaboration.spawn_agent' is reserved for use by this model
and must match the configured schema.

The working pattern is to preserve the reserved schema, encode the requested role in task_name, and inject trusted role, model, and reasoning metadata through a PreToolUse hook before the local spawn handler creates the child.

Compatibility note: multi_agent_v2 is evolving. Validate this pattern against the exact Codex build you deploy and retain rollout-level acceptance tests.

1. Use a v2-only project configuration

model = "gpt-5.6-sol"
model_reasoning_effort = "xhigh"
model_instructions_file = "instructions/prime.md"

[features]
hooks = true

[features.multi_agent_v2]
enabled = true
max_concurrent_threads_per_session = 30
min_wait_timeout_ms = 10000
default_wait_timeout_ms = 30000
max_wait_timeout_ms = 3600000
tool_namespace = "collaboration"
hide_spawn_agent_metadata = true
non_code_mode_only = true

Remove conflicting legacy configuration from active project and named TOMLs:

# Remove legacy v1 feature flags such as:
multi_agent = false

# Remove legacy [agents] controls such as:
[agents]
max_depth = 1
max_threads = 8
job_max_runtime_seconds = 1200

Remove fork_context from launch packets, skills, and active documentation. A fresh bounded v2 child uses fork_turns = "none".

2. Declare each role under .codex/agents/

name = "forager_agent"
description = "Read-only evidence gathering agent."
model = "gpt-5.6-luna"
model_reasoning_effort = "high"
model_instructions_file = "../instructions/forager.md"
sandbox_mode = "read-only"
nickname_candidates = ["forager_agent"]

developer_instructions = """
You are forager_agent.
Complete the supplied evidence task personally.
Do not delegate further.
Return only the requested evidence packet.
"""

The filename, profile name, and hook-injected agent_type must agree exactly.

3. Encode role selection in task_name

With spawn metadata hidden, keep the provider-visible call inside the reserved v2 contract:

{
  "task_name": "forager_agent_evidence_scan",
  "message": "Inspect the supplied evidence packet.",
  "fork_turns": "none"
}

Use an explicit routing allowlist and longest-prefix matching:

ROLE_ROUTES = {
    "forager_agent": {
        "model": "gpt-5.6-luna",
        "reasoning_effort": "high",
    },
    "worker_agent": {
        "model": "gpt-5.6-terra",
        "reasoning_effort": "medium",
    },
    "review_agent": {
        "model": "gpt-5.6-terra",
        "reasoning_effort": "xhigh",
    },
}

Do not accept arbitrary role, model, or reasoning values from the caller.

4. Rewrite the call with PreToolUse

Register the hook in .codex/hooks.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "(^Agent$|spawn_agent$)",
        "hooks": [
          {
            "type": "command",
            "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/pre_spawn_agent_v2.py\"",
            "timeout": 15
          }
        ]
      }
    ]
  }
}

The hook resolves the role from task_name and returns trusted local metadata:

{
  "continue": true,
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "updatedInput": {
      "task_name": "forager_agent_evidence_scan",
      "message": "Inspect the supplied evidence packet.",
      "fork_turns": "none",
      "agent_type": "forager_agent",
      "model": "gpt-5.6-luna",
      "reasoning_effort": "high"
    }
  }
}

Unknown, retired, and prohibited roles should return an explicit denial:

{
  "continue": false,
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Unknown or retired role"
  }
}

For compatibility across adjacent builds, accept snake-case and camel-case hook envelope fields. Some builds may flatten a namespaced tool name to a value such as collaborationspawn_agent, so suffix matching is prudent.

5. Restart and attest the child rollout

Open a fresh task after changing configuration, hooks, profiles, or instruction files. Verify the resulting child rollout rather than trusting hook stdout alone:

agent_role == requested role
model == routed model
effort == routed reasoning level
multi_agent_version == v2
fork_turns == none
base instructions == intended role base
unwanted parent identity markers are absent

Test every role and model combination, caller-supplied metadata overwrite, malformed input denial, unknown-role denial, retired-role denial, and agreement between the routing table and profile TOMLs.

Security boundary

This pattern restores deterministic role, model, and reasoning assignment. It is not a tamper-proof authorization system. Privileged writes, publication, credentials, and constitutional authority still require independently enforced policy or an external verifier.

You must maintain hide_spawn_agent_metadata = true if you want to use the new bigger models, so don’t simply try to set that to false. This DOES mean you can’t see anything in the apps - hence the need to verify through rollout audit that the role and its settings DID take.