@pipelex/sdk 0.17.0 → 0.18.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 CHANGED
@@ -10,6 +10,8 @@ Early. `PipelexApiClient` implements the MTHDS protocol-execution routes (`execu
10
10
 
11
11
  Besides the client, the package exports `runCodegenCheck` — a **pure** offline check that verifies a committed `codegen()` tree still matches its `codegen.lock`. It needs no server, no key, and no client instance, so it fits a CI job. See [`docs/crate-routes.md`](./docs/crate-routes.md#the-offline-check--runcodegencheck).
12
12
 
13
+ It also exports `summarizeUsage`, which folds a completed run's usage records into one null-aware summary — total cost, input and output tokens, and a per-pipe rollup — without any I/O. See [`docs/run-usage.md`](./docs/run-usage.md#summarizing-a-run--summarizeusage).
14
+
13
15
  ## Install
14
16
 
15
17
  ```bash
@@ -32,8 +34,8 @@ const report = await client.validate(["domain = 'demo'"]);
32
34
  if (report.is_valid) {
33
35
  // Run it and wait for the result (durable start + poll on the hosted API).
34
36
  const result = await client.startAndWaitForResult({ pipe_code: "demo.greet" });
35
- // Every completed run delivers a resolved `main_stuff` (the full working memory
36
- // also rides `pipe_output` on the blocking path).
37
+ // Every completed run delivers a resolved `main_stuff`, and every named stuff of
38
+ // the run in `working_memory`.
37
39
  console.log(result.main_stuff);
38
40
  }
39
41
 
@@ -0,0 +1,284 @@
1
+ /**
2
+ * The artifact stack — the download twin of `prepareInputs`, in layers so each
3
+ * operation is usable without the next:
4
+ *
5
+ * - {@link collectArtifacts} — a pure walk of any JSON value for the strings that
6
+ * ARE `pipelex-storage://` references. No network, no key.
7
+ * - {@link resolveArtifacts} — the platform's bulk resolve route over a whole
8
+ * list, chunked at the route's bound, one verdict per reference.
9
+ * - {@link fetchArtifact} — a bounded `Response` for one reference: resolved
10
+ * fresh, timed out, redirects refused, the byte cap enforced mid-stream, no
11
+ * credentials forwarded, headers untouched.
12
+ * - {@link downloadArtifacts} — Node-only: a run's produced files saved under a
13
+ * directory by a bounded pool of workers, as a produced verdict.
14
+ *
15
+ * A produced file is never embedded in a run's results: the content carries its
16
+ * durable `pipelex-storage://` reference beside a signed `public_url` that expires
17
+ * on the provider's schedule. Nothing here reads that embedded link — every link
18
+ * is minted fresh by the platform, and re-minted when it has expired by the time a
19
+ * worker reaches it. See `docs/artifact-download.md`.
20
+ *
21
+ * `node:fs/promises`, `node:path` and `undici` are imported dynamically, behind
22
+ * `isNodeRuntime()`, so the module's static import graph stays free of Node
23
+ * builtins: `collectArtifacts` and `resolveArtifacts` run anywhere, `fetchArtifact`
24
+ * runs anywhere the global `fetch` does (the dispatcher is a Node refinement), and
25
+ * only `downloadArtifacts` touches a filesystem.
26
+ */
27
+ import type { RunResults, RunResultState } from "./runs.js";
28
+ /** The scheme of a durable storage reference. */
29
+ export declare const PIPELEX_STORAGE_SCHEME = "pipelex-storage://";
30
+ /**
31
+ * How many references one bulk resolve request takes — the route's bound, fixed
32
+ * by the platform contract rather than configured per deployment. A longer list
33
+ * is a `422`, so {@link resolveArtifacts} chunks at this size.
34
+ */
35
+ export declare const BULK_RESOLVE_MAX_URIS = 100;
36
+ /**
37
+ * The per-file byte cap. An accident guard against filling a disk from a
38
+ * runaway output, not a judgment about artifact size: a produced file is
39
+ * server-side, so a caller cannot shrink it the way they can shrink an upload.
40
+ */
41
+ export declare const DEFAULT_ARTIFACT_MAX_BYTES: number;
42
+ /** The per-file budget for connecting, receiving the headers and reading the body. */
43
+ export declare const DEFAULT_ARTIFACT_TIMEOUT_MS = 120000;
44
+ /** The cap on the bytes one `downloadArtifacts` call writes in total. */
45
+ export declare const DEFAULT_DOWNLOAD_MAX_TOTAL_BYTES: number;
46
+ /** How many artifacts one `downloadArtifacts` call has in flight at once. */
47
+ export declare const DEFAULT_DOWNLOAD_CONCURRENCY = 4;
48
+ /**
49
+ * Why one reference failed — a value, never a thrown error. `code` is the
50
+ * resolve route's own per-reference code (`invalid_storage_uri`, `forbidden`)
51
+ * or one of the fetch boundary's (see `ArtifactFetchError`), plus the download's
52
+ * own `resolve_failed`, `total_limit_exceeded`, `write_failed` and `aborted`.
53
+ * `detail` is the sentence a person reads.
54
+ */
55
+ export interface ArtifactItemError {
56
+ code: string;
57
+ detail: string;
58
+ }
59
+ /**
60
+ * One reference's resolution — the bulk resolve route's item, verbatim. Either
61
+ * the link fields are set and `error` is null, or `error` is set and the three
62
+ * link fields are null. A consumer branches on `error`, never on an HTTP status:
63
+ * the request was a `200` whenever every reference got a verdict.
64
+ */
65
+ export type ResolvedArtifact = {
66
+ /** The reference exactly as sent. */
67
+ uri: string;
68
+ /** A presigned link, fetchable now and for about fifteen minutes. */
69
+ url: string;
70
+ /** UTC expiry of the link, ISO 8601. */
71
+ expires_at: string;
72
+ /** The platform's content-type guess from the reference's extension; null when it has none. */
73
+ content_type: string | null;
74
+ error: null;
75
+ } | {
76
+ uri: string;
77
+ url: null;
78
+ expires_at: null;
79
+ content_type: null;
80
+ error: ArtifactItemError;
81
+ };
82
+ /** The wire request of `POST /v1/resolve-storage-url/bulk` — at most {@link BULK_RESOLVE_MAX_URIS} references. */
83
+ export interface BulkResolveStorageUrlsInput {
84
+ uris: string[];
85
+ }
86
+ /** The wire response of `POST /v1/resolve-storage-url/bulk` — one item per requested reference, in request order. */
87
+ export interface BulkResolvedStorageUrls {
88
+ items: ResolvedArtifact[];
89
+ }
90
+ /** The bounds `fetchArtifact` applies. Every one has a safe default. */
91
+ export interface FetchArtifactOptions {
92
+ /** Refuse (before a byte is written) and cut (mid-stream) a body over this many bytes. Default 1 GiB. */
93
+ maxBytes?: number;
94
+ /** Budget for the whole exchange — connect, headers and body. Default 120 s. */
95
+ timeoutMs?: number;
96
+ /**
97
+ * Accept a plain `http:` link. Off by default: a general-purpose library does
98
+ * not fetch over plain http silently. The local compose stack's object store
99
+ * hands out such links, which is the case this opts into.
100
+ */
101
+ allowHttp?: boolean;
102
+ /** Cancels the fetch; a body being read is cancelled too. */
103
+ signal?: AbortSignal;
104
+ }
105
+ /** Which of a run's artifacts `downloadArtifacts` walks for references. */
106
+ export type ArtifactScope = "main_stuff" | "working_memory";
107
+ /** The options shared by both forms of a `downloadArtifacts` request. */
108
+ export interface DownloadArtifactsOptions extends FetchArtifactOptions {
109
+ /** The directory to save under, created if missing. Files are never overwritten. */
110
+ dir: string;
111
+ /**
112
+ * `main_stuff` (the default) walks the run's main output. `working_memory` is
113
+ * the opt-in that also brings down the echoed inputs and every intermediate.
114
+ */
115
+ scope?: ArtifactScope;
116
+ /** How many artifacts are in flight at once. Default 4. */
117
+ concurrency?: number;
118
+ /**
119
+ * Cap on the bytes written by the whole call. Default 4 GiB. The item that would
120
+ * cross it is an item error, and the items not yet started are skipped with the
121
+ * same reason.
122
+ */
123
+ maxTotalBytes?: number;
124
+ }
125
+ /**
126
+ * What `downloadArtifacts` takes: the options plus exactly one of `run_id` (the
127
+ * results are re-read, so this works days after the run) or `results` (a
128
+ * `RunResults` already in hand, read as it is). The type pins the other to
129
+ * `never`, so naming both is a compile error.
130
+ */
131
+ export type DownloadArtifactsRequest = DownloadArtifactsOptions & ({
132
+ run_id: string;
133
+ results?: never;
134
+ } | {
135
+ results: RunResults;
136
+ run_id?: never;
137
+ });
138
+ /**
139
+ * One reference's outcome in a download verdict — one shape with nullable fields,
140
+ * like {@link ResolvedArtifact}: either `path` and `size` are set and `error` is
141
+ * null, or `error` is set and both are null. `content_type` is the platform's
142
+ * guess from the reference's extension, known before the fetch, on both arms.
143
+ */
144
+ export type DownloadedArtifact = {
145
+ uri: string;
146
+ /** Absolute path of the written file. */
147
+ path: string;
148
+ content_type: string | null;
149
+ /** Bytes written. */
150
+ size: number;
151
+ error: null;
152
+ } | {
153
+ uri: string;
154
+ path: null;
155
+ content_type: string | null;
156
+ size: null;
157
+ error: ArtifactItemError;
158
+ };
159
+ /**
160
+ * The produced verdict of `downloadArtifacts`. `artifacts.length` is the count
161
+ * of references walked, errors included; an empty list over a present scope is
162
+ * a verdict ("this output references no stored file"), not an error.
163
+ */
164
+ export interface DownloadArtifactsResult {
165
+ scope: ArtifactScope;
166
+ /** One entry per reference, in discovery order. */
167
+ artifacts: DownloadedArtifact[];
168
+ /** The absolute paths of the files saved, in the same order. */
169
+ saved_paths: string[];
170
+ /** True when every walked reference was saved — vacuously true for an empty walk. */
171
+ all_saved: boolean;
172
+ /** Present, and true, only when the caller's `signal` aborted the call. */
173
+ aborted?: true;
174
+ }
175
+ /**
176
+ * The client surface the artifact operations need: the raw bulk resolve call and
177
+ * the single-shot result lookup. Typed as the client's own signatures so
178
+ * `PipelexApiClient` satisfies it structurally, and so tests inject a fake.
179
+ */
180
+ export interface ArtifactCapableClient {
181
+ resolveStorageUrls(input: BulkResolveStorageUrlsInput, options?: {
182
+ signal?: AbortSignal;
183
+ }): Promise<BulkResolvedStorageUrls>;
184
+ getRunResult(runId: string, options?: {
185
+ signal?: AbortSignal;
186
+ }): Promise<RunResultState>;
187
+ }
188
+ /**
189
+ * Every `pipelex-storage://` reference inside a JSON-shaped value, deduplicated,
190
+ * in discovery order. A string counts only when it IS a reference — the whole
191
+ * string, scheme first, with something after the scheme; text that merely
192
+ * contains one does not. The scheme is unambiguous, so this walk is a contract
193
+ * rather than a heuristic: the runtime serializes a produced image or document
194
+ * as content carrying its reference in `url`, beside an expiring `public_url`
195
+ * this walk ignores. Pure — no network, no key — so a consumer can count or
196
+ * list a run's produced files without resolving any of them.
197
+ */
198
+ export declare function collectArtifacts(value: unknown): string[];
199
+ /** A string that is a storage reference: the scheme, then at least one character. */
200
+ export declare function isStorageReference(value: string): boolean;
201
+ /**
202
+ * The bare filename a storage reference is saved under: the last segment of the
203
+ * storage key, reduced to a conservative character set so it can never name
204
+ * anything but a regular file directly inside the target directory. Path
205
+ * separators are the split point, so no traversal survives; leading dots are
206
+ * stripped, so no hidden file and no `..`; everything outside `[A-Za-z0-9._-]`
207
+ * becomes `_`; an empty result falls back to a numbered `artifact-N`. Length is
208
+ * capped with the extension preserved, and an extension is added from the
209
+ * content type when the key carries none. A collision on disk is not this
210
+ * function's concern: `downloadArtifacts` suffixes the stem (`name-1.ext`) on
211
+ * exclusive creation, so a file is never overwritten.
212
+ */
213
+ export declare function artifactFilename(uri: string, contentType: string | null | undefined, index: number): string;
214
+ /**
215
+ * Resolve a list of references through the bulk route, chunked at
216
+ * {@link BULK_RESOLVE_MAX_URIS} per request, and answer one
217
+ * {@link ResolvedArtifact} per reference in request order, duplicates included.
218
+ * Per-reference failure is a value on the item; only what is not about a
219
+ * reference throws — the route's whole-request refusals as `ApiResponseError`
220
+ * (a caller with no organization, a malformed request, a signing failure, or a
221
+ * `404` from a deployment that does not serve the route) and an unreachable
222
+ * host as `ApiUnreachableError`. An empty list resolves to an empty list with
223
+ * no request made.
224
+ */
225
+ export declare function resolveArtifacts(client: Pick<ArtifactCapableClient, "resolveStorageUrls">, uris: string[], options?: {
226
+ signal?: AbortSignal;
227
+ }): Promise<ResolvedArtifact[]>;
228
+ /**
229
+ * A bounded `Response` for one reference: the link is minted fresh through the
230
+ * bulk route, then fetched with `redirect: "manual"` (a presigned link has no
231
+ * reason to redirect, and one that does is refused rather than followed), no
232
+ * headers of ours (the link carries its own authorization in the query string,
233
+ * and nothing must ride along to the object store), a budget covering the
234
+ * headers and the body, and a byte cap checked against `Content-Length` before
235
+ * a byte is read and again on every chunk. The status, status text and headers
236
+ * are the store's own, except that a `Content-Encoding` the fetch already decoded
237
+ * is dropped with the encoded `Content-Length`: a proxy relaying this response sets its own
238
+ * `Content-Disposition`, `X-Content-Type-Options`, CSP and caching headers,
239
+ * because this function does not.
240
+ *
241
+ * Only a `2xx` is returned. Anything else is thrown as `ArtifactFetchError`
242
+ * with a `code` (a redirect, a refused or vanished object, a store fault, a
243
+ * declared oversize, a timeout, a network fault, an unusable link, or the
244
+ * route's own per-reference refusal); a body that crosses the cap mid-stream
245
+ * errors the returned stream with the same error type (`too_large`). The
246
+ * caller's abort propagates as-is. Whole-request failures of the resolve step
247
+ * propagate unchanged (`ApiResponseError`, `ApiUnreachableError`).
248
+ */
249
+ export declare function fetchArtifact(client: Pick<ArtifactCapableClient, "resolveStorageUrls">, uri: string, options?: FetchArtifactOptions): Promise<Response>;
250
+ /** The write side of a streamed download: what {@link writeFully} needs of a `FileHandle`. */
251
+ export interface ChunkWriter {
252
+ write(buffer: Uint8Array, offset: number, length: number): Promise<{
253
+ bytesWritten: number;
254
+ }>;
255
+ }
256
+ /**
257
+ * Write the whole chunk, looping on short writes. `FileHandle.write()` resolves
258
+ * with the count it managed, which can fall short of the chunk on a filesystem
259
+ * under pressure; taking one call as "written" would report a truncated file
260
+ * as saved.
261
+ */
262
+ export declare function writeFully(writer: ChunkWriter, chunk: Uint8Array): Promise<void>;
263
+ /**
264
+ * Save a run's produced files under `dir` — Node-only, like the path-string arm
265
+ * of `uploadFile`. Takes a `run_id` (the results are re-read, so a run is
266
+ * downloadable days later) or a `RunResults` in hand; walks the requested scope
267
+ * with {@link collectArtifacts}; resolves the whole set through the bulk route
268
+ * ahead of the workers; then a bounded pool of workers each fetch → open with
269
+ * `wx` → write, re-resolving any link that has expired by the time a worker
270
+ * reaches it. The embedded `public_url` is never used. Files are named by
271
+ * {@link artifactFilename} and never overwritten; a failed or aborted download
272
+ * unlinks its partial file.
273
+ *
274
+ * Returns a produced verdict: one entry per reference, errors as values. It
275
+ * throws only when no verdict can be produced — `RunStillRunningError` or
276
+ * `RunFailedError` for a run that has not completed, `ScopeUnavailableError`
277
+ * when the scope's artifact is null or missing, `ArtifactAuthenticationError`
278
+ * (carrying the verdict so far) when the resolve route refuses the credential,
279
+ * `ArtifactOperationError` outside Node or for an unusable `dir`, and the
280
+ * transport and lifecycle errors of the reads it makes (`ApiResponseError` for
281
+ * a deployment without the bulk route, `RunLifecycleUnavailableError` for a
282
+ * bare runner asked by id, `ApiUnreachableError`).
283
+ */
284
+ export declare function downloadArtifacts(client: ArtifactCapableClient, request: DownloadArtifactsRequest): Promise<DownloadArtifactsResult>;