@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.
- package/CHANGELOG.md +85 -23
- package/README.md +226 -7
- package/dist/account.d.ts +2 -0
- package/dist/account.js +6 -0
- package/dist/cell.d.ts +27 -0
- package/dist/cell.js +71 -0
- package/dist/client.d.ts +54 -5
- package/dist/client.js +100 -7
- package/dist/computer.d.ts +153 -0
- package/dist/computer.js +229 -0
- package/dist/errors.d.ts +5 -1
- package/dist/errors.js +9 -0
- package/dist/executions.d.ts +2 -6
- package/dist/executions.js +9 -0
- package/dist/exit-code.d.ts +7 -0
- package/dist/exit-code.js +12 -0
- package/dist/generated/app-api.d.ts +1055 -119
- package/dist/generated/cell-api.d.ts +334 -0
- package/dist/http.d.ts +7 -1
- package/dist/http.js +34 -13
- package/dist/index.d.ts +13 -6
- package/dist/index.js +7 -2
- package/dist/ports.d.ts +7 -0
- package/dist/ports.js +1 -1
- package/dist/progress.d.ts +2 -2
- package/dist/progress.js +1 -1
- package/dist/templates.d.ts +12 -0
- package/dist/templates.js +9 -0
- package/dist/testing/index.d.ts +62 -0
- package/dist/testing/index.js +585 -0
- package/dist/testing/seed.d.ts +433 -0
- package/dist/testing/seed.js +449 -0
- package/dist/tools.d.ts +21 -3
- package/dist/tools.js +113 -22
- package/dist/tunnel-assets/linux-amd64.gz +0 -0
- package/dist/tunnel-assets/linux-arm64.gz +0 -0
- package/dist/tunnel-assets.d.ts +10 -0
- package/dist/tunnel-assets.js +11 -0
- package/dist/tunnel-packet.d.ts +3 -0
- package/dist/tunnel-packet.js +43 -0
- package/dist/tunnel-pty.d.ts +86 -0
- package/dist/tunnel-pty.js +243 -0
- package/dist/tunnels.d.ts +47 -0
- package/dist/tunnels.js +454 -0
- package/dist/workspace-ref.d.ts +87 -0
- package/dist/workspace-ref.js +173 -0
- package/dist/workspace.d.ts +40 -1
- package/dist/workspace.js +111 -2
- 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
|
+
}
|
package/dist/workspace.d.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Shardflux TypeScript SDK:
|
|
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": [
|