Turning Ollama-backed Codex subagents into a usable worker pool
After plaintext transport worked, I added role-aware concurrency, context rollover, worker inspection, cleanup, and safe private tools.
- This is the third note in a series about mixed-provider Codex delegation.
- Readable child tasks exposed separate reliability problems in the worker pool.
- I added role-aware concurrency, context rollover guidance, follow-up controls, and explicit agent cleanup.
- Execution reservations keep global and per-role limits accurate when several tasks start together.
- Private lifecycle tools had to stay outside the reserved collaboration namespace.
On this page
In Using Ollama models with Codex providers, I described why direct provider profiles were not enough. In Making Codex subagents readable to Ollama providers, I fixed the encrypted handoff by adding a provider-specific plaintext path.
Once the child could read its task, slow workers, concurrency limits, and child cleanup became the next problems.
A useful mixed-provider system also has to answer several less obvious questions. How many slow workers can run at once? When should a long-lived child be replaced? How does the parent inspect a worker before sending more work? How does Codex close a finished branch without letting one agent interfere with another?
Those questions drove the next part of the work.
What failed after message delivery
The first transport changes concentrated on message delivery:
OpenAI-backed parent
|
v
plaintext child task
|
v
Ollama-backed workerOnce that path worked, a different set of failures became visible. A slow worker could occupy a slot indefinitely. A child could accumulate too much context and still receive another follow-up. Several workers using the same role could start at once and overload the provider. Completed children could remain registered after their work was done.
These problems can affect any provider. Slower and more varied models make them easier to reproduce.
Limits belong to roles as well as sessions
Codex already had an overall limit on agent threads. I added role-level controls because different workers have different operating costs and limits.
A role can now define settings like these:
name = "builder"
model = "example-model:cloud"
model_provider = "halo"
max_concurrent = 2
rollover_context_percent = 80
rollover_uncached_input_tokens = 4000000max_concurrent limits active turns for that role inside one agent tree. The
global thread limit still applies, so a role cannot escape the session-wide
capacity policy.
The two rollover settings solve a different problem. They do not kill a child when it crosses a threshold. They mark that child as a poor candidate for more work.
follow-up requested
|
v
check active context and uncached input
|
+---- below limits ----> continue existing child
|
`---- over a limit ----> recommend a fresh childThat keeps the decision visible. A parent can close the old child and create a fresh worker with a compact handoff. An explicit override remains available for the rare case where preserving the existing thread matters more than the rollover warning.
The agent list became an operating view
The original agent list mainly answered whether a child existed and what state it was in. That was not enough to manage a mixed-provider tree.
I expanded it to report:
- the selected model and role
- active and total context information
- cumulative input and output tokens
- cached and uncached input
- context-window percentage
- whether rollover is recommended
- the reasons for that recommendation
This turned the list into a small control-plane view. The parent can inspect a worker before deciding whether to message it, resume it, or replace it.
I also added an explicit close operation. It only allows an agent to close one of its descendants, and it refuses to close the root or the caller. Closing a child stops its descendants and releases the capacity assigned to that branch.
Admission has to be atomic
Concurrency limits are easy to describe and easy to get subtly wrong.
Checking the active count and starting a turn as two separate actions leaves a gap. Two requests can both see an available slot, then both start. A per-role limit of one becomes two active turns.
The control path now reserves execution capacity before the turn begins. The same reservation follows the request into the active state and is released on completion or error.
request turn
|
v
reserve global and role capacity
|
+---- unavailable ----> reject
|
`---- reserved -------> start turn
|
v
release on exitThat matters when a parent sends several independent tasks at nearly the same time. The configured limits must describe actual execution, not a best-effort count taken just before it.
Why private tools use a separate namespace
The lifecycle work added operations such as follow-up, list, and close. My first instinct was to place every agent operation in the same collaboration namespace.
That failed for a non-obvious reason. The collaboration namespace is a reserved built-in tool surface. Its schemas have to match what the provider expects. Adding a private operation or changing a built-in schema can cause the entire Responses request to fail validation before the model runs.
The final split is deliberate:
reserved collaboration tools
- built-in spawn and message operations
private direct functions
- follow-up
- list
- close
- plaintext transport variantsThis keeps the provider-recognized schema intact while still exposing the extra controls in the private build.
Remaining Desktop integration work
At this point, the parent could limit active work, inspect usage, replace an oversized child, and close a finished branch without crossing agent-tree boundaries.
Desktop still could not use the private fork reliably. Codex needed to understand Ollama model lists, preserve provider identity through model selection, and keep its installed binaries on the same private build. Those integration and distribution changes belong in a future note.
The progression so far was:
configure a provider
|
v
make child tasks readable
|
v
manage the resulting worker poolThe worker tree was now manageable during one session. Model discovery and Desktop routing remained unresolved at the application boundary.