@shardflux/sdk 0.13.1 → 0.15.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.
@@ -0,0 +1,149 @@
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
+ /**
114
+ * Says a tool call is coming. Before the first open it starts the open in the background (`wake` resolves when the
115
+ * workspace runs; nothing has to await it), unless `wake: null`; afterwards it is `Workspace.hint()`.
116
+ */
117
+ async hint(opts = {}) {
118
+ if (this.#workspace)
119
+ return this.#workspace.hint(opts);
120
+ if (opts.wake === null)
121
+ return { residency: null, wake: null };
122
+ const wake = this.open().then(() => true);
123
+ wake.catch(() => undefined); // a failed open is left to the next call, which opens again
124
+ return { residency: null, wake };
125
+ }
126
+ /**
127
+ * Workspace tools for this key (`workspaceTools`), built before any VM exists: the definitions come from the API
128
+ * key's tool permissions (`GET /v1/me`, read once per client), and `computer` only with `computerUse: true` in the
129
+ * params or on an opened workspace whose computer use is on. The first tool call opens the key.
130
+ */
131
+ async tools(opts = {}) {
132
+ // create: false finds the workspace now (a lookup, no VM start), so the tools match its mode.
133
+ if (this.#params.create === false)
134
+ await this.open();
135
+ let tools = opts.tools ?? this.grantedTools ?? undefined;
136
+ if (tools === undefined) {
137
+ const granted = await this.#grants();
138
+ if (granted) {
139
+ const computer = this.#workspace ? this.#workspace.computerUse.enabled : this.#params.computerUse === true;
140
+ tools = granted.filter((t) => t !== 'computer' || computer);
141
+ }
142
+ }
143
+ return workspaceTools(this, { ...opts, ...(tools !== undefined ? { tools } : {}) });
144
+ }
145
+ /** Internal: tool-call capture's read-your-writes barrier, once the workspace is open. */
146
+ [CAPTURE_BARRIER]() {
147
+ return this.#workspace?.[CAPTURE_BARRIER]();
148
+ }
149
+ }
@@ -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, 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, 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,6 +10,8 @@ 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';
14
+ import { WorkspacePorts } from './ports.js';
13
15
  import { WorkspaceSecrets } from './secrets.js';
14
16
  import type { CellClientOptions, Residency, WorkspaceChangesPage, WorkspaceChangesParams } from './cell.js';
15
17
  import type { SaveAsTemplateParams, SaveAsTemplateResponse, WorkspaceStartup } from './templates.js';
@@ -56,11 +58,18 @@ export interface HintResult {
56
58
  }
57
59
  export declare class Workspace {
58
60
  #private;
61
+ /**
62
+ * Whether the open() that returned this handle created the workspace (0.15.0+, contracts §46.2): true for a new key
63
+ * (or a key whose session ended), false when it reconnected to or resumed an existing workspace. Null for handles
64
+ * from get(), list() and findByKey(), and from an API that does not report it.
65
+ */
66
+ readonly created: boolean | null;
59
67
  constructor(ctx: ClientContext, view: WorkspaceView, opts?: {
60
68
  agentLabel?: string | undefined;
61
69
  tools?: ToolName[] | undefined;
62
70
  token?: ToolToken | null;
63
71
  trace?: Trace;
72
+ created?: boolean | null;
64
73
  });
65
74
  /**
66
75
  * Where the time went in the last lifecycle call made through this handle: open(), wake() (also when a tool call
@@ -150,6 +159,29 @@ export declare class Workspace {
150
159
  get executions(): CellClient['executions'];
151
160
  /** Secret names bound to this workspace (injected into every exec/PTY start): `get()`, `set(names)`. */
152
161
  get secrets(): WorkspaceSecrets;
162
+ /**
163
+ * Inbound ports (0.14.0): serve a TCP port of the workspace at its own private HTTPS URL. A request wakes a parked or
164
+ * suspended workspace and is served once it runs.
165
+ *
166
+ * const { url } = await workspace.ports.expose(3000); // the server listens on 0.0.0.0:3000
167
+ * const { token } = await workspace.ports.token(3000); // Authorization: Bearer <token>
168
+ * const link = await workspace.ports.link(3000); // open link.url in a browser
169
+ */
170
+ get ports(): WorkspacePorts;
171
+ /**
172
+ * The workspace desktop (0.15.0+, contracts §45): `act(actions)`, `screenshot()`, `stream()` (a private link to watch
173
+ * it), `status()`, `start()`, `stop()`. Needs computer use on (`setComputerUse(true)`, or the template's switch); the
174
+ * platform starts the desktop on the first call that needs it.
175
+ */
176
+ get computer(): WorkspaceComputer;
177
+ /** Computer use (0.15.0+): `enabled` is what tool tokens carry; `workspace` null follows the `template`'s switch. */
178
+ get computerUse(): ComputerUse;
179
+ /**
180
+ * Switches computer use on or off for this workspace (null follows the template). Takes effect on the next tool token:
181
+ * this handle's cached tokens are dropped. 409 `computer_use_unavailable` when its template version cannot run a
182
+ * desktop.
183
+ */
184
+ setComputerUse(enabled: boolean | null): Promise<this>;
153
185
  /** The workspace's text inputs `{NAME: value}` (0.7.0; secret inputs are bound secrets, never listed here). */
154
186
  inputs(): Promise<Record<string, string>>;
155
187
  get labels(): Record<string, string>;
@@ -200,6 +232,21 @@ export declare class Workspace {
200
232
  * A suspend the request already started is not undone (it shows as `activeOperation`).
201
233
  */
202
234
  cancelSuspendWhenIdle(): Promise<this>;
235
+ /**
236
+ * Resizes this workspace (0.14.0): memory, the held floor, the allocation mode, CPU and disk, whether it is running or
237
+ * suspended, fixed or elastic, without a restart or a fork. Memory changes live on a running workspace (a shrink gives
238
+ * back what the guest frees: `memory.converged`); a suspended one gets its new size when it resumes, before its first
239
+ * call. CPU changes live up to the vCPUs the workspace booted with, beyond that at its next start. Disks grow online.
240
+ * The new caps are stored, so every later start uses them. Resolves once the resize has finished, with per resource
241
+ * when it applies (`now`, `resume` or `next_start` with the `reason`); the handle's view is refreshed. See
242
+ * WorkspacesApi.resize for the errors. A file-first workspace (no VM to resize) is refused locally with
243
+ * NotSupportedForModeError.
244
+ *
245
+ * const r = await workspace.resize({ memoryMib: 6144, cpuMillis: 4000 });
246
+ * r.memory?.applies_at; // 'now'
247
+ * r.cpu?.applies_at; // 'next_start' (reason 'boot_vcpus': more vCPUs than it booted with)
248
+ */
249
+ resize(params: ResizeParams): Promise<ResizeResult>;
203
250
  /**
204
251
  * Resumes a suspended workspace. Resolves when the resume is REQUESTED; with `{ wait: true }`, once the workspace runs.
205
252
  * Tool calls wake a suspended workspace by themselves, so this is rarely needed. With `wait` (0.9.0) it is one held
package/dist/workspace.js CHANGED
@@ -4,6 +4,8 @@ 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";
8
+ import { WorkspacePorts } from "./ports.js";
7
9
  import { WorkspaceSecrets } from "./secrets.js";
8
10
  import { ToolTokenManager } from "./tokens.js";
9
11
  const notRunning = (e) => e instanceof ShardfluxApiError && (e.code === 'workspace_not_running' || (e.code === 'conflict' && e.reason === 'workspace_not_running'));
@@ -20,8 +22,15 @@ export class Workspace {
20
22
  #lastTiming;
21
23
  /** The newest tree revision seen (file-first): the view's, or any cell response's since. */
22
24
  #treeRevision;
25
+ /**
26
+ * Whether the open() that returned this handle created the workspace (0.15.0+, contracts §46.2): true for a new key
27
+ * (or a key whose session ended), false when it reconnected to or resumed an existing workspace. Null for handles
28
+ * from get(), list() and findByKey(), and from an API that does not report it.
29
+ */
30
+ created;
23
31
  constructor(ctx, view, opts = {}) {
24
32
  this.#ctx = ctx;
33
+ this.created = opts.created ?? null;
25
34
  this.#view = view;
26
35
  this.#treeRevision = view.mode === 'file_first' && typeof view.tree_revision === 'number' ? view.tree_revision : null;
27
36
  this.#defaults = { agentLabel: opts.agentLabel, tools: opts.tools };
@@ -200,6 +209,45 @@ export class Workspace {
200
209
  get secrets() {
201
210
  return new WorkspaceSecrets(this.#ctx, this.id);
202
211
  }
212
+ /**
213
+ * Inbound ports (0.14.0): serve a TCP port of the workspace at its own private HTTPS URL. A request wakes a parked or
214
+ * suspended workspace and is served once it runs.
215
+ *
216
+ * const { url } = await workspace.ports.expose(3000); // the server listens on 0.0.0.0:3000
217
+ * const { token } = await workspace.ports.token(3000); // Authorization: Bearer <token>
218
+ * const link = await workspace.ports.link(3000); // open link.url in a browser
219
+ */
220
+ get ports() {
221
+ return new WorkspacePorts(this.#ctx, this.id);
222
+ }
223
+ /**
224
+ * The workspace desktop (0.15.0+, contracts §45): `act(actions)`, `screenshot()`, `stream()` (a private link to watch
225
+ * it), `status()`, `start()`, `stop()`. Needs computer use on (`setComputerUse(true)`, or the template's switch); the
226
+ * platform starts the desktop on the first call that needs it.
227
+ */
228
+ get computer() {
229
+ return new WorkspaceComputer({
230
+ cell: () => this.cell(),
231
+ ports: () => this.ports,
232
+ setEnabled: (enabled) => this.setComputerUse(enabled).then(() => this.computerUse),
233
+ });
234
+ }
235
+ /** Computer use (0.15.0+): `enabled` is what tool tokens carry; `workspace` null follows the `template`'s switch. */
236
+ get computerUse() {
237
+ return this.#view.computer_use;
238
+ }
239
+ /**
240
+ * Switches computer use on or off for this workspace (null follows the template). Takes effect on the next tool token:
241
+ * this handle's cached tokens are dropped. 409 `computer_use_unavailable` when its template version cannot run a
242
+ * desktop.
243
+ */
244
+ async setComputerUse(enabled) {
245
+ const cu = await this.#ctx.workspaces.setComputerUse(this.id, enabled);
246
+ this.#view = { ...this.#view, computer_use: cu };
247
+ for (const m of this.#managers.values())
248
+ m.invalidate();
249
+ return this;
250
+ }
203
251
  /** The workspace's text inputs `{NAME: value}` (0.7.0; secret inputs are bound secrets, never listed here). */
204
252
  inputs() {
205
253
  return this.#ctx.workspaces.inputs(this.id);
@@ -274,6 +322,29 @@ export class Workspace {
274
322
  this.#view = (await this.#ctx.workspaces.cancelSuspendWhenIdle(this.id)).data;
275
323
  return this;
276
324
  }
325
+ /**
326
+ * Resizes this workspace (0.14.0): memory, the held floor, the allocation mode, CPU and disk, whether it is running or
327
+ * suspended, fixed or elastic, without a restart or a fork. Memory changes live on a running workspace (a shrink gives
328
+ * back what the guest frees: `memory.converged`); a suspended one gets its new size when it resumes, before its first
329
+ * call. CPU changes live up to the vCPUs the workspace booted with, beyond that at its next start. Disks grow online.
330
+ * The new caps are stored, so every later start uses them. Resolves once the resize has finished, with per resource
331
+ * when it applies (`now`, `resume` or `next_start` with the `reason`); the handle's view is refreshed. See
332
+ * WorkspacesApi.resize for the errors. A file-first workspace (no VM to resize) is refused locally with
333
+ * NotSupportedForModeError.
334
+ *
335
+ * const r = await workspace.resize({ memoryMib: 6144, cpuMillis: 4000 });
336
+ * r.memory?.applies_at; // 'now'
337
+ * r.cpu?.applies_at; // 'next_start' (reason 'boot_vcpus': more vCPUs than it booted with)
338
+ */
339
+ async resize(params) {
340
+ const refusal = this.#needsVm('resize');
341
+ if (refusal)
342
+ throw refusal;
343
+ const out = await this.#ctx.workspaces.resize(this.id, this.#tracked(params));
344
+ // The resize has happened: a failed view read leaves the old view (the next refresh() reads it again).
345
+ await this.refresh().catch(() => undefined);
346
+ return out;
347
+ }
277
348
  resume(opts = {}) {
278
349
  const refusal = this.#needsVm('resume');
279
350
  if (refusal)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shardflux/sdk",
3
- "version": "0.13.1",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "description": "Shardflux TypeScript SDK: open persistent agent workspaces by key and give your agent workspace tools (exec, files, processes, PTY, git, browser).",
6
6
  "license": "Apache-2.0",