@shardflux/sdk 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +220 -1
- package/README.md +247 -10
- package/dist/account.d.ts +469 -0
- package/dist/account.js +620 -0
- package/dist/cell.d.ts +197 -8
- package/dist/cell.js +449 -31
- package/dist/client.d.ts +76 -5
- package/dist/client.js +114 -6
- package/dist/errors.d.ts +62 -3
- package/dist/errors.js +65 -1
- package/dist/executions.d.ts +120 -0
- package/dist/executions.js +99 -0
- package/dist/feedback.d.ts +67 -0
- package/dist/feedback.js +39 -0
- package/dist/generated/app-api.d.ts +12323 -8072
- package/dist/generated/cell-api.d.ts +463 -8
- package/dist/http.d.ts +7 -1
- package/dist/http.js +26 -7
- package/dist/index.d.ts +18 -7
- package/dist/index.js +5 -1
- package/dist/lifecycle.d.ts +27 -2
- package/dist/lifecycle.js +5 -0
- package/dist/progress.js +4 -2
- package/dist/templates.js +2 -2
- package/dist/tools.d.ts +72 -22
- package/dist/tools.js +156 -23
- package/dist/version-check.d.ts +101 -0
- package/dist/version-check.js +191 -0
- package/dist/workspace.d.ts +77 -5
- package/dist/workspace.js +164 -12
- package/package.json +2 -1
package/dist/workspace.d.ts
CHANGED
|
@@ -6,11 +6,12 @@ import type { ClientContext, DiskLayout, ForkTarget, Operation, WaitOptions, Wor
|
|
|
6
6
|
import { CAPTURE_BARRIER, CellClient } from './cell.js';
|
|
7
7
|
import { ToolCallCapture } from './capture.js';
|
|
8
8
|
import type { ToolCallCaptureOptions } from './capture.js';
|
|
9
|
-
import type {
|
|
9
|
+
import type { WorkspaceMode } from './errors.js';
|
|
10
|
+
import type { FinishedOperation, LifecycleOptions, ResumeOptions, WaitedLifecycleOptions, WaitedResumeOptions } from './lifecycle.js';
|
|
10
11
|
import { Trace } from './progress.js';
|
|
11
12
|
import type { LifecycleTiming, ProgressListener } from './progress.js';
|
|
12
13
|
import { WorkspaceSecrets } from './secrets.js';
|
|
13
|
-
import type { CellClientOptions, WorkspaceChangesPage, WorkspaceChangesParams } from './cell.js';
|
|
14
|
+
import type { CellClientOptions, Residency, WorkspaceChangesPage, WorkspaceChangesParams } from './cell.js';
|
|
14
15
|
import type { SaveAsTemplateParams, SaveAsTemplateResponse, WorkspaceStartup } from './templates.js';
|
|
15
16
|
import { ToolTokenManager } from './tokens.js';
|
|
16
17
|
import type { ToolName, ToolToken } from './tokens.js';
|
|
@@ -20,6 +21,38 @@ export interface WakeOptions {
|
|
|
20
21
|
signal?: AbortSignal;
|
|
21
22
|
/** Progress of the wake: the resume request, observed states, conflicts retried, and `done` with the timing. */
|
|
22
23
|
onProgress?: ProgressListener;
|
|
24
|
+
/**
|
|
25
|
+
* The tool token the wake brings back (0.9.0): the held resume (contracts §22.6) returns one with the running
|
|
26
|
+
* workspace, for this agent label and tool set (defaults: those given to open()). It is kept for `cell()` clients of
|
|
27
|
+
* the same label and tools, whose next call then needs no token request. A `cell()` client's own wake passes its
|
|
28
|
+
* label and tools.
|
|
29
|
+
*/
|
|
30
|
+
agentLabel?: string;
|
|
31
|
+
tools?: ToolName[];
|
|
32
|
+
}
|
|
33
|
+
export interface HintOptions {
|
|
34
|
+
/** The tool token to use: the agent label and tool set of `cell()` (defaults: those given to open()). */
|
|
35
|
+
agentLabel?: string;
|
|
36
|
+
tools?: ToolName[];
|
|
37
|
+
/**
|
|
38
|
+
* When the workspace is not running: how to wake it in the background. Default `workspace.wake()`; `null` does not
|
|
39
|
+
* wake it; a function replaces the wake (the same hook as `CellClientOptions.wake`).
|
|
40
|
+
*/
|
|
41
|
+
wake?: CellClientOptions['wake'];
|
|
42
|
+
/** Bound on the background wake (default 120 000 ms). */
|
|
43
|
+
wakeTimeoutMs?: number;
|
|
44
|
+
signal?: AbortSignal;
|
|
45
|
+
}
|
|
46
|
+
/** `workspace.hint()`: what the host found, or the background wake of a workspace that was not running. */
|
|
47
|
+
export interface HintResult {
|
|
48
|
+
/** The VM's residency when the hint arrived (contracts §25.1); null when the workspace was not running. */
|
|
49
|
+
residency: Residency | null;
|
|
50
|
+
/**
|
|
51
|
+
* The wake started in the background because the workspace was not running (409 `workspace_not_running`), else null.
|
|
52
|
+
* It resolves like `wake()`; nothing needs to await it (a failure is then left to the next tool call, which wakes
|
|
53
|
+
* the workspace itself), and concurrent hints share one wake.
|
|
54
|
+
*/
|
|
55
|
+
wake: Promise<boolean> | null;
|
|
23
56
|
}
|
|
24
57
|
export declare class Workspace {
|
|
25
58
|
#private;
|
|
@@ -69,6 +102,27 @@ export declare class Workspace {
|
|
|
69
102
|
get startup(): WorkspaceStartup | null;
|
|
70
103
|
/** The raw view (GET /v1/workspaces/{id}). */
|
|
71
104
|
get data(): WorkspaceView;
|
|
105
|
+
/**
|
|
106
|
+
* `processful` (one VM keeps processes, memory and files) or `file_first` (0.9.0; contracts §29: a versioned file
|
|
107
|
+
* tree, commands run as executions). Immutable. A view without the field (older API) is processful.
|
|
108
|
+
*/
|
|
109
|
+
get mode(): WorkspaceMode;
|
|
110
|
+
/**
|
|
111
|
+
* File-first workspaces: the newest tree revision this handle has seen (0 when created; each mutating files call or
|
|
112
|
+
* execution that changed something publishes the next). It follows every response of this handle's cell clients
|
|
113
|
+
* (`X-Tree-Revision`), execution results and refresh(); another writer's changes appear once a response reports
|
|
114
|
+
* them. Pass it as `ifTreeRevision` to make a write conditional. Null for processful workspaces.
|
|
115
|
+
*/
|
|
116
|
+
get treeRevision(): number | null;
|
|
117
|
+
/**
|
|
118
|
+
* Executions of this file-first workspace (contracts §29.8) through `cell()`'s default client: `run(argv, opts)` runs
|
|
119
|
+
* a command in a fresh VM on the latest tree and returns an ExecutionResult (output, exit code, `changed`,
|
|
120
|
+
* `treeRevision`); `get(id, { waitMs })` reads one. See CellClient.executions.
|
|
121
|
+
*
|
|
122
|
+
* const r = await workspace.executions.run(['bash', '-lc', 'pytest -q'], { timeoutMs: 600_000 });
|
|
123
|
+
* console.log(r.exitCode, r.stdoutText, r.changed, r.treeRevision);
|
|
124
|
+
*/
|
|
125
|
+
get executions(): CellClient['executions'];
|
|
72
126
|
/** Secret names bound to this workspace (injected into every exec/PTY start): `get()`, `set(names)`. */
|
|
73
127
|
get secrets(): WorkspaceSecrets;
|
|
74
128
|
/** The workspace's text inputs `{NAME: value}` (0.7.0; secret inputs are bound secrets, never listed here). */
|
|
@@ -93,10 +147,13 @@ export declare class Workspace {
|
|
|
93
147
|
suspend(opts?: LifecycleOptions): Promise<Operation>;
|
|
94
148
|
/**
|
|
95
149
|
* Resumes a suspended workspace. Resolves when the resume is REQUESTED; with `{ wait: true }`, once the workspace runs.
|
|
96
|
-
* Tool calls wake a suspended workspace by themselves, so this is rarely needed.
|
|
150
|
+
* Tool calls wake a suspended workspace by themselves, so this is rarely needed. With `wait` (0.9.0) it is one held
|
|
151
|
+
* request (contracts §22.6): the handle takes the running view and a tool token for `agentLabel`/`tools` (defaults:
|
|
152
|
+
* those given to open()), so `cell()` calls with that label and tools start at once. A workspace that is already
|
|
153
|
+
* running is ShardfluxApiError 409 `conflict` (`already_running`).
|
|
97
154
|
*/
|
|
98
|
-
resume(opts:
|
|
99
|
-
resume(opts?:
|
|
155
|
+
resume(opts: WaitedResumeOptions): Promise<FinishedOperation>;
|
|
156
|
+
resume(opts?: ResumeOptions): Promise<Operation>;
|
|
100
157
|
/** Takes a snapshot. Resolves when it is REQUESTED; with `{ wait: true }`, once it is taken. */
|
|
101
158
|
snapshot(opts: WaitedLifecycleOptions & {
|
|
102
159
|
label?: string;
|
|
@@ -151,6 +208,7 @@ export declare class Workspace {
|
|
|
151
208
|
/**
|
|
152
209
|
* The workspace's changes against its template (cell gateway GET /v1/workspaces/{id}/changes; needs the `files` tool).
|
|
153
210
|
* 409 workspace_not_running, or conflict with details.reason legacy_disk_layout / guest_feature_unavailable.
|
|
211
|
+
* File-first workspaces: NotSupportedForModeError (each execution result lists what it changed).
|
|
154
212
|
*/
|
|
155
213
|
changes(opts?: WorkspaceChangesParams & {
|
|
156
214
|
agentLabel?: string;
|
|
@@ -180,8 +238,22 @@ export declare class Workspace {
|
|
|
180
238
|
* operation_in_progress) at once. Cell calls use it automatically when they meet `workspace_not_running`. A resume no
|
|
181
239
|
* host admits within 15 minutes fails with `capacity_unavailable` (OperationFailedError, `retryable`: the workspace
|
|
182
240
|
* stays suspended with its state; try again later).
|
|
241
|
+
*
|
|
242
|
+
* Since 0.9.0 the resume is held by the server until the workspace runs (contracts §22.6): one request returns the
|
|
243
|
+
* running workspace and a tool token for `agentLabel`/`tools`, which this handle keeps, so a tool call that woke the
|
|
244
|
+
* workspace is retried at once (refused call, resume, call). Timing: one `request` phase with reason `held`. A server
|
|
245
|
+
* that does not hold the request answers at once; the wake then waits for the operation and reads the view.
|
|
183
246
|
*/
|
|
184
247
|
wake(opts?: WakeOptions): Promise<boolean>;
|
|
248
|
+
/**
|
|
249
|
+
* Announces an imminent tool call (cell `POST /wake-hint`, contracts §26.6; 0.9.0+) so a parked workspace is restored
|
|
250
|
+
* ahead of it: call it when the model starts emitting a tool call, before its arguments are complete. The agent tools
|
|
251
|
+
* of `workspaceTools()` send it when a call starts. Cheap and best effort: it returns what the host found. A workspace
|
|
252
|
+
* that is not running (suspended, or a token cannot be issued for it) is woken in the background (`wake`, not awaited
|
|
253
|
+
* here) so the call finds it running sooner. Other errors (network, auth) are thrown; callers that fire and forget
|
|
254
|
+
* should catch them.
|
|
255
|
+
*/
|
|
256
|
+
hint(opts?: HintOptions): Promise<HintResult>;
|
|
185
257
|
/** Tools granted by the most recent token (null before one was issued). */
|
|
186
258
|
get grantedTools(): ToolName[] | null;
|
|
187
259
|
}
|
package/dist/workspace.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { CAPTURE_BARRIER, CellClient, DEFAULT_TRANSITION_TIMEOUT_MS } from "./cell.js";
|
|
2
2
|
import { ToolCallCapture } from "./capture.js";
|
|
3
|
-
import { OperationFailedError, ShardfluxApiError } from "./errors.js";
|
|
4
|
-
import { defaultSleep, randomId } from "./http.js";
|
|
5
|
-
import { AFTER_WAIT, TRACE } from "./lifecycle.js";
|
|
3
|
+
import { NotSupportedForModeError, OperationFailedError, ShardfluxApiError } from "./errors.js";
|
|
4
|
+
import { SERVER_WAIT_MAX_S, defaultSleep, randomId } from "./http.js";
|
|
5
|
+
import { AFTER_WAIT, HELD_RESUME, TRACE } from "./lifecycle.js";
|
|
6
6
|
import { Trace, combineListeners, traced } from "./progress.js";
|
|
7
7
|
import { WorkspaceSecrets } from "./secrets.js";
|
|
8
8
|
import { ToolTokenManager } from "./tokens.js";
|
|
9
|
+
const notRunning = (e) => e instanceof ShardfluxApiError && (e.code === 'workspace_not_running' || (e.code === 'conflict' && e.reason === 'workspace_not_running'));
|
|
9
10
|
export class Workspace {
|
|
10
11
|
#view;
|
|
12
|
+
/** The background wake started by hint(), shared by concurrent hints until it settles. */
|
|
13
|
+
#hintWake = null;
|
|
11
14
|
#ctx;
|
|
12
15
|
#defaults;
|
|
13
16
|
#managers = new Map();
|
|
@@ -15,14 +18,27 @@ export class Workspace {
|
|
|
15
18
|
/** The open() that produced this handle; its timing is final once open() has returned. */
|
|
16
19
|
#openTrace;
|
|
17
20
|
#lastTiming;
|
|
21
|
+
/** The newest tree revision seen (file-first): the view's, or any cell response's since. */
|
|
22
|
+
#treeRevision;
|
|
18
23
|
constructor(ctx, view, opts = {}) {
|
|
19
24
|
this.#ctx = ctx;
|
|
20
25
|
this.#view = view;
|
|
26
|
+
this.#treeRevision = view.mode === 'file_first' && typeof view.tree_revision === 'number' ? view.tree_revision : null;
|
|
21
27
|
this.#defaults = { agentLabel: opts.agentLabel, tools: opts.tools };
|
|
22
28
|
this.#openTrace = opts.trace;
|
|
23
29
|
if (opts.token)
|
|
24
30
|
this.tokens().seed(opts.token);
|
|
25
31
|
}
|
|
32
|
+
#noteTreeRevision(rev) {
|
|
33
|
+
if (this.#treeRevision === null || rev > this.#treeRevision)
|
|
34
|
+
this.#treeRevision = rev;
|
|
35
|
+
}
|
|
36
|
+
/** The local refusal of a lifecycle call a file-first workspace does not have (contracts §29.7). */
|
|
37
|
+
#needsVm(operation) {
|
|
38
|
+
if (this.#view.mode !== 'file_first')
|
|
39
|
+
return null;
|
|
40
|
+
return NotSupportedForModeError.local('file_first', operation, 'api', `${operation} is not available for a file-first workspace: it has no VM between executions and is never suspended; its state is its file tree (workspace.treeRevision).`);
|
|
41
|
+
}
|
|
26
42
|
/**
|
|
27
43
|
* Where the time went in the last lifecycle call made through this handle: open(), wake() (also when a tool call
|
|
28
44
|
* woke the workspace), or a lifecycle call with `wait`. Null for handles from get()/list() until such a call.
|
|
@@ -120,6 +136,33 @@ export class Workspace {
|
|
|
120
136
|
get data() {
|
|
121
137
|
return this.#view;
|
|
122
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* `processful` (one VM keeps processes, memory and files) or `file_first` (0.9.0; contracts §29: a versioned file
|
|
141
|
+
* tree, commands run as executions). Immutable. A view without the field (older API) is processful.
|
|
142
|
+
*/
|
|
143
|
+
get mode() {
|
|
144
|
+
return this.#view.mode ?? 'processful';
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* File-first workspaces: the newest tree revision this handle has seen (0 when created; each mutating files call or
|
|
148
|
+
* execution that changed something publishes the next). It follows every response of this handle's cell clients
|
|
149
|
+
* (`X-Tree-Revision`), execution results and refresh(); another writer's changes appear once a response reports
|
|
150
|
+
* them. Pass it as `ifTreeRevision` to make a write conditional. Null for processful workspaces.
|
|
151
|
+
*/
|
|
152
|
+
get treeRevision() {
|
|
153
|
+
return this.mode === 'file_first' ? this.#treeRevision : null;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Executions of this file-first workspace (contracts §29.8) through `cell()`'s default client: `run(argv, opts)` runs
|
|
157
|
+
* a command in a fresh VM on the latest tree and returns an ExecutionResult (output, exit code, `changed`,
|
|
158
|
+
* `treeRevision`); `get(id, { waitMs })` reads one. See CellClient.executions.
|
|
159
|
+
*
|
|
160
|
+
* const r = await workspace.executions.run(['bash', '-lc', 'pytest -q'], { timeoutMs: 600_000 });
|
|
161
|
+
* console.log(r.exitCode, r.stdoutText, r.changed, r.treeRevision);
|
|
162
|
+
*/
|
|
163
|
+
get executions() {
|
|
164
|
+
return this.cell().executions;
|
|
165
|
+
}
|
|
123
166
|
/** Secret names bound to this workspace (injected into every exec/PTY start): `get()`, `set(names)`. */
|
|
124
167
|
get secrets() {
|
|
125
168
|
return new WorkspaceSecrets(this.#ctx, this.id);
|
|
@@ -130,6 +173,8 @@ export class Workspace {
|
|
|
130
173
|
}
|
|
131
174
|
async refresh() {
|
|
132
175
|
this.#view = await this.#ctx.http.json('GET', `/v1/workspaces/${encodeURIComponent(this.id)}`, {}, this.#ctx.authorization);
|
|
176
|
+
if (this.#view.mode === 'file_first' && typeof this.#view.tree_revision === 'number')
|
|
177
|
+
this.#noteTreeRevision(this.#view.tree_revision);
|
|
133
178
|
return this;
|
|
134
179
|
}
|
|
135
180
|
/** Waits for the active operation (if any) and refreshes. */
|
|
@@ -143,15 +188,41 @@ export class Workspace {
|
|
|
143
188
|
return this.#ctx.workspaces.delete(this.id, { ...this.#tracked(opts), [AFTER_WAIT]: this.#refreshAfterWait() });
|
|
144
189
|
}
|
|
145
190
|
suspend(opts = {}) {
|
|
191
|
+
const refusal = this.#needsVm('suspend');
|
|
192
|
+
if (refusal)
|
|
193
|
+
return Promise.reject(refusal);
|
|
146
194
|
return this.#ctx.workspaces.suspend(this.id, { ...this.#tracked(opts), [AFTER_WAIT]: this.#refreshAfterWait() });
|
|
147
195
|
}
|
|
148
196
|
resume(opts = {}) {
|
|
149
|
-
|
|
197
|
+
const refusal = this.#needsVm('resume');
|
|
198
|
+
if (refusal)
|
|
199
|
+
return Promise.reject(refusal);
|
|
200
|
+
// With `wait` the held resume's view and token go straight into this handle (the token for agentLabel/tools).
|
|
201
|
+
const manager = this.tokens({ ...(opts.agentLabel !== undefined ? { agentLabel: opts.agentLabel } : {}), ...(opts.tools !== undefined ? { tools: opts.tools } : {}) });
|
|
202
|
+
return this.#ctx.workspaces.resume(this.id, { ...this.#tracked(opts), [AFTER_WAIT]: this.#refreshAfterWait(), [HELD_RESUME]: this.#heldTarget(manager) });
|
|
203
|
+
}
|
|
204
|
+
/** A held resume's 200 (contracts §22.6) goes straight into this handle: the view, and the token for `manager`. */
|
|
205
|
+
#heldTarget(manager) {
|
|
206
|
+
return {
|
|
207
|
+
agentLabel: manager.agentLabel,
|
|
208
|
+
tools: manager.tools,
|
|
209
|
+
adopt: (view, token) => {
|
|
210
|
+
this.#view = view;
|
|
211
|
+
if (token)
|
|
212
|
+
manager.seed(token);
|
|
213
|
+
},
|
|
214
|
+
};
|
|
150
215
|
}
|
|
151
216
|
snapshot(opts = {}) {
|
|
217
|
+
const refusal = this.#needsVm('snapshot');
|
|
218
|
+
if (refusal)
|
|
219
|
+
return Promise.reject(refusal);
|
|
152
220
|
return this.#ctx.workspaces.snapshot(this.id, { ...this.#tracked(opts), [AFTER_WAIT]: this.#refreshAfterWait() });
|
|
153
221
|
}
|
|
154
222
|
fork(target, opts = {}) {
|
|
223
|
+
const refusal = this.#needsVm('fork');
|
|
224
|
+
if (refusal)
|
|
225
|
+
return Promise.reject(refusal);
|
|
155
226
|
return this.#ctx.workspaces.fork(this.id, target, this.#tracked(opts));
|
|
156
227
|
}
|
|
157
228
|
async close(opts = {}) {
|
|
@@ -169,6 +240,9 @@ export class Workspace {
|
|
|
169
240
|
return out.operation;
|
|
170
241
|
}
|
|
171
242
|
async reset(opts = {}) {
|
|
243
|
+
const refusal = this.#needsVm('reset');
|
|
244
|
+
if (refusal)
|
|
245
|
+
throw refusal;
|
|
172
246
|
const op = await this.#ctx.workspaces.reset(this.id, { ...this.#tracked(opts), [AFTER_WAIT]: this.#refreshAfterWait() });
|
|
173
247
|
for (const m of this.#managers.values())
|
|
174
248
|
m.invalidate();
|
|
@@ -197,7 +271,7 @@ export class Workspace {
|
|
|
197
271
|
maxRetries: 0,
|
|
198
272
|
...(opts.transitionTimeoutMs !== undefined ? { transitionTimeoutMs: opts.transitionTimeoutMs } : {}),
|
|
199
273
|
...(this.#ctx.onProgress ? { onProgress: this.#ctx.onProgress } : {}),
|
|
200
|
-
wake: opts.wake === undefined ? (timeoutMs, signal) => this.wake({ timeoutMs, ...(signal ? { signal } : {}) }) : opts.wake,
|
|
274
|
+
wake: opts.wake === undefined ? (timeoutMs, signal) => this.wake({ timeoutMs, ...(signal ? { signal } : {}), ...(agentLabel !== undefined ? { agentLabel } : {}), ...(tools !== undefined ? { tools } : {}) }) : opts.wake,
|
|
201
275
|
});
|
|
202
276
|
return new ToolCallCapture({ workspaceId: this.id, cell, registry: this.#ctx.captures, ...(this.#ctx.sleep !== defaultSleep ? { sleep: this.#ctx.sleep } : {}) }, opts);
|
|
203
277
|
}
|
|
@@ -207,11 +281,15 @@ export class Workspace {
|
|
|
207
281
|
}
|
|
208
282
|
/** Saves this layered workspace as the next version of an organization template (contracts §19.8). */
|
|
209
283
|
saveAsTemplate(params) {
|
|
284
|
+
const refusal = this.#needsVm('save_as_template');
|
|
285
|
+
if (refusal)
|
|
286
|
+
return Promise.reject(refusal);
|
|
210
287
|
return this.#ctx.workspaces.saveAsTemplate(this.id, params);
|
|
211
288
|
}
|
|
212
289
|
/**
|
|
213
290
|
* The workspace's changes against its template (cell gateway GET /v1/workspaces/{id}/changes; needs the `files` tool).
|
|
214
291
|
* 409 workspace_not_running, or conflict with details.reason legacy_disk_layout / guest_feature_unavailable.
|
|
292
|
+
* File-first workspaces: NotSupportedForModeError (each execution result lists what it changed).
|
|
215
293
|
*/
|
|
216
294
|
changes(opts = {}) {
|
|
217
295
|
const { agentLabel, tools, ...params } = opts;
|
|
@@ -249,8 +327,14 @@ export class Workspace {
|
|
|
249
327
|
userAgent: this.#ctx.userAgent,
|
|
250
328
|
sleep: this.#ctx.sleep,
|
|
251
329
|
...cellOpts,
|
|
330
|
+
// The view's mode (undefined from an older API: calls are sent and the server decides).
|
|
331
|
+
mode: () => this.#view.mode,
|
|
332
|
+
onTreeRevision: (rev) => this.#noteTreeRevision(rev),
|
|
252
333
|
...(listener ? { onProgress: listener } : {}),
|
|
253
|
-
|
|
334
|
+
// The wake's held resume brings back this client's token (its label and tools).
|
|
335
|
+
wake: wake === undefined
|
|
336
|
+
? (timeoutMs, signal) => this.wake({ timeoutMs, ...(signal ? { signal } : {}), ...(onProgress ? { onProgress } : {}), ...(agentLabel !== undefined ? { agentLabel } : {}), ...(tools !== undefined ? { tools } : {}) })
|
|
337
|
+
: wake,
|
|
254
338
|
[CAPTURE_BARRIER]: () => this.#ctx.captures.settle(this.id),
|
|
255
339
|
});
|
|
256
340
|
this.#cells.set(k, c);
|
|
@@ -267,8 +351,16 @@ export class Workspace {
|
|
|
267
351
|
* operation_in_progress) at once. Cell calls use it automatically when they meet `workspace_not_running`. A resume no
|
|
268
352
|
* host admits within 15 minutes fails with `capacity_unavailable` (OperationFailedError, `retryable`: the workspace
|
|
269
353
|
* stays suspended with its state; try again later).
|
|
354
|
+
*
|
|
355
|
+
* Since 0.9.0 the resume is held by the server until the workspace runs (contracts §22.6): one request returns the
|
|
356
|
+
* running workspace and a tool token for `agentLabel`/`tools`, which this handle keeps, so a tool call that woke the
|
|
357
|
+
* workspace is retried at once (refused call, resume, call). Timing: one `request` phase with reason `held`. A server
|
|
358
|
+
* that does not hold the request answers at once; the wake then waits for the operation and reads the view.
|
|
270
359
|
*/
|
|
271
360
|
async wake(opts = {}) {
|
|
361
|
+
// A file-first workspace runs from creation and is never suspended (contracts §29.7): nothing to wake.
|
|
362
|
+
if (this.#view.mode === 'file_first')
|
|
363
|
+
return false;
|
|
272
364
|
const trace = new Trace('wake', combineListeners(this.#ctx.onProgress, this.#tracked(opts).onProgress), { workspaceId: this.id });
|
|
273
365
|
return traced(trace, () => this.#wake(opts, trace));
|
|
274
366
|
}
|
|
@@ -276,17 +368,39 @@ export class Workspace {
|
|
|
276
368
|
const deadline = Date.now() + (opts.timeoutMs ?? DEFAULT_TRANSITION_TIMEOUT_MS);
|
|
277
369
|
const waitFor = (operationId) => this.#ctx.workspaces.waitForOperation(operationId, { timeoutMs: Math.max(1, deadline - Date.now()), ...(opts.signal ? { signal: opts.signal } : {}), [TRACE]: trace });
|
|
278
370
|
const resumePath = `/v1/workspaces/${encodeURIComponent(this.id)}/resume`;
|
|
371
|
+
// The held resume's token is for the caller's client (agent label and tools).
|
|
372
|
+
const target = this.#heldTarget(this.tokens({ ...(opts.agentLabel !== undefined ? { agentLabel: opts.agentLabel } : {}), ...(opts.tools !== undefined ? { tools: opts.tools } : {}) }));
|
|
279
373
|
for (let i = 0; i < 4; i += 1) {
|
|
280
374
|
if (opts.signal?.aborted)
|
|
281
375
|
throw opts.signal.reason;
|
|
282
376
|
let active;
|
|
283
377
|
try {
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
trace.
|
|
289
|
-
await
|
|
378
|
+
// Held resume (contracts §22.6): answered once the workspace runs, with the view and a token; it returns the
|
|
379
|
+
// active resume/open when there is one, so concurrent wakes join a single operation. The request is made here
|
|
380
|
+
// rather than through resume() so the wake is one trace.
|
|
381
|
+
const waitS = Math.min(SERVER_WAIT_MAX_S, Math.floor((deadline - Date.now()) / 1000));
|
|
382
|
+
trace.phase('request', waitS >= 1 ? 'held' : i === 0 ? null : 'retry');
|
|
383
|
+
const answer = await this.#ctx.workspaces.requestResume(this.id, {
|
|
384
|
+
waitS,
|
|
385
|
+
agentLabel: target.agentLabel,
|
|
386
|
+
tools: target.tools,
|
|
387
|
+
idempotencyKey: randomId('op-'),
|
|
388
|
+
signal: opts.signal,
|
|
389
|
+
onRetry: trace.onRetry,
|
|
390
|
+
});
|
|
391
|
+
if (answer.operation)
|
|
392
|
+
trace.observe(answer.operation);
|
|
393
|
+
if (answer.ready) {
|
|
394
|
+
target.adopt(answer.workspace, answer.toolToken);
|
|
395
|
+
// No operation: it was already running (after a conflict this wake did wait for an operation).
|
|
396
|
+
return answer.operation !== null || i > 0;
|
|
397
|
+
}
|
|
398
|
+
// Not held (an older server), or not finished within the hold: wait for the operation, then read the view.
|
|
399
|
+
const op = answer.operation;
|
|
400
|
+
if (op.state === 'failed' || op.state === 'canceled')
|
|
401
|
+
throw new OperationFailedError(op);
|
|
402
|
+
if (op.state !== 'succeeded')
|
|
403
|
+
await waitFor(op.id);
|
|
290
404
|
await trace.span('view', () => this.refresh());
|
|
291
405
|
return true;
|
|
292
406
|
}
|
|
@@ -318,6 +432,44 @@ export class Workspace {
|
|
|
318
432
|
}
|
|
319
433
|
throw new Error(`workspace ${this.id} did not become runnable after repeated lifecycle conflicts`);
|
|
320
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* Announces an imminent tool call (cell `POST /wake-hint`, contracts §26.6; 0.9.0+) so a parked workspace is restored
|
|
437
|
+
* ahead of it: call it when the model starts emitting a tool call, before its arguments are complete. The agent tools
|
|
438
|
+
* of `workspaceTools()` send it when a call starts. Cheap and best effort: it returns what the host found. A workspace
|
|
439
|
+
* that is not running (suspended, or a token cannot be issued for it) is woken in the background (`wake`, not awaited
|
|
440
|
+
* here) so the call finds it running sooner. Other errors (network, auth) are thrown; callers that fire and forget
|
|
441
|
+
* should catch them.
|
|
442
|
+
*/
|
|
443
|
+
async hint(opts = {}) {
|
|
444
|
+
// Nothing of a file-first workspace sleeps: resident, without a request.
|
|
445
|
+
if (this.#view.mode === 'file_first')
|
|
446
|
+
return { residency: 'resident', wake: null };
|
|
447
|
+
const { agentLabel, tools, signal } = opts;
|
|
448
|
+
try {
|
|
449
|
+
const r = await this.cell({ ...(agentLabel !== undefined ? { agentLabel } : {}), ...(tools !== undefined ? { tools } : {}), wake: null }).wakeHint(signal);
|
|
450
|
+
return { residency: r.residency, wake: null };
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
if (!notRunning(err) || signal?.aborted)
|
|
454
|
+
throw err;
|
|
455
|
+
if (opts.wake === null)
|
|
456
|
+
return { residency: null, wake: null };
|
|
457
|
+
if (!this.#hintWake) {
|
|
458
|
+
const timeoutMs = opts.wakeTimeoutMs ?? DEFAULT_TRANSITION_TIMEOUT_MS;
|
|
459
|
+
// The default wake brings back the token of the hinted client (its label and tools).
|
|
460
|
+
const started = opts.wake
|
|
461
|
+
? opts.wake(timeoutMs).then((r) => r !== false)
|
|
462
|
+
: this.wake({ timeoutMs, ...(agentLabel !== undefined ? { agentLabel } : {}), ...(tools !== undefined ? { tools } : {}) });
|
|
463
|
+
const shared = started.finally(() => {
|
|
464
|
+
if (this.#hintWake === shared)
|
|
465
|
+
this.#hintWake = null;
|
|
466
|
+
});
|
|
467
|
+
shared.catch(() => undefined); // nobody has to await it: a failed wake is left to the next tool call
|
|
468
|
+
this.#hintWake = shared;
|
|
469
|
+
}
|
|
470
|
+
return { residency: null, wake: this.#hintWake };
|
|
471
|
+
}
|
|
472
|
+
}
|
|
321
473
|
/** Tools granted by the most recent token (null before one was issued). */
|
|
322
474
|
get grantedTools() {
|
|
323
475
|
for (const m of this.#managers.values())
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardflux/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"ajv": "8.20.0",
|
|
61
61
|
"eslint": "10.11.0",
|
|
62
62
|
"langchain": "1.5.14",
|
|
63
|
+
"openai": "7.23.0",
|
|
63
64
|
"openapi-typescript": "7.13.0",
|
|
64
65
|
"typescript": "5.9.3",
|
|
65
66
|
"typescript-eslint": "8.70.1",
|