Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- **`RunErrorReport` carries every field of the runner's report and moves to `pipelex_sdk.error_models` (Breaking)**: import it from `pipelex_sdk.error_models` instead of `pipelex_sdk.product_models`. Beside `message` and `error_type` it now declares `title`, `type_uri`, `error_domain`, `error_category`, `retryable`, `user_action`, `model`, `provider`, `provider_metadata`, `caller_facing_message`, `validation_errors` and `migration`, every one optional, the model open to fields the runner adds, and each field read leniently — a value that does not fit its type reads as `None` rather than failing the status read, the run list or the results read that carries the report — so `PipelineRun.error` in the run lists reads the whole report too.
- **`RunRead.error` is the typed report, no longer a raw dict (Breaking)**: `error` is now declared on `RunPublic`, so the status read's report is a `RunErrorReport` rather than the dict that rode `model_extra`; read `run.error.message` where code read `run.error["message"]` or `run.model_extra["error"]`.
- **A failed run's status comes from the results read's `run_status` member (Breaking)**: `get_run_result` no longer parses the status out of the `409`'s `detail` sentence; it reads the problem document's `run_status` member, and a `409` without a status this SDK knows reads as `FAILED`.
- **Requires `mthds` 0.16.0 (Breaking)**: the exact pin moves from 0.15.0 to the version `pipelex` pins exactly, so `pipelex-sdk` and `pipelex` can be installed together again. Nothing in this client's own surface changes: `PipelexAPIClient` keeps building its `User-Agent` with `pipelex_sdk.user_agent` and its own `AppInfo`, and does not yet use the builder that `mthds` 0.16.0 adds to `MthdsAPIClient`.

## [v0.12.0] - 2026-09-24

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ Each stays deferred rather than silently missing. Everything else — the protoc

**The run-results surface** — `RunResults` tracks `@pipelex/sdk` 0.20.0 field for field, and is at parity with it. Every field is declared, with the two-path semantics [`run-results.md`](./run-results.md) states: `graph_spec` (lifted on the blocking path instead of written `None`), `graph_assembly_error`, the three I/O artifacts typed from `mthds.protocol` with `pipe_io_artifacts_error` beside them, the usage pair, `working_memory` (the hosted artifact relayed as its own key, the blocking one lifted off `pipe_output.working_memory`), and `pipe_output`; and the usage pair's fold, `summarize_usage`, with the summary types it returns. What stays different is idiomatic and deliberate: a key the hosted body did not carry is absent from `model_fields_set` where the JS reads `undefined`, and the page says how to read that.

**Models** — field-for-field across the run-lifecycle types and the product wire models. Deliberate idiomatic ports (not gaps): milliseconds → seconds (`interval_seconds` / `timeout_seconds` / `elapsed_seconds`); the JS `AbortSignal` → Python `asyncio` cancellation (no `signal` field, and no `aborted` flag on the download verdict — a cancelled `download_artifacts` raises `CancelledError` with its partial files already unlinked); the JS artifact request object → `DownloadArtifactsOptions` beside an explicit `dir_path` (`dir` being a Python builtin) and the raw bulk call named `resolve_storage_urls_bulk` after its route rather than `resolveStorageUrls`, so it cannot be misread as the single-reference `resolve_storage_url`; the returned `Response` of `fetchArtifact` → an async context manager, because an httpx stream is only live inside its own block; JS inline string-unions promoted to `StrEnum`s (`OrgRole`, `PipeStatus`, the onboarding fields) with identical wire values; response models are `extra="allow"` for forward-compat. The Pipelex validation narrowing is **owned here** (`pipelex_sdk.validation_models`), narrowing `mthds`'s neutral verdict bases (the resolved follow-up #9); the brand-neutral `Dict*` wire concretes (`DictRunResultExecute`, and the `DictPipeOutputAbstract` / `DictWorkingMemoryAbstract` pair that types `RunResults.pipe_output` and `RunResults.working_memory`) are reused from `mthds` by inheritance or by import — they are a shared wire contract the `pipelex` runtime also builds on — rather than duplicated as `pipelex-sdk-js` does. One addition runs ahead of the JS SDK: `write_codegen_tree` has no `@pipelex/sdk` counterpart, because the JS writer lives inside `pipelex-starter-js`'s own harness, mixed in with project policy; the byte-fidelity contract is small and load-bearing enough to belong to the SDK, where every consumer shares one correct implementation. The drift check goes the other way and takes a different shape on purpose: `@pipelex/sdk`'s `runCodegenCheck` is **pure** — the caller walks its own tree and hands in the text — because that module must stay free of Node builtins for a browser bundle, and its doc consequently loads the caller with obligations (walk the whole tree, do not reformat, decode strictly) whose every breach yields a wrong verdict rather than an error. Python has no such constraint, this package already does filesystem work, and `pipelex`'s own surface takes a root — so `run_codegen_check(root=…)` takes one too. Every caller obligation becomes the library's, the verdict is provable against `pipelex` by calling both with the same directory, and it composes with `write_codegen_tree(report, output_dir=…)` as the same path in and out. Two divergences worth naming: the page envelopes keep the wire's snake_case `next_cursor`, where the JS mirror renamed it `nextCursor` for its own consumers; and the method-files catalog converter (`parse_method_files` / `serialize_method_files`) lives in this package, where the JS pair lives in `mthds-js` because `pipelex-mcp` consumes the same format and wanted one owner. There is no second Python consumer, and the catalog serialization is a Pipelex product concern rather than an MTHDS protocol one, so this SDK is a proper home for it. `mthds` has since grown the canonical pair as `mthds.protocol.method_files`, shipped in the `mthds==0.15.0` this package pins exactly, and the local pair is kept beside it on purpose rather than left over. Adoption is a step of its own rather than a rename: that parser raises `PipelineRequestError`, which does not subclass `ValueError`, and pydantic converts only `ValueError` out of a validator — so re-pointing `MethodData`'s validator at it would stop every caller's `except ValidationError` from catching a malformed stored source. The exception's base class is `mthds`'s to settle, and it is settled first; until then the two implementations are kept in step, and they disagree on blankness (this one is Python's `str.strip`, the canonical one is ECMAScript's), on the serialized bytes (default separators and ASCII escapes here, `JSON.stringify`'s there), and on the JSON constants and integer-literal cap the canonical one closes with `parse_constant=` and `parse_int=float`.
**Models** — field-for-field across the run-lifecycle types and the product wire models. Deliberate idiomatic ports (not gaps): milliseconds → seconds (`interval_seconds` / `timeout_seconds` / `elapsed_seconds`); the JS `AbortSignal` → Python `asyncio` cancellation (no `signal` field, and no `aborted` flag on the download verdict — a cancelled `download_artifacts` raises `CancelledError` with its partial files already unlinked); the JS artifact request object → `DownloadArtifactsOptions` beside an explicit `dir_path` (`dir` being a Python builtin) and the raw bulk call named `resolve_storage_urls_bulk` after its route rather than `resolveStorageUrls`, so it cannot be misread as the single-reference `resolve_storage_url`; the returned `Response` of `fetchArtifact` → an async context manager, because an httpx stream is only live inside its own block; JS inline string-unions promoted to `StrEnum`s (`OrgRole`, `PipeStatus`, the onboarding fields) with identical wire values; response models are `extra="allow"` for forward-compat. The Pipelex validation narrowing is **owned here** (`pipelex_sdk.validation_models`), narrowing `mthds`'s neutral verdict bases (the resolved follow-up #9); the brand-neutral `Dict*` wire concretes (`DictRunResultExecute`, and the `DictPipeOutputAbstract` / `DictWorkingMemoryAbstract` pair that types `RunResults.pipe_output` and `RunResults.working_memory`) are reused from `mthds` by inheritance or by import — they are a shared wire contract the `pipelex` runtime also builds on — rather than duplicated as `pipelex-sdk-js` does. One addition runs ahead of the JS SDK: `write_codegen_tree` has no `@pipelex/sdk` counterpart, because the JS writer lives inside `pipelex-starter-js`'s own harness, mixed in with project policy; the byte-fidelity contract is small and load-bearing enough to belong to the SDK, where every consumer shares one correct implementation. The drift check goes the other way and takes a different shape on purpose: `@pipelex/sdk`'s `runCodegenCheck` is **pure** — the caller walks its own tree and hands in the text — because that module must stay free of Node builtins for a browser bundle, and its doc consequently loads the caller with obligations (walk the whole tree, do not reformat, decode strictly) whose every breach yields a wrong verdict rather than an error. Python has no such constraint, this package already does filesystem work, and `pipelex`'s own surface takes a root — so `run_codegen_check(root=…)` takes one too. Every caller obligation becomes the library's, the verdict is provable against `pipelex` by calling both with the same directory, and it composes with `write_codegen_tree(report, output_dir=…)` as the same path in and out. Two divergences worth naming: the page envelopes keep the wire's snake_case `next_cursor`, where the JS mirror renamed it `nextCursor` for its own consumers; and the method-files catalog converter (`parse_method_files` / `serialize_method_files`) lives in this package, where the JS pair lives in `mthds-js` because `pipelex-mcp` consumes the same format and wanted one owner. There is no second Python consumer, and the catalog serialization is a Pipelex product concern rather than an MTHDS protocol one, so this SDK is a proper home for it. `mthds` has since grown the canonical pair as `mthds.protocol.method_files`, shipped since `mthds` 0.15.0 and present in the `mthds==0.16.0` this package pins exactly, and the local pair is kept beside it on purpose rather than left over. Adoption is a step of its own rather than a rename: that parser raises `PipelineRequestError`, which does not subclass `ValueError`, and pydantic converts only `ValueError` out of a validator — so re-pointing `MethodData`'s validator at it would stop every caller's `except ValidationError` from catching a malformed stored source. The exception's base class is `mthds`'s to settle, and it is settled first; until then the two implementations are kept in step, and they disagree on blankness (this one is Python's `str.strip`, the canonical one is ECMAScript's), on the serialized bytes (default separators and ASCII escapes here, `JSON.stringify`'s there), and on the JSON constants and integer-literal cap the canonical one closes with `parse_constant=` and `parse_int=float`.

**Errors** — `ApiResponseError`, `ApiUnreachableError`, `PipelineExecuteTimeoutError`, `RunFailedError`, `RunTimeoutError`, `RunLifecycleUnavailableError`, `PagingNotTerminatingError` are owned here; `RunStillRunningError` is re-exported from `mthds`. `ClientAuthenticationError` is **not** ported: it is a dormant export in the JS barrel (defined and exported but never raised by the client), and in Python it already lives in `mthds.runners.api.exceptions` — importable directly if ever needed, with no barrel here to re-export it through.

Expand Down
2 changes: 1 addition & 1 deletion docs/client-identification.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ Do not put a secret, a user identifier, an email address or a hostname in `app_i

## Relation to `mthds`

The spec places the header builder of the `mthds` library in `mthds.runners.api.user_agent`. The `mthds` version this SDK pins does not ship it yet, so `pipelex_sdk.user_agent` builds the whole header itself and mirrors the public shape the `mthds` builder has: an `AppInfo` model with `name`, `version`, `url` and `details`, and a `ValueError` on an invalid token.
The spec places the header builder of the `mthds` library in `mthds.runners.api.user_agent`, which `mthds` ships since 0.16.0, the version this SDK pins: `MthdsAPIClient` builds its own `User-Agent` there and lets a subclass prepend its token through `user_agent_sdk_tokens()` and `init_user_agent(app_info)`. This SDK does not adopt that seam yet. `pipelex_sdk.user_agent` still builds the whole header itself, with its own `AppInfo` model of the same shape (`name`, `version`, `url` and `details`, and a `ValueError` on an invalid token), and `PipelexAPIClient` sets `client.user_agent` from it, which the inherited transport sends on every request.
5 changes: 4 additions & 1 deletion pipelex_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,10 @@ def __init__(
request_timeout_seconds if request_timeout_seconds is not None else self._DEFAULT_REQUEST_TIMEOUT_SECONDS
)
#: The integrator's own name, placed before this SDK's tokens in the `User-Agent`.
self.app_info: AppInfo | None = app_info
# Since `mthds` 0.16.0 the base declares `app_info` as its own `AppInfo`, which it only ever
# writes (in `init_user_agent`, never called here). This SDK keeps its own model and builder
# until it adopts the base's `user_agent_sdk_tokens` seam, so the narrow ignore covers that one divergence.
self.app_info: AppInfo | None = app_info # type: ignore[assignment]
#: The `User-Agent` sent on every request (spec: `docs/specs/client-identification.md`),
#: built once here so an over-long header fails at construction, not on the first call.
self.user_agent: str = build_user_agent(app_info)
Expand Down
4 changes: 2 additions & 2 deletions pipelex_sdk/user_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

The header is self-declared and unauthenticated: the platform reads it for analytics only.

The builder lives here rather than in `mthds` because the pinned `mthds` does not ship one yet;
the public shape (`AppInfo`, `ValueError` on an invalid token) matches the one `mthds` will expose.
The builder predates the one `mthds` ships in `mthds.runners.api.user_agent` since 0.16.0, and this
SDK has not adopted that one yet; the public shape (`AppInfo`, `ValueError` on an invalid token) matches it.
"""

from __future__ import annotations
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ classifiers = [
]

dependencies = [
"mthds==0.15.0",
"mthds==0.16.0",
"pydantic>=2.10.6,<3.0.0",
"typing-extensions>=4.0.0",
"httpx>=0.23.0,<1.0.0",
Expand Down
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading