@shardflux/sdk 0.13.0 → 0.14.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/progress.js CHANGED
@@ -1,3 +1,5 @@
1
+ /** Every {@link ColdBootReason} this version knows (0.14.0+). */
2
+ export const COLD_BOOT_REASONS = ['runtime_changed', 'host_lost', 'runtime_retired'];
1
3
  const DURABILITY_STATES = new Set(['pending', 'durable', 'lost']);
2
4
  /**
3
5
  * The durable copy of a suspend or fork operation (0.12.0+): `result.durability` camelCased, or null when the result has
@@ -40,6 +42,22 @@ export function lostSuspendOf(op) {
40
42
  stateAsOf: str(r.state_as_of),
41
43
  };
42
44
  }
45
+ /**
46
+ * An operation's `result.host_lost` camelCased (0.13.1+), or null: on a resume or open that restored a workspace whose
47
+ * machine failed, and on a suspend that found it so (`detectedAt` only).
48
+ */
49
+ export function hostLostOf(op) {
50
+ const h = op.result?.host_lost;
51
+ if (typeof h !== 'object' || h === null || Array.isArray(h))
52
+ return null;
53
+ const r = h;
54
+ return {
55
+ detectedAt: str(r.detected_at),
56
+ restoredFrom: r.restored_from === 'disk' || r.restored_from === 'checkpoint' ? r.restored_from : null,
57
+ restoredCheckpointId: str(r.restored_checkpoint_id),
58
+ stateAsOf: str(r.state_as_of),
59
+ };
60
+ }
43
61
  /**
44
62
  * Whether a suspend or fork operation's capture is in durable storage (0.12.0+): `result.durable`, else `true` for a
45
63
  * succeeded suspend or fork whose result predates the field, else null.
@@ -122,6 +140,7 @@ export function serverTiming(op) {
122
140
  suspendPath: str(r.suspend_path),
123
141
  durability: durabilityOf(op),
124
142
  lostSuspend: lostSuspendOf(op),
143
+ hostLost: hostLostOf(op),
125
144
  bootToReadyMs: num(r.boot_to_ready_ms),
126
145
  hostTimingsMs,
127
146
  };
@@ -275,6 +294,19 @@ export async function traced(trace, fn) {
275
294
  }
276
295
  }
277
296
  const fmt = (ms) => (ms === null ? '?' : ms < 1000 ? `${Math.round(ms)} ms` : `${(ms / 1000).toFixed(2)} s`);
297
+ /**
298
+ * The server-line part of `result.host_lost` (0.13.1+): the checkpoint a resume restored, or a suspend's detection time.
299
+ * A resume from the disk says nothing more here: its cold boot reads `processes restarted (host_lost)`.
300
+ */
301
+ function hostLostText(h) {
302
+ if (!h)
303
+ return null;
304
+ if (h.restoredFrom === 'checkpoint')
305
+ return `restored checkpoint ${h.restoredCheckpointId ?? '?'} (host_lost; state as of ${h.stateAsOf ?? '?'})`;
306
+ if (h.restoredFrom === null)
307
+ return `host_lost${h.detectedAt ? ` (detected ${h.detectedAt})` : ''}`;
308
+ return null;
309
+ }
278
310
  /**
279
311
  * A human-readable account of a timing, for logs and bug reports. A resume in production:
280
312
  *
@@ -285,7 +317,10 @@ const fmt = (ms) => (ms === null ? '?' : ms < 1000 ? `${Math.round(ms)} ms` : `$
285
317
  *
286
318
  * A call that retried a request adds `retries: <n> (<request>, <cause>, after <delay>)`.
287
319
  * A resume that booted the workspace instead of restoring its memory (0.11.0+) says so in the server line:
288
- * `resume from cold_boot: processes restarted (runtime_changed)`.
320
+ * `resume from cold_boot: processes restarted (runtime_changed)`, or `(host_lost)` (0.13.1+) when the machine the
321
+ * workspace ran on failed and it booted from its disk. A resume that restored the newest checkpoint after such a
322
+ * failure adds `restored checkpoint <id> (host_lost; state as of <time>)`; a suspend that found the machine failed
323
+ * adds `host_lost (detected <time>)`.
289
324
  */
290
325
  export function formatTiming(t) {
291
326
  const ids = [t.workspaceId ? `workspace ${t.workspaceId}` : null, t.operationId ? `operation ${t.operationId}` : null].filter(Boolean).join(', ');
@@ -308,6 +343,7 @@ export function formatTiming(t) {
308
343
  s.startPath ? `start ${s.startPath}${s.warmFallback ? ` (warm fallback: ${s.warmFallback})` : ''}` : null,
309
344
  s.resumePath ? `resume from ${s.resumePath}${s.memoryRestored === false ? `: processes restarted${s.coldBootReason ? ` (${s.coldBootReason})` : ''}` : ''}` : null,
310
345
  s.lostSuspend ? `restored ${s.lostSuspend.restoredCheckpointId ?? 'no checkpoint'} (latest suspend ${s.lostSuspend.checkpointId} ${s.lostSuspend.reason ?? 'lost'})` : null,
346
+ hostLostText(s.hostLost),
311
347
  s.suspendPath === 'local_commit' ? 'sealed on host' : null,
312
348
  s.durability?.state === 'durable' ? `durable${s.durability.localCommitToDurableMs !== null ? ` ${fmt(s.durability.localCommitToDurableMs)} later` : ''}` : s.durability ? `durable copy ${s.durability.state}` : null,
313
349
  s.bootToReadyMs !== null ? `boot to ready ${fmt(s.bootToReadyMs)}` : null,
package/dist/tools.d.ts CHANGED
@@ -33,6 +33,8 @@ export interface WorkspaceTool<A extends Record<string, unknown> = Record<string
33
33
  };
34
34
  /** Which workspace tool permission the call needs (exec, files, pty, process, git, browser). */
35
35
  permission: ToolName;
36
+ /** Opt-in input-start hook (0.14.0+): call when the model starts this tool's input. Returns immediately. */
37
+ onInputStart?: () => void;
36
38
  /** `toolCallId` (0.7.0+): the model's call id, recorded by tool-call capture (executeToolCall passes it). */
37
39
  execute(args: A, options?: {
38
40
  signal?: AbortSignal;
@@ -74,6 +76,12 @@ export interface WorkspaceToolsOptions {
74
76
  * suspended workspace (or restore a hibernated one) that the read does not need.
75
77
  */
76
78
  hint?: boolean;
79
+ /**
80
+ * Add `onInputStart` hooks (0.14.0+) to tools that need the VM. Invoke the matching hook when the model starts
81
+ * streaming that tool's input, ahead of execute. Default false: no hook, timer or request is installed.
82
+ * Offline file reads and file-first workspaces have no hook.
83
+ */
84
+ prewake?: boolean;
77
85
  /**
78
86
  * The mode to build the tools for (0.9.0+; default `workspace.mode`). Given, the workspace is not touched until a tool
79
87
  * runs, so definitions can be built without one (e.g. to publish them before any workspace exists).
package/dist/tools.js CHANGED
@@ -87,6 +87,13 @@ const ALL = ['exec', 'files', 'pty', 'process', 'git', 'browser'];
87
87
  const FILE_FIRST_TOOLS = ['exec', 'files'];
88
88
  /** Changed paths returned to the model per execution (the rest is flagged `changed_truncated`). */
89
89
  const MAX_CHANGED_LISTED = 200;
90
+ /** The processful exec's memory hint (0.14.0; RunOptions.resourceHint). Sent only when the model sets it. */
91
+ const RESOURCE_HINT_SCHEMA = {
92
+ type: 'string',
93
+ enum: ['auto', 'light', 'heavy'],
94
+ description: 'heavy: give this command more memory before it starts (a build, a test suite, a package install, a training run); light: start it at once. Default auto: decided from the command.',
95
+ };
96
+ const RESOURCE_HINTS = new Set(['auto', 'light', 'heavy']);
90
97
  /** Tools served from a sleeping workspace's disk without waking it: the runner sends no hint. */
91
98
  const DISK_READS = new Set(['read_file', 'list_files', 'search_files']);
92
99
  const obj = (properties, required = []) => ({ type: 'object', properties, required, additionalProperties: false });
@@ -267,6 +274,7 @@ export function workspaceTools(workspace, opts = {}) {
267
274
  type: 'boolean',
268
275
  description: 'Start the command and return its session_id at once instead of waiting for it to exit (default false). Use it for anything that may run longer than a few minutes, such as a build, a test suite, a training run or a server. Keep using the other tools meanwhile, read it with exec_read and stop it with exec_cancel. Set timeout_ms to the longest it may run: the workspace stays awake until the command ends or that time passes (1 hour when unset).',
269
276
  },
277
+ resource_hint: RESOURCE_HINT_SCHEMA,
270
278
  ...(opts.burst
271
279
  ? {
272
280
  burst: {
@@ -330,6 +338,7 @@ export function workspaceTools(workspace, opts = {}) {
330
338
  const burst = opts.burst && (a.burst === 'always' || a.burst === 'never') ? a.burst : undefined;
331
339
  const burstVcpus = opts.burst && typeof a.burst_vcpus === 'number' ? a.burst_vcpus : undefined;
332
340
  const burstMemoryMib = opts.burst && typeof a.burst_memory_mib === 'number' ? a.burst_memory_mib : undefined;
341
+ const resourceHint = RESOURCE_HINTS.has(a.resource_hint) ? a.resource_hint : undefined;
333
342
  const cwd = typeof a.cwd === 'string' ? a.cwd : opts.defaultCwd;
334
343
  if (a.background === true) {
335
344
  // A session of its own: the start answers at once, and exec_read / exec_cancel find it by session_id.
@@ -339,6 +348,7 @@ export function workspaceTools(workspace, opts = {}) {
339
348
  ...(cwd !== undefined ? { cwd } : {}),
340
349
  ...(typeof a.timeout_ms === 'number' ? { timeout_ms: a.timeout_ms } : {}),
341
350
  ...(typeof a.stdin === 'string' ? { stdin: Buffer.from(a.stdin, 'utf8').toString('base64') } : {}),
351
+ ...(resourceHint !== undefined ? { resource_hint: resourceHint } : {}),
342
352
  ...(burst !== undefined ? { burst } : {}),
343
353
  ...(burstVcpus !== undefined ? { burst_vcpus: burstVcpus } : {}),
344
354
  ...(burstMemoryMib !== undefined ? { burst_memory_mib: burstMemoryMib } : {}),
@@ -353,6 +363,7 @@ export function workspaceTools(workspace, opts = {}) {
353
363
  ...(cwd !== undefined ? { cwd } : {}),
354
364
  timeoutMs: typeof a.timeout_ms === 'number' ? a.timeout_ms : 600_000,
355
365
  ...(typeof a.stdin === 'string' ? { stdin: a.stdin } : {}),
366
+ ...(resourceHint !== undefined ? { resourceHint } : {}),
356
367
  ...(burst !== undefined ? { burst } : {}),
357
368
  ...(burstVcpus !== undefined ? { burstVcpus } : {}),
358
369
  ...(burstMemoryMib !== undefined ? { burstMemoryMib } : {}),
@@ -698,6 +709,15 @@ export function workspaceTools(workspace, opts = {}) {
698
709
  description: d.description,
699
710
  parameters: d.parameters,
700
711
  permission: d.permission,
712
+ ...(opts.prewake === true && !fileFirst && !DISK_READS.has(d.name) ? {
713
+ onInputStart: () => {
714
+ workspace.hint({
715
+ ...(opts.agentLabel !== undefined ? { agentLabel: opts.agentLabel } : {}),
716
+ wake: opts.wake ?? null,
717
+ ...(opts.transitionTimeoutMs !== undefined ? { wakeTimeoutMs: opts.transitionTimeoutMs } : {}),
718
+ }).catch(() => undefined);
719
+ },
720
+ } : {}),
701
721
  execute: async (args, options = {}) => {
702
722
  const issues = validateArgs(d.parameters, args);
703
723
  if (issues.length === 0 && d.refuse)
package/dist/usage.d.ts CHANGED
@@ -56,7 +56,9 @@ export declare class UsageApi {
56
56
  constructor(ctx: () => ClientContext);
57
57
  /**
58
58
  * Current-period usage per meter, allowances with enforcement and cap state (`overage` while opt-in overage covers
59
- * usage past a CPU-hours or RAM GiB-hours allowance), measurement freshness, `exhausted_reason` (the 402
59
+ * usage past a CPU-hours or RAM GiB-hours allowance; `storage_blocked` (0.14.0+, enforcement `storage_block`) while
60
+ * the Retained state allowance is used up: opening a new key and forking are refused with 403 `quota_exceeded`,
61
+ * details.limit `retained_state`), measurement freshness, `exhausted_reason` (the 402
60
62
  * allowance_exhausted reason while starts are refused: `allowance_used`, `overage_paused`, `spend_cap_reached`) and
61
63
  * `spend_cap` (0.10.0).
62
64
  */
package/dist/usage.js CHANGED
@@ -9,7 +9,9 @@ export class UsageApi {
9
9
  }
10
10
  /**
11
11
  * Current-period usage per meter, allowances with enforcement and cap state (`overage` while opt-in overage covers
12
- * usage past a CPU-hours or RAM GiB-hours allowance), measurement freshness, `exhausted_reason` (the 402
12
+ * usage past a CPU-hours or RAM GiB-hours allowance; `storage_blocked` (0.14.0+, enforcement `storage_block`) while
13
+ * the Retained state allowance is used up: opening a new key and forking are refused with 403 `quota_exceeded`,
14
+ * details.limit `retained_state`), measurement freshness, `exhausted_reason` (the 402
13
15
  * allowance_exhausted reason while starts are refused: `allowance_used`, `overage_paused`, `spend_cap_reached`) and
14
16
  * `spend_cap` (0.10.0).
15
17
  */
@@ -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, 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,7 @@ 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 { WorkspacePorts } from './ports.js';
13
14
  import { WorkspaceSecrets } from './secrets.js';
14
15
  import type { CellClientOptions, Residency, WorkspaceChangesPage, WorkspaceChangesParams } from './cell.js';
15
16
  import type { SaveAsTemplateParams, SaveAsTemplateResponse, WorkspaceStartup } from './templates.js';
@@ -67,7 +68,9 @@ export declare class Workspace {
67
68
  * woke the workspace), or a lifecycle call with `wait`. Null for handles from get()/list() until such a call.
68
69
  * `formatTiming(workspace.lastTiming)` prints it. After a resume or wake, `server.memoryRestored === false` (0.11.0+)
69
70
  * means the workspace booted from its saved disk instead of restoring its memory (`server.resumePath` `cold_boot`,
70
- * reason in `server.coldBootReason`): files are as of the suspend, running processes were restarted.
71
+ * reason in `server.coldBootReason`): files are as of the suspend, running processes were restarted. Reason
72
+ * `host_lost` (0.13.1+): the machine the workspace ran on failed and it booted from its disk, files kept;
73
+ * `server.hostLost` says what the resume restored (`hostLostOf(op)` on the operation).
71
74
  */
72
75
  get lastTiming(): LifecycleTiming | null;
73
76
  get id(): string;
@@ -148,6 +151,15 @@ export declare class Workspace {
148
151
  get executions(): CellClient['executions'];
149
152
  /** Secret names bound to this workspace (injected into every exec/PTY start): `get()`, `set(names)`. */
150
153
  get secrets(): WorkspaceSecrets;
154
+ /**
155
+ * Inbound ports (0.14.0): serve a TCP port of the workspace at its own private HTTPS URL. A request wakes a parked or
156
+ * suspended workspace and is served once it runs.
157
+ *
158
+ * const { url } = await workspace.ports.expose(3000); // the server listens on 0.0.0.0:3000
159
+ * const { token } = await workspace.ports.token(3000); // Authorization: Bearer <token>
160
+ * const link = await workspace.ports.link(3000); // open link.url in a browser
161
+ */
162
+ get ports(): WorkspacePorts;
151
163
  /** The workspace's text inputs `{NAME: value}` (0.7.0; secret inputs are bound secrets, never listed here). */
152
164
  inputs(): Promise<Record<string, string>>;
153
165
  get labels(): Record<string, string>;
@@ -174,6 +186,8 @@ export declare class Workspace {
174
186
  * resolve once it has FINISHED, with `workspace.state` then `suspended`. That is as soon as the workspace is sealed on
175
187
  * its host, typically in a few hundred ms. `result.durable` (also `lastTiming.server.durable`, 0.12.0+) turns true
176
188
  * when the copy lands in durable storage, typically within a second; `{ durable: true }` resolves only then.
189
+ * A suspend that finds the machine the workspace ran on failed succeeds at once (0.13.1+): `result.durable` is true
190
+ * and `result.host_lost` (`hostLostOf(op)`) has `detectedAt`; the next resume restores the workspace.
177
191
  *
178
192
  * await workspace.suspend({ wait: true });
179
193
  * await workspace.suspend({ durable: true }); // 0.12.0+: also wait for the durable copy
@@ -196,6 +210,21 @@ export declare class Workspace {
196
210
  * A suspend the request already started is not undone (it shows as `activeOperation`).
197
211
  */
198
212
  cancelSuspendWhenIdle(): Promise<this>;
213
+ /**
214
+ * Resizes this workspace (0.14.0): memory, the held floor, the allocation mode, CPU and disk, whether it is running or
215
+ * suspended, fixed or elastic, without a restart or a fork. Memory changes live on a running workspace (a shrink gives
216
+ * back what the guest frees: `memory.converged`); a suspended one gets its new size when it resumes, before its first
217
+ * call. CPU changes live up to the vCPUs the workspace booted with, beyond that at its next start. Disks grow online.
218
+ * The new caps are stored, so every later start uses them. Resolves once the resize has finished, with per resource
219
+ * when it applies (`now`, `resume` or `next_start` with the `reason`); the handle's view is refreshed. See
220
+ * WorkspacesApi.resize for the errors. A file-first workspace (no VM to resize) is refused locally with
221
+ * NotSupportedForModeError.
222
+ *
223
+ * const r = await workspace.resize({ memoryMib: 6144, cpuMillis: 4000 });
224
+ * r.memory?.applies_at; // 'now'
225
+ * r.cpu?.applies_at; // 'next_start' (reason 'boot_vcpus': more vCPUs than it booted with)
226
+ */
227
+ resize(params: ResizeParams): Promise<ResizeResult>;
199
228
  /**
200
229
  * Resumes a suspended workspace. Resolves when the resume is REQUESTED; with `{ wait: true }`, once the workspace runs.
201
230
  * Tool calls wake a suspended workspace by themselves, so this is rarely needed. With `wait` (0.9.0) it is one held
@@ -205,11 +234,16 @@ export declare class Workspace {
205
234
  * (also `lastTiming.server.memoryRestored`, 0.11.0+) is false when the resume booted the saved disk instead
206
235
  * (`resume_path` `cold_boot`): files kept, processes restarted. `result.lost_suspend` (also
207
236
  * `lastTiming.server.lostSuspend`, `lostSuspendOf(op)`, 0.12.0+) names a suspend this resume could not restore and the
208
- * checkpoint it restored instead (see the lifecycle reference).
237
+ * checkpoint it restored instead (see the lifecycle reference). `result.host_lost` (also `lastTiming.server.hostLost`,
238
+ * `hostLostOf(op)`, 0.13.1+): the machine the workspace ran on failed and this resume restored it, from its disk
239
+ * (`cold_boot_reason` `host_lost`) or from its newest checkpoint (`state_as_of`).
209
240
  */
210
241
  resume(opts: WaitedResumeOptions): Promise<FinishedOperation>;
211
242
  resume(opts?: ResumeOptions): Promise<Operation>;
212
- /** Takes a snapshot. Resolves when it is REQUESTED; with `{ wait: true }`, once it is taken. */
243
+ /**
244
+ * Takes a snapshot. Resolves when it is REQUESTED; with `{ wait: true }`, once it is taken. After the machine the
245
+ * workspace ran on failed, resume it first: until then the snapshot fails `resume_required` (0.13.1+).
246
+ */
213
247
  snapshot(opts: WaitedLifecycleOptions & {
214
248
  label?: string;
215
249
  }): Promise<FinishedOperation>;
@@ -218,7 +252,8 @@ export declare class Workspace {
218
252
  }): Promise<Operation>;
219
253
  /**
220
254
  * Forks into a new key. Resolves when the fork is REQUESTED (the copy's handle is returned at once); with
221
- * `{ wait: true }`, once the copy exists, with its handle refreshed.
255
+ * `{ wait: true }`, once the copy exists, with its handle refreshed. After the machine the workspace ran on failed,
256
+ * resume it first: until then the fork fails `resume_required` (0.13.1+).
222
257
  */
223
258
  fork(target: ForkTarget, opts: WaitedForkOptions): Promise<{
224
259
  operation: FinishedOperation;
@@ -301,8 +336,9 @@ export declare class Workspace {
301
336
  * that does not hold the request answers at once; the wake then waits for the operation and reads the view.
302
337
  *
303
338
  * A wake whose resume could not restore the workspace's memory (0.11.0+; the platform's VM runtime changed after the
304
- * suspend) still resolves `true`: the workspace runs from its saved disk, with every process restarted. `lastTiming`
305
- * and the `done` progress event say so (`server.memoryRestored === false`, `server.resumePath` `cold_boot`).
339
+ * suspend, or, 0.13.1+, the machine the workspace ran on failed) still resolves `true`: the workspace runs from its
340
+ * saved disk, with every process restarted. `lastTiming` and the `done` progress event say so
341
+ * (`server.memoryRestored === false`, `server.resumePath` `cold_boot`, `server.coldBootReason`).
306
342
  */
307
343
  wake(opts?: WakeOptions): Promise<boolean>;
308
344
  /**
package/dist/workspace.js CHANGED
@@ -4,6 +4,7 @@ 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 { WorkspacePorts } from "./ports.js";
7
8
  import { WorkspaceSecrets } from "./secrets.js";
8
9
  import { ToolTokenManager } from "./tokens.js";
9
10
  const notRunning = (e) => e instanceof ShardfluxApiError && (e.code === 'workspace_not_running' || (e.code === 'conflict' && e.reason === 'workspace_not_running'));
@@ -44,7 +45,9 @@ export class Workspace {
44
45
  * woke the workspace), or a lifecycle call with `wait`. Null for handles from get()/list() until such a call.
45
46
  * `formatTiming(workspace.lastTiming)` prints it. After a resume or wake, `server.memoryRestored === false` (0.11.0+)
46
47
  * means the workspace booted from its saved disk instead of restoring its memory (`server.resumePath` `cold_boot`,
47
- * reason in `server.coldBootReason`): files are as of the suspend, running processes were restarted.
48
+ * reason in `server.coldBootReason`): files are as of the suspend, running processes were restarted. Reason
49
+ * `host_lost` (0.13.1+): the machine the workspace ran on failed and it booted from its disk, files kept;
50
+ * `server.hostLost` says what the resume restored (`hostLostOf(op)` on the operation).
48
51
  */
49
52
  get lastTiming() {
50
53
  return this.#lastTiming ?? this.#openTrace?.finished ?? null;
@@ -198,6 +201,17 @@ export class Workspace {
198
201
  get secrets() {
199
202
  return new WorkspaceSecrets(this.#ctx, this.id);
200
203
  }
204
+ /**
205
+ * Inbound ports (0.14.0): serve a TCP port of the workspace at its own private HTTPS URL. A request wakes a parked or
206
+ * suspended workspace and is served once it runs.
207
+ *
208
+ * const { url } = await workspace.ports.expose(3000); // the server listens on 0.0.0.0:3000
209
+ * const { token } = await workspace.ports.token(3000); // Authorization: Bearer <token>
210
+ * const link = await workspace.ports.link(3000); // open link.url in a browser
211
+ */
212
+ get ports() {
213
+ return new WorkspacePorts(this.#ctx, this.id);
214
+ }
201
215
  /** The workspace's text inputs `{NAME: value}` (0.7.0; secret inputs are bound secrets, never listed here). */
202
216
  inputs() {
203
217
  return this.#ctx.workspaces.inputs(this.id);
@@ -272,6 +286,29 @@ export class Workspace {
272
286
  this.#view = (await this.#ctx.workspaces.cancelSuspendWhenIdle(this.id)).data;
273
287
  return this;
274
288
  }
289
+ /**
290
+ * Resizes this workspace (0.14.0): memory, the held floor, the allocation mode, CPU and disk, whether it is running or
291
+ * suspended, fixed or elastic, without a restart or a fork. Memory changes live on a running workspace (a shrink gives
292
+ * back what the guest frees: `memory.converged`); a suspended one gets its new size when it resumes, before its first
293
+ * call. CPU changes live up to the vCPUs the workspace booted with, beyond that at its next start. Disks grow online.
294
+ * The new caps are stored, so every later start uses them. Resolves once the resize has finished, with per resource
295
+ * when it applies (`now`, `resume` or `next_start` with the `reason`); the handle's view is refreshed. See
296
+ * WorkspacesApi.resize for the errors. A file-first workspace (no VM to resize) is refused locally with
297
+ * NotSupportedForModeError.
298
+ *
299
+ * const r = await workspace.resize({ memoryMib: 6144, cpuMillis: 4000 });
300
+ * r.memory?.applies_at; // 'now'
301
+ * r.cpu?.applies_at; // 'next_start' (reason 'boot_vcpus': more vCPUs than it booted with)
302
+ */
303
+ async resize(params) {
304
+ const refusal = this.#needsVm('resize');
305
+ if (refusal)
306
+ throw refusal;
307
+ const out = await this.#ctx.workspaces.resize(this.id, this.#tracked(params));
308
+ // The resize has happened: a failed view read leaves the old view (the next refresh() reads it again).
309
+ await this.refresh().catch(() => undefined);
310
+ return out;
311
+ }
275
312
  resume(opts = {}) {
276
313
  const refusal = this.#needsVm('resume');
277
314
  if (refusal)
@@ -438,8 +475,9 @@ export class Workspace {
438
475
  * that does not hold the request answers at once; the wake then waits for the operation and reads the view.
439
476
  *
440
477
  * A wake whose resume could not restore the workspace's memory (0.11.0+; the platform's VM runtime changed after the
441
- * suspend) still resolves `true`: the workspace runs from its saved disk, with every process restarted. `lastTiming`
442
- * and the `done` progress event say so (`server.memoryRestored === false`, `server.resumePath` `cold_boot`).
478
+ * suspend, or, 0.13.1+, the machine the workspace ran on failed) still resolves `true`: the workspace runs from its
479
+ * saved disk, with every process restarted. `lastTiming` and the `done` progress event say so
480
+ * (`server.memoryRestored === false`, `server.resumePath` `cold_boot`, `server.coldBootReason`).
443
481
  */
444
482
  async wake(opts = {}) {
445
483
  // A file-first workspace runs from creation and is never suspended: nothing to wake.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shardflux/sdk",
3
- "version": "0.13.0",
3
+ "version": "0.14.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",