Distributed execution and policy / Field essay 16

Parallel AI checks need a release contract before they need more workers.

Parallel checks help only when the release condition, ownership of output fields and behavior of late branches are defined in advance.

Published

ARCHITECTURE
FIELD ESSAY 16 / DISTRIBUTED EXECUTION AND POLICY
THE OVERVIEW FILMWatch the full argument

Parallel AI checks need a release contract before they need more workers

A parallel check that finishes after release did not protect the release.

That sentence became one of the most important constraints in my design for parallel AI actions. It cuts through a surprisingly attractive demo: fork a request to several models, show their findings arriving in a dashboard, and call the result a safer inference pipeline. The dashboard can be accurate while the safety claim is false. If the required finding arrives after the answer reached the user, it is an observation about something that already happened.

In OptinRouter, I wrote the action graph around a stricter promise: all required branches must join before their protected release boundary. The downloadable preview contract spells that out as all_required_before_release. It also keeps external side effects disabled. Those two choices make the design easier to reason about because the first implementation target is bounded inspection and enrichment, rather than an unrestricted swarm with business credentials.

I have working public contract checks and a concrete architecture direction here. The execution engine remains implementation work. That distinction matters to the engineering lesson: a graph that looks sensible needs a precise acceptance contract before it needs more workers.

Start with the boundary, then draw the graph

The first question I ask is what event the check is supposed to prevent.

An input risk check may need to finish before the main provider receives the request. An output schema check may need to finish before the application receives the response. A content-free trace event may be allowed to finish later. These are three different boundaries, even when the user experiences one request.

If I put them into a single list called “parallel checks,” I lose the distinction that makes their results useful. Waiting for everything can add unnecessary delay. Waiting for too little can release the exact thing the check was meant to stop. The graph needs phases and required status because the protected event gives those fields meaning.

The published example has three nodes. An input risk check is required, uses a customer-server placement, and gets a 1,500-millisecond timeout. An optional context lookup runs in the input phase with a 1,000-millisecond timeout. A required output-format check has a 500-millisecond timeout and depends on the risk node. These are configured example limits, not measured response times.

That example is useful precisely because it is small. I can name each owner, each dependency and each release obligation without hiding the policy behind a graph visualizer. A larger graph should earn its complexity by preserving the same answers.

Required is a release obligation

A required branch cannot use a failure behavior that says to continue without its result. The preview checker rejects that combination directly:

if(n.required&&n.on_failure==='continue_without_result')errors.push('required_branch_continues:'+n.id);

That line is small, but it protects a significant design decision. Required does not mean “we usually run this.” It means the request cannot cross the relevant boundary without the required result under the selected policy.

Block and hold can both be coherent outcomes. A block says the attempt cannot proceed. A hold says the request needs a defined resolution path. Neither should be disguised as a successful check. A future hold implementation also needs an owner, expiry and reconciliation behavior; otherwise the safety state becomes an indefinite queue.

An optional branch has a different contract. It can enrich the request or produce a later finding, but the design must specify whether release waits for it and whether its absence changes the result. Once release happens, a late optional result cannot rewrite the delivered answer or retroactively claim to have protected it.

This is where I would insist on a durable release decision in the executor. The worker result and the release decision should be separate records. A worker can report “completed” after the release decision is final. That does not make the release decision mutable. Keeping those records distinct also makes incident reconstruction much clearer.

Give outputs owners before allowing concurrency

A second rule in the preview rejects duplicate output slots. Two branches cannot both claim the same destination and let network timing choose the winner.

Arrival-order merging is seductive because it is easy to implement. Store each result in a shared object and let the last update win. But then retry timing, provider load and worker scheduling become part of the business policy. The same logical request can produce different output because one branch happened to finish first.

I prefer declared ownership. A risk branch owns checks.risk. An enrichment branch owns enrichment.context. A format branch owns checks.format. A separate deterministic decision consumes those results according to a pinned plan. The worker does not acquire authority over its siblings merely because it returned later.

The preview enforces exact slot uniqueness. My boundary exercise also makes its limit explicit: checks and checks.risk are different strings, so the current checker accepts them together. If a future executor interprets dotted strings as nested object paths, those destinations overlap. A compiler must either reject ancestor/descendant overlap or define a non-overlapping representation. An exact-string check cannot prove the stronger property.

That is the kind of detail I want to resolve before adding concurrency. The contract and the runtime must agree on what a destination means. Otherwise the graph can pass validation while the object merge still has a race.

Topology checks need more than a happy path

The public checker builds an index of node IDs and walks dependencies using visiting and visited sets. A node encountered while it is already being visited indicates a cycle. A dependency absent from the index is an error. An input node that depends on an output node is also an error, because that dependency reverses the intended inference phases.

I tested a self-cycle, a two-node cycle, a longer disconnected cycle, missing dependencies in separate components, and an input-to-output inversion. The disconnected cases matter: validating only the first reachable component would leave an invalid island in the same graph.

Positive cases matter just as much. A diamond is legal: two branches can depend on the same predecessor, and a later node can depend on both. That shared dependency is not a cycle. Array order should not force a dependency to be declared before its consumer. Reversing the example's node array still produces a valid graph.

I also compare error sets under a node-order change. The order of diagnostic messages is an implementation detail; the presence of the relevant errors is the semantic property. Testing that distinction avoids turning incidental traversal order into an accidental public API.

There are still two separate questions: is this a valid graph, and is this graph allowed to execute here? The topology walker answers only part of the first. It does not authenticate a tenant, resolve an approved tool registry, or prove the selected placement can access the requested data.

Shape validation and graph validation are different layers

The JSON Schema supplies bounds and types that the topology helper does not inspect. The preview allows at most 32 nodes, a max_parallel value from one through eight, and per-node timeouts from one through 30,000 milliseconds. Dependency lists allow at most 16 entries; input projections allow at most 12. Unknown fields are rejected at the contract's defined object boundaries.

These numbers make the preview finite. They are chosen contract ceilings, not evidence that an eight-worker runtime has been load-tested or that every allowed graph meets a latency objective.

The composition matters. The topology helper alone accepts a graph whose root join field has been changed to first_result, because it never reads that field. The schema rejects it. Calling only the helper therefore does not establish the published contract. Conversely, a schema-valid list of dependency names can still contain references to nonexistent nodes; that requires graph analysis.

I tested the numeric boundaries on both sides: zero, one, eight and nine parallel slots; zero, one, 32 and 33 nodes; and one, 30,000 and 30,001 milliseconds. Fractional values fail where integers are required. Exact-boundary positives protect against accidentally tightening the accepted contract while trying to make rejection stricter.

This is a broader engineering habit I use with agent systems: name the layers of a verdict. “Validated” is too vague if nobody can tell whether it means JSON shape, dependency consistency, capability eligibility or current authorization.

A plausible tool name is not a capability

The schema restricts the syntax of a tool reference. It rejects shell-like text such as a tool name containing spaces and command fragments. That is useful input hygiene, but syntax cannot establish that a tool exists or that a caller may use it.

The exercise includes a well-formed unregistered@999 tool reference. It passes the current schema and topology checks. That is an intentional characterization of the preview boundary. A production compiler needs a registry lookup against the selected tool version, its input/output schemas and the caller's allowed capabilities.

Runtime dispatch then needs a current authority check. A graph can be valid when compiled and ineligible when executed: permissions may be revoked, a placement may be unavailable, or a required policy revision may expire. Pinning a revision makes the decision reproducible; it does not make revocation irrelevant.

I also keep network destinations out of model-authored free text. The architecture calls for preconfigured destinations and authentication adapters, with independent redirect and SSRF protection. A model selecting a declared read action should not thereby gain the ability to choose an arbitrary endpoint, attach credentials or invent a new command.

Minimal projections reduce both risk and coupling

Each node declares an input projection. In the example, the risk check receives a data-class field and redacted request text. The context lookup receives a project identifier. The format validator receives the result body.

That is an architectural choice about information flow. A context lookup does not need the full conversation just because the orchestrator happens to have it. A tracing branch does not need a response body merely to record a stage outcome. Minimizing inputs reduces what each branch can disclose and makes its behavior easier to test.

The projection also creates an interface worth versioning. If a branch silently starts depending on a field it was never promised, it should fail contract review rather than receive the entire parent context as a convenient fallback. Otherwise every worker eventually becomes coupled to every detail of the request envelope.

The schema's maximum of 12 projection fields is only a count bound. It does not prove the selected fields are appropriate for the tool or the principal. That requires semantic compilation and authorization against the tool's actual contract. The important design move is to make the projection explicit enough that those checks have something concrete to evaluate.

Budgets belong to the whole request

Parallelism can shorten elapsed time while increasing total work. Three branches may run at once, but the bill still includes all three, including retries and any classifier or repair calls.

My architecture therefore puts reservation and reconciliation around the request rather than treating the main inference call as the whole budget. Before fan-out, the executor needs a bound on admitted work. Branch deadlines must also fit within the request's remaining deadline; a 30-second per-node schema maximum does not authorize every node to consume a fresh 30 seconds after its predecessor finishes.

Depth and fan-out are different variables. A narrow chain can exceed an acceptable end-to-end deadline without high concurrency. A wide graph can exhaust a reservation quickly even with a shallow dependency structure. A production compiler and scheduler need explicit rules for both, alongside a bounded retry policy.

The current preview schema does not implement that full accounting model. I would carry these as executor acceptance obligations, with fixtures that exhaust the budget, expire a parent deadline, and leave required descendants unable to start. Passing the public graph checker would remain a necessary early check, not the final admission verdict.

Cancellation needs an accounting answer

A cancellation event does not make in-flight work disappear. One branch may have completed, another may be waiting for a provider, and a third may never have started. A useful cancellation record needs to distinguish those states.

For the planned executor, my rule is to stop descendants, prevent unauthorized release, and reconcile unused reservations separately from work already performed. A late completion can be recorded without reopening the result. A timeout should not trigger an unbounded retry that escapes the original deadline or changes the policy revision.

The architecture also names durable state, transactional outbox delivery, idempotency and fencing for worker crashes and retries. Those are implementation obligations, not properties supplied by a directed acyclic graph. A graph specifies dependencies. It does not by itself establish who owns an attempt after a lease expires or whether a duplicate completion can overwrite the accepted result.

Keeping the initial slice to controlled reads, validation and content-free trace events reduces the consequence of getting this machinery wrong. Messages, purchases, account mutations and arbitrary shell execution remain outside the enabled action design. Adding them later would require their own exact-payload approval and execution-time authorization path.

What I would accept before enabling the engine

I would want a release trace that shows the policy revision, declared graph revision, admitted branches, required results and final release decision. It should be possible to reconstruct why the boundary opened without storing private prompt or response bodies by default.

I would also want failure fixtures with a required branch timing out, an optional branch completing after release, a worker restarting, a duplicate completion arriving, and a policy being revoked between compilation and dispatch. Each fixture should state the expected release decision and the expected accounting outcome. “The worker logged an error” would not be enough.

For deterministic joins, I would replay different completion orders and require the same accepted result for the same pinned inputs and policy. For exclusive output ownership, I would include exact-slot and ancestor/descendant conflicts under the executor's actual path semantics. For projections, I would verify that forbidden fields never reach the branch, rather than merely checking that a projection list exists.

The useful promise to a product team is specific: parallel execution can be introduced while preserving a defined release obligation. It does not require promising that every check will succeed, every provider will respond, or every request will become faster.

The practical next step is to take one real workflow and write down its protected boundary, required results, optional results, output owners and failure outcomes. If those answers are unclear, adding more agents will multiply the ambiguity. If they are clear, the graph becomes an executable contract worth implementing.

How this was verified

The retained packet pins OptinRouter commit 88eb34a7d635ce8076b8a06ca90d5e451ce4b78b. It includes the exact public checker, action-graph JSON Schema and example, owning test bodies and architecture direction. A local exercise runs 55 behavioral cases through the actual checker and Ajv 8.20.0. Three author-designed faults independently remove cycle detection, remove exact-slot conflict detection, or relax the parallel ceiling; each makes the exercise fail, and the restored source passes.

The six owning developer-documentation tests pass against a scratch copy of the pinned files and fixtures. The initial live-tree attempt failed because Ajv was not installed there; the successful run used an isolated dependency installation without changing the product checkout. These checks establish preview contract behavior, not an activated scheduler, runtime isolation, measured latency, cost savings or deployed enterprise certification.

Independent review of the original 51-case exercise detected 20 of 24 planted faults, with control exit 0. I then added four observations: unique diagnostics for a shared invalid predecessor, rejection of retry_forever, rejection of anywhere placement, and rejection of a missing on_failure field. The expanded 55-case exercise detects all 24 in an author-run same-set follow-up, with syntax checks and restored control passing. That follow-up is not a fresh independent score; peer confirmation is retained with the exact source and exercise identities. Original source, exercise and review receipt are retained.

Peer replay confirmed 24/24 on the same set with control exit 0 and unchanged source hashes. This remains a same-set follow-up; the original independent result is retained.

CONNECTED READINGForward and back
Keep reading

The answer deserves
another experiment.

Back to the journal