spawn_agent rejects empty optional message/items fields as mutually exclusive

Open 💬 1 comment Opened Aug 5, 2026 by yangwuan55

Summary

spawn_agent rejects otherwise valid calls when unused optional fields are serialized as empty values. In the current multi_agent_v1.spawn_agent surface, a payload with a non-empty message plus items: [] fails as if both inputs were supplied, and a payload with items: [...] plus message: "" fails the same way. Empty optional strings such as reasoning_effort: "" fail during argument parsing instead of behaving like omitted optional fields.

This is narrower than the existing MultiAgent payload-delivery reports such as #36321: the failure happens on the parent/tool argument boundary before a child agent can be spawned.

Environment

  • Codex CLI on PATH: codex-cli 0.146.0
  • Runtime: Codex Desktop / ChatGPT-auth session exposing multi_agent_v1.spawn_agent
  • OS: macOS 26.5.2, Darwin 25.5.0 arm64
  • Node: v26.6.0
  • Upstream source checked: openai/codex main at 1fe6be9719ac4a18ad08f8341b89f9a0f386105e
  • LazyCodex source checked for routing comparison: code-yeongyu/lazycodex main at fb48ddc4bc8be02a0cfe0a509a30cf3543edf72a

Repository Decision

  • Target repository: openai/codex
  • Why this belongs here: the observed failure text and parser behavior come from upstream Codex's multi-agent tool handlers and protocol parsing.
  • Upstream Codex source evidence:
  • codex-rs/core/src/tools/handlers/multi_agents_common.rs:137-144 treats (Some(message), Some(items)) as an unconditional error, so message: "task" plus items: [] is rejected before the empty branch can be ignored.
  • codex-rs/core/src/tools/handlers/multi_agents/spawn.rs:55-62 parses SpawnAgentArgs and passes args.message / args.items directly into parse_collab_input.
  • codex-rs/core/src/tools/handlers/multi_agents/spawn.rs:228-234 models message, items, model, reasoning_effort, and service_tier as optional fields, but empty serialized values still become Some(...) or parse errors.
  • codex-rs/protocol/src/openai_models.rs:109-115 deserializes ReasoningEffort from a string, and :119-133 rejects "" with reasoning_effort must not be empty.
  • LazyCodex evidence: LazyCodex skills and rules reference this upstream tool shape heavily, but the concrete parser failure is in upstream Codex. A separate downstream compatibility issue will link to this one.

Reproduction

From a session where multi_agent_v1.spawn_agent is exposed, call it with a non-empty task and an empty unused optional branch:

{
  "agent_type": "explorer",
  "fork_context": false,
  "items": [],
  "message": "TASK: no-op reproduction. DELIVERABLE: immediately report that this should not have spawned if validation is correct.",
  "model": "",
  "reasoning_effort": "low",
  "service_tier": ""
}

Observed:

Provide either message or items, but not both

The inverse shape also fails:

{
  "agent_type": "explorer",
  "fork_context": false,
  "items": [{"type":"text","text":"TASK: no-op reproduction. DELIVERABLE: immediately report that this should not have spawned if validation is correct."}],
  "message": "",
  "model": "",
  "reasoning_effort": "low",
  "service_tier": ""
}

Observed:

Provide either message or items, but not both

A payload with an empty optional effort fails even earlier:

{
  "agent_type": "explorer",
  "fork_context": false,
  "items": [],
  "message": "TASK: no-op reproduction. DELIVERABLE: immediately report that this should not have spawned if validation is correct.",
  "model": "",
  "reasoning_effort": "",
  "service_tier": ""
}

Observed:

failed to parse function arguments: reasoning_effort must not be empty at line 1 column 219

Expected Behavior

One of these should be true:

  1. The model-visible schema/tool binding should allow unused optional branches to be omitted entirely and should not materialize items: [], message: "", or empty optional strings.
  2. The parser should defensively normalize empty optional values before validation: items: [] should count as absent when message is non-empty, message: "" should count as absent when items is non-empty, and model / reasoning_effort / service_tier empty strings should count as absent or produce a field-specific guidance error before deserialization.
  3. The error should show the exact valid call shape that the model can emit from the advertised schema.

Actual Behavior

  • message plus items: [] is rejected as Provide either message or items, but not both.
  • items plus message: "" is rejected as Provide either message or items, but not both.
  • reasoning_effort: "" fails parsing with reasoning_effort must not be empty before the tool can interpret it as an omitted optional override.

Evidence

  • Live tool-call failure in Codex Desktop session on 2026-08-05 with multi_agent_v1.spawn_agent.
  • Source sync performed immediately before filing against openai/codex 1fe6be9719ac4a18ad08f8341b89f9a0f386105e.
  • Duplicate search found many spawn_agent issues and one adjacent exact-message mention in #36321, but that issue's primary failure is child-side empty payload delivery; this report is parent-side argument parsing/normalization.

Root Cause

parse_collab_input treats Option presence as semantic presence. Because Some(vec![]) and Some("") are still Some, the mutually-exclusive validation fires before the parser can treat the empty unused branch as absent. Separately, ReasoningEffort deserialization rejects "" at parse time, so an empty serialized optional field cannot reach spawn-agent override logic as None.

Proposed Fix

Normalize spawn-agent arguments before mutually-exclusive validation and before optional override parsing. Concretely:

  • Convert items: Some([]) to None before parse_collab_input checks (message, items).
  • Convert message: Some(s) where s.trim().is_empty() to None when items is non-empty; continue rejecting empty message when it is the only input.
  • For optional override strings (model, reasoning_effort, service_tier), either deserialize through an empty-string-as-None adapter or pre-normalize string values before typed parsing.
  • Add regression tests for message + items: [], items + message: "", and empty optional override fields.

Verification Plan

  • Add a unit test around parse_collab_input proving Some("task") + Some(vec![]) resolves to the text task.
  • Add a unit test proving Some("") + Some(non_empty_items) resolves to the item payload.
  • Add a spawn-agent argument parsing regression test proving empty optional override strings do not block a valid message-only spawn.
  • Run targeted codex-core multi-agent handler tests and a live spawn_agent call from a Codex session.

---
This issue or PR was generated by LazyCodex.
Tag: lazycodex-generated

View original on GitHub ↗

1 Comment

yangwuan55 · 23 days ago

I tested a minimal fix locally and pushed it to a fork branch for maintainer review:

Summary of the fix:

  • Normalize empty message / items placeholders before the mutually-exclusive collaboration input validation.
  • Treat empty v1 optional override strings (model, reasoning_effort, service_tier) as omitted before typed parsing.
  • Add regression tests for message-only, items-only, and empty optional override payloads.

Validation run locally:

  • cargo test -p codex-core spawn_agent_ignores_empty -- --nocapture
  • cargo test -p codex-core spawn_agent_rejects_when_message_and_items_are_both_set -- --nocapture
  • just clippy -p codex-core

I also tried opening a PR, but GitHub rejected it with yangwuan55 does not have the correct permissions to execute CreatePullRequest, which appears consistent with the repository's invitation-only external contribution policy. Posting the branch here instead so maintainers can inspect or cherry-pick it.