diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d32411..ee8c1c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## [v0.29.0] - 2026-09-27 + +### Highlights + +**A caught error reaches the caller with its reason and its place.** A run of an invalid bundle is refused before any pipe runs with the same located validation items `/validate` gives, a failed run names the pipe that failed and its root fault, and a run the caller's own method refuses keeps its explanation under STRICT disclosure. + +### Added + +- **Validation items carry more locators**: a TOML syntax error's item carries the 1-based `line` and `column` the parser stopped at, an `unresolved_concept` item carries `declared_concepts`, and the new `unknown_model` item carries `model_reference` (the reference as the bundle wrote it), `model_type` and `suggestions` (the model deck's close matches of that kind), with an `unsafe` rename fix when there is exactly one. The OpenAPI artifact publishes the new fields and the new `unknown_model` value of `PipeValidationErrorType`. +- **A run graph marks a list-valued stuff**: every io item of a `graph_spec` carries `multiplicity`, `true` when the stuff is a list, a fixed-count one included, and `null` otherwise, so a renderer can show a `Document[]` input or a `Record[]` output as a list. The schema also admits a positive integer, which the runtime does not emit. + +### Changed + +- **Pinned `pipelex` 0.67.0 (Breaking)**: up from `==0.66.1`, exactly, the release that carries the located error reporting the entries below describe. The `.pipelex/` config shipped here already sits at the current schema, so no migration is required. The next step of an invalid-bundle verdict, its `user_action.detail`, now reads "Edit the bundle as each validation error says: apply its suggested fix where it has one, after confirming an unsafe one". +- **The run routes refuse an invalid bundle with its validation verdict (Breaking)**: `POST /v1/execute` and `POST /v1/start` answer every refusal of the bundle while it loads, before any pipe runs, with the `422` `ValidateBundleError` problem document carrying the same located `validation_errors` `POST /v1/validate` gives, which STRICT disclosure keeps. A misspelled concept or a wiring mismatch used to answer a `500`, a TOML fault a `422` without items, and an unknown model the raw model-choice error. An entry pipe the bundle does not declare still answers its own `PipeNotFoundError`. +- **A failed run reports its root fault, located at the failing pipe (Breaking)**: a run failure's problem document carries the `error_type`, `title`, `type`, `error_domain` and status of the innermost Pipelex error rather than the run-level wrapper's, and its `detail` opens with `Pipe '' failed ( → … → ):`, so a consumer branching on `error_type == "PipelineExecutionError"` branches on the root fault's type. A run the caller's own method refuses, such as a `PipeCondition` whose outcome is `fail`, a `PipeParallel` branch whose multiplicity does not fit its field, a step missing a required input or an inline model the deck does not define, answers a `422` with a `detail` STRICT disclosure keeps, where it answered `500` with `An internal error occurred.`. The completion webhook's `error` carries the same report. +- **Every load-time refusal is an item of the validation verdict (Breaking)**: `POST /v1/validate`, `POST /v1/resolve`, `POST /v1/codegen` and `POST /v1/build/*` answer any refusal of the submitted bundle, an unknown model and a pipe factory's refusal included, as a `200` `is_valid: false` verdict with a located item, where some of them escaped as a no-verdict problem document. +- **A failing dry run is one located `dry_run` item per failing pipe (Breaking)**: an invalid verdict's `validation_errors` carries one `dry_run` item per pipe whose dry run failed, with the `pipe_code`, `domain_code` and `source` of the innermost pipe that failed, where it carried a single message-only item for the whole sweep. Its message is the failure's own when that is caller-facing and its title otherwise, so a configuration fault met during a dry run no longer reaches the caller through the verdict. +- **`POST /v1/codegen` stamps `engine_version` `0.67.0`**: the stamp is the pinned `pipelex` version, so a `codegen.lock` committed against `0.66.1` no longer matches until it is regenerated. `POST /v1/build/runner` carries the same stamp. + +### Fixed + +- **A verdict names no path on the server**: an item located inside a package a method depends on by address names the bundle by the package's address and its path inside it, and an item located in one of the server's own library directories carries no `source` or `field_path` naming its file, so STRICT disclosure no longer hands a caller a path on the host. + ## [v0.28.1] - 2026-09-27 ### Changed diff --git a/api/routes/pipelex/validate.py b/api/routes/pipelex/validate.py index 8a5d8d7..bcb8324 100644 --- a/api/routes/pipelex/validate.py +++ b/api/routes/pipelex/validate.py @@ -334,8 +334,8 @@ async def validate_mthds(request: Request, request_data: ValidateRequest) -> JSO separate questions — a caller building a fill-in form wants the inputs, a caller rendering a result or registering a tool signature with a return type wants the output. - **Invalid verdict (200, `is_valid: false`):** the `InvalidReport` arm — `validation_errors[]` - (the structured per-error diagnostics, built by pipelex's one shared builder, incl. the - `dry_run` residual item) + `message`, with the structural artifacts absent. The runner + (the structured per-error diagnostics, built by pipelex's one shared builder, incl. one + located `dry_run` item per pipe whose dry run failed) + `message`, with the structural artifacts absent. The runner returns this as a value (`ErrorReport` with `validation_errors`) regardless of backend — the in-process arm from the bundle's `ValidateBundleError`, the dispatched arm recovered from the worker — so the route maps it to a 200 by matching validation diagnostics, never by catching an @@ -449,8 +449,9 @@ def _invalid_report_response(error_report: ErrorReport, *, requested_formats: se The `validation_errors[]` come straight from pipelex's one shared builder via `ValidateBundleError.to_error_report()`, so the hosted invalid arm carries the same typed - items the agent CLI emits (including the `dry_run` residual item — the structured-info - invariant guarantees this list is non-empty on every invalid verdict that reaches the wire, + items the agent CLI emits (including one located `dry_run` item per pipe whose dry run failed — + the structured-info invariant, which the parse-level `blueprint_validation` residual makes total, + guarantees this list is non-empty on every invalid verdict that reaches the wire, since the empty-`mthds_contents` edge case is a request-shape 422 via `min_length=1`). `message` is the caller-facing summary the error report already carries. """ diff --git a/docs/error-responses.md b/docs/error-responses.md index f945e5e..104be1f 100644 --- a/docs/error-responses.md +++ b/docs/error-responses.md @@ -46,15 +46,19 @@ When a bundle fails validation, the `ValidateBundleError` carries a `validation_ | `category` | The failure family — one of `blueprint_validation`, `pipe_factory`, `pipe_validation`, `dry_run`. | | `message` | Human-readable description of this specific error. | | `error_type` | Finer error subtype within the category, when the source error provides one. | -| `source` | The owning file of the error — present on `pipe_validation` and `blueprint_validation` items that the runtime could attribute to a file. On the in-memory submit path it is the matching `mthds_sources[i]` (see [Sourcing submitted files](pipe-validate.md)); `null` when the caller sent no sources. Absent for `pipe_factory` and `dry_run` errors (the latter is graph-level), and absent on the parse-level `blueprint_validation` residual (a raw TOML-syntax error, an empty blueprint, or an elaborator failure), which carries the failure message but no file attribution — see the note below. | -| `pipe_code`, `concept_code`, `domain_code` | The pipe / concept / domain the error is about, when applicable. | +| `source` | The owning file of the error, when the runtime could attribute it to one: on `blueprint_validation` and `pipe_validation` items, and on a `dry_run` item, where it is the file of the pipe whose dry run failed. On the in-memory submit path it is the matching `mthds_sources[i]` (see [Sourcing submitted files](pipe-validate.md)), so it is absent when the caller sent no sources. Absent on `pipe_factory` items and on the parse-level residual described below. Beside a server's own library directories, an item never names one of their files. | +| `pipe_code`, `concept_code`, `domain_code` | The pipe / concept / domain the error is about, when applicable. On a `dry_run` item they name the innermost pipe that failed, not the controller the failure passed through. | | `field_path`, `field_name` | The offending field within the bundle, when the error localizes to one. | | `variable_names`, `missing_concept_code`, `missing_pipe_code`, `declared_concepts` | Extra context for specific failure shapes (undefined variables, an unresolved concept or pipe reference, the set of concepts that were declared). | +| `line`, `column` | The 1-based position where the TOML parser stopped, on a TOML syntax error. | +| `model_reference`, `model_type`, `suggestions` | On an `unknown_model` item — a pipe naming a model the deployment's model deck does not define — the reference as the bundle wrote it, the kind of model the pipe needs (`llm`, `extract`, …), and the deck's close matches of that kind. With exactly one suggestion the item also carries an `unsafe` `suggested_fix` renaming the model. | | `suggested_fix` | A structured, deterministic fix for this error — present only when the fix planner derived one. See [Suggested fixes](#suggested-fixes). | Items carry only the fields that apply to their category — absent fields are omitted, not null. `validation_errors` is **retained under STRICT disclosure** (it describes the caller's own submitted bundle, not server internals). It is present only on `ValidateBundleError`; other error types omit it. -Every invalid verdict carries a **non-empty** `validation_errors` array — the structured-info invariant is total. A parse-level failure the runtime cannot attribute to a known pipe/concept/field — a raw TOML-syntax error, an empty blueprint, or an elaborator failure — still becomes one `blueprint_validation` residual item carrying the failure message (no `source`, no `error_type` at this layer), so the array is never empty on an invalid verdict. The richer, locator-bearing items appear only when the runtime could attribute the failure; the human-readable summary (the `detail` on a run-route 422, the `message` on a diagnostic-route 200 invalid verdict) stays available alongside, but a consumer can always read at least one structured item. +Every invalid verdict carries a **non-empty** `validation_errors` array — the structured-info invariant is total. A parse-level failure the runtime cannot attribute to a known pipe/concept/field — an empty blueprint, an elaborator failure — still becomes one `blueprint_validation` residual item carrying the failure message (no `source`, no `error_type` at this layer), so the array is never empty on an invalid verdict. A TOML syntax error is an item of the same category without an `error_type`, which carries the `line` and `column` the parser stopped at and the `source` when the caller sent one. A failing dry run is one `dry_run` item per failing pipe, located at the innermost pipe that failed. The richer, locator-bearing items appear only when the runtime could attribute the failure; the human-readable summary (the `detail` on a run-route 422, the `message` on a diagnostic-route 200 invalid verdict) stays available alongside, but a consumer can always read at least one structured item. + +On the run routes, **every refusal of the bundle while it loads is this verdict**, and the load happens before any pipe runs: a misspelled concept, a wiring mismatch, an unknown model, a TOML fault or a refusal raised while a pipe is built all answer the same **422** carrying the same items validating the bundle gives. None of them reaches a run as a `500`. An entry pipe the bundle does not declare keeps its own `PipeNotFoundError`. ## Suggested fixes @@ -88,7 +92,7 @@ A validation error item may carry a `suggested_fix`: a deterministic repair the - `fix_code` — the kebab-case rule id that produced the fix (`match-sequence-output`, `sync-controller-inputs`, `strip-native-concept-redecl`, `strip-namespace`, …). Stable; use it to allow-list or suppress rules. - `description` — human-readable summary of what the fix does. -- `safety` — `safe` or `unsafe`. Only apply an `unsafe` fix behind an explicit opt-in: it resolves an ambiguity the runtime could not resolve on the caller's behalf. +- `safety` — `safe` or `unsafe`. Only apply an `unsafe` fix behind an explicit opt-in: it is a likely correction, such as the one close match for an unknown model, that a person or an agent must confirm, and `pipelex fix bundle` never applies one on its own. - `source` — the file the ops target, when known. **An applier must only apply ops to the file they target** — in a multi-file library the ops are meaningless against any other file. - `ops` — the semantic TOML patch operations, in order. @@ -110,6 +114,34 @@ For `ensure_table` and `delete_table`, `table_path` addresses the table itself r **The ops are the machine contract; any rendered diff is presentation.** Apply them with a style-preserving TOML editor rather than reconstructing the file from a diff: that is what keeps the caller's formatting, comments, and key order intact. +## Run failures: the root fault, located at the failing pipe + +When a run fails, the problem document describes the **root fault**, the innermost Pipelex error on the cause chain, never the run-level wrapper around it: `error_type`, `title` and `type` are the root fault's, and so are `error_domain`, the HTTP status and whether STRICT disclosure keeps the `detail`. The `detail` names the pipe that failed and its path from the entry pipe, `Pipe '' failed ( → … → ): `, and `user_action` names the next step for that pipe. A consumer that branched on `error_type == "PipelineExecutionError"` branches on the root fault's type instead. + +So a run the caller's own method refuses reads its reason even under STRICT: a `PipeCondition` whose outcome is `fail`, a `PipeParallel` branch whose multiplicity does not match its output field, a step started without a required input, or a model named inline that the deck does not define all answer a **422** in the `input` domain, with a `detail` that says what to change. A model the deck names but does not serve stays a redacted `config` failure, since the deployment, not the caller, has to fix it. + +```http +HTTP/1.1 422 Unprocessable Entity +Content-Type: application/problem+json + +{ + "type": "https://docs.pipelex.com/latest/errors/stuff-factory-error/", + "title": "Stuff factory", + "status": 422, + "detail": "Pipe 'analyze_topic' failed (review_topic → analyze_topic): PipeParallel 'analyze_topic' cannot combine its branch results into its output 'TopicReview'. Branch 'draft_idea' gives result 'ideas' as a single 'Idea', but field 'ideas' of 'TopicReview' holds a list. Declare the field as a single concept in the structure of 'TopicReview', with type 'concept' and concept_ref 'Idea', or make branch 'draft_idea' output 'Idea[]'.", + "instance": "/v1/execute", + "error_type": "StuffFactoryError", + "error_domain": "input", + "user_action": { + "kind": "change_input", + "detail": "Branch 'draft_idea' gives result 'ideas' as a single 'Idea', but field 'ideas' of 'TopicReview' holds a list. Declare the field as a single concept in the structure of 'TopicReview', with type 'concept' and concept_ref 'Idea', or make branch 'draft_idea' output 'Idea[]'." + }, + "request_id": "9f2c1ab3-…" +} +``` + +The failure of a `/start` run reaches its completion webhook as the same report, under the payload's `error` key (see [Async callbacks](#async-callbacks-webhook-payload)). + ## Status codes The HTTP status follows pipelex's `error_domain_to_http_status`: @@ -146,7 +178,7 @@ The `ERROR_DISCLOSURE` env var controls how much of the originating error makes - `verbose` (default) — renders the full `ErrorReport`. Use in dev, staging, and any deployment where the caller is trusted. - `strict` — redacts `detail` and provider fields for errors that do not author caller-facing messages. Specifically: - - `detail` is preserved only for error classes flagged as authoring caller-facing messages (today: `MthdsParserError`, `ValidateBundleError`). Everything else has `detail` replaced with a generic title-derived string. + - `detail` is preserved only when the error authored a caller-facing message: the bundle's validation verdict (`ValidateBundleError`), a parse error (`MthdsParserError`), and a failure the runtime classifies as the caller's own, such as a pipe refused while it is built or a run that the submitted method itself refuses (see [Run failures](#run-failures-the-root-fault-located-at-the-failing-pipe)). Everything else has `detail` replaced with a generic title-derived string. - `model`, `provider`, `provider_metadata` are always stripped — they have no business on a caller-facing surface. - The redaction is keyed on the **provenance of the message** (`_authors_caller_facing_message` ClassVar), not on `error_domain`. A `RuntimeError` raised `from` an `INPUT`-domain cause does not leak the wrapper's internal message. @@ -194,13 +226,13 @@ X-Request-ID: 9f2c1ab3-… "type": "https://docs.pipelex.com/latest/errors/validate-bundle-error/", "title": "Validate bundle", "status": 422, - "detail": "Validation error(s):\n\nValue errors: 'main_pipe': Value error, Invalid main pipe syntax 'Not A Valid Pipe Code!'. Must be in snake_case.", + "detail": "Value error, Invalid main pipe syntax 'Not A Valid Pipe Code!'. Must be in snake_case.", "instance": "/v1/execute", "error_type": "ValidateBundleError", "error_domain": "input", "user_action": { "kind": "change_input", - "detail": "Check the validation_errors array for specific issues" + "detail": "Edit the bundle as each validation error says: apply its suggested fix where it has one, after confirming an unsafe one" }, "request_id": "9f2c1ab3-…", "validation_errors": [ diff --git a/docs/openapi/pipelex-api.openapi.yaml b/docs/openapi/pipelex-api.openapi.yaml index 5910e86..807dbc3 100644 --- a/docs/openapi/pipelex-api.openapi.yaml +++ b/docs/openapi/pipelex-api.openapi.yaml @@ -11,7 +11,7 @@ info: license: name: Elastic License 2.0 identifier: Elastic-2.0 - version: 0.28.1 + version: 0.29.0 paths: /health: get: @@ -915,19 +915,19 @@ paths: \ to a request that omits `views`. They are separate tokens because they answer\n separate questions — a caller building\ \ a fill-in form wants the inputs, a caller rendering a\n result or registering a tool signature with a return type\ \ wants the output.\n- **Invalid verdict (200, `is_valid: false`):** the `InvalidReport` arm — `validation_errors[]`\n\ - \ (the structured per-error diagnostics, built by pipelex's one shared builder, incl. the\n `dry_run` residual item)\ - \ + `message`, with the structural artifacts absent. The runner\n returns this as a value (`ErrorReport` with `validation_errors`)\ - \ regardless of backend — the\n in-process arm from the bundle's `ValidateBundleError`, the dispatched arm recovered\ - \ from the\n worker — so the route maps it to a 200 by matching validation diagnostics, never by catching an\n exception.\ - \ Returned `ErrorReport`s without validation diagnostics are backend/config/runtime\n faults and keep the global\ - \ RFC 7807 problem response path.\n- **No verdict (non-2xx):** a malformed request body, an `mthds_sources` length\ - \ mismatch, or\n both/neither of `mthds_contents` / `method_ref` is a request-shape **422**; a forbidden\n `orchestration_mode`\ - \ override is a **403**; a host-wiring programmer error or a genuine\n orchestrator fault is a **5xx**; auth is **401/403**.\ - \ A `method_ref` **resolution failure**\n is also a no-verdict condition — never an `is_valid: false`: a malformed\ - \ reference or a\n failed fetch is a **422**, no matching package in the repository a **404**, and the\n custom-Python\ - \ policy (the sandbox gate, the structures refusal) a **403**, each with the\n originating error class as `error_type`.\ - \ All are RFC 7807 `application/problem+json`\n rendered by the global handler in `api.exception_handlers` — routes\ - \ never shape them." + \ (the structured per-error diagnostics, built by pipelex's one shared builder, incl. one\n located `dry_run` item\ + \ per pipe whose dry run failed) + `message`, with the structural artifacts absent. The runner\n returns this as\ + \ a value (`ErrorReport` with `validation_errors`) regardless of backend — the\n in-process arm from the bundle's\ + \ `ValidateBundleError`, the dispatched arm recovered from the\n worker — so the route maps it to a 200 by matching\ + \ validation diagnostics, never by catching an\n exception. Returned `ErrorReport`s without validation diagnostics\ + \ are backend/config/runtime\n faults and keep the global RFC 7807 problem response path.\n- **No verdict (non-2xx):**\ + \ a malformed request body, an `mthds_sources` length mismatch, or\n both/neither of `mthds_contents` / `method_ref`\ + \ is a request-shape **422**; a forbidden\n `orchestration_mode` override is a **403**; a host-wiring programmer\ + \ error or a genuine\n orchestrator fault is a **5xx**; auth is **401/403**. A `method_ref` **resolution failure**\n\ + \ is also a no-verdict condition — never an `is_valid: false`: a malformed reference or a\n failed fetch is a **422**,\ + \ no matching package in the repository a **404**, and the\n custom-Python policy (the sandbox gate, the structures\ + \ refusal) a **403**, each with the\n originating error class as `error_type`. All are RFC 7807 `application/problem+json`\n\ + \ rendered by the global handler in `api.exception_handlers` — routes never shape them." operationId: validate_mthds_v1_validate_post requestBody: content: @@ -3145,7 +3145,9 @@ components: - safe - unsafe title: FixSafety - description: Whether a fix is safe to auto-apply (SAFE) or requires explicit opt-in (UNSAFE). + description: 'Whether a fix is safe to auto-apply (SAFE), or a likely correction that needs a person''s or an agent''s + + confirmation (UNSAFE), which ``pipelex fix bundle`` never applies.' FormatRequest: properties: content: @@ -3377,6 +3379,13 @@ components: type: array - type: 'null' title: Data + multiplicity: + anyOf: + - type: boolean + - type: integer + exclusiveMinimum: 0.0 + - type: 'null' + title: Multiplicity extra: additionalProperties: true type: object @@ -3396,7 +3405,16 @@ components: The optional `data` field can hold the full serialized content when - full data capture is enabled (via --graph-full-data CLI option).' + full data capture is enabled (via --graph-full-data CLI option). + + + `concept` is always the bare concept code; whether the stuff is a list is `multiplicity`, + + in the encoding the pipe registry uses for a stuff spec''s multiplicity: `True` for a + + variable-length list, a positive integer for a fixed count, `None` (or `False`) for a single + + value, a count of one reading as single. A reader takes absence as single.' ImageField: properties: title: @@ -5829,6 +5847,7 @@ components: - native_concept_redeclaration - unresolved_concept - unresolved_pipe_dependency + - unknown_model - unknown_validation_error title: PipeValidationErrorType description: 'Types of pipe validation errors. @@ -7629,13 +7648,9 @@ components: missing concept), pipe/concept validation (e.g. a missing input variable - or a type mismatch), and the ``dry_run`` residual — a dry-run failure with no - - structured locator (graph-level), carried as a single message-only item so an + or a type mismatch), and ``dry_run`` — one item per pipe whose dry run failed, - invalid verdict always surfaces a non-empty ``validation_errors[]`` (the - - structured-info invariant) instead of a bare ``detail``.' + located at the innermost failing pipe.' ValidationErrorItem: properties: category: @@ -7705,6 +7720,33 @@ components: type: array - type: 'null' title: Declared Concepts + line: + anyOf: + - type: integer + - type: 'null' + title: Line + column: + anyOf: + - type: integer + - type: 'null' + title: Column + model_reference: + anyOf: + - type: string + - type: 'null' + title: Model Reference + model_type: + anyOf: + - type: string + - type: 'null' + title: Model Type + suggestions: + anyOf: + - items: + type: string + type: array + - type: 'null' + title: Suggestions suggested_fix: anyOf: - $ref: '#/components/schemas/SuggestedFix' @@ -7764,27 +7806,27 @@ components: enum: - DryRunError title: ValidationResidualErrorType - description: 'The ``error_type`` of a validation residual — a failure with no stage-level error data. + description: 'The ``error_type`` of a failure no validation stage classifies into a code. - A residual is what the wire projection emits when a bundle failed but no categorized + There is exactly one that names itself: a dry-run failure is raised as an error object, a - validation stage produced structured data to report. There is exactly one residual that names + ``DryRunError`` carrying one located failure per pipe whose dry run failed, so each of its items is - itself: a dry-run failure surfaces a single message from a raised ``DryRunError`` / ``PipeRunError``, + tagged with that exception''s own class name. - so the item is tagged with that exception''s own class name. + The items no code identifies — the parse-level residual, for a bundle that could not be turned - The other residual — the parse-level one, for a bundle that could not be turned into a + into a blueprint at all (a TOML-syntax error, an empty blueprint, an elaborator failure), and a - blueprint at all (a TOML-syntax error, an empty blueprint, an elaborator failure) — carries no + pydantic error no categorizer knows — carry no ``error_type`` and therefore no member here. That - ``error_type`` and therefore no member here. That is not an omission: it fires for several + is not an omission: they stand for many distinct underlying errors, and inventing one code for all - distinct underlying errors, and inventing one code for all of them would tell a consumer it + of them would tell a consumer it knows which fault occurred when it does not. Their message is the - knows which fault occurred when it does not. Its message is the authoritative diagnostic.' + authoritative diagnostic.' WorkingMemory: properties: root: diff --git a/docs/pipe-run.md b/docs/pipe-run.md index 9b2b305..18940b6 100644 --- a/docs/pipe-run.md +++ b/docs/pipe-run.md @@ -79,7 +79,7 @@ Execute a Pipelex pipeline with flexible inputs and wait for completion. - `pipe_output.pipe_io_artifacts` (object | null): The I/O artifacts describing the graph's data, carried beside `graph_spec` so that wherever the graph travels its description travels with it. It groups the same artifacts `/validate` produces — `pipe_io_contracts`, `input_form` and `output_form` — each keyed by namespaced `pipe_ref`, over the **run library's own pipes**: a dependency package's pipes are keyed by alias in the library but by bare `pipe_ref` in the artifacts, where they would collide with the host's, so they are left out and a graph node contributed by a dependency has no entry here. Holding it with the graph is what lets a viewer show a data node's value rather than only its shape. It is built only when the run generated a graph **and** `interpreter.pipeline_execution.graph.graphs_inclusion.graphspec_json` is on (both default to `true`), and only when `graph_spec` itself came out non-null — so a run with `graphspec_json = false` returns a graph with no artifacts beside it and no error, which is the one case where the two fields disagree. `pipe_output.pipe_io_artifacts_error` (string | null) is non-null when the build ran and failed. See [Pipe Validate](pipe-validate.md) for the artifacts' own field reference. - `method_provenance` (object, only on `method_ref` runs): `{address, tag, commit_sha}` — the package's resolved full address, the requested tag (`null` for a bare address), and the commit SHA that was actually fetched. Absent for runs from inline source or a bundle. See [Running a method by address](#running-a-method-by-address-method_ref). -**Errors** are returned as [RFC 7807 `application/problem+json`](error-responses.md) bodies with HTTP 4xx/5xx status codes. The successful response body has no `status`/`error` field — the HTTP status code is the source of truth. +**Errors** are returned as [RFC 7807 `application/problem+json`](error-responses.md) bodies with HTTP 4xx/5xx status codes. The successful response body has no `status`/`error` field — the HTTP status code is the source of truth. An invalid bundle is refused before any pipe runs with a `422` carrying the same located `validation_errors` validating it gives ([Structured validation errors](error-responses.md#structured-validation-errors)), and a run that fails reports its root fault at the pipe that failed ([Run failures](error-responses.md#run-failures-the-root-fault-located-at-the-failing-pipe)). --- diff --git a/docs/pipe-validate.md b/docs/pipe-validate.md index cc21e59..a979567 100644 --- a/docs/pipe-validate.md +++ b/docs/pipe-validate.md @@ -43,8 +43,8 @@ The 200 body is one of two arms, discriminated on the mandatory `is_valid` field }, "pipe_io_contracts": { "my_domain.my_pipe": { - "inputs": { "text": { "concept_ref": "native.Text", "json_schema": { "...": "..." } } }, - "output": { "concept_ref": "MyResult", "multiplicity": "single" } + "inputs": { "text": { "concept_ref": "native.Text", "presence": "plain", "multiplicity": "single", "item_count": null, "json_schema": { "...": "..." } } }, + "output": { "concept_ref": "my_domain.MyResult", "multiplicity": "single", "item_count": null, "optional": false, "json_schema": { "...": "..." } } } }, "graph_spec": { "...": "..." }, @@ -63,7 +63,7 @@ The 200 body is one of two arms, discriminated on the mandatory `is_valid` field - `is_valid` (`true`): the discriminant of the valid arm — always `true` on this report - `bundle_blueprint` (object): the batch's primary blueprint — the first file declaring `main_pipe`, else the first file -- `pipe_io_contracts` (object): per-pipe input/output contracts, keyed by the namespaced `pipe_ref` (`domain.code`); each entry carries the JSON Schema of every declared input and the output's concept + multiplicity (`single` | `variable`) +- `pipe_io_contracts` (object): per-pipe input/output contracts, keyed by the namespaced `pipe_ref` (`domain.code`). Each input carries its `concept_ref`, `presence`, `multiplicity` (`single` | `variable` | `fixed`), `item_count` and the JSON Schema of its content; the output carries its fully qualified `concept_ref`, `multiplicity`, `item_count`, `optional` and the JSON Schema of its payload. `item_count` is the exact count on `fixed`, always greater than one since `Concept[1]` reads as `single`, and `null` otherwise, so an output declared `Text[3]` reports `"multiplicity": "fixed", "item_count": 3` - `graph_spec` (object | null): best-effort execution graph of the pipe a selector-less run of this request would execute — the pipe [`default_pipe_ref`](#the-effective-entry-pipe) names — dry-run against the validated library; `null` when no entry pipe is determined or the graph dry-run degrades - `validated_pipes` (list): per-pipe sweep outcomes — `{pipe_ref, status}` entries with status `SUCCESS` | `FAILURE` | `SKIPPED` - `pending_signatures` (list[str]): namespaced refs of pipes still declared as signatures (contract-only pipes — `inputs`/`output` with no `type` and no implementation) in the assembled library — what remains to implement @@ -97,17 +97,17 @@ The 200 body is one of two arms, discriminated on the mandatory `is_valid` field ], "pending_signatures": [], "is_runnable": false, - "message": "Validation error(s): ..." + "message": "Value error, Invalid main pipe syntax 'Not A Valid Pipe Code!'. Must be in snake_case." } ``` **Response Fields (invalid arm):** - `is_valid` (`false`): the discriminant of the invalid arm -- `validation_errors` (list): the structured per-error diagnostics a client maps to per-line problems — built by pipelex's one shared builder, so they are byte-for-byte the same items the agent CLI emits. Each item carries a `category` (the closed set `blueprint_validation` | `pipe_factory` | `pipe_validation` | `dry_run`), a `message`, and the locators the runtime can attribute (`error_type`, `pipe_code` / `concept_code` / `domain_code`, `field_path` / `field_name`, and `source`). Absent locators are omitted, not null. An item may also carry a **`suggested_fix`** — a structured, deterministic repair (`fix_code`, `description`, `safety: safe|unsafe`, and the `ops[]` of semantic TOML patch operations to apply). It is present only when the fix planner derived one from the typed error data; a client that ignores it behaves exactly as before. See [Error Responses → Structured validation errors](error-responses.md#structured-validation-errors) and [→ Suggested fixes](error-responses.md#suggested-fixes) for every field. The array is **never empty on an invalid verdict** (the structured-info invariant is total): a dry-run residual failure becomes one `dry_run` item carrying the message (graph-level, so usually no `source`), and a parse-level failure with no attributable locator (a raw TOML-syntax error, an empty blueprint, an elaborator failure) becomes one `blueprint_validation` residual item carrying the message (no `source`) +- `validation_errors` (list): the structured per-error diagnostics a client maps to per-line problems — built by pipelex's one shared builder, so they are byte-for-byte the same items the agent CLI emits. Each item carries a `category` (the closed set `blueprint_validation` | `pipe_factory` | `pipe_validation` | `dry_run`), a `message`, and the locators the runtime can attribute (`error_type`, `pipe_code` / `concept_code` / `domain_code`, `field_path` / `field_name`, and `source`). Absent locators are omitted, not null. An item may also carry a **`suggested_fix`** — a structured, deterministic repair (`fix_code`, `description`, `safety: safe|unsafe`, and the `ops[]` of semantic TOML patch operations to apply). It is present only when the fix planner derived one from the typed error data; a client that ignores it behaves exactly as before. See [Error Responses → Structured validation errors](error-responses.md#structured-validation-errors) and [→ Suggested fixes](error-responses.md#suggested-fixes) for every field. The array is **never empty on an invalid verdict** (the structured-info invariant is total): a failing dry run becomes one `dry_run` item per failing pipe, located at the innermost pipe that failed (its `pipe_code`, `domain_code` and, when known, `source`), a TOML syntax error carries the `line` and `column` the parser stopped at, and a parse-level failure with no attributable locator (an empty blueprint, an elaborator failure) becomes one `blueprint_validation` residual item carrying the message (no `source`). Every refusal of the bundle while it loads is an item of the verdict, an unknown model included (an `unknown_model` item on the pipe's `model` field), never a no-verdict `5xx` - `pending_signatures` (list[str]): best-effort outstanding signatures (empty on the invalid arm, since no library was assembled) - `is_runnable` (`false`): an invalid bundle is never runnable -- `message` (string): the human-readable verdict summary (the caller-facing pipelex error message) +- `message` (string): the human-readable verdict summary: the lone item's message when there is one item, `N validation errors (first: )` when there are several, and the parser's own message for a parse-level failure ## What This Endpoint Does diff --git a/pyproject.toml b/pyproject.toml index 0cd09e0..4e68b6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pipelex-api" -version = "0.28.1" +version = "0.29.0" description = "Pipelex API" authors = [{ name = "Evotis S.A.S.", email = "oss@pipelex.com" }] maintainers = [{ name = "Pipelex staff", email = "oss@pipelex.com" }] @@ -18,7 +18,7 @@ classifiers = [ dependencies = [ # The extras deliberately leave out `cli`: the runner selects the `json` log sink and a Rich-free # pretty-print mode, so nothing it does on a request renders a terminal. - "pipelex[mistralai,anthropic,google,google-genai,bedrock,fal]==0.66.1", + "pipelex[mistralai,anthropic,google,google-genai,bedrock,fal]==0.67.0", "fastapi>=0.118.0", "pyjwt>=2.10.1", "uvicorn>=0.37.0", diff --git a/tests/unit/_constants.py b/tests/unit/_constants.py index 3503002..f92a466 100644 --- a/tests/unit/_constants.py +++ b/tests/unit/_constants.py @@ -232,6 +232,91 @@ class RoutePath(StrEnum): """, ] +# VALID_MTHDS with its output concept misspelled: the load refuses it as an `unresolved_concept` +# item located on `pipe.echo.output`. A run of it used to escape the load as a raw 500. +MISSPELLED_CONCEPT_MTHDS = """\ +domain = "smoke" +main_pipe = "echo" + +[pipe.echo] +type = "PipeLLM" +description = "Echo" +inputs = { text = "Text" } +output = "Summry" +prompt = "@text" +""" + +# VALID_MTHDS naming a model the model deck does not define: the load refuses it as an +# `unknown_model` item located on `pipe.echo.model`. A run of it used to raise the raw +# `PipeOperatorModelChoiceError`, with no validation item. +UNKNOWN_MODEL_MTHDS = """\ +domain = "smoke" +main_pipe = "echo" + +[pipe.echo] +type = "PipeLLM" +description = "Echo" +inputs = { text = "Text" } +output = "Text" +model = "no-such-model-in-any-deck" +prompt = "@text" +""" + +# A bundle that loads but whose run fails one step down, in a live run and a dry run alike since no +# pipe calls a model: `review_topic` runs the parallel `analyze_topic`, whose branch `draft_idea` gives +# one `Idea` for the field `ideas`, which `TopicReview` declares as a list. The combine of the branch +# results refuses it at `analyze_topic`, and the refusal is the caller's own method to fix. +MISMATCHED_PARALLEL_MTHDS = """\ +domain = "brainstorm" +description = "A parallel feeding a single branch into a list field" +main_pipe = "review_topic" + +[concept.Idea] +description = "One idea about a topic" +refines = "Text" + +[concept.Overview] +description = "A one-line overview" +refines = "Text" + +[concept.TopicReview] +description = "Ideas and an overview" + +[concept.TopicReview.structure] +ideas = { type = "list", item_type = "concept", item_concept_ref = "Idea", description = "The ideas", required = true } +overview = { type = "concept", concept_ref = "Overview", description = "The overview", required = true } + +[pipe.review_topic] +type = "PipeSequence" +description = "Review a topic" +inputs = { topic = "Text" } +output = "TopicReview" +steps = [ { pipe = "analyze_topic", result = "review" } ] + +[pipe.analyze_topic] +type = "PipeParallel" +description = "Draft the idea and the overview at the same time" +inputs = { topic = "Text" } +output = "TopicReview" +branches = [ + { pipe = "draft_idea", result = "ideas" }, + { pipe = "write_overview", result = "overview" }, +] + +[pipe.draft_idea] +type = "PipeCompose" +description = "Draft one idea about the topic" +inputs = { topic = "Text" } +output = "Idea" +template = "An idea worth exploring about $topic" + +[pipe.write_overview] +type = "PipeCompose" +description = "Write a one-line overview" +output = "Overview" +template = "One idea." +""" + # A bundle whose PipeSequence references an unimplemented PipeSignature step. It loads and wires # cleanly, so the only thing that rejects it in strict mode is the signature pre-pass — isolating # the `allow_signatures` behavior from any other validation failure. diff --git a/tests/unit/test_run_refusals.py b/tests/unit/test_run_refusals.py new file mode 100644 index 0000000..bea3287 --- /dev/null +++ b/tests/unit/test_run_refusals.py @@ -0,0 +1,149 @@ +"""What the run routes answer when the caller's own method refuses the run. + +Two moments, both the caller's to fix, so both are a 422 that STRICT disclosure keeps readable: + +- **An invalid bundle is refused before any pipe runs, with the validation verdict.** A run loads + its bundle before it dispatches anything, and the runtime turns every refusal of that load into a + `ValidateBundleError` carrying the same located `validation_errors` validating the bundle gives, + whatever check refused it: a misspelled concept used to escape as a 500, and an unknown model as a + raw model-choice error with no item. The orchestrator is a recording stub here, so the tests also + pin that nothing was dispatched. +- **A run that fails reports its root fault, located at the failing pipe.** The problem document's + `error_type`, `title` and `type` are the innermost Pipelex error's, never the run-level wrapper's, + and its `detail` names the failing pipe and its path from the entry pipe. This one runs the real + in-process orchestrator on a bundle whose pipes call no model, so it fails the same way whether + the run is live or dry. +""" + +from typing import Any + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from pipelex.base_exceptions import DisclosureMode, ValidationErrorCategory +from pipelex.pipe_run.delivery_assignment import DeliveryAssignment +from pipelex.pipe_run.pipe_job import PipeJob +from pipelex.plugins.orchestrator_registry import OrchestratorRegistry +from pipelex.runtime_bridge.payloads import PipelexPipeDispatchAck, PipelexPipeRunOutput +from pytest_mock import MockerFixture + +from api.api_config import ApiConfig +from api.exception_handlers import register_exception_handlers +from api.routes import router as api_router +from tests.unit._constants import MISMATCHED_PARALLEL_MTHDS, MISSPELLED_CONCEPT_MTHDS, UNKNOWN_MODEL_MTHDS + +_PIPELINE_NS = "api.routes.pipelex.pipeline" + + +class _RecordingOrchestrator: + """An async-capable orchestrator that records every dispatch; an invalid bundle must reach neither arm.""" + + supports_fire_and_forget = True + + def __init__(self) -> None: + self.dispatches: list[tuple[str, DeliveryAssignment | None]] = [] + + async def execute(self, *, pipe_job: PipeJob, delivery_assignment: DeliveryAssignment | None) -> PipelexPipeRunOutput: + self.dispatches.append((pipe_job.pipe.code, delivery_assignment)) + msg = "An invalid bundle must be refused before the run is dispatched." + raise AssertionError(msg) + + async def start(self, *, pipe_job: PipeJob, delivery_assignment: DeliveryAssignment | None) -> PipelexPipeDispatchAck: + self.dispatches.append((pipe_job.pipe.code, delivery_assignment)) + msg = "An invalid bundle must be refused before the run is dispatched." + raise AssertionError(msg) + + +def _build_client(mocker: MockerFixture, *, disclosure_mode: DisclosureMode, orchestrator: _RecordingOrchestrator | None = None) -> TestClient: + """An app on the `direct` mode; `orchestrator` replaces the registered one when given.""" + mocker.patch( + f"{_PIPELINE_NS}.get_api_config", return_value=ApiConfig(orchestration_mode="direct", allow_request_orchestration_mode_override=False) + ) + if orchestrator is not None: + mocker.patch(f"{_PIPELINE_NS}.get_orchestrator_registry", return_value=OrchestratorRegistry({"direct": orchestrator})) + app = FastAPI() + app.include_router(api_router, prefix="/v1") + register_exception_handlers(app, disclosure_mode=disclosure_mode) + return TestClient(app) + + +class TestRunRefusals: + @pytest.mark.parametrize("disclosure_mode", [DisclosureMode.VERBOSE, DisclosureMode.STRICT]) + @pytest.mark.parametrize("path", ["/v1/execute", "/v1/start"]) + @pytest.mark.parametrize( + ("mthds_content", "expected_item"), + [ + ( + MISSPELLED_CONCEPT_MTHDS, + { + "error_type": "unresolved_concept", + "concept_code": "Summry", + "field_path": "pipe.echo.output", + "field_name": "output", + }, + ), + ( + UNKNOWN_MODEL_MTHDS, + { + "error_type": "unknown_model", + "model_reference": "no-such-model-in-any-deck", + "model_type": "llm", + "field_path": "pipe.echo.model", + "field_name": "model", + }, + ), + ], + ids=["misspelled_concept", "unknown_model"], + ) + def test_invalid_bundle_is_a_422_with_located_items( + self, + mocker: MockerFixture, + disclosure_mode: DisclosureMode, + path: str, + mthds_content: str, + expected_item: dict[str, Any], + ): + orchestrator = _RecordingOrchestrator() + client = _build_client(mocker, disclosure_mode=disclosure_mode, orchestrator=orchestrator) + + response = client.post(path, json={"pipe_code": "echo", "mthds_contents": [mthds_content], "inputs": {"text": "hello"}}) + + assert response.status_code == 422, response.text + assert response.headers["content-type"].startswith("application/problem+json") + body = response.json() + assert body["error_type"] == "ValidateBundleError" + assert body["error_domain"] == "input" + assert body["user_action"]["kind"] == "change_input" + items: list[dict[str, Any]] = body["validation_errors"] + assert len(items) == 1, items + item = items[0] + assert item["category"] == ValidationErrorCategory.PIPE_VALIDATION + assert item["pipe_code"] == "echo" + assert item["domain_code"] == "smoke" + for field_name, expected_value in expected_item.items(): + assert item[field_name] == expected_value, item + # The verdict is the caller's own bundle: STRICT keeps the human summary, which is the item's message. + assert body["detail"] == item["message"] + # Refused while the bundle loads: nothing reached the orchestrator. + assert orchestrator.dispatches == [] + + @pytest.mark.parametrize("disclosure_mode", [DisclosureMode.VERBOSE, DisclosureMode.STRICT]) + def test_run_failure_reports_its_root_fault_at_the_failing_pipe(self, mocker: MockerFixture, disclosure_mode: DisclosureMode): + client = _build_client(mocker, disclosure_mode=disclosure_mode) + + response = client.post("/v1/execute", json={"mthds_contents": [MISMATCHED_PARALLEL_MTHDS], "inputs": {"topic": "tides"}}) + + assert response.status_code == 422, response.text + assert response.headers["content-type"].startswith("application/problem+json") + body = response.json() + # The root fault's class, not the run-level `PipelineExecutionError` that wraps it. + assert body["error_type"] == "StuffFactoryError" + assert body["type"] == "https://docs.pipelex.com/latest/errors/stuff-factory-error/" + assert body["error_domain"] == "input" + # Located at the failing step, with its path from the entry pipe, and kept under STRICT + # because the fault is the caller's own method. + assert body["detail"].startswith("Pipe 'analyze_topic' failed (review_topic → analyze_topic): "), body["detail"] + # The next step says which of the two multiplicities to change. + assert body["user_action"]["kind"] == "change_input" + assert "Branch 'draft_idea' gives result 'ideas' as a single 'Idea'" in body["user_action"]["detail"] + assert "validation_errors" not in body diff --git a/tests/unit/test_validate_errors.py b/tests/unit/test_validate_errors.py index fa749c5..c79449e 100644 --- a/tests/unit/test_validate_errors.py +++ b/tests/unit/test_validate_errors.py @@ -22,7 +22,12 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from pipelex.base_exceptions import PipelexConfigError, ValidationErrorCategory -from pipelex.core.exceptions import PipeFactoryErrorData, PipelexBundleBlueprintValidationErrorData, PipesAndConceptValidationErrorData +from pipelex.core.exceptions import ( + DryRunFailureErrorData, + PipeFactoryErrorData, + PipelexBundleBlueprintValidationErrorData, + PipesAndConceptValidationErrorData, +) from pipelex.pipeline.exceptions import ValidateBundleError from pipelex.validation_error_types import PipeFactoryErrorType, PipeValidationErrorType from pytest_mock import MockerFixture @@ -30,7 +35,7 @@ from api.exception_handlers import register_exception_handlers from api.routes import router as api_router from api.routes.pipelex.pipeline import ApiRunner -from tests.unit._constants import INVALID_MAIN_PIPE_MTHDS, VALID_MTHDS +from tests.unit._constants import INVALID_MAIN_PIPE_MTHDS, UNKNOWN_MODEL_MTHDS, VALID_MTHDS # Structural artifacts that exist only on the valid arm — the invalid arm must NOT carry them. _STRUCTURAL_FIELDS = ("bundle_blueprint", "pipe_io_contracts", "graph_spec", "validated_pipes") @@ -147,6 +152,30 @@ def test_toml_syntax_errors_carry_threaded_source(self): assert any(item["category"] == ValidationErrorCategory.BLUEPRINT_VALIDATION for item in sourced) assert any("TOML syntax error" in item["message"] for item in sourced) + def test_unknown_model_is_a_located_item_of_the_invalid_verdict(self): + # A model the deck does not define is refused while the pipe is built. That refusal is an + # item of the verdict like any other, located on the pipe's `model` field and carrying the + # reference as written, not a no-verdict fault. + client = _build_client() + response = client.post( + "/v1/validate", + json={"mthds_contents": [UNKNOWN_MODEL_MTHDS], "mthds_sources": ["echo.mthds"]}, + ) + + assert response.status_code == 200, response.text + body = response.json() + assert body["is_valid"] is False + items: list[dict[str, Any]] = body["validation_errors"] + assert len(items) == 1, items + item = items[0] + assert item["category"] == ValidationErrorCategory.PIPE_VALIDATION + assert item["error_type"] == "unknown_model" + assert item["pipe_code"] == "echo" + assert item["source"] == "echo.mthds" + assert item["field_path"] == "pipe.echo.model" + assert item["model_reference"] == "no-such-model-in-any-deck" + assert item["model_type"] == "llm" + def test_all_categories_project_onto_invalid_report(self, mocker: MockerFixture): # Every structured category lands on the 200 InvalidReport, and collectively the items cover # the full ValidationErrorItem field set (so a dropped field would fail here, not silently @@ -186,12 +215,20 @@ def test_all_categories_project_onto_invalid_report(self, mocker: MockerFixture) } ), f"missing fields: {populated}" - def test_dry_run_residual_becomes_single_dry_run_item(self, mocker: MockerFixture): - # A dry-run failure with no structured locator becomes ONE `dry_run` item carrying the - # message — the structured-info invariant (never a bare detail with an empty list). It is - # graph-level, so it carries no `source`. - residual = ValidateBundleError(message="Dry run failed: boom.", dry_run_error_message="Dry run failed: boom.") - mocker.patch.object(ApiRunner, "validate_verdict", new=mocker.AsyncMock(return_value=residual.to_error_report())) + def test_dry_run_failures_become_one_located_item_per_failing_pipe(self, mocker: MockerFixture): + # A failing dry run is one `dry_run` item per failing pipe, located at the innermost pipe + # that failed (its `pipe_code`, `domain_code` and `source`), never one item holding the whole + # sweep. A failure the sweep could not locate still becomes its own item carrying only the + # message — the structured-info invariant (never a bare detail with an empty list) — and so + # carries no locator. + failing_dry_run = ValidateBundleError( + message="Dry run failed.", + dry_run_failures=[ + DryRunFailureErrorData(pipe_code="summarize", domain_code="legal", source="pipe.mthds", message="Pipe 'summarize' failed: boom."), + DryRunFailureErrorData(message="Dry run failed: unlocated."), + ], + ) + mocker.patch.object(ApiRunner, "validate_verdict", new=mocker.AsyncMock(return_value=failing_dry_run.to_error_report())) client = _build_client() response = client.post("/v1/validate", json={"mthds_contents": [VALID_MTHDS]}) @@ -199,12 +236,17 @@ def test_dry_run_residual_becomes_single_dry_run_item(self, mocker: MockerFixtur body = response.json() assert body["is_valid"] is False items: list[dict[str, Any]] = body["validation_errors"] - assert len(items) == 1, items - dry_run_item = items[0] - assert dry_run_item["category"] == ValidationErrorCategory.DRY_RUN - assert dry_run_item["error_type"] == "DryRunError" - assert dry_run_item["message"] == "Dry run failed: boom." - assert "source" not in dry_run_item + assert len(items) == 2, items + assert all(item["category"] == ValidationErrorCategory.DRY_RUN for item in items) + assert all(item["error_type"] == "DryRunError" for item in items) + located_item, unlocated_item = items + assert located_item["pipe_code"] == "summarize" + assert located_item["domain_code"] == "legal" + assert located_item["source"] == "pipe.mthds" + assert located_item["message"] == "Pipe 'summarize' failed: boom." + assert unlocated_item["message"] == "Dry run failed: unlocated." + for locator in ("pipe_code", "domain_code", "source"): + assert locator not in unlocated_item, unlocated_item def test_non_verdict_failure_is_not_a_200_verdict(self, mocker: MockerFixture): # The runner returns a produced verdict (valid report | invalid ErrorReport) as a value; only diff --git a/uv.lock b/uv.lock index ea752d8..912ae6b 100644 --- a/uv.lock +++ b/uv.lock @@ -2311,7 +2311,7 @@ wheels = [ [[package]] name = "pipelex" -version = "0.66.1" +version = "0.67.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiofiles" }, @@ -2352,9 +2352,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/44/c2/bba218b12df70949bcaed4ca3ff449d4d04b55370e07ba1aee4b6e05e3ad/pipelex-0.66.1.tar.gz", hash = "sha256:da3c18544b5544d78c94b9cb9a65d876a7ec92ddb8d032075879a0f70872da41", size = 1671273, upload-time = "2026-09-26T19:42:15.899Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/c1/3b80b054cff2531d7071dc76d35ba7609b18ae2ea6836cb3261c7a20252c/pipelex-0.67.0.tar.gz", hash = "sha256:9a441afb65933dcb0179460f1c4f7ffca6ec23e59637593c54cc8ab15e980ea0", size = 1708767, upload-time = "2026-09-27T10:17:19.543Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/43/ecf03bfafd2a07b3b460d1c4d1ce33550917394d472e13e6540679e8085a/pipelex-0.66.1-py3-none-any.whl", hash = "sha256:a48b07baa05e9a4888bd183e5cb300e4a7a06b0685b24fb112ee75fa699c30ad", size = 2306691, upload-time = "2026-09-26T19:42:13.818Z" }, + { url = "https://files.pythonhosted.org/packages/8a/68/2b48e642e9efb463a65f975c1eedc5c03dfa55bc0ccc7b4f3a280de1357b/pipelex-0.67.0-py3-none-any.whl", hash = "sha256:921d87907ce810a698a65abe17cf9feceb1240b255a395844ad6ee76a6317f29", size = 2347034, upload-time = "2026-09-27T10:17:17.711Z" }, ] [package.optional-dependencies] @@ -2383,7 +2383,7 @@ mistralai = [ [[package]] name = "pipelex-api" -version = "0.28.1" +version = "0.29.0" source = { editable = "." } dependencies = [ { name = "fastapi" }, @@ -2435,7 +2435,7 @@ requires-dist = [ { name = "mkdocs-meta-manager", marker = "extra == 'docs'", specifier = "==1.1.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.11.2" }, { name = "pandas-stubs", marker = "extra == 'dev'", specifier = ">=2.2.3.241126" }, - { name = "pipelex", extras = ["mistralai", "anthropic", "google", "google-genai", "bedrock", "fal"], specifier = "==0.66.1" }, + { name = "pipelex", extras = ["mistralai", "anthropic", "google", "google-genai", "bedrock", "fal"], specifier = "==0.67.0" }, { name = "pyjwt", specifier = ">=2.10.1" }, { name = "pylint", marker = "extra == 'dev'", specifier = ">=3.3.8" }, { name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.405" },