@prized/sdk 0.1.0 → 0.1.2

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.
Files changed (48) hide show
  1. package/README.md +76 -21
  2. package/dist/cjs/commands.d.ts +47 -14
  3. package/dist/cjs/commands.js +94 -26
  4. package/dist/cjs/errors.d.ts +43 -3
  5. package/dist/cjs/errors.js +54 -6
  6. package/dist/cjs/files.d.ts +21 -5
  7. package/dist/cjs/files.js +38 -14
  8. package/dist/cjs/http.d.ts +74 -15
  9. package/dist/cjs/http.js +208 -32
  10. package/dist/cjs/index.d.ts +3 -3
  11. package/dist/cjs/index.js +2 -1
  12. package/dist/cjs/outcome.d.ts +11 -1
  13. package/dist/cjs/outcome.js +28 -5
  14. package/dist/cjs/output.d.ts +19 -8
  15. package/dist/cjs/output.js +152 -56
  16. package/dist/cjs/sandbox.d.ts +3 -2
  17. package/dist/cjs/sandbox.js +40 -5
  18. package/dist/cjs/shell.d.ts +2 -1
  19. package/dist/cjs/shell.js +2 -1
  20. package/dist/cjs/toolbox.d.ts +2 -0
  21. package/dist/cjs/toolbox.js +4 -1
  22. package/dist/cjs/types.d.ts +13 -1
  23. package/dist/cjs/version.d.ts +1 -1
  24. package/dist/cjs/version.js +1 -1
  25. package/dist/esm/commands.d.ts +47 -14
  26. package/dist/esm/commands.js +95 -27
  27. package/dist/esm/errors.d.ts +43 -3
  28. package/dist/esm/errors.js +52 -5
  29. package/dist/esm/files.d.ts +21 -5
  30. package/dist/esm/files.js +39 -15
  31. package/dist/esm/http.d.ts +74 -15
  32. package/dist/esm/http.js +207 -31
  33. package/dist/esm/index.d.ts +3 -3
  34. package/dist/esm/index.js +1 -1
  35. package/dist/esm/outcome.d.ts +11 -1
  36. package/dist/esm/outcome.js +27 -5
  37. package/dist/esm/output.d.ts +19 -8
  38. package/dist/esm/output.js +153 -58
  39. package/dist/esm/sandbox.d.ts +3 -2
  40. package/dist/esm/sandbox.js +40 -5
  41. package/dist/esm/shell.d.ts +2 -1
  42. package/dist/esm/shell.js +2 -1
  43. package/dist/esm/toolbox.d.ts +2 -0
  44. package/dist/esm/toolbox.js +4 -1
  45. package/dist/esm/types.d.ts +13 -1
  46. package/dist/esm/version.d.ts +1 -1
  47. package/dist/esm/version.js +1 -1
  48. package/package.json +1 -1
package/dist/esm/files.js CHANGED
@@ -1,10 +1,16 @@
1
1
  // sandbox.files: read, write, list, remove, upload, download.
2
2
  import { PrizedError } from './errors.js';
3
- import { request } from './http.js';
3
+ import { request, timing } from './http.js';
4
4
  import { mayHaveActed, removeEffect, withOutcome, writeEffect } from './outcome.js';
5
5
  import { existsScript, listScript, removeScript, sandboxPath, shellQuote, WORKSPACE_DIR } from './shell.js';
6
6
  import { helperExec, MAX_FILE_BYTES, rawExec } from './toolbox.js';
7
- /** The sandbox's file transfers take at most this long, retries included. */
7
+ /**
8
+ * The sandbox's file transfers take at most this long, retries included:
9
+ * the edge's own limit on one transfer. An attempt that moves no bytes for
10
+ * timing.transferStallMs (2 minutes) is given up; a download, or a write's
11
+ * staged upload, is then sent again (as after a lost answer or a 502, 503 or
12
+ * 504) while time is left.
13
+ */
8
14
  export const FILE_TIMEOUT_MS = 10 * 60_000;
9
15
  function byteSize(data) {
10
16
  if (typeof data === 'string')
@@ -40,12 +46,16 @@ export const EXIT_TARGET_NOT_REGULAR = 48;
40
46
  const DISCARD_MS = 10_000;
41
47
  /** A write's own commands: a rename, a chmod or an rm. */
42
48
  const WRITE_COMMAND_MS = 60_000;
43
- /** For the staged upload: the target is changed only by the command that follows it. */
49
+ /**
50
+ * For the staged upload: the target is changed only by the command that
51
+ * follows it. The upload may have been sent again, so it says only what to
52
+ * do (langchain-prized's staged_upload_effect words it the same).
53
+ */
44
54
  function stagedEffect(path) {
45
55
  return {
46
56
  maybe: `${path} was not changed (its new bytes may have reached the sandbox, but were not put in place)`,
47
57
  did: `${path} was not changed yet`,
48
- check: 'It was not sent again: write it again.',
58
+ check: 'Write it again.',
49
59
  };
50
60
  }
51
61
  /** For the command that puts the staged upload in place: a 2xx proves it ran, not how it ended. */
@@ -154,6 +164,7 @@ export class Files {
154
164
  retry: 'safe',
155
165
  read: 'bytes',
156
166
  timeoutMs: FILE_TIMEOUT_MS,
167
+ stallMs: timing.transferStallMs,
157
168
  deadline: this.toolbox.config.now() + FILE_TIMEOUT_MS,
158
169
  sandboxId: this.toolbox.sandboxId,
159
170
  });
@@ -165,9 +176,13 @@ export class Files {
165
176
  * A file that is replaced keeps its mode, so a script made executable
166
177
  * stays executable; a new file gets the sandbox's default (0600 in
167
178
  * prized-base-v1 and prized-agent-v1). `mode` sets the permission bits either way.
168
- * Sent again only if it provably never arrived: a write whose answer was
169
- * lost throws, since it may have landed and the sandbox written the file
170
- * since (read it back, or write again, as suits the file).
179
+ * The bytes go up under a temporary name beside the file, sent again if
180
+ * need be (a lost answer, a 502, 503 or 504, 2 minutes with no bytes
181
+ * moving; all copies within the transfer's 10 minutes),
182
+ * and one command that is never sent again puts them in place: a write
183
+ * whose answer to that command was lost throws, since it may have landed
184
+ * and the sandbox written the file since (read it back, or write again,
185
+ * as suits the file).
171
186
  */
172
187
  async write(path, data, options = {}) {
173
188
  const sandboxId = this.toolbox.sandboxId;
@@ -179,14 +194,18 @@ export class Files {
179
194
  const target = sandboxPath(path);
180
195
  const plan = stagingFor(target);
181
196
  if (plan === null)
182
- return this.send(target, data, size, target, writeEffect(target));
197
+ return this.send(target, data, size, target, writeEffect(target), 'exec');
183
198
  const { clean, staging } = plan;
184
199
  try {
185
- await this.send(staging, data, size, clean, stagedEffect(clean));
200
+ // Every copy to the same fresh name, which only the command below puts in place.
201
+ await this.send(staging, data, size, clean, stagedEffect(clean), 'staged');
186
202
  }
187
203
  catch (error) {
188
204
  // A 2xx whose body could not be read: the staged bytes are all there.
189
205
  if (!landed(error)) {
206
+ // Removed only if a copy may have landed: a refusal, or copies that
207
+ // never arrived, left nothing, and a command would wake a sandbox
208
+ // the edge's refusal left paused.
190
209
  if (mayHaveActed(error))
191
210
  await this.discard(staging);
192
211
  throw error;
@@ -212,7 +231,7 @@ export class Files {
212
231
  return { path: clean, size };
213
232
  if (result.exitCode === EXIT_TARGET_IS_LINK) {
214
233
  // A link: the files API follows it or refuses it, as for any upload.
215
- const info = await this.send(clean, data, size, clean, writeEffect(clean));
234
+ const info = await this.send(clean, data, size, clean, writeEffect(clean), 'exec');
216
235
  if (mode !== null)
217
236
  await this.chmod(clean, mode);
218
237
  return info;
@@ -223,18 +242,23 @@ export class Files {
223
242
  const denied = /Permission denied|Operation not permitted/.test(result.stderr);
224
243
  throw new PrizedError(denied ? 'permission_denied' : 'command_failed', `Could not write ${clean}: ${commandDetail(result)}`, { sandboxId });
225
244
  }
226
- /** One upload to `path`, as the files API takes it; `target` is the file the caller asked for, and `effect` what the upload does to it. */
227
- async send(path, data, size, target, effect) {
245
+ /**
246
+ * One upload to `path`, as the files API takes it; `target` is the file the
247
+ * caller asked for, and `effect` what the upload does to it. `exec` for an
248
+ * upload straight to the file: like a command, a resend after the answer
249
+ * was lost could overwrite what the sandbox wrote to the path since.
250
+ * `staged` for one to a fresh name: sent again like a read.
251
+ */
252
+ async send(path, data, size, target, effect, retry) {
228
253
  const result = await request(this.toolbox.config, {
229
254
  method: 'POST',
230
255
  url: this.url('upload', path),
231
256
  body: data,
232
257
  contentType: 'application/octet-stream',
233
- // Like a command: a resend after the answer was lost could overwrite
234
- // what the sandbox wrote to the path since.
235
- retry: 'exec',
258
+ retry,
236
259
  effect,
237
260
  timeoutMs: FILE_TIMEOUT_MS,
261
+ stallMs: timing.transferStallMs,
238
262
  deadline: this.toolbox.config.now() + FILE_TIMEOUT_MS,
239
263
  sandboxId: this.toolbox.sandboxId,
240
264
  });
@@ -6,11 +6,16 @@ export declare const DEFAULT_EDGE_URL = "https://term.prized.dev";
6
6
  export interface ClientOptions {
7
7
  /** Your API key (`dcp_…`). Default: the `PRIZED_API_KEY` environment variable. */
8
8
  apiKey?: string;
9
- /** The API origin. Default: `https://api.prized.dev`. */
9
+ /**
10
+ * The API origin: https, or plain http only on this machine (`localhost`,
11
+ * `127.0.0.1`, `[::1]`). Default: `https://api.prized.dev`.
12
+ */
10
13
  baseUrl?: string;
11
14
  /**
12
- * The origin that serves exec and files. Default: the `toolboxBase` the API
13
- * returns for each sandbox (today `https://term.prized.dev`).
15
+ * The origin that serves exec and files, by the same rule as `baseUrl`.
16
+ * Default: the `toolboxBase` the API returns for each sandbox (today
17
+ * `https://term.prized.dev`), on a host trusted beside the API's
18
+ * (toolboxBaseFor).
14
19
  */
15
20
  edgeUrl?: string;
16
21
  /** A fetch implementation. Default: the global `fetch`. */
@@ -23,10 +28,13 @@ export interface ClientOptions {
23
28
  * the request itself and the API's hold of a create, pause or resume. Default 500. */
24
29
  pollIntervalMs?: number;
25
30
  /**
26
- * How long a streamed or long `exec` keeps polling while its polls fail
27
- * (no answer, a 5xx, a rate limit), in ms, before it gives up and stops
28
- * the command. Default 120 000. `0` gives up once one poll's retries run
29
- * out. `exec` takes it per call too.
31
+ * How long a wait keeps polling while its polls fail (no answer, a 5xx, a
32
+ * rate limit), in ms, before it gives up: a streamed or long `exec` then
33
+ * stops the command, a handle's `wait()` throws. Default 120 000. `0`
34
+ * gives up once one poll's retries run out. `exec` and `wait()` take it
35
+ * per call too. The start of a background, streamed or long `exec` is
36
+ * sent again for as long, so an edge that restarts mid-start does not
37
+ * cost the command.
30
38
  */
31
39
  pollOutageMs?: number;
32
40
  /** Sleep seam (tests inject one that never really waits). */
@@ -48,13 +56,21 @@ export interface Config {
48
56
  sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
49
57
  now: () => number;
50
58
  random: () => number;
59
+ /**
60
+ * Whether an upload's body goes out as a stream the SDK feeds piece by
61
+ * piece, so a stalled one is seen (RequestSpec.stallMs): Node's own fetch
62
+ * only, which asks for each piece once the last has gone into the socket.
63
+ * Elsewhere (another runtime, a fetch you pass) the body goes as it is.
64
+ */
65
+ streamBodies: boolean;
51
66
  }
52
67
  export declare function abortableSleep(ms: number, signal?: AbortSignal): Promise<void>;
53
68
  /**
54
- * How long a streamed or long exec keeps polling while its polls fail
55
- * before it stops the command (pollOutageMs): long enough to ride out a
56
- * dropped connection or an edge that restarts. The Python SDK's
57
- * DEFAULT_POLL_OUTAGE.
69
+ * How long a wait (a streamed or long exec, a handle's wait()) keeps polling
70
+ * while its polls fail before it gives up (pollOutageMs), and a streamed or
71
+ * long exec stops its command, and how long a start is sent again: long
72
+ * enough to ride out a dropped connection or an edge that restarts (about
73
+ * 70 s, the last time it crashed). The Python SDK's DEFAULT_POLL_OUTAGE.
58
74
  */
59
75
  export declare const DEFAULT_POLL_OUTAGE_MS = 120000;
60
76
  /** A pollOutageMs given (a number of ms ≥ 0), or undefined. */
@@ -63,7 +79,10 @@ export declare function resolveConfig(options?: ClientOptions): Config;
63
79
  /**
64
80
  * Where exec and files go for one sandbox. An explicit edgeUrl wins; else the
65
81
  * API's own `toolboxBase` when it has the expected shape (https, or plain
66
- * http on loopback for local development); else the default edge.
82
+ * http on loopback for local development, ending /v1/sandbox/<id>) on a host
83
+ * trusted beside the API's (term.prized.dev beside api.prized.dev; see
84
+ * trustedEdge); else the default edge, for Prized's own API only. The API key
85
+ * is never sent anywhere else: discovery is routing, not authority.
67
86
  */
68
87
  export declare function toolboxBaseFor(config: Config, sandboxId: string, toolboxBase: string | null): string;
69
88
  /**
@@ -88,13 +107,20 @@ export declare function warmEdge(config: Config): Promise<void>;
88
107
  * Retries network failures, 429/502/503/504, and everything `exec`
89
108
  * retries (the edge's 500 control_plane_unavailable among them). A GET
90
109
  * retries any 500 too (the API's `internal`).
91
- * - `exec`: a command, an upload or a remove: what the sandbox does
92
- * meanwhile could be undone by a second copy. Retried only when it
110
+ * - `exec`: a command, an upload, a remove or a label-selected delete:
111
+ * a second copy could affect later work. Retried only when it
93
112
  * provably never arrived: a connection that never opened, or an answer
94
113
  * saying so (the edge's rate limit, its lookup failing). A lost answer
95
114
  * is never retried: it may already have run.
115
+ * - `staged`: a write's upload to a fresh name beside its target, which only
116
+ * a command after it puts in place (files.ts). Retried as `safe` (a lost
117
+ * answer, a timeout, 429/502/503/504) and `exec` both, a GET's 500 aside:
118
+ * every copy writes the same bytes under the same name, which nothing else
119
+ * writes, so a second one changes no file of the caller's. A 2xx whose body
120
+ * broke off is not sent again: the bytes are there. Its error says whether
121
+ * a copy may have reached the sandbox.
96
122
  */
97
- export type RetryMode = 'safe' | 'exec';
123
+ export type RetryMode = 'safe' | 'exec' | 'staged';
98
124
  /**
99
125
  * Whether a fetch that threw never sent its request: the connection was
100
126
  * refused, the name did not resolve, or connecting timed out. Anything else
@@ -123,6 +149,14 @@ export interface RequestSpec {
123
149
  sandboxId?: string | null;
124
150
  /** How to read a 2xx body. Default `json`. */
125
151
  read?: 'json' | 'bytes' | 'none';
152
+ /**
153
+ * A file transfer's no-progress bound: an attempt that moves no bytes
154
+ * either way for this long (a connection dropped without a reset) ends as
155
+ * a timeout, sent again by `retry`'s rules while `deadline` allows. An
156
+ * upload's sending is watched only where its body is streamed
157
+ * (Config.streamBodies); its answer always is.
158
+ */
159
+ stallMs?: number;
126
160
  /**
127
161
  * Told of every attempt that failed (no answer, an error answer, or a 2xx
128
162
  * that could not be read), before it is retried or thrown. A create or
@@ -137,6 +171,12 @@ export interface RequestSpec {
137
171
  * its error says so too.
138
172
  */
139
173
  effect?: Effect;
174
+ /**
175
+ * An earlier copy of this keyed request may have reached the sandbox (the
176
+ * background start, sent again through an outage), so its error says so
177
+ * too.
178
+ */
179
+ acted?: boolean;
140
180
  }
141
181
  export interface HttpResult {
142
182
  status: number;
@@ -199,6 +239,25 @@ export declare class Polls {
199
239
  }
200
240
  /** Delay before retry number `attempt` (0-based): exponential with jitter. */
201
241
  export declare function backoffMs(config: Config, attempt: number): number;
242
+ /**
243
+ * How long a file transfer may move no bytes before the attempt is given up
244
+ * (sdk/retry-spec.json transferStallMs; the Python SDK's TRANSFER_STALL).
245
+ * The edge holds a request about 20 s at most before it reads the body (its
246
+ * admission queue, then waking a paused sandbox), and ends a body that brings
247
+ * nothing for 60 s itself: kept above that, a stalled copy is over at the
248
+ * edge before the next one goes to the same staged name, so the two never
249
+ * race. Once an upload's last byte is handed over, its answer is waited for
250
+ * that and a second more for every uploadAnswerBytesPerSecond it sent: what
251
+ * the socket's buffers, or a proxy that takes a whole body before it sends it
252
+ * on, still hold moves unseen, and must not read as a stall (the Python SDK's
253
+ * UPLOAD_ANSWER_RATE). Mutable for tests only.
254
+ */
255
+ export declare const timing: {
256
+ transferStallMs: number;
257
+ uploadAnswerBytesPerSecond: number;
258
+ };
259
+ /** A streamed upload goes out this many bytes at a time. */
260
+ export declare const UPLOAD_CHUNK: number;
202
261
  /**
203
262
  * What an error answer (not a 2xx: request() judges those) says about
204
263
  * whether the sandbox acted: `never` for the codes that prove the request