@pipelex/sdk 0.50.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/cli/script.d.ts +1 -1
- package/dist/client.d.ts +106 -2
- package/dist/client.js +138 -10
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +6 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/product-models.d.ts +144 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/architecture.md +1 -0
- package/docs/errors.md +6 -2
- package/docs/method-copy.md +128 -0
- package/package.json +1 -1
package/dist/product-models.d.ts
CHANGED
|
@@ -132,6 +132,26 @@ export interface MethodData {
|
|
|
132
132
|
* same field on the list, where a method mid-erasure stays listed.
|
|
133
133
|
*/
|
|
134
134
|
deletion_state?: MethodDeletionState | null;
|
|
135
|
+
/**
|
|
136
|
+
* The method this one was copied from, on a method `copyMethod` made; `null` on a method
|
|
137
|
+
* created afresh, and absent from a platform that predates copies. Written once, when the copy
|
|
138
|
+
* is made, and never rewritten. The list rows (`MethodSummary`) do not carry it.
|
|
139
|
+
*/
|
|
140
|
+
copied_from?: MethodCopiedFrom | null;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The method a copy was made from, as it stood at the copy — `MethodData.copied_from`. Opaque ids
|
|
144
|
+
* only, never a name: the original may have changed or been erased since, and its organization
|
|
145
|
+
* may be one the reader does not belong to, so nothing here is a link the reader can follow
|
|
146
|
+
* unless they belong to that organization.
|
|
147
|
+
*/
|
|
148
|
+
export interface MethodCopiedFrom {
|
|
149
|
+
/** The canonical id of the organization that holds the original. */
|
|
150
|
+
org_id: string;
|
|
151
|
+
/** The original's method id. */
|
|
152
|
+
method_id: string;
|
|
153
|
+
/** The digest of the original's draft the copy took; `null` for an empty draft. */
|
|
154
|
+
draft_digest: string | null;
|
|
135
155
|
}
|
|
136
156
|
/**
|
|
137
157
|
* A saved method with its draft's files — what `getMethod` answers. `files` lists the draft's
|
|
@@ -357,6 +377,130 @@ export interface MethodVersionPage {
|
|
|
357
377
|
items: MethodVersionSummary[];
|
|
358
378
|
nextCursor: string | null;
|
|
359
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* The copy — `copyMethod`, `POST /v1/methods/{id}/copy`. Its wire body is closed: the client
|
|
382
|
+
* sends these members and nothing else, a `null` reading as not given.
|
|
383
|
+
*/
|
|
384
|
+
export interface MethodCopyInput {
|
|
385
|
+
/**
|
|
386
|
+
* The canonical id of the organization the copy is made in, where the caller is an admin or a
|
|
387
|
+
* member: the method's own organization for a duplicate, another team, or the caller's
|
|
388
|
+
* personal workspace.
|
|
389
|
+
*/
|
|
390
|
+
destination_org_id: string;
|
|
391
|
+
/** The copy's name, under the creation's rules; the original's when omitted or `null`. */
|
|
392
|
+
name?: string | null;
|
|
393
|
+
/**
|
|
394
|
+
* The original's draft token (`MethodData.updated_at`) the caller last saw, echoed verbatim:
|
|
395
|
+
* a draft that moved since is refused with a `409` `method_update_conflict`, and nothing is
|
|
396
|
+
* copied. Omitted or `null`, the copy takes the draft as it stands when the request is served.
|
|
397
|
+
*/
|
|
398
|
+
expected_draft_updated_at?: string | null;
|
|
399
|
+
}
|
|
400
|
+
/** What a copy did — the discriminant of `MethodCopyResult`. */
|
|
401
|
+
export type MethodCopyOutcome = "copied" | "refused";
|
|
402
|
+
/**
|
|
403
|
+
* Why a copy was refused: `bundle_names_other_storage`, a `.mthds` file of the draft holds a
|
|
404
|
+
* `pipelex-storage://` reference the copy cannot rely on: one the destination cannot read, which
|
|
405
|
+
* every run there would refuse, or, in a duplicate, one under the original method's own run files
|
|
406
|
+
* (`pipelex-storage://<org_id>/<method_id>/…`), which erasing the original would remove, failing
|
|
407
|
+
* every run of the duplicate.
|
|
408
|
+
*/
|
|
409
|
+
export type MethodCopyRefusalReason = "bundle_names_other_storage";
|
|
410
|
+
/**
|
|
411
|
+
* One stored file a bundle of the draft names that keeps it from being copied: a file the
|
|
412
|
+
* destination cannot read, or, in a duplicate, a file under the original's run files, which
|
|
413
|
+
* erasing the original would remove.
|
|
414
|
+
*/
|
|
415
|
+
export interface BundleStorageReference {
|
|
416
|
+
/** The tree path of the `.mthds` file that holds the reference. */
|
|
417
|
+
path: string;
|
|
418
|
+
/** The `pipelex-storage://` reference as the file writes it, a malformed one included. */
|
|
419
|
+
uri: string;
|
|
420
|
+
}
|
|
421
|
+
/** A copy that was made: `method` is the copy, in the destination, `copied_from` included. */
|
|
422
|
+
export interface MethodCopied {
|
|
423
|
+
outcome: "copied";
|
|
424
|
+
method: MethodData;
|
|
425
|
+
/** How many saved examples the copy holds, each a new example at `input_revision` 1. */
|
|
426
|
+
examples_copied: number;
|
|
427
|
+
/** How many distinct stored files were copied into the destination for the copy's inputs. */
|
|
428
|
+
stored_files_copied: number;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* A copy refused before anything was written: a bundle of the draft names stored files the copy
|
|
432
|
+
* cannot rely on, files the destination cannot read, which every run there would refuse, and, in a
|
|
433
|
+
* duplicate, files under the original method's own run files, which erasing the original would
|
|
434
|
+
* remove. The person removes or replaces each reference in the original and copies again, since
|
|
435
|
+
* rewriting the bundle would change the author's code without asking them.
|
|
436
|
+
*/
|
|
437
|
+
export interface MethodCopyRefused {
|
|
438
|
+
outcome: "refused";
|
|
439
|
+
reason: MethodCopyRefusalReason;
|
|
440
|
+
/** What was refused and what to do, for a person. */
|
|
441
|
+
message: string;
|
|
442
|
+
/**
|
|
443
|
+
* Every reference the destination cannot read and, in a duplicate, every reference under the
|
|
444
|
+
* original's run files, ordered by bundle path, then by reference.
|
|
445
|
+
*/
|
|
446
|
+
references: BundleStorageReference[];
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* The answer of `copyMethod`, a `200` discriminated on `outcome`, because a bundle the copy cannot
|
|
450
|
+
* rely on is a verdict about the method, not a failure of the request. Branch on `outcome`,
|
|
451
|
+
* never on the status: a destination the caller may not write to, a stale token, a method being
|
|
452
|
+
* deleted or a copy past its bounds are thrown as `ApiResponseError` instead, since they produce
|
|
453
|
+
* no verdict.
|
|
454
|
+
*/
|
|
455
|
+
export type MethodCopyResult = MethodCopied | MethodCopyRefused;
|
|
456
|
+
/**
|
|
457
|
+
* The bound a `422` `method_copy_too_large` names, read from the refusal's
|
|
458
|
+
* `ApiResponseError.problemDocument.limit`: `max_stored_files`, the most distinct stored files one
|
|
459
|
+
* copy copies, or `max_stored_bytes`, the most bytes they add up to, each set per deployment; its
|
|
460
|
+
* `value`, and what the copy `reached`. Removing examples, or the files they hold, brings the copy
|
|
461
|
+
* under it.
|
|
462
|
+
*/
|
|
463
|
+
export interface MethodCopyLimit {
|
|
464
|
+
name: "max_stored_files" | "max_stored_bytes";
|
|
465
|
+
value: number;
|
|
466
|
+
reached: number;
|
|
467
|
+
}
|
|
468
|
+
/** Query for one page of a method's copies. */
|
|
469
|
+
export interface ListMethodCopiesQuery {
|
|
470
|
+
/**
|
|
471
|
+
* Page size, from 1 to 100; the API defaults to 50 and refuses a `limit` outside that range
|
|
472
|
+
* with a `422`.
|
|
473
|
+
*/
|
|
474
|
+
limit?: number;
|
|
475
|
+
/** Opaque `nextCursor` from the previous page. */
|
|
476
|
+
cursor?: string;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* The original's record of one copy made of it — a row of `listMethodCopies`. It names the
|
|
480
|
+
* destination by its id alone, never its name, so the listing tells no reader the name of an
|
|
481
|
+
* organization they do not belong to: show a destination's name only from the reader's own
|
|
482
|
+
* memberships. The marker stays when its copy is erased, and the copy then reads as not found.
|
|
483
|
+
*/
|
|
484
|
+
export interface MethodCopyMarker {
|
|
485
|
+
/** The copy's method id, in the destination organization. */
|
|
486
|
+
copy_method_id: string;
|
|
487
|
+
/** The canonical id of the organization the copy was made in. */
|
|
488
|
+
destination_org_id: string;
|
|
489
|
+
/** The canonical id of the member who made the copy. */
|
|
490
|
+
copied_by_user_id: string;
|
|
491
|
+
/** ISO 8601 UTC instant the copy's row was written. */
|
|
492
|
+
copied_at: string;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* One page of a method's copies, in the order of the copies' ids, which says nothing of when each
|
|
496
|
+
* was made. `nextCursor` is opaque — pass it straight back to `listMethodCopies` to continue;
|
|
497
|
+
* `null` means this was the last page. A method never copied, and a copy, which names its
|
|
498
|
+
* original through `copied_from` and has no copies of its own, answer an empty page.
|
|
499
|
+
*/
|
|
500
|
+
export interface MethodCopyPage {
|
|
501
|
+
items: MethodCopyMarker[];
|
|
502
|
+
nextCursor: string | null;
|
|
503
|
+
}
|
|
360
504
|
/**
|
|
361
505
|
* One saved example of a method — what `createMethodExample`, `getMethodExample`,
|
|
362
506
|
* `updateMethodExample` and both arms of `recordMethodExampleLatestRun` and
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/docs/architecture.md
CHANGED
|
@@ -154,6 +154,7 @@ The hosted catalog/account routes the webapp drives. All ride the same `{base}/v
|
|
|
154
154
|
- **User profile:** `getMe` (`GET /v1/me`).
|
|
155
155
|
- **Methods catalog:** `listMethods`, `getMethod`, `createMethod`, `writeDraft`, `renameMethod`, `publishMethod`, `listMethodVersions`, `getMethodVersion`, `deleteMethod`. **A saved method has a draft and immutable published versions.** Its draft is a method tree with its `input_data`, which `writeDraft` (`PUT /v1/methods/{id}/draft`) replaces whole, without validating it; `updated_at` is the draft's token, which moves on every draft write and nothing else, and `draft_digest` the draft's tree digest (`sha256:` and 64 hex digits), `null` for an empty draft. A save takes the tree as `files`, sent as given, or as `directory`, `tree` or `contents`, sent inline or by hash after uploading the blobs the organization lacks, and a read lists the tree's files, each with a short-lived link to its bytes, which `writeMethodTree` writes to disk ([method-trees.md](./method-trees.md)). A draft write may carry `expected_updated_at`, the token the caller last saw, and is then a compare-and-swap: a draft that moved since is a `409` `method_update_conflict` and nothing is written. `renameMethod` (`PATCH /v1/methods/{id}`) changes the name alone and moves no token. `publishMethod` (`POST /v1/methods/{id}/publish`) takes the required `expected_draft_updated_at` and answers a `MethodPublishResult` discriminated on `outcome` — `published` with the new version, `unchanged` with the existing latest version when the draft's digest equals it, `refused` with a `reason` (`invalid` or `not_runnable`), a `message` and the runner's `validation` verdict — every arm carrying the `method`; the SDK reads the nested method as `getMethod` does, and an answer whose `outcome` is none of these is an answer it cannot read. A publish that produced no verdict is thrown rather than answered: only a draft that differs from the latest version reaches the runner, and a runner that cannot be reached, answers unusably or does not finish within the platform's deadline is a `502` or `503` with nothing written, so a retry is safe, and a retry of a publish that landed while its answer was lost answers `unchanged` with the version it wrote. A method is stored as one item that keeps room for the summary a publish adds, so a draft that saves can always be published: a creation or a draft write is a `413` `payload_too_large` when it would grow the method past that room, below the store's item limit, and a rename is the same `413` for a name so long it would leave the method too large to publish. A version is numbered from 1, never reused, and holds the draft's tree as it stood, its `tree_digest` (`null` for a version stored before methods were trees), its `crate_fingerprint` (`null` when the runner could not resolve the bundle in memory), the runner's version, and when and by whom it was published; `listMethodVersions` pages the summaries newest first (`MethodVersionPage`, `{items, nextCursor}`, a `limit` from 1 to 100, 20 by default, and a `422` outside that range), and `getMethodVersion` reads one with its `files` listed as a method's are, or answers a `409` `conflict`, whose verdict is `runtime`, for a version stored before methods were trees and never rewritten as one. A method read carries `latest_version` and `latest_published`, both `null` until the first publish, so the publish state is derived: never published, draft unchanged (`draft_digest` not `null` and equal to `latest_published.tree_digest`), or draft ahead. The method routes take a bare id and address the method itself, never one of its versions; the platform does not parse a suffix there, so `mt_x@3` would answer a `404` that reads as a method that does not exist, and each route refuses a suffixed id with `RequestArgumentError` before anything is sent, naming `parseMethodSelector` and `getMethodVersion`. The whole-method `updateMethod` is gone, replaced by the draft write and the rename. **`deleteMethod` is asynchronous and its return type says so:** `DELETE /v1/methods/{id}` answers `202` with a `MethodDeletionAccepted` (`method_id`, `deletion_state`, `deletion_job_id`) the moment the platform has claimed the method and terminated its in-flight workflows — the rest of the cascade (runs, events, S3 objects) is enqueued. A resolved promise means "accepted", never "gone"; completion is the row disappearing from `listMethods`, not any field of that body. The claim is a conditional write, so a double-clicked delete is a `409 method_being_deleted` rather than a second cascade over the same runs, and an unknown or foreign-org id is a `404`. The erasure deletes the method's versions with the rest. (This doc previously stated the SDK had no `delete` because the platform had no such route — the route has always existed; the claim was simply false.) `listMethods` returns **one page**, not the whole catalog — a `MethodPage` (`{items, nextCursor}`) served from a narrow index projection, so its cost is the page size rather than the size of the catalog. It used to return every method with its full `.mthds` bundle attached, which hit DynamoDB's 1 MB page cap at a couple hundred methods and came back truncated with no error and no flag, so methods silently vanished from the UI. Rows are therefore `MethodSummary`, **not** `MethodDetail`: no `files`, no `updated_at`, because neither is in the projection and putting the source back is what restored the truncation. Reach for `getMethod(id)` when you need a method's files. `query.q` is applied server-side across the whole catalog, not over the page — filtering one page client-side would be searching 50 rows of 10,000 and calling it a search. `query.published` filters the same way, sent as `published=true` (the methods with a published version) or `published=false` (those never published), and combines with `q`. Each row carries the method's publish state, so a list tells its methods apart without reading each one: `latest_version` and `latest_published` (a `MethodVersionSummary`), both `null` when the method was never published, and `draft_digest`, `null` for an empty draft; all three are optional, absent from a platform that does not serve them. Never published is `latest_version === null`, the draft unchanged is a `draft_digest` that is not `null` and equals `latest_published.tree_digest`, and the draft ahead is two digests that differ. Both filters run after the index read, so **a filtered page can come back empty and still carry a `nextCursor`**: an empty page is not the end of the list, and only a `null` cursor is. `iterateMethods` is the async iterator that follows the cursor for callers who genuinely want everything (`for await (const m of client.iterateMethods())`); it keeps going **past empty pages**, since `{items: [], nextCursor: "…"}` means "nothing matched in the slice I just read, keep going". A server that keeps minting fresh cursors forever stops it at a page ceiling far past any real catalog with a `PagingNotTerminatingError` carrying `pageLimit`, never a silently truncated stream; `iterateRuns` shares the same loop. It gives up only when the server stops advancing its cursor, and **throws** rather than returning a partial answer; like `iterateRuns` it is deliberately not a `listAllMethods(): Promise<MethodSummary[]>`, because an all-at-once helper needs a page cap and a cap means silently returning a truncated list. `MethodSummary.deletion_state` rides the list rows: a method mid-erasure stays listed so the UI can render it as "Deleting…", while `getMethod` refuses it with a 409. Full read responses (`MethodDetail`, from `getMethod`) carry `org_id`, `created_by_user_id`, a server-derived `description` (recomputed from the draft's top-level `description` on every draft write), the draft token and digest, the latest version's summary and the draft's `files` on top of the write contract's `name` / `input_data`; the writes and the rename answer `MethodData`, the same method without `files`.
|
|
156
156
|
- **Method examples:** `createMethodExample`, `listMethodExamples`, `iterateMethodExamples`, `getMethodExample`, `updateMethodExample`, `recordMethodExampleLatestRun`, `saveAssistantMethodExample`, `deleteMethodExample`, on `/v1/methods/{id}/examples`. An example is a named set of inputs saved on a method, shared across the organization, with an `input_revision` that rises each time its inputs change and the `latest_run_id` of those inputs, which an inputs edit clears. `recordMethodExampleLatestRun` records a run against the revision whose inputs it used and answers a `MethodExampleLatestRunResult` discriminated on `outcome`, `recorded` or `stale`, each arm carrying the `example`: a run that outlasted an edit is a verdict, never thrown, and an answer off the union is one the SDK cannot read; `saveAssistantMethodExample` is the build assistant's save, which writes into the example it names only while that example is still the assistant's (`kept_by_assistant`) at the revision it read, and otherwise creates one, answering a `MethodExampleAssistantSaveResult` discriminated on `outcome`, `kept` or `created`; one helper, `readVerdict`, reads these verdicts and `publishMethod`'s, the example checked once, by the reader every example goes through. Every answer carrying an example is read with `parseExactJson` and every request body written with `stringifyExactJson`, so an integer past `Number.MAX_SAFE_INTEGER` in the inputs reads as a `bigint` and goes back digit for digit, to an edit or as a run's inputs; `3.0` still reads as `3`, and inputs holding one sent back unchanged are an edit for the platform, which compares inputs as canonical JSON. Every example the client hands back, the page rows and the verdict's `example` included, is checked by `readMethodExample` against the fields the Python SDK's model requires, with the strict types it holds them to, an example breaking one being an answer the SDK cannot read. The list is paged oldest first by an opaque cursor, and a page may be short while `nextCursor` is set; `iterateMethodExamples` follows it through the loop `iterateMethods` and `iterateRuns` share, and refuses a method id at the call rather than at the first pull. Every method takes a bare catalog id, and the client refuses an example id that is empty, `.` or `..` or holds a lone surrogate, a create without its name or inputs or whose `run_id` is given but empty, an edit naming neither `name` nor `inputs`, and a latest-run write without a run id or a positive integer revision before sending. See [method-examples.md](./method-examples.md).
|
|
157
|
+
- **Method copies:** `copyMethod`, `listMethodCopies`, on `/v1/methods/{id}/copy` and `/v1/methods/{id}/copies`. `copyMethod` makes a new, unpublished method in an organization the caller may change methods in, the method's own for a duplicate, holding the original's draft tree, its form inputs and its examples, the stored files they name copied into the destination, and answers a `MethodCopyResult` discriminated on `outcome`: `copied` with the copy as `method` and what was copied, or `refused` with the `references` of a bundle naming storage the destination cannot read or, in a duplicate, a file under the original's run files, which erasing the original would remove. The two arms carry different members, so the verdict is read by the copy itself rather than by `readVerdict`, which reads verdicts whose arms share one: an `outcome` off the union, a `copied` answer whose `method` is not an object and a `refused` one whose `references` is not an array are each an answer the SDK cannot read. Its body is closed, `destination_org_id` with `name` and `expected_draft_updated_at` when given, and its last argument takes an `idempotencyKey`, sent as the `Idempotency-Key` header through the same `requestRaw` option and the same `idempotencyKeyOf` check a start uses. `listMethodCopies` pages the original's `MethodCopyMarker` rows through `requestPage`, as `listMethodVersions` does, with no iterator. Every method read carries `copied_from`, the original a copy names, `null` on a method created afresh. `MethodErrorCode` names the copy's own codes, `organization_not_found` and `method_copy_too_large`, whose `limit` member is typed `MethodCopyLimit`. Both take a bare catalog id. See [method-copy.md](./method-copy.md).
|
|
157
158
|
- **Organizations:** `listMemberships`, `createOrganization`, `renameOrganization` (`PATCH`). Each membership carries the caller's `role_in_org`, an `OrgRole` (`admin`, `member` or `runner`) read open: a role a later platform adds arrives as its raw string, so a `switch` over it says what an unknown role does. The role is for display; what it may do is the platform's to decide, and a call refused to the caller's role is an `ApiResponseError`. The org "switch" is a WorkOS-session op (`/api/active-org`), not a `/v1` route — out of SDK scope.
|
|
158
159
|
- **Organization branding:** `getBranding` (`GET /v1/branding`, any role, web session or API key) and `updateOrganizationBranding` (`PATCH /v1/organizations/{org_id}/branding`, an admin's write) answer the same `OrganizationBranding`: the organization's name, its light and dark colour sets (`BrandingColors`) and its square and wide logos (`LogoDescriptor`). The write's `OrganizationBrandingUpdate` sets a key given a value, clears a key given `null` and keeps a key left out; a refused logo or colour is named in `ApiResponseError.errors`, by slot or by dotted key, a logo the platform measured with a `LogoRefusalCode`. See [organization-branding.md](./organization-branding.md).
|
|
159
160
|
- **Billing:** `getSubscription`, `listPlans`, `listInvoices`, `createCheckout`, `changePlan` (409 `conflict` ⇒ no subscription), `getBillingPortal` (409 `conflict` ⇒ no subscription).
|
package/docs/errors.md
CHANGED
|
@@ -43,7 +43,7 @@ On an error from a request that may have started a run, `retryable` is `false` w
|
|
|
43
43
|
| Class | `errorDomain` | `retryable` | Why |
|
|
44
44
|
|---|---|---|---|
|
|
45
45
|
| `ApiResponseError` | the server's, else the fallback | the server's, else no for a sent `input` or `config` domain, else the fallback | See [below](#a-refused-requests-verdict). |
|
|
46
|
-
| `RequestArgumentError` | `input` | no | The client refused a call's arguments before sending any request: no run source given to `execute` or `start`, run sources or method selectors that exclude each other, a selector rule of `validate`, the method's tree given in two forms, holding no file, or in a directory the standard's lister refuses (its `MethodTreeError` as the `cause`), a tree past the inline limit of an API that keeps no blob store, a reserved key in `extra`, a `publishMethod` without its token, a `getMethodVersion` version that is not a positive integer, an id a route puts in its path (a method's, an example's, a run's, an organization's or an API key's) that is empty, `.` or `..` or is not a string, since the URL would resolve it into another route's path, or that holds a lone surrogate, which a URL cannot carry, a `createMethodExample` without its `name` or `inputs` or whose `run_id` is given but empty, an `updateMethodExample` naming neither `name` nor `inputs`, a `recordMethodExampleLatestRun` without a run id or a positive integer `input_revision`, a selector `parseMethodSelector` refuses, an artifact selection that is empty or names an unknown artifact, a start's `idempotencyKey` that is not 1 to 255 visible ASCII characters ([below](#a-starts-idempotency-key--idempotencykey)), or a request body JSON cannot carry, such as one holding a cycle, or a `bigint` on a runtime without `JSON.rawJSON` (the body is written with `stringifyExactJson`, which writes a `bigint` as its exact digits; see [exact-integers.md](./exact-integers.md)), with the failure as `cause`. Its options take an optional `verdict`, and the client declares `config` for a base URL that is not host-only, since it typically comes from `PIPELEX_BASE_URL`. That refusal shows the base URL's scheme and host alone and names the other parts it carried (credentials, a path, a query, a fragment) without their text, since those are where a secret travels in a URL; a value that is not an http or https URL is not shown at all. It is a `PipelineRequestError`, as the bare refusal it replaced was, and a refusal the standard's own run-source check raises keeps that original as `cause`. |
|
|
46
|
+
| `RequestArgumentError` | `input` | no | The client refused a call's arguments before sending any request: no run source given to `execute` or `start`, run sources or method selectors that exclude each other, a selector rule of `validate`, the method's tree given in two forms, holding no file, or in a directory the standard's lister refuses (its `MethodTreeError` as the `cause`), a tree past the inline limit of an API that keeps no blob store, a reserved key in `extra`, a `publishMethod` without its token, a `getMethodVersion` version that is not a positive integer, an id a route puts in its path (a method's, an example's, a run's, an organization's or an API key's) that is empty, `.` or `..` or is not a string, since the URL would resolve it into another route's path, or that holds a lone surrogate, which a URL cannot carry, a `createMethodExample` without its `name` or `inputs` or whose `run_id` is given but empty, an `updateMethodExample` naming neither `name` nor `inputs`, a `recordMethodExampleLatestRun` without a run id or a positive integer `input_revision`, a selector `parseMethodSelector` refuses, an artifact selection that is empty or names an unknown artifact, a `copyMethod` whose `destination_org_id` is not a non-empty string or whose `name` or `expected_draft_updated_at` is given but is not a string, a start's or a copy's `idempotencyKey` that is not 1 to 255 visible ASCII characters ([below](#a-starts-idempotency-key--idempotencykey)), or a request body JSON cannot carry, such as one holding a cycle, or a `bigint` on a runtime without `JSON.rawJSON` (the body is written with `stringifyExactJson`, which writes a `bigint` as its exact digits; see [exact-integers.md](./exact-integers.md)), with the failure as `cause`. Its options take an optional `verdict`, and the client declares `config` for a base URL that is not host-only, since it typically comes from `PIPELEX_BASE_URL`. That refusal shows the base URL's scheme and host alone and names the other parts it carried (credentials, a path, a query, a fragment) without their text, since those are where a secret travels in a URL; a value that is not an http or https URL is not shown at all. It is a `PipelineRequestError`, as the bare refusal it replaced was, and a refusal the standard's own run-source check raises keeps that original as `cause`. |
|
|
47
47
|
| `ApiUnreachableError` | `config` | yes | The address or the network must be checked, and a later attempt can get through. With the code `ABORT_TIMEOUT`, the SDK's own request timeout, it is `runtime`: the API took the request and did not answer in time. |
|
|
48
48
|
| `PipelineExecuteTimeoutError` | `input` | no | The blocking `execute` cannot outlast the gateway's limit; start the run and poll instead. The run goes on, so its `runMayHaveStarted` is always `true`. |
|
|
49
49
|
| `RunFailedError` | the report's | the report's | See [a failed run](#a-failed-run--runerrorreport). |
|
|
@@ -168,6 +168,8 @@ What the hosted API answers a start sent under a key:
|
|
|
168
168
|
|
|
169
169
|
**A bare runner ignores the key.** It has no idempotency to honour, so a key sent to one protects nothing, and `startAndWaitForResult`'s blocking execute against one sends none.
|
|
170
170
|
|
|
171
|
+
**A copy takes a key too.** `copyMethod` sends an `idempotencyKey` the same way, held to the same rule, and the hosted API keeps it for the same 24 hours, scoped to the caller and the method's organization. A copy binds no run, so its answers under a key differ from a start's: [method-copy.md](./method-copy.md#retrying-a-copy--idempotencykey) gives them.
|
|
172
|
+
|
|
171
173
|
## A failed run — `RunErrorReport`
|
|
172
174
|
|
|
173
175
|
A run that ends without completing (`FAILED`, `CANCELLED`, `TERMINATED` or `TIMED_OUT`) has no result, and the results read answers `409`. The body of that `409` says why: its `detail` names the status and then the report's message, its `run_status` member holds the status, and its `error` member holds the run's stored error report, or `null` when the run has none. The SDK reads the status from `run_status`, falling back to the status word of the `detail` sentence on a platform that does not send the member yet, and types the report as `RunErrorReport`.
|
|
@@ -307,7 +309,7 @@ try {
|
|
|
307
309
|
|
|
308
310
|
### The method codes
|
|
309
311
|
|
|
310
|
-
A refusal about a saved method
|
|
312
|
+
A refusal about a saved method, one of its versions or a copy of it carries one of the platform codes the exported type `MethodErrorCode` names. `code` itself stays a `string`, since the platform adds codes; every one of these is `input` and not retryable by the fallback, since asking again unchanged meets the same answer.
|
|
311
313
|
|
|
312
314
|
| `code` | Status | When | What the caller does |
|
|
313
315
|
|---|---|---|---|
|
|
@@ -315,6 +317,8 @@ A refusal about a saved method or one of its versions carries one of the platfor
|
|
|
315
317
|
| `method_being_deleted` | `409` | The method's erasure has started, on every route that addresses it; the detail tells an erasure in flight from a failed one. | Stop offering the method; retry the delete when the detail says it failed. |
|
|
316
318
|
| `method_not_published` | `409` | A bare `method_id`, which names the latest published version, on a run or tooling route, for a method never published. The detail names `mt_…@draft` and publishing. | Publish the method, or address its draft as `mt_…@draft`. |
|
|
317
319
|
| `method_version_not_found` | `404` | `mt_…@<n>`, or `getMethodVersion`, names a version the method never published. An unknown method is `not_found` instead, so the two are told apart by the code. | Pick a version `listMethodVersions` lists. |
|
|
320
|
+
| `organization_not_found` | `404` | A `copyMethod` destination the caller does not belong to, whose invitation they have not accepted, that is still being set up or that does not exist, all answered alike, so the platform never says whether an organization exists. | Copy into an organization `listMemberships` lists, where the caller's role changes methods. |
|
|
321
|
+
| `method_copy_too_large` | `422` | A `copyMethod` whose stored files pass one of the copy's bounds; `problemDocument.limit`, typed `MethodCopyLimit`, names the bound (`max_stored_files` or `max_stored_bytes`), its `value` and what the copy `reached`. | Remove examples, or the files their inputs name, from the original, then copy again. |
|
|
318
322
|
|
|
319
323
|
A malformed version suffix (`@0`, `@03`, `@Draft`, two suffixes) is a `422` `validation_failed` whose `errors` name the field `method_id`; `parseMethodSelector` refuses the same selectors locally with a `RequestArgumentError`, before anything is sent. So is any suffix on the run routes' linkage form, a `method_id` beside an inline source, which `execute` and `start` refuse locally the same way.
|
|
320
324
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Copying a method into another organization
|
|
2
|
+
|
|
3
|
+
A stored method belongs to the organization that holds it, and access to it comes from membership in that organization. `copyMethod` makes a new method in another organization the caller may change methods in, holding the original's draft, its form inputs and its saved examples, and leaves the original as it was. It is how a method reaches a team from the personal workspace that holds it, how it moves from a team into the caller's personal workspace, and, with the method's own organization as the destination, how a method is duplicated. `listMethodCopies` lists the copies made of a method, so a surface can say where they went. The client reaches them on the hosted API's `POST /v1/methods/{method_id}/copy` and `GET /v1/methods/{method_id}/copies`, and the types are in `src/product-models.ts`.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { PipelexApiClient } from "@pipelex/sdk";
|
|
7
|
+
|
|
8
|
+
const client = new PipelexApiClient();
|
|
9
|
+
const method = await client.getMethod("mt_abc123");
|
|
10
|
+
const result = await client.copyMethod(
|
|
11
|
+
method.method_id,
|
|
12
|
+
{ destination_org_id: "org_team", expected_draft_updated_at: method.updated_at },
|
|
13
|
+
{ idempotencyKey: crypto.randomUUID() },
|
|
14
|
+
);
|
|
15
|
+
if (result.outcome === "refused") {
|
|
16
|
+
for (const { path, uri } of result.references) console.error(`${path} names ${uri}`);
|
|
17
|
+
} else {
|
|
18
|
+
console.log(`copied as ${result.method.method_id} in ${result.method.org_id}`);
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## The methods
|
|
23
|
+
|
|
24
|
+
| Method | Route | Answer |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `copyMethod(methodId, { destination_org_id, name?, expected_draft_updated_at? }, { idempotencyKey? })` | `POST /v1/methods/{id}/copy` | A `MethodCopyResult`, discriminated on `outcome`: `copied` or `refused`. |
|
|
27
|
+
| `listMethodCopies(methodId, { limit?, cursor? })` | `GET /v1/methods/{id}/copies` | One `MethodCopyPage`, `{ items, nextCursor }`, of `MethodCopyMarker` rows. `limit` is 1 to 100, 50 by default. |
|
|
28
|
+
|
|
29
|
+
Both take a bare catalog id, as every method route does, and refuse a suffixed one (`mt_…@3`) with a `RequestArgumentError` before anything is sent.
|
|
30
|
+
|
|
31
|
+
**The copy's body is closed.** The client sends `destination_org_id` and, when given, `name` and `expected_draft_updated_at`, and nothing else; a `null` reads as not given. `destination_org_id` is the canonical id of the destination, as a membership's `org_id` gives it; `name` is the copy's name, the original's when it is left out; `expected_draft_updated_at` is the original's draft token the caller last saw (`MethodData.updated_at`). A `destination_org_id` that is not a non-empty string, and a `name` or a token given that is not a string, are refused with a `RequestArgumentError` before any request.
|
|
32
|
+
|
|
33
|
+
**Only a web session may copy.** A Pipelex API key belongs to one organization and cannot speak for another, so the platform refuses a copy sent with one `403` `forbidden`, whatever its role. The listing is a read, open to every member of the method's organization and to every credential.
|
|
34
|
+
|
|
35
|
+
## What a copy holds
|
|
36
|
+
|
|
37
|
+
| Part of the method | In the copy |
|
|
38
|
+
|---|---|
|
|
39
|
+
| The draft's files | The same tree, held in the destination, so the copy's `draft_digest` equals the original's; a draft holding no files copies to an empty draft, its `draft_digest` `null` |
|
|
40
|
+
| `name` | The original's, unless the request names another |
|
|
41
|
+
| `description` | The original's, which comes from the same tree |
|
|
42
|
+
| `input_data` | The original's form inputs, every stored file they name in the original's organization copied into the destination |
|
|
43
|
+
| Saved examples | Every example, in its order, each with a new id, at `input_revision` 1, with no latest run and the copier as its author, its inputs' stored files copied as the form inputs' are |
|
|
44
|
+
| Published versions | None: the copy starts unpublished, its `latest_version` and `latest_published` `null`, and its own publish makes version 1 |
|
|
45
|
+
| Runs | None: a run stays in the organization that ran it and was billed for it |
|
|
46
|
+
| `method_id`, `org_id`, `created_by_user_id`, `created_at` | A new id, the destination, the copier and the moment of the copy |
|
|
47
|
+
| `copied_from` | `{ org_id, method_id, draft_digest }` of the original, as it stood at the copy, its `draft_digest` `null` when the original's draft held no files |
|
|
48
|
+
|
|
49
|
+
**The copy takes the draft, never the latest published version.** The draft is what the person sees in the editor, and the copy goes through its own first publish anyway; an invalid draft is copied as it is, and the copy's publish refuses it as it would any draft. With `expected_draft_updated_at`, the copy takes exactly the draft the caller last saw: a draft that moved since is a `409` `method_update_conflict`, and nothing is copied. A client that has just saved the editor's pending edits sends the token that save answered.
|
|
50
|
+
|
|
51
|
+
**A stored file named by an input is copied.** A `pipelex-storage://` address resolves only inside the organization it names, so every such address in the original's organization, in the form inputs and in each example's inputs, is copied into the destination's `assets/` under a new address, and the value is rewritten to it; a file named twice is copied once. A duplicate keeps every address of the organization as it is, an upload's above all, since the organization is the same, and copies only the files the original method's own runs stored, under `pipelex-storage://<org_id>/<method_id>/…`, which erasing the original would sweep away. An address of another organization, a malformed one and one whose file is gone are kept unchanged. The destination records each copied file among its files, attributed to the copier. `stored_files_copied` counts the distinct files copied.
|
|
52
|
+
|
|
53
|
+
## The verdict
|
|
54
|
+
|
|
55
|
+
The answer is a `200` discriminated on `outcome`, as `publishMethod`'s is, because a bundle the copy cannot rely on is a verdict about the method, not a failure of the request. Branch on `outcome`, never on the status:
|
|
56
|
+
|
|
57
|
+
| `outcome` | Members | When |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `copied` | `method`, the copy, a `MethodData` with its `copied_from`; `examples_copied`; `stored_files_copied` | The copy exists in the destination. |
|
|
60
|
+
| `refused` | `reason`, `bundle_names_other_storage`; `message`, a sentence for a person; `references`, every `{ path, uri }` the destination cannot read and, in a duplicate, every one under the original's run files, ordered by path and then by reference | A `.mthds` file of the draft names a stored file outside the destination, which every run there would refuse, or, in a duplicate, a file under the original method's own run files, which erasing the original would remove. Nothing was written. |
|
|
61
|
+
|
|
62
|
+
**A duplicate is held to one more rule.** Every reference to the original's organization is one the destination can read, the original's storage being the destination's, but a reference under the original method's own run files, `pipelex-storage://<org_id>/<method_id>/…`, names a file one of the original's runs stored, and erasing the original sweeps those away: every run of the duplicate would then fail to fetch it. So such a reference is refused too, with the same verdict and the same reason, beside any the destination cannot read. It is the rule the copy's inputs follow: an input naming such a file is re-homed, while a bundle naming one is refused, since the platform rewrites a copy's inputs but never its bundles. Every other reference of the organization, an upload under its `assets/` or another method's run file, passes in a duplicate as it does for the original. A copy into another organization needs no such rule, since it already refuses every reference to the original's organization. The `message` says why for each kind of reference.
|
|
63
|
+
|
|
64
|
+
A refused copy is fixed in the original: the person removes or replaces each reference the verdict names, then copies again. The platform does not rewrite the bundle, since that would change the author's code and the tree's digest without asking them.
|
|
65
|
+
|
|
66
|
+
The client checks the member a caller branches on before it hands the answer back: an `outcome` that is neither arm's, a `copied` answer whose `method` is not an object, and a `refused` one whose `references` is not an array are each an answer the SDK cannot read, thrown as an `ApiResponseError`, `runtime` and not retryable.
|
|
67
|
+
|
|
68
|
+
## What is refused, and where
|
|
69
|
+
|
|
70
|
+
A non-2xx answer means no verdict could be produced, and none of them leaves anything a reader sees, but for the `503` whose `detail` names the copy (below). Each is an `ApiResponseError`; branch on `code` and on the verdict, never on the status alone.
|
|
71
|
+
|
|
72
|
+
| Status | `code` | When |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `403` | `forbidden` | The request is not from a web session. |
|
|
75
|
+
| `403` | `role_not_permitted` | The caller's role changes no method, `runner` or a role the platform does not know, in the method's organization or in the destination. `config`: the role must change. |
|
|
76
|
+
| `404` | `organization_not_found` | The destination is one the caller does not belong to, one whose invitation they have not accepted, one still being set up, or one that does not exist. The platform answers them alike, so it never says whether an organization exists. |
|
|
77
|
+
| `404` | `not_found` | An unknown method. The destination is judged first, so an unknown method in a destination the caller may not write to reads `organization_not_found`. |
|
|
78
|
+
| `409` | `method_being_deleted` | The original's erasure has started. |
|
|
79
|
+
| `409` | `method_update_conflict` | The draft moved since `expected_draft_updated_at`; `errors[0].field` names that member. Read the method again and copy with the fresh token. |
|
|
80
|
+
| `409` | `conflict` | The method's organization holds no record of the draft's tree, or no longer holds every file of it: `runtime` and not retryable, a stored record contradicting itself that neither the caller nor a retry can mend. Its `errorDomain` tells it from the `input` `conflict` of an idempotency key reused with other arguments (below). |
|
|
81
|
+
| `413` | `payload_too_large` | A copied example, or the copy's row, does not fit in one stored item. The row keeps room for the summary a publish adds, so a `name` that would leave none is refused. |
|
|
82
|
+
| `422` | `validation_failed` | A body that does not validate, such as a name holding a lone surrogate. |
|
|
83
|
+
| `422` | `method_copy_too_large` | The copy passes one of its bounds on the stored files it copies. Its `problemDocument.limit`, typed `MethodCopyLimit`, names the bound, `max_stored_files` or `max_stored_bytes`, its `value` and what the copy `reached`. Removing examples, or the files they hold, brings it under. |
|
|
84
|
+
| `502` | `bad_gateway` | The platform could not read the caller's memberships. Retryable. |
|
|
85
|
+
| `500` | `storage_backend_failed` | Storage failed a read, a copy or a write, retryable; or the platform has no usable credentials for storage, not retryable. |
|
|
86
|
+
| `503` | `service_unavailable` | The copy did not finish inside its deadline, a stored file it copied was written over meanwhile, or DynamoDB failed a read or a write. Retryable. When DynamoDB failed both the write of the copy's row and the read that would confirm it, the `detail` names the copy's method id and says that a retry may make a second copy: the copy may stand in the destination, without the original's marker of it and without the destination's records of its files, so look for that id in the destination before copying again. |
|
|
87
|
+
|
|
88
|
+
`MethodErrorCode` names `organization_not_found` and `method_copy_too_large` beside the other method codes ([errors.md](./errors.md#the-method-codes)).
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { ApiResponseError } from "@pipelex/sdk";
|
|
92
|
+
import type { MethodCopyLimit } from "@pipelex/sdk";
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
await client.copyMethod("mt_abc123", { destination_org_id: "org_team" });
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (err instanceof ApiResponseError && err.code === "method_copy_too_large") {
|
|
98
|
+
const limit = err.problemDocument?.limit as MethodCopyLimit;
|
|
99
|
+
console.error(`${limit.name}: ${limit.reached} of at most ${limit.value}`);
|
|
100
|
+
} else throw err;
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Retrying a copy — `idempotencyKey`
|
|
105
|
+
|
|
106
|
+
`copyMethod`'s last argument takes an `idempotencyKey`, sent as the `Idempotency-Key` header of the copy and not sent at all when absent or `null`. Generate one key per copy the caller means to make (a `crypto.randomUUID()` is a good one), and keep it, with the arguments unchanged, across that copy's retries. A key is 1 to 255 visible ASCII characters, `!` to `~`; anything else, an empty string included, is a `RequestArgumentError` before anything is sent, as a start's is.
|
|
107
|
+
|
|
108
|
+
| The copy | The answer |
|
|
109
|
+
|---|---|
|
|
110
|
+
| The first under the key | The copy is made, or refused, and its answer is stored under the key for 24 hours, scoped to the caller and the method's organization, unless it is a `5xx` or a refusal for its timing (`408`, `425` or `429`). |
|
|
111
|
+
| The same arguments under the same key, once the first was answered with a verdict, or with a `4xx` refusal other than one for its timing | The stored answer, and nothing more is copied: a copy whose answer was lost, as on a client timeout, makes one copy, not two, and a copy refused for its content is refused again. |
|
|
112
|
+
| The same arguments under the same key, after a `5xx` or a refusal for its timing, such as a `429` | A fresh attempt: neither is stored, the key was freed, and the failed copy left nothing visible, but for the `503` whose `detail` names the copy, which may stand. |
|
|
113
|
+
| The same arguments under the same key, while the first is still being handled, or while its stored answer cannot be read | A `409` `idempotency_key_in_use`, `runtime` and retryable, answered by the key and never stored: send it again under the same key in a few seconds. |
|
|
114
|
+
| Other arguments under the same key | A `409` `conflict`, `input` and not retryable. A changed copy, such as one with a fresh draft token, goes under a new key. |
|
|
115
|
+
|
|
116
|
+
Without a key, a copy whose answer was lost cannot be told from one that never ran, and sending it again may make a second copy. Even with one, two rare windows can make a second copy on a retry. A platform process can die after writing the copy and before storing its answer, a window the creation of a method shares. And the `503` whose `detail` names the copy released the key while the copy may stand, which is why it names the copy: look for that method id in the destination before copying again.
|
|
117
|
+
|
|
118
|
+
## The copies of a method
|
|
119
|
+
|
|
120
|
+
The original keeps a marker of each copy made of it, and `listMethodCopies` pages them. A `MethodCopyMarker` holds `copy_method_id`, `destination_org_id`, `copied_by_user_id` and `copied_at`. The destination is named by its id alone, never by its name, so the listing tells no reader the name of an organization they do not belong to: a surface shows a destination's name from the reader's own memberships (`listMemberships`), and a copy in an organization the reader does not belong to stays an id. The rows come in the order of the copies' ids, which says nothing of when each was made; sort by `copied_at` to show them by date.
|
|
121
|
+
|
|
122
|
+
Pass the page's `nextCursor` back as `cursor` to continue; `null` is the last page. A method never copied lists an empty page, as does a copy, which names its original through `copied_from` and has no copies of its own. A marker stays when its copy is erased, and the copy then reads as not found. The marker is written once the copy exists, and a failure to write it does not fail the copy, so a copy can, rarely, be missing from the list. The original's erasure and its discard delete its markers with it.
|
|
123
|
+
|
|
124
|
+
The listing answers `404` `not_found` for an unknown method, `409` `method_being_deleted` while its erasure runs, `400` `malformed_request` for a cursor it did not issue for that method, and `422` for a `limit` outside 1 to 100. A page whose `items` is not an array or whose `next_cursor` is neither a string nor `null` is an answer the SDK cannot read.
|
|
125
|
+
|
|
126
|
+
## `copied_from` on a method
|
|
127
|
+
|
|
128
|
+
Every read of a method, `getMethod` and the `method` of every write, carries `copied_from`: `{ org_id, method_id, draft_digest }` of the original on a copy, `null` on a method created afresh, and absent from a platform that predates copies. It is written once, when the copy is made, and never rewritten, so it names the draft the copy started from even after the original has changed or been erased; its `draft_digest` is `null` for a draft that held no tree. It holds opaque ids only, never a name, and the original's organization may be one the reader does not belong to. The list rows (`MethodSummary`) do not carry it.
|