@shardflux/sdk 0.8.0 → 0.10.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 +258 -0
- package/README.md +338 -7
- package/dist/account.d.ts +493 -0
- package/dist/account.js +641 -0
- package/dist/cell.d.ts +203 -8
- package/dist/cell.js +457 -32
- package/dist/client.d.ts +120 -5
- package/dist/client.js +142 -6
- package/dist/errors.d.ts +90 -3
- package/dist/errors.js +93 -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 +10546 -5855
- package/dist/generated/cell-api.d.ts +501 -9
- 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 +28 -1
- package/dist/tools.js +172 -21
- package/dist/usage.d.ts +36 -6
- package/dist/usage.js +19 -4
- package/dist/version-check.d.ts +101 -0
- package/dist/version-check.js +191 -0
- package/dist/workspace.d.ts +100 -6
- package/dist/workspace.js +198 -12
- package/package.json +1 -1
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.
|
|
@@ -116,10 +132,45 @@ export class Workspace {
|
|
|
116
132
|
get startup() {
|
|
117
133
|
return this.#view.startup ?? null;
|
|
118
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* The pending suspend-when-idle request as of the last view (0.10.0; `idle.suspend_request`): `{requested_at,
|
|
137
|
+
* after_seconds, not_before}`, or null when there is none, when the workspace is not running, or once a tool call or
|
|
138
|
+
* a resume after the request cancelled it. `refresh()` reads it again.
|
|
139
|
+
*/
|
|
140
|
+
get suspendRequest() {
|
|
141
|
+
return this.#view.idle?.suspend_request ?? null;
|
|
142
|
+
}
|
|
119
143
|
/** The raw view (GET /v1/workspaces/{id}). */
|
|
120
144
|
get data() {
|
|
121
145
|
return this.#view;
|
|
122
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* `processful` (one VM keeps processes, memory and files) or `file_first` (0.9.0; contracts §29: a versioned file
|
|
149
|
+
* tree, commands run as executions). Immutable. A view without the field (older API) is processful.
|
|
150
|
+
*/
|
|
151
|
+
get mode() {
|
|
152
|
+
return this.#view.mode ?? 'processful';
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* File-first workspaces: the newest tree revision this handle has seen (0 when created; each mutating files call or
|
|
156
|
+
* execution that changed something publishes the next). It follows every response of this handle's cell clients
|
|
157
|
+
* (`X-Tree-Revision`), execution results and refresh(); another writer's changes appear once a response reports
|
|
158
|
+
* them. Pass it as `ifTreeRevision` to make a write conditional. Null for processful workspaces.
|
|
159
|
+
*/
|
|
160
|
+
get treeRevision() {
|
|
161
|
+
return this.mode === 'file_first' ? this.#treeRevision : null;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Executions of this file-first workspace (contracts §29.8) through `cell()`'s default client: `run(argv, opts)` runs
|
|
165
|
+
* a command in a fresh VM on the latest tree and returns an ExecutionResult (output, exit code, `changed`,
|
|
166
|
+
* `treeRevision`); `get(id, { waitMs })` reads one. See CellClient.executions.
|
|
167
|
+
*
|
|
168
|
+
* const r = await workspace.executions.run(['bash', '-lc', 'pytest -q'], { timeoutMs: 600_000 });
|
|
169
|
+
* console.log(r.exitCode, r.stdoutText, r.changed, r.treeRevision);
|
|
170
|
+
*/
|
|
171
|
+
get executions() {
|
|
172
|
+
return this.cell().executions;
|
|
173
|
+
}
|
|
123
174
|
/** Secret names bound to this workspace (injected into every exec/PTY start): `get()`, `set(names)`. */
|
|
124
175
|
get secrets() {
|
|
125
176
|
return new WorkspaceSecrets(this.#ctx, this.id);
|
|
@@ -130,6 +181,8 @@ export class Workspace {
|
|
|
130
181
|
}
|
|
131
182
|
async refresh() {
|
|
132
183
|
this.#view = await this.#ctx.http.json('GET', `/v1/workspaces/${encodeURIComponent(this.id)}`, {}, this.#ctx.authorization);
|
|
184
|
+
if (this.#view.mode === 'file_first' && typeof this.#view.tree_revision === 'number')
|
|
185
|
+
this.#noteTreeRevision(this.#view.tree_revision);
|
|
133
186
|
return this;
|
|
134
187
|
}
|
|
135
188
|
/** Waits for the active operation (if any) and refreshes. */
|
|
@@ -143,15 +196,67 @@ export class Workspace {
|
|
|
143
196
|
return this.#ctx.workspaces.delete(this.id, { ...this.#tracked(opts), [AFTER_WAIT]: this.#refreshAfterWait() });
|
|
144
197
|
}
|
|
145
198
|
suspend(opts = {}) {
|
|
199
|
+
const refusal = this.#needsVm('suspend');
|
|
200
|
+
if (refusal)
|
|
201
|
+
return Promise.reject(refusal);
|
|
146
202
|
return this.#ctx.workspaces.suspend(this.id, { ...this.#tracked(opts), [AFTER_WAIT]: this.#refreshAfterWait() });
|
|
147
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Suspends the workspace once it has been idle for `afterSeconds` (30..3600; 0.9.0): call it when your agent's turn
|
|
206
|
+
* ends, so the workspace stops using RAM soon after instead of waiting out its idle policy. A command still running,
|
|
207
|
+
* an attached stream or a keepalive postpones the suspend until `afterSeconds` after it ends; the next tool call (the
|
|
208
|
+
* next turn) or a resume cancels it. Resolves with the recorded `suspendRequest` (also `workspace.suspendRequest`), or
|
|
209
|
+
* with the suspend already in progress as `operation`. See WorkspacesApi.suspendWhenIdle for the errors. A file-first
|
|
210
|
+
* workspace (never suspended) is refused locally with NotSupportedForModeError.
|
|
211
|
+
*
|
|
212
|
+
* const { suspendRequest } = await workspace.suspendWhenIdle({ afterSeconds: 60 });
|
|
213
|
+
*/
|
|
214
|
+
async suspendWhenIdle(opts) {
|
|
215
|
+
const refusal = this.#needsVm('suspend_when_idle');
|
|
216
|
+
if (refusal)
|
|
217
|
+
throw refusal;
|
|
218
|
+
const out = await this.#ctx.workspaces.suspendWhenIdle(this.id, opts);
|
|
219
|
+
this.#view = out.workspace.data;
|
|
220
|
+
return { ...out, workspace: this };
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Cancels a pending suspend-when-idle request (0.10.0; idempotent, in any state) and refreshes this handle's view.
|
|
224
|
+
* A suspend the request already started is not undone (it shows as `activeOperation`).
|
|
225
|
+
*/
|
|
226
|
+
async cancelSuspendWhenIdle() {
|
|
227
|
+
this.#view = (await this.#ctx.workspaces.cancelSuspendWhenIdle(this.id)).data;
|
|
228
|
+
return this;
|
|
229
|
+
}
|
|
148
230
|
resume(opts = {}) {
|
|
149
|
-
|
|
231
|
+
const refusal = this.#needsVm('resume');
|
|
232
|
+
if (refusal)
|
|
233
|
+
return Promise.reject(refusal);
|
|
234
|
+
// With `wait` the held resume's view and token go straight into this handle (the token for agentLabel/tools).
|
|
235
|
+
const manager = this.tokens({ ...(opts.agentLabel !== undefined ? { agentLabel: opts.agentLabel } : {}), ...(opts.tools !== undefined ? { tools: opts.tools } : {}) });
|
|
236
|
+
return this.#ctx.workspaces.resume(this.id, { ...this.#tracked(opts), [AFTER_WAIT]: this.#refreshAfterWait(), [HELD_RESUME]: this.#heldTarget(manager) });
|
|
237
|
+
}
|
|
238
|
+
/** A held resume's 200 (contracts §22.6) goes straight into this handle: the view, and the token for `manager`. */
|
|
239
|
+
#heldTarget(manager) {
|
|
240
|
+
return {
|
|
241
|
+
agentLabel: manager.agentLabel,
|
|
242
|
+
tools: manager.tools,
|
|
243
|
+
adopt: (view, token) => {
|
|
244
|
+
this.#view = view;
|
|
245
|
+
if (token)
|
|
246
|
+
manager.seed(token);
|
|
247
|
+
},
|
|
248
|
+
};
|
|
150
249
|
}
|
|
151
250
|
snapshot(opts = {}) {
|
|
251
|
+
const refusal = this.#needsVm('snapshot');
|
|
252
|
+
if (refusal)
|
|
253
|
+
return Promise.reject(refusal);
|
|
152
254
|
return this.#ctx.workspaces.snapshot(this.id, { ...this.#tracked(opts), [AFTER_WAIT]: this.#refreshAfterWait() });
|
|
153
255
|
}
|
|
154
256
|
fork(target, opts = {}) {
|
|
257
|
+
const refusal = this.#needsVm('fork');
|
|
258
|
+
if (refusal)
|
|
259
|
+
return Promise.reject(refusal);
|
|
155
260
|
return this.#ctx.workspaces.fork(this.id, target, this.#tracked(opts));
|
|
156
261
|
}
|
|
157
262
|
async close(opts = {}) {
|
|
@@ -169,6 +274,9 @@ export class Workspace {
|
|
|
169
274
|
return out.operation;
|
|
170
275
|
}
|
|
171
276
|
async reset(opts = {}) {
|
|
277
|
+
const refusal = this.#needsVm('reset');
|
|
278
|
+
if (refusal)
|
|
279
|
+
throw refusal;
|
|
172
280
|
const op = await this.#ctx.workspaces.reset(this.id, { ...this.#tracked(opts), [AFTER_WAIT]: this.#refreshAfterWait() });
|
|
173
281
|
for (const m of this.#managers.values())
|
|
174
282
|
m.invalidate();
|
|
@@ -197,7 +305,7 @@ export class Workspace {
|
|
|
197
305
|
maxRetries: 0,
|
|
198
306
|
...(opts.transitionTimeoutMs !== undefined ? { transitionTimeoutMs: opts.transitionTimeoutMs } : {}),
|
|
199
307
|
...(this.#ctx.onProgress ? { onProgress: this.#ctx.onProgress } : {}),
|
|
200
|
-
wake: opts.wake === undefined ? (timeoutMs, signal) => this.wake({ timeoutMs, ...(signal ? { signal } : {}) }) : opts.wake,
|
|
308
|
+
wake: opts.wake === undefined ? (timeoutMs, signal) => this.wake({ timeoutMs, ...(signal ? { signal } : {}), ...(agentLabel !== undefined ? { agentLabel } : {}), ...(tools !== undefined ? { tools } : {}) }) : opts.wake,
|
|
201
309
|
});
|
|
202
310
|
return new ToolCallCapture({ workspaceId: this.id, cell, registry: this.#ctx.captures, ...(this.#ctx.sleep !== defaultSleep ? { sleep: this.#ctx.sleep } : {}) }, opts);
|
|
203
311
|
}
|
|
@@ -207,11 +315,15 @@ export class Workspace {
|
|
|
207
315
|
}
|
|
208
316
|
/** Saves this layered workspace as the next version of an organization template (contracts §19.8). */
|
|
209
317
|
saveAsTemplate(params) {
|
|
318
|
+
const refusal = this.#needsVm('save_as_template');
|
|
319
|
+
if (refusal)
|
|
320
|
+
return Promise.reject(refusal);
|
|
210
321
|
return this.#ctx.workspaces.saveAsTemplate(this.id, params);
|
|
211
322
|
}
|
|
212
323
|
/**
|
|
213
324
|
* The workspace's changes against its template (cell gateway GET /v1/workspaces/{id}/changes; needs the `files` tool).
|
|
214
325
|
* 409 workspace_not_running, or conflict with details.reason legacy_disk_layout / guest_feature_unavailable.
|
|
326
|
+
* File-first workspaces: NotSupportedForModeError (each execution result lists what it changed).
|
|
215
327
|
*/
|
|
216
328
|
changes(opts = {}) {
|
|
217
329
|
const { agentLabel, tools, ...params } = opts;
|
|
@@ -249,8 +361,14 @@ export class Workspace {
|
|
|
249
361
|
userAgent: this.#ctx.userAgent,
|
|
250
362
|
sleep: this.#ctx.sleep,
|
|
251
363
|
...cellOpts,
|
|
364
|
+
// The view's mode (undefined from an older API: calls are sent and the server decides).
|
|
365
|
+
mode: () => this.#view.mode,
|
|
366
|
+
onTreeRevision: (rev) => this.#noteTreeRevision(rev),
|
|
252
367
|
...(listener ? { onProgress: listener } : {}),
|
|
253
|
-
|
|
368
|
+
// The wake's held resume brings back this client's token (its label and tools).
|
|
369
|
+
wake: wake === undefined
|
|
370
|
+
? (timeoutMs, signal) => this.wake({ timeoutMs, ...(signal ? { signal } : {}), ...(onProgress ? { onProgress } : {}), ...(agentLabel !== undefined ? { agentLabel } : {}), ...(tools !== undefined ? { tools } : {}) })
|
|
371
|
+
: wake,
|
|
254
372
|
[CAPTURE_BARRIER]: () => this.#ctx.captures.settle(this.id),
|
|
255
373
|
});
|
|
256
374
|
this.#cells.set(k, c);
|
|
@@ -267,8 +385,16 @@ export class Workspace {
|
|
|
267
385
|
* operation_in_progress) at once. Cell calls use it automatically when they meet `workspace_not_running`. A resume no
|
|
268
386
|
* host admits within 15 minutes fails with `capacity_unavailable` (OperationFailedError, `retryable`: the workspace
|
|
269
387
|
* stays suspended with its state; try again later).
|
|
388
|
+
*
|
|
389
|
+
* Since 0.9.0 the resume is held by the server until the workspace runs (contracts §22.6): one request returns the
|
|
390
|
+
* running workspace and a tool token for `agentLabel`/`tools`, which this handle keeps, so a tool call that woke the
|
|
391
|
+
* workspace is retried at once (refused call, resume, call). Timing: one `request` phase with reason `held`. A server
|
|
392
|
+
* that does not hold the request answers at once; the wake then waits for the operation and reads the view.
|
|
270
393
|
*/
|
|
271
394
|
async wake(opts = {}) {
|
|
395
|
+
// A file-first workspace runs from creation and is never suspended (contracts §29.7): nothing to wake.
|
|
396
|
+
if (this.#view.mode === 'file_first')
|
|
397
|
+
return false;
|
|
272
398
|
const trace = new Trace('wake', combineListeners(this.#ctx.onProgress, this.#tracked(opts).onProgress), { workspaceId: this.id });
|
|
273
399
|
return traced(trace, () => this.#wake(opts, trace));
|
|
274
400
|
}
|
|
@@ -276,17 +402,39 @@ export class Workspace {
|
|
|
276
402
|
const deadline = Date.now() + (opts.timeoutMs ?? DEFAULT_TRANSITION_TIMEOUT_MS);
|
|
277
403
|
const waitFor = (operationId) => this.#ctx.workspaces.waitForOperation(operationId, { timeoutMs: Math.max(1, deadline - Date.now()), ...(opts.signal ? { signal: opts.signal } : {}), [TRACE]: trace });
|
|
278
404
|
const resumePath = `/v1/workspaces/${encodeURIComponent(this.id)}/resume`;
|
|
405
|
+
// The held resume's token is for the caller's client (agent label and tools).
|
|
406
|
+
const target = this.#heldTarget(this.tokens({ ...(opts.agentLabel !== undefined ? { agentLabel: opts.agentLabel } : {}), ...(opts.tools !== undefined ? { tools: opts.tools } : {}) }));
|
|
279
407
|
for (let i = 0; i < 4; i += 1) {
|
|
280
408
|
if (opts.signal?.aborted)
|
|
281
409
|
throw opts.signal.reason;
|
|
282
410
|
let active;
|
|
283
411
|
try {
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
trace.
|
|
289
|
-
await
|
|
412
|
+
// Held resume (contracts §22.6): answered once the workspace runs, with the view and a token; it returns the
|
|
413
|
+
// active resume/open when there is one, so concurrent wakes join a single operation. The request is made here
|
|
414
|
+
// rather than through resume() so the wake is one trace.
|
|
415
|
+
const waitS = Math.min(SERVER_WAIT_MAX_S, Math.floor((deadline - Date.now()) / 1000));
|
|
416
|
+
trace.phase('request', waitS >= 1 ? 'held' : i === 0 ? null : 'retry');
|
|
417
|
+
const answer = await this.#ctx.workspaces.requestResume(this.id, {
|
|
418
|
+
waitS,
|
|
419
|
+
agentLabel: target.agentLabel,
|
|
420
|
+
tools: target.tools,
|
|
421
|
+
idempotencyKey: randomId('op-'),
|
|
422
|
+
signal: opts.signal,
|
|
423
|
+
onRetry: trace.onRetry,
|
|
424
|
+
});
|
|
425
|
+
if (answer.operation)
|
|
426
|
+
trace.observe(answer.operation);
|
|
427
|
+
if (answer.ready) {
|
|
428
|
+
target.adopt(answer.workspace, answer.toolToken);
|
|
429
|
+
// No operation: it was already running (after a conflict this wake did wait for an operation).
|
|
430
|
+
return answer.operation !== null || i > 0;
|
|
431
|
+
}
|
|
432
|
+
// Not held (an older server), or not finished within the hold: wait for the operation, then read the view.
|
|
433
|
+
const op = answer.operation;
|
|
434
|
+
if (op.state === 'failed' || op.state === 'canceled')
|
|
435
|
+
throw new OperationFailedError(op);
|
|
436
|
+
if (op.state !== 'succeeded')
|
|
437
|
+
await waitFor(op.id);
|
|
290
438
|
await trace.span('view', () => this.refresh());
|
|
291
439
|
return true;
|
|
292
440
|
}
|
|
@@ -318,6 +466,44 @@ export class Workspace {
|
|
|
318
466
|
}
|
|
319
467
|
throw new Error(`workspace ${this.id} did not become runnable after repeated lifecycle conflicts`);
|
|
320
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* Announces an imminent tool call (cell `POST /wake-hint`, contracts §26.6; 0.9.0+) so a parked workspace is restored
|
|
471
|
+
* ahead of it: call it when the model starts emitting a tool call, before its arguments are complete. The agent tools
|
|
472
|
+
* of `workspaceTools()` send it when a call starts. Cheap and best effort: it returns what the host found. A workspace
|
|
473
|
+
* that is not running (suspended, or a token cannot be issued for it) is woken in the background (`wake`, not awaited
|
|
474
|
+
* here) so the call finds it running sooner. Other errors (network, auth) are thrown; callers that fire and forget
|
|
475
|
+
* should catch them.
|
|
476
|
+
*/
|
|
477
|
+
async hint(opts = {}) {
|
|
478
|
+
// Nothing of a file-first workspace sleeps: resident, without a request.
|
|
479
|
+
if (this.#view.mode === 'file_first')
|
|
480
|
+
return { residency: 'resident', wake: null };
|
|
481
|
+
const { agentLabel, tools, signal } = opts;
|
|
482
|
+
try {
|
|
483
|
+
const r = await this.cell({ ...(agentLabel !== undefined ? { agentLabel } : {}), ...(tools !== undefined ? { tools } : {}), wake: null }).wakeHint(signal);
|
|
484
|
+
return { residency: r.residency, wake: null };
|
|
485
|
+
}
|
|
486
|
+
catch (err) {
|
|
487
|
+
if (!notRunning(err) || signal?.aborted)
|
|
488
|
+
throw err;
|
|
489
|
+
if (opts.wake === null)
|
|
490
|
+
return { residency: null, wake: null };
|
|
491
|
+
if (!this.#hintWake) {
|
|
492
|
+
const timeoutMs = opts.wakeTimeoutMs ?? DEFAULT_TRANSITION_TIMEOUT_MS;
|
|
493
|
+
// The default wake brings back the token of the hinted client (its label and tools).
|
|
494
|
+
const started = opts.wake
|
|
495
|
+
? opts.wake(timeoutMs).then((r) => r !== false)
|
|
496
|
+
: this.wake({ timeoutMs, ...(agentLabel !== undefined ? { agentLabel } : {}), ...(tools !== undefined ? { tools } : {}) });
|
|
497
|
+
const shared = started.finally(() => {
|
|
498
|
+
if (this.#hintWake === shared)
|
|
499
|
+
this.#hintWake = null;
|
|
500
|
+
});
|
|
501
|
+
shared.catch(() => undefined); // nobody has to await it: a failed wake is left to the next tool call
|
|
502
|
+
this.#hintWake = shared;
|
|
503
|
+
}
|
|
504
|
+
return { residency: null, wake: this.#hintWake };
|
|
505
|
+
}
|
|
506
|
+
}
|
|
321
507
|
/** Tools granted by the most recent token (null before one was issued). */
|
|
322
508
|
get grantedTools() {
|
|
323
509
|
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.10.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",
|