Skip to content

[codex] Add Pipelex API orchestration and tools endpoints#30

Merged
lchoquel merged 18 commits into
devfrom
feature/pipelex-api-plxt
Jun 24, 2026
Merged

[codex] Add Pipelex API orchestration and tools endpoints#30
lchoquel merged 18 commits into
devfrom
feature/pipelex-api-plxt

Conversation

@lchoquel

@lchoquel lchoquel commented Jun 24, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds request-level orchestration mode handling for execute/start/validate flows and related API config/error handling.
  • Adds Pipe Tools endpoints for linting and formatting single .mthds files, backed by pipelex-tools-py.
  • Updates docs, OpenAPI, and unit coverage for the new API contract.

Validation

  • .venv/bin/python -m pytest tests/unit/test_tools_routes.py -q
  • .venv/bin/ruff format --check api/routes/pipelex/tools.py tests/unit/test_tools_routes.py
  • .venv/bin/ruff check api/routes/pipelex/tools.py tests/unit/test_tools_routes.py
  • .venv/bin/pyright api/routes/pipelex/tools.py tests/unit/test_tools_routes.py
  • .venv/bin/python scripts/export_openapi.py --check docs/openapi/pipelex-api.openapi.yaml

Notes

  • Draft PR opened against dev as requested.
  • Focused validation was run for the tools endpoint change; broader branch validation should run in CI.

Summary by cubic

Make the API orchestrator-agnostic and add lightweight MTHDS Tools. Routes now dispatch by orchestration_mode via a new api.toml, /start refuses non-async backends, and /validate cleanly separates invalid bundles (200) from faults (problem+json).

  • New Features

    • Orchestration mode: new api/api.toml with orchestration_mode (default direct) and allow_request_orchestration_mode_override. /v1/execute, /v1/start, and /v1/validate honor it.
    • Honest async: /v1/start returns 400 (StartRequiresAsyncOrchestration) when the resolved backend can’t do fire-and-forget.
    • MTHDS Tools: /v1/lint and /v1/format for single .mthds files, backed by pipelex-tools-py and capped by MAX_MTHDS_FILE_BYTES. Docs page: “MTHDS Tools”.
    • Error handling: RFC 7807 across the board with plugin-contributed transport mappers; /validate returns 200 for user-fixable invalid bundles and uses problem+json for backend/runner faults. OpenAPI and docs updated; broad unit test coverage.
    • Deps: pin pipelex to a git revision for CI; no behavior change.
  • Migration

    • Pick an orchestrator flavor and set orchestration_mode in ~/.pipelex/api_override.toml or .pipelex/api_{env}.toml (the base image runs direct in-process).
    • To let callers switch backends per request, set allow_request_orchestration_mode_override = true.
    • If you use direct, call /execute for runs; /start requires an async-capable orchestrator.

Written for commit 82f3537. Summary will update on new commits.

Review in cubic

lchoquel and others added 15 commits June 21, 2026 17:00
…upling)

Make pipelex-api an orchestrator-agnostic runner: the published base names no
orchestrator and runs every pipeline in-process, while distributed execution
(Temporal, Mistral, …) becomes a deployment flavor = base + one orchestrator
plugin + that plugin's activation. `api/` imports no `pipelex.temporal` /
`temporalio`.

- F1 execution: `ApiRunner.start()` builds the run job locally (preserving
  request_id / output_multiplicity / dynamic_output_concept_ref / run
  registration / telemetry) and dispatches through the hub's OrchestratorRegistry
  under the resolved execution_mode — the identical final dispatch the runtime
  bridge performs, fed the rich PipeJob instead of the lossy PipelexPipeRunInput.
  Byte-equivalent to the old make_temporal_pipe_run().start() for the Temporal
  flavor; `direct` runs in-process (workflow_id null).
- F2 validate: always DIRECT in-process; the Temporal dispatch path is gone. The
  runner loads the method library API-side (documented; size a flavor's runner).
- F3 errors: exception handlers consume orchestrator-plugin HTTP-error mappers
  (discovered via build_registrar at app construction) instead of a hardcoded
  temporal handler; base imports no orchestrator SDK.
- Config: new pipelex-api-owned api.toml (execution_mode default `direct`,
  allow_request_execution_mode_override `false`), loaded via core's env-aware
  load_plugin_config. POST /v1/start honors a per-request execution_mode override
  only when the deployment opts in, else 403.
- Deps: dropped the `temporal` extra from pipelex; removed the [temporal] block
  from the base .pipelex/pipelex.toml. Editable `pipelex` pin (../_plugins) for
  local dev against the Phase A/B core — release-time flips to the published pin.

Tests rewritten to the agnostic shape (synthetic-plugin mapper proves the F3
seam without temporalio; conformance mocks the orchestrator registry; new
api_config override-policy tests). Gates green: ruff, pyright 0, mypy 0,
pylint 10, 308 tests, openapi-check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019aGUg464jUnCsALuf88r89
…e F2

/validate is now execution_mode-aware, mirroring /start: ApiRunner.validate_verdict
resolves the deployment's execution_mode and dispatches through the hub's
BundleValidatorRegistry, returning the verdict as a value — a 200 valid
PipelexValidationReport or a 200 invalid ErrorReport. The route discriminates on
isinstance(verdict, PipelexValidationReport) instead of catching ValidateBundleError.
`direct` validates in-process (F2's behavior, kept for the agnostic base); a `temporal_*`
mode dispatches the whole job to a worker (pipelex-temporal) and assembles the same
canonical report API-side. A per-request execution_mode override is gated by the same
policy as /start (403 when forbidden).

Drops the ApiRunner.validate @OverRide (its only caller was the route); adds
execution_mode to ValidateRequest. Reverses the F2 "always DIRECT in-process" prose
across the route/runner docstrings and docs/pipe-validate.md (the library load is now
flavor-conditional). CHANGELOG [Unreleased] updated in place (F2 never shipped).

Tests: new test_validate_dispatch.py (stub validator proves registry dispatch + verdict
mapping, missing-mode, forbidden-override 403; no temporalio import); the three tests
that mocked the removed validate are re-pointed at validate_verdict. All other /validate
tests pass unchanged (byte-identical verdict wire).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgkGZcadLLZfWMH8br4XAZ
…istry (extend F1)

`POST /v1/execute` now selects its backend from the resolved `execution_mode`
(deployment default + optional policy-gated per-request override), symmetric with
`/start` and `/validate`, instead of the boot-global pipe-run slot. `execution_mode`
becomes the single source of truth for top-level dispatch; `boot_orchestrator`
narrows to the execution stack. For a correctly-configured deployment the two
already agree, so the real-world delta is nil.

- `ApiRunner.execute` override: resolve `execution_mode` FIRST (403 on a forbidden
  override, before any library load), refuse a fire-and-forget resolution with a
  400 (`FireAndForgetNotSupported` — `/execute` is synchronous), look up the
  orchestrator (`MissingOrchestratorError` if absent), then inject it as the
  runner's `_pipe_run` and delegate to the base `execute`, which keeps the entire
  run lifecycle (library setup/teardown, tracer close, pipeline-manager cleanup,
  telemetry, error mapping).
- `_OrchestratorPipeRun` (a `PipeRunProtocol` adapter) + `_pipe_output_from_run_output`
  rehydrate the orchestrator's JSON-safe `PipelexPipeRunOutput` back into the rich
  `PipeOutput` the response wraps: `hydrate_working_memory` (run inside the still-open
  run library) + `PipeOutput.model_validate(..., strict=False)` — `strict=False` is
  required to reverse the orchestrator's `model_dump(mode="json")` of `graph_spec`
  (its `created_at` is a strict datetime). The full output is preserved.
- Route threads `requested_execution_mode=extras.execution_mode` (stops discarding
  the parsed extras). New `ErrorType.FIRE_AND_FORGET_NOT_SUPPORTED` (400).
- Tests: `tests/unit/test_execute_dispatch.py` (direct dispatch + full-output
  rehydration, graph_spec strict=False round-trip, per-request override honored,
  forbidden override 403, fire-and-forget 400, missing orchestrator).
- Docs: CHANGELOG, configuration.md (the `execution_mode` vs `boot_orchestrator`
  dual-knob model), pipe-run.md. Regenerated the OpenAPI artifact — which also
  absorbs pre-existing `/validate`-work drift (the `PipelexExecutionMode` component
  + validate `execution_mode` field were never regenerated at the base tip).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Bktem3rLWRE2R7dmkEfbf
…table path)

The `pipelex = { path = "../_plugins", editable = true }` source only resolves in
the local workspace; CI checks out pipelex-api alone, so `../_plugins` is absent and
every install-dependent job failed at `uv sync` ("Distribution not found"). Pin
`pipelex` to the unreleased core branch by commit SHA via git+https (the core repo is
public, so no CI credentials are needed) and relock. Flip back to the published
`==<ver>` PyPI pin once the core change ships.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Bktem3rLWRE2R7dmkEfbf
…hema

Review feedback (Greptile P1, Codex P2): `/execute` reads `execution_mode` from
the body and honors it, but its OpenAPI request body was generated from
`RunRequest.model_json_schema()`, which doesn't declare the field — so schema-driven
clients couldn't discover or set the per-request override.

Mirror the existing `PipelexApiStartRequest` pattern: add `PipelexApiExecuteRequest
(RunRequest)` declaring `execution_mode`, wire it into the `/execute` route's
`openapi_extra`, and regenerate the artifact. Factor the shared field description into
`_EXECUTION_MODE_DESCRIPTION` so the extras model and the doc model can't drift.

Scope: only `/execute` (the endpoint this PR changes). `/start`'s schema has the same
pre-existing gap (its `execution_mode` consumption predates this PR); left for its
owning work and recorded in wip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Bktem3rLWRE2R7dmkEfbf
Fire-and-forget vs blocking is a property of the endpoint, not the deployment. /execute and /validate dispatch execution_mode as-is (synchronous); /start derives its fire-and-forget sibling (temporal_blocking -> temporal_fire_and_forget; direct/mistral have none). A deployment configures one synchronous execution_mode and every endpoint is coherent — no /execute 400, no forced-direct footgun.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Bktem3rLWRE2R7dmkEfbf
…tion

Review-driven follow-up to f5e3ef7.

Reject a fire-and-forget execution_mode as a CONFIGURED value (ApiConfig field
validator keyed on is_fire_and_forget). configuration.md declared
temporal_fire_and_forget unconfigurable but nothing enforced it — a baked value
would 400 every /execute and misroute /validate. Now fails fast at config load
(matching extra="forbid"). A per-request f&f override on /start is unaffected:
it stays gated by the override policy, not this field.

Fix the mistral_native workflow_id claim in the _async_start_mode docstring, the
call-site comment, and configuration.md: mistral_native runs per-call and answers
with a non-null workflow_id (the run id), not None — only direct returns None.

Add a pure-mapping test of _async_start_mode over every mode and a mistral_native
end-to-end /start case (both previously unverified despite the test docstring
claiming the coverage). Switch test_api_config's locked-Temporal helper to the
synchronous temporal_blocking it should always have used, and assert a configured
f&f mode raises.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Bktem3rLWRE2R7dmkEfbf
CI's `ruff format --check` gate (separate from `ruff check`) flagged the
multi-line signature in test_start_dispatch.py; the repo's line length keeps it
on one line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Bktem3rLWRE2R7dmkEfbf
…art, /validate (#28)

* refactor(api): split execution_mode into orchestration_mode (open token) + endpoint-set delivery (Phase 3)

Replace the conflated `execution_mode` enum with the two-axis model from core
Phase 1: `orchestration_mode` (an open string token naming only the backend) and
delivery (BLOCKING vs FIRE_AND_FORGET) set by the endpoint, never by the caller.

- ApiConfig: `orchestration_mode: str` + `allow_request_orchestration_mode_override`;
  drop the fire-and-forget field-validator (no f&f token can be configured anymore);
  `resolve_execution_mode` -> `resolve_orchestration_mode` (same default/override/403).
- error_types: `EXECUTION_MODE_OVERRIDE_FORBIDDEN` -> `ORCHESTRATION_MODE_OVERRIDE_FORBIDDEN`;
  delete `FIRE_AND_FORGET_NOT_SUPPORTED`; add `START_REQUIRES_ASYNC_ORCHESTRATION` (400).
- /execute: drop the f&f-reject; dispatch BLOCKING via `_OrchestratorPipeRun(delivery=...)`.
- /start: HONEST capability check before any library load — a blocking-only orchestrator
  (the in-process `direct` base) is refused with a 400 instead of silently blocking and
  acking; dispatch FIRE_AND_FORGET. Delete `_async_start_mode`.
- /validate: thread `requested_orchestration_mode`; validation stays blocking.
- wire models + api.toml + open-token descriptions; regenerate the OpenAPI artifact.
- Pin core editable (../_plugins) so the Phase 1 split is live (flip to a pushed git SHA
  for CI/PR). Update docs + CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154Kn5AhmT3LfrQfA1nWLMG

* build: pin pipelex to the pushed core split git rev so CI can resolve it

CI cannot resolve the editable ../_plugins path pin. Point pipelex at the pushed
core orchestration-mode/delivery split SHA (c48c5773) and relock. Flip back to
editable for local co-dev, or to the ==<ver> PyPI pin once core ships.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154Kn5AhmT3LfrQfA1nWLMG

* fix(api): advertise orchestration_mode in the /start OpenAPI request schema

/v1/start already honors a per-request orchestration_mode override at runtime
(parsed by PipelineApiExtras, threaded into runner.start), but its documented
body PipelexApiStartRequest omitted the field while /execute and /validate
expose it — so clients generated from the committed artifact could not drive
the override on /start. Add the field (reusing the shared description, symmetric
with PipelexApiExecuteRequest), regenerate the artifact, and add a regression
test asserting /start <-> /execute request-schema parity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZHTC4u7JcCHZSfKvZc6LN

---------

Co-authored-by: Louis Choquel <8851983+lchoquel@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move get_api_config() inside the lifespan try block so a malformed
api.toml / baked override that raises during warmup still triggers
Pipelex.teardown_if_needed() via the finally. Previously the warmup
ran above the try, so a raise left Pipelex.make()'s live, ready
singleton registered and unguarded — a startup retry or second app
in the same process would then fail as "already initialized".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZHTC4u7JcCHZSfKvZc6LN
- Changed the Python version requirement in pyproject.toml and uv.lock to allow versions up to <3.15.
- Added pipelex-tools-py as a dependency in both pyproject.toml and uv.lock with a minimum version of 0.1.0.
- Updated the resolution markers in uv.lock to reflect the new Python version constraints.
@lchoquel lchoquel marked this pull request as ready for review June 24, 2026 13:56
@greptile-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR makes the API choose orchestration backends per request and adds editor tooling endpoints. The main changes are:

  • New api.toml orchestration-mode config and override policy.
  • /execute, /start, and /validate dispatch through plugin registries.
  • Plugin-supplied HTTP error mappers replace direct Temporal handling.
  • New /lint and /format routes backed by pipelex-tools-py.
  • Updated OpenAPI, docs, dependencies, and focused unit tests.

Confidence Score: 4/5

This is close, but I would fix the packaging mismatch before merging.

  • Standard package installs can resolve an older Pipelex core than the code imports.
  • Validation dispatch has a conditional path where backend fault reports can be shaped as invalid content verdicts.
  • The main orchestration and tools paths have focused test coverage.

pyproject.toml and api/routes/pipelex/validate.py

Important Files Changed

Filename Overview
api/routes/pipelex/pipeline.py Adds registry-based orchestration dispatch for run, start, and validation flows.
api/routes/pipelex/validate.py Maps registry validation verdicts to valid or invalid 200 responses, with one conditional fault-classification issue.
api/routes/pipelex/tools.py Adds lint and format endpoints with bounded request bodies and response models.
api/main.py Registers plugin-provided transport error mappers during app setup.
pyproject.toml Adds the tools dependency and git-pins Pipelex for uv, but leaves package metadata pointing at an older released core.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[HTTP request] --> B{Endpoint}
  B -->|/execute| C[Resolve orchestration_mode]
  C --> D[OrchestratorRegistry]
  D --> E[BLOCKING run]
  E --> F[Rehydrate PipeOutput]
  B -->|/start| G[Resolve orchestration_mode]
  G --> H{supports fire-and-forget?}
  H -->|no| I[400 start requires async]
  H -->|yes| J[FIRE_AND_FORGET run]
  J --> K[202 start ack]
  B -->|/validate| L[Resolve orchestration_mode]
  L --> M[BundleValidatorRegistry]
  M --> N{Verdict type}
  N -->|PipelexValidationReport| O[200 valid report]
  N -->|ErrorReport| P[200 invalid report]
  B -->|/lint or /format| Q[pipelex-tools-py]
  Q --> R[200 diagnostics or formatted output]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
  A[HTTP request] --> B{Endpoint}
  B -->|/execute| C[Resolve orchestration_mode]
  C --> D[OrchestratorRegistry]
  D --> E[BLOCKING run]
  E --> F[Rehydrate PipeOutput]
  B -->|/start| G[Resolve orchestration_mode]
  G --> H{supports fire-and-forget?}
  H -->|no| I[400 start requires async]
  H -->|yes| J[FIRE_AND_FORGET run]
  J --> K[202 start ack]
  B -->|/validate| L[Resolve orchestration_mode]
  L --> M[BundleValidatorRegistry]
  M --> N{Verdict type}
  N -->|PipelexValidationReport| O[200 valid report]
  N -->|ErrorReport| P[200 invalid report]
  B -->|/lint or /format| Q[pipelex-tools-py]
  Q --> R[200 diagnostics or formatted output]
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
pyproject.toml:19
**Package installs wrong core** The new API code imports the orchestration registry and runtime-bridge modules from the git-pinned Pipelex core, but the published dependency still says `pipelex==0.35.0`. `tool.uv.sources` is only used by uv, so a wheel, Docker layer, or pip install can resolve the released 0.35.0 package and then crash on import when `api.main` or `api.routes.pipelex.pipeline` loads the new symbols. Please make the runtime dependency point at the same commit, or bump it to the released Pipelex version that contains these APIs.

### Issue 2 of 2
api/routes/pipelex/validate.py:190-191
**Separate fault verdicts** This branch treats every non-`PipelexValidationReport` returned by a validator as a 200 invalid-bundle verdict. That is correct for a `ValidateBundleError` report with `validation_errors`, but a registry-backed validator can also return an `ErrorReport` for a backend/config/transport failure. In that case this route would drop the report's server-fault classification and return `is_valid: false` with an empty validation list, so clients see a content error instead of a problem response. Please check that the returned report is actually a validation verdict before building the 200 invalid response, and let non-verdict faults reach the global error handler.

Reviews (1): Last reviewed commit: "fix(dependencies): update pipelex depend..." | Re-trigger Greptile

Comment thread pyproject.toml
Comment thread api/routes/pipelex/validate.py Outdated
@lchoquel lchoquel merged commit e526815 into dev Jun 24, 2026
15 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 24, 2026
@lchoquel lchoquel deleted the feature/pipelex-api-plxt branch June 24, 2026 14:53
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant