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:
Parent agent
|
v
spawn_agent
|
v
agent_message
|-- visible routing metadata
`-- encrypted_content containing the task
|
v
provider cannot read itSince 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:
[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:
encryptedplaintext
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:
spawn_agent spawn_agent_plaintext
send_message send_message_plaintext
followup_task followup_task_plaintextCodex 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.
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 errorThis 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.
Codex agent tree
|
| AgentMessage with plaintext task
v
ModelClient
|
| provider uses plaintext transport
v
ordinary user InputText
|
v
Ollama-compatible Responses endpointThe 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:
Add provider-level multi-agent message transport
Route multi-agent messages by provider transport
Test plaintext multi-agent provider interoperabilityThe 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:
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 taskThis 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.