@shardflux/sdk 0.8.0 → 0.9.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/dist/cell.d.ts CHANGED
@@ -14,8 +14,17 @@
14
14
  * Exec output is retained in the guest and addressed by byte offsets, so
15
15
  * `exec.run()` survives gateway restarts/disconnects by reconnecting with the
16
16
  * offsets it already processed; it never starts the command a second time.
17
+ *
18
+ * File-first workspaces (contracts §29) serve the files routes from a
19
+ * versioned tree and run commands as executions (`executions.run()`); the
20
+ * client tracks the tree revision every response reports (`X-Tree-Revision`)
21
+ * and, when it knows the workspace's mode, refuses calls the mode does not
22
+ * have before sending them (NotSupportedForModeError).
17
23
  */
18
24
  import type { components, paths } from './generated/cell-api.js';
25
+ import type { WorkspaceMode } from './errors.js';
26
+ import { ExecutionResult } from './executions.js';
27
+ import type { ExecutionGetOptions, ExecutionRunOptions } from './executions.js';
19
28
  import type { RequestOptions } from './http.js';
20
29
  import type { ProgressListener } from './progress.js';
21
30
  import type { ToolTokenManager } from './tokens.js';
@@ -30,6 +39,81 @@ export type ProcessList = S['ProcessList'];
30
39
  export type FileInfo = S['FileInfo'];
31
40
  export type FileList = S['FileList'];
32
41
  export type FileWriteResult = S['FileWriteResult'];
42
+ /** Search request/result of `POST /files/search` (contracts §26.1). */
43
+ export type FileSearchRequest = S['FileSearchRequest'];
44
+ export type FileSearchMatch = S['FileSearchMatch'];
45
+ export type FileSearchResult = S['FileSearchResult'];
46
+ /** One text edit of `POST /files/patch` (contracts §26.2), as sent on the wire. */
47
+ export type FileEdit = S['FileEdit'];
48
+ export type FilePatchRequest = S['FilePatchRequest'];
49
+ export type FilePatchResult = S['FilePatchResult'];
50
+ /** A content SHA-256 (lowercase hex), or `absent` for a path that must not exist. */
51
+ export type FileRevision = S['FileRevision'];
52
+ export type WakeHintResult = S['WakeHintResult'];
53
+ /** Where a running workspace's VM is (contracts §25.1): resident, frozen, hibernated or restoring. */
54
+ export type Residency = WakeHintResult['residency'];
55
+ /** `X-Served-From`: `disk` when a read was served from a suspended or hibernated workspace's disk (contracts §26.4). */
56
+ export type ServedFrom = 'guest' | 'disk';
57
+ /** `files.readWithInfo()`: the bytes plus what the gateway reported about the file. */
58
+ export interface FileReadResult {
59
+ data: Uint8Array;
60
+ /** `X-File-Size`: the file's size when the read started (null when the gateway did not send it). */
61
+ size: number | null;
62
+ /**
63
+ * `X-File-Revision`: SHA-256 of the whole file (regular files of 16 MiB or less), usable as `expectedRevision` of
64
+ * `files.patch()`. Null when absent, and when a read continued over several requests saw different revisions (the
65
+ * file changed while it was read).
66
+ */
67
+ revision: string | null;
68
+ /** `X-Served-From`: `disk` for a read of a sleeping workspace's disk (the state at suspension); null when absent. */
69
+ servedFrom: ServedFrom | null;
70
+ }
71
+ export interface FileSearchOptions {
72
+ /** RE2 syntax when true; a literal substring otherwise (default false). */
73
+ regex?: boolean;
74
+ caseInsensitive?: boolean;
75
+ /**
76
+ * Globs (`*`, `**`, `?`, `[...]`), gitignore-style: without `/` a pattern matches a name at any depth (`*.py`); with
77
+ * `/` it matches the path relative to the searched directory (`src/**\/*.ts`; a leading `/` anchors it there, a
78
+ * trailing `/` matches directories only). A file must match one.
79
+ */
80
+ include?: string[];
81
+ /** Globs skipped, same syntax (e.g. `build/`; default `.git` and `node_modules` anywhere; `[]` searches everything). */
82
+ exclude?: string[];
83
+ /** 1-5000 (gateway default 200). */
84
+ maxMatches?: number;
85
+ /** Files above this size are skipped (gateway default 1 MiB, at most 64 MiB). */
86
+ maxFileBytes?: number;
87
+ /** Lines of context before and after each match, 0-5. */
88
+ contextLines?: number;
89
+ signal?: AbortSignal;
90
+ }
91
+ /** `files.search()`: the gateway's result plus `served_from` (`X-Served-From`; null when the gateway sent none). */
92
+ export type FileSearchResponse = FileSearchResult & {
93
+ served_from: ServedFrom | null;
94
+ };
95
+ /** One edit of `files.patch()`: `oldText` must occur exactly once in the file, or any number of times with `replaceAll`. */
96
+ export interface FilePatchEdit {
97
+ oldText: string;
98
+ newText: string;
99
+ replaceAll?: boolean;
100
+ }
101
+ interface FilePatchCommon {
102
+ path: string;
103
+ /** The file's current revision (`files.stat(path, { revision: true })`, a read, write or patch result), or `absent`. */
104
+ expectedRevision?: FileRevision;
105
+ createParents?: boolean;
106
+ /** Permission bits for a new file (an existing file keeps its mode and owner). */
107
+ mode?: string;
108
+ }
109
+ /** `files.patch()`: exactly one of `edits` (applied in order to UTF-8 text) or `content` (the whole new file). */
110
+ export type FilePatchParams = (FilePatchCommon & {
111
+ edits: FilePatchEdit[];
112
+ content?: undefined;
113
+ }) | (FilePatchCommon & {
114
+ content: string;
115
+ edits?: undefined;
116
+ });
33
117
  export type GitCloneRequest = S['GitCloneRequest'];
34
118
  export type GitCommitRequest = S['GitCommitRequest'];
35
119
  export type GitResult = S['GitResult'];
@@ -72,7 +156,8 @@ export interface CellClientOptions {
72
156
  * surfaces. It throws when the wake fails (OperationFailedError) or outlasts `timeoutMs` (OperationTimeoutError).
73
157
  * Workspace.cell() supplies `workspace.wake()`. A call refused with `workspace_not_running` wakes the workspace and is
74
158
  * retried; a refused call was never executed (contracts §20.4), so the retry cannot duplicate it. `null` surfaces
75
- * the refusal instead.
159
+ * the refusal instead. A wake that put a new token into this client's manager (the held resume, contracts §22.6;
160
+ * `workspace.wake({ agentLabel, tools })`) is retried with it; otherwise the token is replaced before the retry.
76
161
  */
77
162
  wake?: ((timeoutMs: number, signal?: AbortSignal) => Promise<boolean | void>) | null;
78
163
  /**
@@ -88,11 +173,32 @@ export interface CellClientOptions {
88
173
  onProgress?: ProgressListener;
89
174
  /** Internal: see CAPTURE_BARRIER. */
90
175
  [CAPTURE_BARRIER]?: (() => Promise<unknown> | undefined) | undefined;
176
+ /**
177
+ * The workspace's mode (contracts §29), or a function returning it (undefined: unknown). When known, calls the mode
178
+ * does not have fail at once with NotSupportedForModeError (`local` true) instead of a request: exec sessions, PTY,
179
+ * processes, version control, browser and changes on a file-first workspace; executions and `ifTreeRevision` on a
180
+ * processful one. Workspace.cell() supplies the workspace's.
181
+ */
182
+ mode?: WorkspaceMode | (() => WorkspaceMode | undefined);
183
+ /** Called with every tree revision a response reports (`X-Tree-Revision`, execution results, mismatch refusals). */
184
+ onTreeRevision?: (revision: number) => void;
185
+ }
186
+ /** `ifTreeRevision` (file-first workspaces, contracts §29.8): the tree revision a mutating files call applies to. */
187
+ export interface TreeRevisionOptions {
188
+ /**
189
+ * Sent as `If-Match`: the call applies only while the tree is at this revision (`workspace.treeRevision`, or a
190
+ * revision an earlier call or execution reported), else it fails with TreeRevisionMismatchError (409 `conflict`,
191
+ * `tree_revision_mismatch`, `currentTreeRevision`) and nothing changes. File-first only: a processful workspace would
192
+ * ignore it, so the SDK refuses it there (NotSupportedForModeError).
193
+ */
194
+ ifTreeRevision?: number;
91
195
  }
92
196
  /** Per-request transition handling (internal to CellClient). */
93
197
  interface TransitionOptions {
94
198
  /** Wake a suspended workspace for this call (default true; exec output follow reconnects pass false). */
95
199
  wake?: boolean;
200
+ /** Wait out `workspace_busy` (default true; the wake hint passes false: it is best effort). */
201
+ busy?: boolean;
96
202
  }
97
203
  export declare const DEFAULT_TRANSITION_TIMEOUT_MS = 120000;
98
204
  /**
@@ -149,6 +255,13 @@ export declare class CellClient {
149
255
  readonly workspaceId: string;
150
256
  readonly tokens: ToolTokenManager;
151
257
  constructor(workspaceId: string, tokens: ToolTokenManager, opts?: CellClientOptions);
258
+ /** The workspace's mode when known (CellClientOptions.mode), else undefined. */
259
+ get mode(): WorkspaceMode | undefined;
260
+ /**
261
+ * The newest tree revision this client has seen (file-first workspaces: `X-Tree-Revision` of any response, execution
262
+ * results, `tree_revision_mismatch` refusals); null before the first. `workspace.treeRevision` combines every client's.
263
+ */
264
+ get treeRevision(): number | null;
152
265
  /** True after close(): every request (and stream) of this client is aborted. */
153
266
  get closed(): boolean;
154
267
  /**
@@ -164,6 +277,10 @@ export declare class CellClient {
164
277
  * Refused calls were never executed, so retrying is safe. When the budget is spent the refusal surfaces.
165
278
  */
166
279
  request(method: string, path: string, init?: RequestOptions & TransitionOptions): Promise<Response>;
280
+ /**
281
+ * Exec sessions of a processful workspace. On a file-first workspace these are refused (409 not_supported_for_mode;
282
+ * locally when the mode is known): use `executions.run()`.
283
+ */
167
284
  readonly exec: {
168
285
  /** Starts argv (no shell). Idempotent by session_id: an existing session is returned, never re-run. */
169
286
  start: (req: ExecStartRequest, signal?: AbortSignal) => Promise<ExecSession>;
@@ -180,9 +297,35 @@ export declare class CellClient {
180
297
  /**
181
298
  * Starts (or re-attaches to) a session and collects its output until it exits, reconnecting
182
299
  * with offsets after dropped streams. Never issues a second start for the same session_id.
300
+ * A file-first workspace has no sessions: use `executions.run()` (refused locally when the mode is known).
183
301
  */
184
302
  run: (argv: string[], opts?: RunOptions) => Promise<RunResult>;
185
303
  };
304
+ readonly executions: {
305
+ /**
306
+ * Runs argv (no shell; `['bash', '-lc', line]` for shell syntax) as an execution of this file-first workspace: a
307
+ * fresh VM on the latest tree revision, answered when the command ended. The files it changed under /home/user are
308
+ * the next revision (`treeRevision`, `changed`); processes, memory and files elsewhere do not survive it.
309
+ *
310
+ * The execution id (`executionId`, default a fresh `ex-<uuid>`) is the idempotency key: network failures and
311
+ * retryable 429/5xx answers (503 `no_execution_host`: no host has room, with Retry-After) are retried with the same
312
+ * id and request, at most `maxRetries` times, and an attempt that waits longer than `attemptTimeoutMs` re-attaches
313
+ * with the same id. The cell answers 201 for the call that ran the command and 200 (`replayed`) with the recorded
314
+ * result otherwise, so the command runs at most once. `workspace_busy` (`execution_in_progress`: another execution
315
+ * holds the workspace) is waited out within `transitionTimeoutMs`; `execution_id_reused` (the id with another
316
+ * request) and validation errors are thrown at once. The SDK never retries with a new id: a result whose `state` is
317
+ * `failed` or `lost` (nothing published; `error.details.reason` says why) is returned, and running it again is the
318
+ * caller's decision. Processful workspaces: NotSupportedForModeError (locally when the mode is known).
319
+ */
320
+ run: (argv: string[], opts?: ExecutionRunOptions) => Promise<ExecutionResult>;
321
+ /**
322
+ * The execution `executionId` of this file-first workspace (`GET /executions/{id}`): its result once it ended, or a
323
+ * pending result (`pending`, state `queued`/`running`, no output yet) while it runs; with `waitMs`, polls until it
324
+ * ended or the wait elapsed. Results are kept 7 days; an unknown id is 404 `not_found`. `replayed` is true.
325
+ */
326
+ get: (executionId: string, opts?: ExecutionGetOptions) => Promise<ExecutionResult>;
327
+ };
328
+ /** PTY sessions of a processful workspace (file-first: NotSupportedForModeError). */
186
329
  readonly pty: {
187
330
  open: (req?: PtyOpenRequest) => Promise<PtySession>;
188
331
  get: (sessionId: string) => Promise<PtySession>;
@@ -224,33 +367,79 @@ export declare class CellClient {
224
367
  offset?: number;
225
368
  length?: number;
226
369
  }) => Promise<Uint8Array>;
370
+ /**
371
+ * Like `read()`, plus the file's size, `revision` (SHA-256 of the whole file, `X-File-Revision`, for regular files of
372
+ * 16 MiB or less; pass it as `expectedRevision` to `patch()`) and `servedFrom` (`disk` when a sleeping workspace was
373
+ * read from its disk without waking it, contracts §26.4). 0.9.0+.
374
+ */
375
+ readWithInfo: (path: string, opts?: {
376
+ offset?: number;
377
+ length?: number;
378
+ }) => Promise<FileReadResult>;
227
379
  readText: (path: string, opts?: {
228
380
  offset?: number;
229
381
  length?: number;
230
382
  }) => Promise<string>;
231
- /** Atomic replace (or append), acknowledged after fsync of file and parent directory. */
383
+ /**
384
+ * Atomic replace (or append), acknowledged after fsync of file and parent directory (file-first: once the new tree
385
+ * revision is published). `ifTreeRevision` (file-first) applies it only at that tree revision.
386
+ */
232
387
  write: (path: string, data: string | Uint8Array, opts?: {
233
388
  mode?: string;
234
389
  createParents?: boolean;
235
390
  append?: boolean;
236
391
  idempotencyKey?: string;
237
- }) => Promise<FileWriteResult>;
392
+ } & TreeRevisionOptions) => Promise<FileWriteResult>;
238
393
  remove: (path: string, opts?: {
239
394
  recursive?: boolean;
240
- }) => Promise<void>;
241
- stat: (path: string) => Promise<FileInfo>;
395
+ } & TreeRevisionOptions) => Promise<void>;
396
+ /** `revision: true` (0.9.0+) adds `revision`, the SHA-256 of a regular file's content (files of up to 256 MiB). */
397
+ stat: (path: string, opts?: {
398
+ revision?: boolean;
399
+ }) => Promise<FileInfo>;
242
400
  list: (path: string, opts?: {
243
401
  limit?: number;
244
402
  }) => Promise<FileList>;
245
403
  mkdir: (path: string, opts?: {
246
404
  parents?: boolean;
247
405
  mode?: string;
248
- }) => Promise<FileInfo>;
406
+ } & TreeRevisionOptions) => Promise<FileInfo>;
249
407
  move: (from: string, to: string, opts?: {
250
408
  overwrite?: boolean;
251
- }) => Promise<FileInfo>;
409
+ } & TreeRevisionOptions) => Promise<FileInfo>;
410
+ /**
411
+ * Searches file contents under `path`, a directory or one file (contracts §26.1; 0.9.0+): matching lines in path
412
+ * order, bounded by `maxMatches`, a 10 s budget and 4 MiB of results (`truncated`, `stop_reason` `max_matches`,
413
+ * `budget` or `max_bytes`). Binary files, symbolic links, special files and files above `maxFileBytes` are
414
+ * skipped. Read-only, so transient failures (e.g. 503 `host_capacity`) are retried; a suspended workspace whose
415
+ * disk a host still holds is searched there without waking it (`served_from: 'disk'`).
416
+ */
417
+ search: (path: string, pattern: string, opts?: FileSearchOptions) => Promise<FileSearchResponse>;
418
+ /**
419
+ * Applies text `edits` (each `oldText` must occur exactly once unless `replaceAll`; in order) or replaces the whole
420
+ * file with `content`, atomically and durably (contracts §26.2; 0.9.0+). With `expectedRevision` the file must
421
+ * still have that revision (`absent`: must not exist), else 409 `conflict` `revision_mismatch` with
422
+ * `details.current_revision`. Edits that do not apply: 422 `validation_failed` `edit_not_found` / `edit_ambiguous`
423
+ * (`details.index`), `edit_not_text`. Limits: a request of up to 7 MiB (else 413 `payload_too_large`; larger
424
+ * content goes through `write()`), files of up to 64 MiB, no symlink targets. Always sends an Idempotency-Key, so
425
+ * a retried call is applied once (a replay returns the recorded result, refusals included). `ifTreeRevision`
426
+ * (file-first) applies it only at that tree revision.
427
+ */
428
+ patch: (params: FilePatchParams, opts?: {
429
+ idempotencyKey?: string;
430
+ signal?: AbortSignal;
431
+ } & TreeRevisionOptions) => Promise<FilePatchResult>;
252
432
  };
253
- /** One page of the workspace's changes against its template (needs the `files` tool). */
433
+ /**
434
+ * `POST /wake-hint` (contracts §26.6; 0.9.0+): announces an imminent tool call so a hibernated workspace is restored
435
+ * ahead of it. It never wakes a suspended workspace, never waits out `workspace_busy` and is not retried: a suspended
436
+ * workspace answers 409 `workspace_not_running` (`Workspace.hint()` then wakes it in the background).
437
+ */
438
+ wakeHint(signal?: AbortSignal): Promise<WakeHintResult>;
439
+ /**
440
+ * One page of the workspace's changes against its template (needs the `files` tool). File-first workspaces: each
441
+ * execution's result lists what it changed (`changed`); this route is refused (NotSupportedForModeError).
442
+ */
254
443
  changes(params?: WorkspaceChangesParams): Promise<WorkspaceChangesPage>;
255
444
  /** Every change under `pathPrefix`, following next_cursor. */
256
445
  changesAll(params?: Omit<WorkspaceChangesParams, 'cursor' | 'summary'>): AsyncGenerator<WorkspaceChange>;