@shardflux/sdk 0.14.0 → 0.16.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +85 -23
  2. package/README.md +226 -7
  3. package/dist/account.d.ts +2 -0
  4. package/dist/account.js +6 -0
  5. package/dist/cell.d.ts +27 -0
  6. package/dist/cell.js +71 -0
  7. package/dist/client.d.ts +54 -5
  8. package/dist/client.js +100 -7
  9. package/dist/computer.d.ts +153 -0
  10. package/dist/computer.js +229 -0
  11. package/dist/errors.d.ts +5 -1
  12. package/dist/errors.js +9 -0
  13. package/dist/executions.d.ts +2 -6
  14. package/dist/executions.js +9 -0
  15. package/dist/exit-code.d.ts +7 -0
  16. package/dist/exit-code.js +12 -0
  17. package/dist/generated/app-api.d.ts +1055 -119
  18. package/dist/generated/cell-api.d.ts +334 -0
  19. package/dist/http.d.ts +7 -1
  20. package/dist/http.js +34 -13
  21. package/dist/index.d.ts +13 -6
  22. package/dist/index.js +7 -2
  23. package/dist/ports.d.ts +7 -0
  24. package/dist/ports.js +1 -1
  25. package/dist/progress.d.ts +2 -2
  26. package/dist/progress.js +1 -1
  27. package/dist/templates.d.ts +12 -0
  28. package/dist/templates.js +9 -0
  29. package/dist/testing/index.d.ts +62 -0
  30. package/dist/testing/index.js +585 -0
  31. package/dist/testing/seed.d.ts +433 -0
  32. package/dist/testing/seed.js +449 -0
  33. package/dist/tools.d.ts +21 -3
  34. package/dist/tools.js +113 -22
  35. package/dist/tunnel-assets/linux-amd64.gz +0 -0
  36. package/dist/tunnel-assets/linux-arm64.gz +0 -0
  37. package/dist/tunnel-assets.d.ts +10 -0
  38. package/dist/tunnel-assets.js +11 -0
  39. package/dist/tunnel-packet.d.ts +3 -0
  40. package/dist/tunnel-packet.js +43 -0
  41. package/dist/tunnel-pty.d.ts +86 -0
  42. package/dist/tunnel-pty.js +243 -0
  43. package/dist/tunnels.d.ts +47 -0
  44. package/dist/tunnels.js +454 -0
  45. package/dist/workspace-ref.d.ts +87 -0
  46. package/dist/workspace-ref.js +173 -0
  47. package/dist/workspace.d.ts +40 -1
  48. package/dist/workspace.js +111 -2
  49. package/package.json +7 -2
@@ -0,0 +1,173 @@
1
+ import { CAPTURE_BARRIER } from "./cell.js";
2
+ import { ShardfluxApiError, isWorkspaceGone } from "./errors.js";
3
+ import { workspaceTools } from "./tools.js";
4
+ const FILE_METHODS = ['read', 'readText', 'readWithInfo', 'write', 'list', 'stat', 'remove', 'mkdir', 'move', 'search', 'patch'];
5
+ /**
6
+ * A workspace named by its key. Nothing is requested until the first call: `exec()`, `files`, a tool from `tools()`,
7
+ * `hint()` or `open()`. Concurrent first calls share one open; a failed open is not kept, so the next call opens again.
8
+ * A workspace deleted under the ref (409 `workspace_deleted`, `isWorkspaceGone`) fails the call that meets it and is
9
+ * forgotten: the next call opens the key again (a new workspace, as `open()` would). `open()` returns the full
10
+ * `Workspace` (suspend, fork, ports, computer, tool-call capture).
11
+ */
12
+ export class WorkspaceRef {
13
+ key;
14
+ #api;
15
+ #params;
16
+ #grants;
17
+ #workspace = null;
18
+ #opening = null;
19
+ /** Internal: use `cloud.workspace(key, params)` or the module-level `workspace(key, params)`. */
20
+ constructor(api, key, params, grants) {
21
+ if (typeof key !== 'string' || key.length === 0)
22
+ throw new TypeError('workspace(key, params): key must be a non-empty string');
23
+ if (params.create !== false && (typeof params.template !== 'string' || params.template.length === 0)) {
24
+ throw new TypeError(`workspace("${key}", params): params.template is required (a template slug, e.g. "default")`);
25
+ }
26
+ this.key = key;
27
+ this.#api = api;
28
+ this.#params = params;
29
+ this.#grants = grants;
30
+ }
31
+ /**
32
+ * Whether the open that produced the current workspace created it (contracts §46.2): undefined until the ref has
33
+ * opened (or when the API does not report it), false for `create: false`.
34
+ */
35
+ get created() {
36
+ if (!this.#workspace)
37
+ return undefined;
38
+ if (this.#params.create === false)
39
+ return false;
40
+ return this.#workspace.created ?? undefined;
41
+ }
42
+ /** The opened workspace's mode, else the params' (`processful` when neither says). */
43
+ get mode() {
44
+ return this.#workspace?.mode ?? this.#params.mode ?? 'processful';
45
+ }
46
+ /** Tools granted by the opened workspace's last token; null before the open. */
47
+ get grantedTools() {
48
+ return this.#workspace?.grantedTools ?? null;
49
+ }
50
+ /** The workspace: opened (or, with `create: false`, found) on the first call and kept. */
51
+ open() {
52
+ if (this.#workspace)
53
+ return Promise.resolve(this.#workspace);
54
+ this.#opening ??= this.#open().then((ws) => {
55
+ this.#workspace = ws;
56
+ this.#opening = null;
57
+ return ws;
58
+ }, (err) => {
59
+ this.#opening = null;
60
+ throw err;
61
+ });
62
+ return this.#opening;
63
+ }
64
+ async #open() {
65
+ const params = this.#params;
66
+ // open() sends only the open's own parameters (`create` is the ref's).
67
+ if (params.create !== false)
68
+ return this.#api.open({ ...params, key: this.key });
69
+ const found = await this.#api.findByKey(this.key, {
70
+ includeDeleted: false,
71
+ ...(params.projectId !== undefined ? { projectId: params.projectId } : {}),
72
+ ...(params.agentLabel !== undefined ? { agentLabel: params.agentLabel } : {}),
73
+ ...(params.tools !== undefined ? { tools: params.tools } : {}),
74
+ });
75
+ if (found)
76
+ return found;
77
+ throw new ShardfluxApiError(404, { error: { code: 'not_found', message: `No workspace with key "${this.key}" in this project (create: false).`, request_id: '', retryable: false, details: { key: this.key } } }, 'api');
78
+ }
79
+ /** Runs `fn` on the workspace; a workspace found deleted is forgotten so the next call opens the key again. */
80
+ async #use(fn) {
81
+ const ws = await this.open();
82
+ try {
83
+ return await fn(ws);
84
+ }
85
+ catch (err) {
86
+ if (isWorkspaceGone(err) && this.#workspace === ws)
87
+ this.#workspace = null;
88
+ throw err;
89
+ }
90
+ }
91
+ /**
92
+ * Runs a command and collects its output (`cell().exec.run()`). A string runs through `bash -lc`, so shell syntax
93
+ * works; an argv array runs without a shell. A file-first workspace runs commands with `executions.run()`.
94
+ */
95
+ exec(command, opts = {}) {
96
+ const argv = typeof command === 'string' ? ['bash', '-lc', command] : [...command];
97
+ return this.#use((ws) => ws.cell().exec.run(argv, opts));
98
+ }
99
+ /** The workspace's files, as on `cell().files`. */
100
+ files = Object.fromEntries(FILE_METHODS.map((name) => [
101
+ name,
102
+ (...args) => this.#use((ws) => ws.cell().files[name](...args)),
103
+ ]));
104
+ /** A file-first workspace's executions (`run`, `get`), as on `Workspace.executions`. */
105
+ executions = {
106
+ run: (argv, opts = {}) => this.#use((ws) => ws.executions.run(argv, opts)),
107
+ get: (executionId, opts = {}) => this.#use((ws) => ws.executions.get(executionId, opts)),
108
+ };
109
+ /** The workspace's cell client (`Workspace.cell()`), opening the key first if needed. */
110
+ async cell(opts = {}) {
111
+ return (await this.open()).cell(opts);
112
+ }
113
+ /** Hints without waiting, runs the turn, and requests idle suspension even if the body throws. */
114
+ async turn(fn, { afterSeconds = 0 } = {}) {
115
+ void this.hint().catch(() => undefined);
116
+ let result;
117
+ let failure;
118
+ let failed = false;
119
+ try {
120
+ result = await fn(this);
121
+ }
122
+ catch (err) {
123
+ failed = true;
124
+ failure = err;
125
+ }
126
+ try {
127
+ await this.#use((ws) => ws.suspendWhenIdle({ afterSeconds }));
128
+ }
129
+ catch (err) {
130
+ if (!failed)
131
+ throw err;
132
+ }
133
+ if (failed)
134
+ throw failure;
135
+ return result;
136
+ }
137
+ /**
138
+ * Says a tool call is coming. Before the first open it starts the open in the background (`wake` resolves when the
139
+ * workspace runs; nothing has to await it), unless `wake: null`; afterwards it is `Workspace.hint()`.
140
+ */
141
+ async hint(opts = {}) {
142
+ if (this.#workspace)
143
+ return this.#workspace.hint(opts);
144
+ if (opts.wake === null)
145
+ return { residency: null, wake: null };
146
+ const wake = this.open().then(() => true);
147
+ wake.catch(() => undefined); // a failed open is left to the next call, which opens again
148
+ return { residency: null, wake };
149
+ }
150
+ /**
151
+ * Workspace tools for this key (`workspaceTools`), built before any VM exists: the definitions come from the API
152
+ * key's tool permissions (`GET /v1/me`, read once per client), and `computer` only with `computerUse: true` in the
153
+ * params or on an opened workspace whose computer use is on. The first tool call opens the key.
154
+ */
155
+ async tools(opts = {}) {
156
+ // create: false finds the workspace now (a lookup, no VM start), so the tools match its mode.
157
+ if (this.#params.create === false)
158
+ await this.open();
159
+ let tools = opts.tools ?? this.grantedTools ?? undefined;
160
+ if (tools === undefined) {
161
+ const granted = await this.#grants();
162
+ if (granted) {
163
+ const computer = this.#workspace ? this.#workspace.computerUse.enabled : this.#params.computerUse === true;
164
+ tools = granted.filter((t) => t !== 'computer' || computer);
165
+ }
166
+ }
167
+ return workspaceTools(this, { ...opts, ...(tools !== undefined ? { tools } : {}) });
168
+ }
169
+ /** Internal: tool-call capture's read-your-writes barrier, once the workspace is open. */
170
+ [CAPTURE_BARRIER]() {
171
+ return this.#workspace?.[CAPTURE_BARRIER]();
172
+ }
173
+ }
@@ -2,7 +2,7 @@
2
2
  * A workspace handle: the latest view from the application API plus managed
3
3
  * tool tokens and cell clients (one per agent label / tool set).
4
4
  */
5
- import type { AllocationMode, ClientContext, IdlePolicy, DiskLayout, ForkTarget, Operation, ResizeParams, ResizeResult, SuspendRequest, SuspendWhenIdleOptions, SuspendWhenIdleResult, WaitOptions, WorkspaceLifetime, WorkspaceMemory, WorkspaceOrigin, WorkspacePurpose, WorkspaceView } from './client.js';
5
+ import type { AllocationMode, ComputerUse, ClientContext, IdlePolicy, DiskLayout, ForkTarget, Operation, ResizeParams, ResizeResult, SuspendRequest, SuspendWhenIdleOptions, SuspendWhenIdleResult, WaitOptions, RetentionPolicy, WorkspaceLifetime, WorkspaceMemory, WorkspaceOrigin, WorkspacePurpose, WorkspaceView } from './client.js';
6
6
  import { CAPTURE_BARRIER, CellClient } from './cell.js';
7
7
  import { ToolCallCapture } from './capture.js';
8
8
  import type { ToolCallCaptureOptions } from './capture.js';
@@ -10,7 +10,9 @@ import type { WorkspaceMode } from './errors.js';
10
10
  import type { FinishedOperation, ForkOptions, LifecycleOptions, ResumeOptions, SuspendOptions, WaitedForkOptions, WaitedLifecycleOptions, WaitedResumeOptions, WaitedSuspendOptions } from './lifecycle.js';
11
11
  import { Trace } from './progress.js';
12
12
  import type { LifecycleTiming, ProgressListener } from './progress.js';
13
+ import { WorkspaceComputer } from './computer.js';
13
14
  import { WorkspacePorts } from './ports.js';
15
+ import { WorkspaceTunnels } from './tunnels.js';
14
16
  import { WorkspaceSecrets } from './secrets.js';
15
17
  import type { CellClientOptions, Residency, WorkspaceChangesPage, WorkspaceChangesParams } from './cell.js';
16
18
  import type { SaveAsTemplateParams, SaveAsTemplateResponse, WorkspaceStartup } from './templates.js';
@@ -57,11 +59,18 @@ export interface HintResult {
57
59
  }
58
60
  export declare class Workspace {
59
61
  #private;
62
+ /**
63
+ * Whether the open() that returned this handle created the workspace (0.15.0+, contracts §46.2): true for a new key
64
+ * (or a key whose session ended), false when it reconnected to or resumed an existing workspace. Null for handles
65
+ * from get(), list() and findByKey(), and from an API that does not report it.
66
+ */
67
+ readonly created: boolean | null;
60
68
  constructor(ctx: ClientContext, view: WorkspaceView, opts?: {
61
69
  agentLabel?: string | undefined;
62
70
  tools?: ToolName[] | undefined;
63
71
  token?: ToolToken | null;
64
72
  trace?: Trace;
73
+ created?: boolean | null;
65
74
  });
66
75
  /**
67
76
  * Where the time went in the last lifecycle call made through this handle: open(), wake() (also when a tool call
@@ -83,6 +92,8 @@ export declare class Workspace {
83
92
  get cellEndpoint(): string | null;
84
93
  /** Actual grants reported by the cell for the running workspace (null until known). */
85
94
  get grants(): WorkspaceView['grants'];
95
+ /** Stored caps every later start uses, as of the latest workspace view. */
96
+ get caps(): WorkspaceView['caps'];
86
97
  get ceilings(): WorkspaceView['ceilings'];
87
98
  get template(): WorkspaceView['template'];
88
99
  /**
@@ -160,10 +171,28 @@ export declare class Workspace {
160
171
  * const link = await workspace.ports.link(3000); // open link.url in a browser
161
172
  */
162
173
  get ports(): WorkspacePorts;
174
+ /** Forward a guest TCP port to this machine. Close the returned handle in finally. */
175
+ get tunnels(): WorkspaceTunnels;
176
+ /**
177
+ * The workspace desktop (0.15.0+, contracts §45): `act(actions)`, `screenshot()`, `stream()` (a private link to watch
178
+ * it), `status()`, `start()`, `stop()`. Needs computer use on (`setComputerUse(true)`, or the template's switch); the
179
+ * platform starts the desktop on the first call that needs it.
180
+ */
181
+ get computer(): WorkspaceComputer;
182
+ /** Computer use (0.15.0+): `enabled` is what tool tokens carry; `workspace` null follows the `template`'s switch. */
183
+ get computerUse(): ComputerUse;
184
+ /**
185
+ * Switches computer use on or off for this workspace (null follows the template). Takes effect on the next tool token:
186
+ * this handle's cached tokens are dropped. 409 `computer_use_unavailable` when its template version cannot run a
187
+ * desktop.
188
+ */
189
+ setComputerUse(enabled: boolean | null): Promise<this>;
163
190
  /** The workspace's text inputs `{NAME: value}` (0.7.0; secret inputs are bound secrets, never listed here). */
164
191
  inputs(): Promise<Record<string, string>>;
165
192
  get labels(): Record<string, string>;
166
193
  setLabels(labels: Record<string, string>): Promise<this>;
194
+ get retention(): WorkspaceView['retention'];
195
+ setRetention(policy: RetentionPolicy | null): Promise<this>;
167
196
  setIdlePolicy(policy: IdlePolicy | null): Promise<this>;
168
197
  idle(signal?: AbortSignal): ReturnType<CellClient['idle']>;
169
198
  keepalive(seconds: number, signal?: AbortSignal): ReturnType<CellClient['keepalive']>;
@@ -278,6 +307,12 @@ export declare class Workspace {
278
307
  * confirm_destructive). Returns the `reset` operation: requested, or with `{ wait: true }` finished. Tool tokens of
279
308
  * the old epoch are dropped.
280
309
  */
310
+ /** Upgrade in place: keep files/packages/home; cold start drops memory and processes. */
311
+ upgrade(opts?: LifecycleOptions & {
312
+ at?: 'now' | 'next_resume';
313
+ }): Promise<Operation>;
314
+ get upgradeAvailable(): WorkspaceView['upgrade_available'];
315
+ get upgradePending(): WorkspaceView['upgrade_pending'];
281
316
  reset(opts: WaitedLifecycleOptions): Promise<FinishedOperation>;
282
317
  reset(opts?: LifecycleOptions): Promise<Operation>;
283
318
  /**
@@ -341,6 +376,10 @@ export declare class Workspace {
341
376
  * (`server.memoryRestored === false`, `server.resumePath` `cold_boot`, `server.coldBootReason`).
342
377
  */
343
378
  wake(opts?: WakeOptions): Promise<boolean>;
379
+ /** Hints without waiting, runs the turn, and requests idle suspension even if the body throws. */
380
+ turn<T>(fn: (workspace: this) => T | Promise<T>, { afterSeconds }?: {
381
+ afterSeconds?: number;
382
+ }): Promise<T>;
344
383
  /**
345
384
  * Announces an imminent tool call (cell `POST /wake-hint`; 0.9.0+) so a parked workspace is restored
346
385
  * ahead of it: call it when the model starts emitting a tool call, before its arguments are complete. The agent tools
package/dist/workspace.js CHANGED
@@ -4,7 +4,9 @@ import { NotSupportedForModeError, OperationFailedError, ShardfluxApiError } fro
4
4
  import { SERVER_WAIT_MAX_S, defaultSleep, randomId } from "./http.js";
5
5
  import { AFTER_WAIT, HELD_RESUME, TRACE } from "./lifecycle.js";
6
6
  import { Trace, combineListeners, traced } from "./progress.js";
7
+ import { WorkspaceComputer } from "./computer.js";
7
8
  import { WorkspacePorts } from "./ports.js";
9
+ import { WorkspaceTunnels } from "./tunnels.js";
8
10
  import { WorkspaceSecrets } from "./secrets.js";
9
11
  import { ToolTokenManager } from "./tokens.js";
10
12
  const notRunning = (e) => e instanceof ShardfluxApiError && (e.code === 'workspace_not_running' || (e.code === 'conflict' && e.reason === 'workspace_not_running'));
@@ -15,14 +17,23 @@ export class Workspace {
15
17
  #ctx;
16
18
  #defaults;
17
19
  #managers = new Map();
20
+ #computer;
18
21
  #cells = new Map();
19
22
  /** The open() that produced this handle; its timing is final once open() has returned. */
20
23
  #openTrace;
21
24
  #lastTiming;
22
25
  /** The newest tree revision seen (file-first): the view's, or any cell response's since. */
23
26
  #treeRevision;
27
+ #tunnels;
28
+ /**
29
+ * Whether the open() that returned this handle created the workspace (0.15.0+, contracts §46.2): true for a new key
30
+ * (or a key whose session ended), false when it reconnected to or resumed an existing workspace. Null for handles
31
+ * from get(), list() and findByKey(), and from an API that does not report it.
32
+ */
33
+ created;
24
34
  constructor(ctx, view, opts = {}) {
25
35
  this.#ctx = ctx;
36
+ this.created = opts.created ?? null;
26
37
  this.#view = view;
27
38
  this.#treeRevision = view.mode === 'file_first' && typeof view.tree_revision === 'number' ? view.tree_revision : null;
28
39
  this.#defaults = { agentLabel: opts.agentLabel, tools: opts.tools };
@@ -93,6 +104,10 @@ export class Workspace {
93
104
  get grants() {
94
105
  return this.#view.grants;
95
106
  }
107
+ /** Stored caps every later start uses, as of the latest workspace view. */
108
+ get caps() {
109
+ return this.#view.caps;
110
+ }
96
111
  get ceilings() {
97
112
  return this.#view.ceilings;
98
113
  }
@@ -212,6 +227,38 @@ export class Workspace {
212
227
  get ports() {
213
228
  return new WorkspacePorts(this.#ctx, this.id);
214
229
  }
230
+ /** Forward a guest TCP port to this machine. Close the returned handle in finally. */
231
+ get tunnels() {
232
+ return this.#tunnels ??= new WorkspaceTunnels(() => this.cell({ transitionTimeoutMs: 30_000 }), () => this.grantedTools);
233
+ }
234
+ /**
235
+ * The workspace desktop (0.15.0+, contracts §45): `act(actions)`, `screenshot()`, `stream()` (a private link to watch
236
+ * it), `status()`, `start()`, `stop()`. Needs computer use on (`setComputerUse(true)`, or the template's switch); the
237
+ * platform starts the desktop on the first call that needs it.
238
+ */
239
+ get computer() {
240
+ return this.#computer ??= new WorkspaceComputer({
241
+ cell: () => this.cell(),
242
+ ports: () => this.ports,
243
+ setEnabled: (enabled) => this.setComputerUse(enabled).then(() => this.computerUse),
244
+ });
245
+ }
246
+ /** Computer use (0.15.0+): `enabled` is what tool tokens carry; `workspace` null follows the `template`'s switch. */
247
+ get computerUse() {
248
+ return this.#view.computer_use;
249
+ }
250
+ /**
251
+ * Switches computer use on or off for this workspace (null follows the template). Takes effect on the next tool token:
252
+ * this handle's cached tokens are dropped. 409 `computer_use_unavailable` when its template version cannot run a
253
+ * desktop.
254
+ */
255
+ async setComputerUse(enabled) {
256
+ const cu = await this.#ctx.workspaces.setComputerUse(this.id, enabled);
257
+ this.#view = { ...this.#view, computer_use: cu };
258
+ for (const m of this.#managers.values())
259
+ m.invalidate();
260
+ return this;
261
+ }
215
262
  /** The workspace's text inputs `{NAME: value}` (0.7.0; secret inputs are bound secrets, never listed here). */
216
263
  inputs() {
217
264
  return this.#ctx.workspaces.inputs(this.id);
@@ -221,6 +268,11 @@ export class Workspace {
221
268
  this.#view = (await this.#ctx.workspaces.setLabels(this.id, labels)).data;
222
269
  return this;
223
270
  }
271
+ get retention() { return this.#view.retention ?? null; }
272
+ async setRetention(policy) {
273
+ this.#view = (await this.#ctx.workspaces.setRetention(this.id, policy)).data;
274
+ return this;
275
+ }
224
276
  async setIdlePolicy(policy) {
225
277
  this.#view = (await this.#ctx.workspaces.setIdlePolicy(this.id, policy)).data;
226
278
  return this;
@@ -306,7 +358,10 @@ export class Workspace {
306
358
  throw refusal;
307
359
  const out = await this.#ctx.workspaces.resize(this.id, this.#tracked(params));
308
360
  // The resize has happened: a failed view read leaves the old view (the next refresh() reads it again).
309
- await this.refresh().catch(() => undefined);
361
+ if (params.wait !== false)
362
+ await this.refresh().catch(() => undefined);
363
+ else if (out.caps)
364
+ this.#view = { ...this.#view, caps: out.caps };
310
365
  return out;
311
366
  }
312
367
  resume(opts = {}) {
@@ -342,19 +397,49 @@ export class Workspace {
342
397
  return this.#ctx.workspaces.fork(this.id, target, this.#tracked(opts));
343
398
  }
344
399
  async close(opts = {}) {
400
+ let tunnelError;
401
+ try {
402
+ await this.#tunnels?.close();
403
+ }
404
+ catch (e) {
405
+ tunnelError = e instanceof Error ? e : new Error(String(e));
406
+ }
345
407
  // Closing aborts in-flight cell requests: tool-call capture writes recorded before it are flushed first (bounded).
346
408
  await this.#ctx.captures.settle(this.id);
347
409
  for (const c of this.#cells.values())
348
410
  c.close();
349
411
  this.#cells.clear();
350
- if (this.lifetime !== 'session')
412
+ if (this.lifetime !== 'session') {
413
+ if (tunnelError)
414
+ throw tunnelError;
351
415
  return null;
416
+ }
352
417
  const out = await this.#ctx.workspaces.closeWithView(this.id, this.#tracked(opts));
353
418
  this.#view = out.workspace;
354
419
  for (const m of this.#managers.values())
355
420
  m.invalidate();
421
+ if (tunnelError)
422
+ throw tunnelError;
356
423
  return out.operation;
357
424
  }
425
+ /**
426
+ * Wipes every change in this layered workspace and restarts it on its template (sends
427
+ * confirm_destructive). Returns the `reset` operation: requested, or with `{ wait: true }` finished. Tool tokens of
428
+ * the old epoch are dropped.
429
+ */
430
+ /** Upgrade in place: keep files/packages/home; cold start drops memory and processes. */
431
+ async upgrade(opts = {}) {
432
+ const refusal = this.#needsVm('upgrade');
433
+ if (refusal)
434
+ throw refusal;
435
+ const op = await this.#ctx.workspaces.upgrade(this.id, { ...this.#tracked(opts), [AFTER_WAIT]: this.#refreshAfterWait() });
436
+ if (opts.at !== 'next_resume')
437
+ for (const m of this.#managers.values())
438
+ m.invalidate();
439
+ return op;
440
+ }
441
+ get upgradeAvailable() { return this.#view.upgrade_available ?? null; }
442
+ get upgradePending() { return this.#view.upgrade_pending ?? null; }
358
443
  async reset(opts = {}) {
359
444
  const refusal = this.#needsVm('reset');
360
445
  if (refusal)
@@ -554,6 +639,30 @@ export class Workspace {
554
639
  }
555
640
  throw new Error(`workspace ${this.id} did not become runnable after repeated lifecycle conflicts`);
556
641
  }
642
+ /** Hints without waiting, runs the turn, and requests idle suspension even if the body throws. */
643
+ async turn(fn, { afterSeconds = 0 } = {}) {
644
+ void this.hint().catch(() => undefined);
645
+ let result;
646
+ let failure;
647
+ let failed = false;
648
+ try {
649
+ result = await fn(this);
650
+ }
651
+ catch (err) {
652
+ failed = true;
653
+ failure = err;
654
+ }
655
+ try {
656
+ await this.suspendWhenIdle({ afterSeconds });
657
+ }
658
+ catch (err) {
659
+ if (!failed)
660
+ throw err;
661
+ }
662
+ if (failed)
663
+ throw failure;
664
+ return result;
665
+ }
557
666
  /**
558
667
  * Announces an imminent tool call (cell `POST /wake-hint`; 0.9.0+) so a parked workspace is restored
559
668
  * ahead of it: call it when the model starts emitting a tool call, before its arguments are complete. The agent tools
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@shardflux/sdk",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
- "description": "Shardflux TypeScript SDK: open persistent agent workspaces by key and give your agent workspace tools (exec, files, processes, PTY, git, browser).",
5
+ "description": "Shardflux TypeScript SDK: serverless VMs for AI agents. Open a workspace by key and give your agent workspace tools (exec, files, processes, PTY, git, browser).",
6
6
  "license": "Apache-2.0",
7
7
  "homepage": "https://shardflux.dev",
8
8
  "bugs": {
@@ -29,6 +29,11 @@
29
29
  "types": "./dist/index.d.ts",
30
30
  "import": "./dist/index.js",
31
31
  "default": "./dist/index.js"
32
+ },
33
+ "./testing": {
34
+ "types": "./dist/testing/index.d.ts",
35
+ "import": "./dist/testing/index.js",
36
+ "default": "./dist/testing/index.js"
32
37
  }
33
38
  },
34
39
  "files": [