ADR-0001: Expose the Rust orchestration engine over HTTP and reduce the Cloud Function to a proxy - #3
Merged
nicholas-ruest merged 5 commits intoJul 27, 2026
Conversation
…on to a proxy
The deployed service now orchestrates. Before this, `functions/index.js` answered every agent
request with a synchronous pure function that echoed `tasks.length` back with a status field:
a cyclic workflow returned `status: "resolved"` with HTTP 200, and no handler ever called the
44,189 lines of tested Rust that implement the real thing.
Adds the seven `/v1/orchestrator/*` routes to the existing axum server in a new
`orchestrator_routes` module, each deserializing into the agent's existing request type and
awaiting the existing async method. Nothing in llm-orchestrator-core changed; the routes are
the missing link the ADR identified.
Details worth knowing:
- The response envelope (`execution_metadata`, `layers_executed`) is rebuilt in Rust to be
byte-compatible with `functions/index.js:53-74`, including the `ORCHESTRATOR_<AGENT>` layer
naming and its underscore substitution, so existing consumers see an unchanged shape. A test
pins each field against the JavaScript it replaces.
- Required-field validation reproduces `validateRequiredFields` including its exact
`Missing required fields: ...` message.
- Errors map by cause: a cycle, a dangling dependency, or a bad request body is 400; engine
faults are 500. The dependency resolver reports a cycle in its status rather than as an Err,
so an unsuccessful resolution is explicitly mapped to 400 -- returning 200 there would
reproduce the exact defect this removes.
- `StateTransitionRequest`, `ParallelizationRequest` and `SwarmCoordinationRequest` all require
a `timestamp` that none of the published contracts list as required, so a contract-conformant
body would fail to deserialize. The routes fill it in at the boundary; reconciling the two is
a change to agentics-contracts, which two other repositories vendor and which ADR-0001 puts
out of scope.
- `/v1/orchestrator/workflow` takes the engine-native `{ workflow, inputs }` shape and rejects
the contract's flat `{ workflow_id, workflow_name, tasks }` shape with a pointer to the right
one. Those `tasks` carry no step type or configuration, so there is no faithful translation
into `Workflow.steps` and guessing one would execute something the caller did not ask for.
The Cloud Function keeps CORS, routing and contract publishing and stops fabricating results.
It forwards each agent request to `ORCHESTRATOR_ENGINE_URL` with a Google-signed ID token
fetched from the metadata server, propagates `X-Correlation-ID`, and returns the upstream
status and body unmodified. Every failure mode fails closed -- unconfigured 503, unreachable
502, timeout 504, non-JSON upstream body 502 -- and `/health` and `/ready` now reflect the
engine instead of checking that seven constants are seven constants. The local state-machine
transition table is deleted so there is one source of truth.
`functions/test.js` is rewritten to assert proxy behaviour instead of envelope shape: the old
suite asserted only shape and so passed against a service that orchestrated nothing.
Also drops the unused `claude-flow` dependency, and grants the Cloud Function's service account
`roles/run.invoker` on the engine service specifically rather than project-wide.
Implements orchestrator/docs/adr/ADR-0001-implement-real-orchestration-logic.md.
445 passed, 3 failed, 2 skipped of 450, up from 437 -- the 13 new tests are the orchestrator_routes suite. llm-orchestrator-core remains 198/198 and the three failures are the same pre-existing ones ADR-0002 surfaced, unchanged by this work. Implements orchestrator/docs/adr/ADR-0001-implement-real-orchestration-logic.md.
ADR-0001 is stacked on ADR-0002, so the certification job that runs the Rust route tests lands here via its base branch rather than being duplicated.
…n returning The Cloud Function's tests had no CI job at all -- before this branch the suite asserted only envelope shape, and even that never ran. Now that the function is a proxy, its 113 tests assert the properties that matter: every agent request reaches the engine on the right path with the correlation ID attached, the engine's status and body come back unmodified, and every failure mode fails closed (unconfigured 503, unreachable 502, timeout 504, non-JSON upstream 502, readiness non-200). No install step is needed: no lockfile is committed and the suite stubs fetch rather than reaching the network. A second step greps for the two constructs that would mean the function had started answering by itself again -- the `tasks_count: Array.isArray(...)` echo and the local state-machine transition-table lookup. Both were verified to fire against the version of functions/index.js on main, so they are not vacuous checks. The Rust route tests, including ADR-0001's diamond-DAG, cycle and dangling-dependency verification tests, need no new job: they are bin-target tests and already run in the nextest step this branch inherits from ADR-0002. They appear in the certification as `llm-orchestrator-cli | 13 | 0 | 0 | 13`. Not done, and not fakeable here: step 12's containerised end-to-end job. `serve_http` calls `validate_phase3_startup`, which requires RUVECTOR_SERVICE_ENDPOINT and RUVECTOR_API_KEY and hard-exits when the Ruvector service is unreachable, deliberately, to trigger a Cloud Run crashloop on misconfiguration. A CI container would therefore crashloop, and standing up a fake Ruvector is new infrastructure ADR-0001 does not specify. Recorded in the pull request instead of guessed at. Implements step 12 of orchestrator/docs/adr/ADR-0001-implement-real-orchestration-logic.md, in part.
The Lint job runs `cargo fmt --all -- --check`, and the new module had 5 diffs. Formatted with rustfmt directly rather than `cargo fmt -p` so main.rs's 38 pre-existing diffs are not swept into this branch as unrelated churn. The job stays red regardless: the repository has 521 formatting diffs on main and this branch does not take that on. Clippy reports nothing against the new module.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements
docs/adr/ADR-0001-implement-real-orchestration-logic.md.What changed
The deployed service now orchestrates. Before this, every handler in
functions/index.jswas a synchronous pure function with no I/O that echoedtasks.lengthback with a status field. A cyclic workflow returnedstatus: "resolved"with HTTP 200. Nothing ever called the Rust engine.Seven routes added to the axum server (
crates/llm-orchestrator-cli/src/orchestrator_routes.rs, new module, mounted atmain.rs:4670). Each deserializes into the agent's existing request type and awaits the existing async method. Nothing inllm-orchestrator-corechanged — the routes were the missing link, exactly as the ADR predicted./v1/orchestrator/workflowWorkflowOrchestratorAgentexecute/v1/orchestrator/schedulerTaskSchedulerAgentschedule/v1/orchestrator/dependenciesDependencyResolverAgentresolve/v1/orchestrator/retryRetryRecoveryAgentevaluate/v1/orchestrator/parallelParallelizationAgentanalyze/v1/orchestrator/state-machineStateMachineAgenttransition/v1/orchestrator/swarmSwarmCoordinatorAgentcoordinateEnvelope ported byte-compatibly.
execution_metadata(trace_id,timestamp,service,execution_id) andlayers_executedare rebuilt in Rust to matchfunctions/index.js:53-74, including theORCHESTRATOR_<AGENT>layer naming and its hyphen→underscore substitution. A test pins each field against the JavaScript it replaces.Validation and error mapping ported. Required-field checks reproduce
validateRequiredFieldsincluding its exactMissing required fields: a, bmessage. Cycles, dangling dependencies and malformed bodies are 400; engine faults are 500.The Cloud Function is now a proxy. It keeps CORS, routing and contract publishing. It forwards each agent request to
ORCHESTRATOR_ENGINE_URLwith a Google-signed ID token fetched from the metadata server (no new dependency — Node 20fetch), propagatesX-Correlation-ID, and returns the upstream status and body unmodified. The local state-machine transition table at:191-193is deleted, so there is one source of truth. Dispatch nowawaits.Everything fails closed: unconfigured → 503, unreachable → 502, timeout → 504, non-JSON upstream body → 502.
/healthand/readyreflect the engine rather than checking that seven constants are seven constants.Real test output
Rust route tests —
cargo test -p llm-orchestrator-cli --bin llm-orchestrator:These include the ADR's own Verification section, run against the real agents:
diamond_dag_comes_back_in_dependency_ordercycle_is_rejected_with_400dangling_dependency_is_rejected_with_400Test 1 posts the four tasks in an order that is not a valid execution order, so echoing the input cannot pass. Test 2 gets HTTP 400 where the old handler returned 200 with
status: "resolved".Node proxy tests —
npm test:Including ADR Test 5 (readiness fails closed):
/readyreturns 503 withready: falsewhen the engine is unreachable, where it previously returned 200 unconditionally.Full workspace —
cargo nextest run --all --lib --bins --no-fail-fast:No regressions.
llm-orchestrator-corestays 198/198. The +13 are exactly the new route tests. The 3 failures are the same pre-existing ones documented in #1 (twocohere_embeddingsmockito aborts, one timing-sensitive secrets cache test) — untouched by this work.docs/TEST_CERTIFICATION.mdis regenerated from this run.Deviations, and why
1.
/v1/orchestrator/workflowdoes not accept the contract's flat shape. The published contract requires{ workflow_id, workflow_name, tasks }, butExecuteRequestis{ workflow: Workflow, inputs }andWorkflow.stepsneeds aStepTypeand a step config per step. The contract'staskscarry neither. I did not invent a mapping — a guessed translation would execute something the caller did not ask for, on an endpoint that runs real LLM steps. The route returns 400 naming the shape it wants. Reconciling the contract withExecuteRequestis a contract change and needs a human decision.2. Three request types require a
timestampthat no contract lists as required.StateTransitionRequest,ParallelizationRequestandSwarmCoordinationRequestall declaretimestamp: DateTime<Utc>with no serde default, so a contract-conformant body fails to deserialize. Fixing it properly means changingagentics-contracts, whichedge-agentandinference-gatewayeach vendor and which the ADR puts explicitly out of scope. The routes fill the value in at the boundary instead. This is a latent contract bug worth a follow-up.3. The retry contract's
failurefield iserrorin Rust.RecoveryRequestnames iterror: FailureDetails. The route accepts the contract'sfailurename on the wire and renames it, rather than breaking existing callers.4. The agent is constructed per request.
StateMachineAgent::transitiontakes&mut selfand keeps history in memory. Sharing one across requests would leak one caller's entity states into another's validation, so each request gets a fresh agent — the same thing the CLI does, and the same persistence behaviour as the handler it replaces (none).Steps 6 and 7: deploy to Cloud Run and grant IAM. I updated
deploy/gcloud/setup-iam.shto grant the Cloud Function's service accountroles/run.invokeron thellm-orchestratorservice specifically rather than project-wide, and it degrades gracefully if the service does not exist yet. I did not run it, and did not deploy. Deploying toagentics-devis a production change requiring GCP credentials and a human decision about rollout order. The sequence a human needs:./deploy/gcloud/deploy.sh agentics-dev dev us-central1— builds and deploys the engine../deploy/gcloud/setup-iam.sh agentics-dev dev us-central1— grants the function invoker access (setFUNCTION_SA_EMAILif the function does not use the default App Engine SA).ORCHESTRATOR_ENGINE_URLon the Cloud Function to the Cloud Run URL. Until this is set the function returns 503 for every agent route — deliberately, since failing closed is the point.Rollout risk to check first: responses change from always-200-with-placeholder to genuinely failing on invalid input. Any downstream consumer that assumes success will start seeing 400s. The ADR calls this out and it has not been audited.
Step 12: the CI job that runs Verification against the container. Blocked by the same missing
workflowtoken permission described in #1 —.github/workflows/ci.ymlcannot be pushed from here.Latency SLOs. Any latency figure in
docs/derived from the old stub timings measured JSON echoing and is now invalid. Not re-measured.ADR Verification criteria
diamond_dag_comes_back_in_dependency_ordercycle_is_rejected_with_400dangling_dependency_is_rejected_with_400ready: falsecargo test --allgreenagentics-contracts::integration_testsbreakage, see #1target/was removed after building; the working tree is clean.