All notes

Making Codex subagents readable to Ollama providers

I changed Codex to route multi-agent messages by provider, keeping encrypted transport as the default while adding a plaintext path for Ollama-compatible endpoints.

  • This is the second note in a series about mixed-provider Codex delegation.
  • Remote inference worked, but Ollama-backed subagents could not read tasks stored in encrypted inter-agent messages.
  • The provider configuration now selects encrypted or plaintext transport.
  • Encrypted transport remains the default. Plaintext is an explicit opt-in for providers that need it.
  • Codex now exposes separate plaintext spawn, message, and follow-up tools when a configured provider or role requires them.
  • The model client converts plaintext agent messages into ordinary user text before sending a Responses request.
  • Readable transport fixes message delivery. Responses events, tool calling, and MCP compatibility still require separate validation.
On this page

In Using Ollama models with Codex providers, I ended with a specific failure: remote inference worked, but native mixed-provider delegation did not.

An OpenAI-backed parent could create a Halo or Ollama-backed child, but the child task arrived as an agent_message with its useful content inside encrypted_content. Codex did not decrypt that content locally. An ordinary Ollama-compatible Responses endpoint could see that a task existed, but not the task itself.

The visible payload was effectively empty:

TEXT
Parent agent
    |
    v
spawn_agent
    |
    v
agent_message
    |-- visible routing metadata
    `-- encrypted_content containing the task
                         |
                         v
              provider cannot read it

Since writing that note, I changed Codex to give those providers a readable multi-agent path. Turning encryption off globally would have changed every agent in the session, so I made transport a provider capability.

The transport belongs to the provider

My first design question was where the setting should live. A global plaintext switch would have applied to every agent in the session.

One Codex session can use several providers. An OpenAI-backed parent may support the normal encrypted message shape while a Halo-backed worker needs ordinary text. A global switch would force every agent onto the least capable transport in the session.

I added a provider capability instead:

TOML
[model_providers.halo]
name = "Halo Ollama"
base_url = "https://halo.example.com/ollama/v1"
wire_api = "responses"
multi_agent_message_transport = "plaintext"

The new MultiAgentMessageTransport setting has two values:

  • encrypted
  • plaintext

The default is still encrypted. An existing provider keeps the previous behavior unless its configuration explicitly asks for plaintext.

The default keeps the existing encrypted path unchanged while providers that cannot consume it opt into plaintext.

Plaintext uses separate tools

The next choice was how the parent agent should send a task.

I kept the existing encrypted tools and added explicit plaintext variants:

TEXT
spawn_agent                 spawn_agent_plaintext
send_message                send_message_plaintext
followup_task               followup_task_plaintext

Codex only exposes the plaintext tools when a configured provider or visible agent role requires plaintext transport. That keeps the ordinary tool list unchanged for sessions that do not need the compatibility path.

The explicit names also show the orchestrating model which transport it is using. A tool called send_message never changes its wire shape silently.

Codex checks the requested tool against the target child's saved transport. If the parent uses the encrypted tool for a plaintext child, or the plaintext tool for an encrypted child, the call fails with a corrective error. The parent can then retry with the right tool.

TEXT
Parent selects a child
        |
        v
Codex loads the child's saved configuration
        |
        v
requested transport matches provider transport?
        |                         |
       yes                        no
        |                         |
        v                         v
deliver message           return corrective error

This prevents a child from being created under one transport and resumed later under another by accident.

The conversion happens at the provider boundary

The plaintext tools solve only the first half of the problem. Codex still has an internal AgentMessage type that carries the sender, recipient, and message content through the agent tree.

For a provider configured with plaintext transport, the model client rewrites a wholly plaintext AgentMessage into a normal user message containing InputText before it builds the Responses request.

TEXT
Codex agent tree
    |
    | AgentMessage with plaintext task
    v
ModelClient
    |
    | provider uses plaintext transport
    v
ordinary user InputText
    |
    v
Ollama-compatible Responses endpoint

The encrypted path does not go through that conversion. Codex continues to store and relay encrypted content as opaque data for providers that support it.

This keeps the adaptation narrow. Codex retains its internal multi-agent model and changes only the wire shape sent to a provider that needs ordinary text.

The setting has to survive the child lifecycle

Adding one field to provider configuration was not enough. A child agent may be created, compacted, reloaded, resumed, or reconstructed from remote thread configuration.

The selected transport therefore had to flow through:

  • provider configuration and its generated schema
  • the child agent's configuration snapshot
  • remote thread configuration and protobuf serialization
  • role resolution for visible agent profiles
  • tool planning and conditional tool exposure
  • spawn, message, follow-up, reload, completion, and failure paths

Without that plumbing, the first spawn could work and a later follow-up could fall back to encrypted transport. Persisting the provider choice is what turns a one-request rewrite into a usable agent lifecycle.

Testing the seam

The work landed as three focused commits on my experimental Codex branch:

TEXT
Add provider-level multi-agent message transport
Route multi-agent messages by provider transport
Test plaintext multi-agent provider interoperability

The changes include focused coverage for provider parsing, defaults, schema and thread-config persistence, tool exposure, transport mismatch errors, request conversion, child reloads, subagent notifications, completion, and failure.

A request can return a thread identifier even when the child received no usable task. A follow-up can also appear to succeed while using the wrong wire shape. The tests therefore inspect the actual message delivered to the provider.

What this changed

The original failure can now take this path:

TEXT
OpenAI-backed parent
        |
        v
spawn_agent_plaintext
        |
        v
Halo child configured for plaintext transport
        |
        v
AgentMessage converted to ordinary user InputText
        |
        v
provider can receive the complete task

This preserves the architecture I wanted. The stronger hosted model can remain the orchestrator while a different provider receives bounded child work in a message shape it can read.

It also preserves the local execution boundary. The Ollama-backed model still does not open files or run commands on my Mac. It requests tools, and the Codex runtime performs those actions under the local sandbox and approval policy.

Remaining provider compatibility work

Readable task text fixes message delivery. Other parts of the provider contract can still fail.

An endpoint can accept a Responses request and still disagree with Codex about model discovery, response events, tool schemas, function calls, MCP routing, or reasoning parameters. During later Halo worker experiments, I still found provider-specific failures after the message-shape change was in place. One path, for example, returned an Ollama-style model list where Codex expected a different schema. I later fixed that model-discovery problem separately. It was another example of why readable tasks alone do not establish full provider compatibility.

I also tested Halo-backed models against real worker-shaped tasks instead of grading a raw prompt response. The useful checks were whether the worker used the expected repository tools, changed only allowed files, passed tests, handled contradictory instructions, and returned output the parent could verify.

Some models produced good bounded patches. Others were too slow, ignored the expected retrieval path, or failed before the task began. That reinforced the same lesson from the provider work: configuration is not proof of delegation, and message delivery is not proof of full tool compatibility.

Where it stands

The provider-aware transport began as three focused commits on my experimental Codex branch. It now sits inside a larger private patch stack covering agent lifecycle controls, routed model selection, and distribution. Stock Codex Desktop and arbitrary Ollama endpoints do not gain mixed-provider subagent support from these private changes.

Readable task text solved the first blocker, but it did not make the private fork operational. Codex still needed lifecycle controls, provider-aware model discovery, Desktop routing, and a reliable way to install the matching binaries. Those changes belong in a separate follow-up note.

The original blocker now has a specific fix: store transport as a provider capability, keep the encrypted default, expose explicit plaintext tools, and translate the message at the provider boundary. The child can finally read its task. A future note will cover the lifecycle controls needed once that child is running.