@shardflux/sdk 0.5.0 → 0.6.1
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 +73 -0
- package/README.md +228 -24
- package/dist/cell.d.ts +67 -2
- package/dist/cell.js +98 -8
- package/dist/client.d.ts +148 -22
- package/dist/client.js +237 -29
- package/dist/errors.d.ts +20 -0
- package/dist/errors.js +10 -0
- package/dist/generated/app-api.d.ts +10223 -5857
- package/dist/generated/cell-api.d.ts +140 -8
- package/dist/http.d.ts +30 -1
- package/dist/http.js +57 -3
- package/dist/index.d.ts +13 -9
- package/dist/index.js +5 -4
- package/dist/lifecycle.d.ts +48 -0
- package/dist/lifecycle.js +33 -0
- package/dist/progress.d.ts +166 -0
- package/dist/progress.js +240 -0
- package/dist/secrets.d.ts +83 -6
- package/dist/secrets.js +53 -1
- package/dist/templates.d.ts +279 -7
- package/dist/templates.js +216 -4
- package/dist/tokens.d.ts +8 -3
- package/dist/tokens.js +25 -13
- package/dist/tools.d.ts +17 -0
- package/dist/tools.js +5 -1
- package/dist/workspace.d.ts +112 -20
- package/dist/workspace.js +176 -10
- package/package.json +2 -1
package/dist/tokens.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Trace, traced } from "./progress.js";
|
|
1
2
|
export class ToolTokenManager {
|
|
2
3
|
workspaceId;
|
|
3
4
|
agentLabel;
|
|
@@ -8,6 +9,7 @@ export class ToolTokenManager {
|
|
|
8
9
|
#now;
|
|
9
10
|
#current;
|
|
10
11
|
#inflight;
|
|
12
|
+
#invalidated = false;
|
|
11
13
|
/** Number of tokens fetched from the API (observability / tests). */
|
|
12
14
|
fetched = 0;
|
|
13
15
|
constructor(http, authorization, workspaceId, opts = {}) {
|
|
@@ -30,24 +32,33 @@ export class ToolTokenManager {
|
|
|
30
32
|
#fresh(t) {
|
|
31
33
|
return t !== undefined && Date.parse(t.expires_at) - this.#now() > this.#skew;
|
|
32
34
|
}
|
|
33
|
-
|
|
35
|
+
/**
|
|
36
|
+
* The current token, or a new one when there is none or it expires within the skew. A fetch is a traced `token` call:
|
|
37
|
+
* `onProgress` sees it (phase `request` with the reason `initial`, `expiring` or `invalidated`) and its timing.
|
|
38
|
+
*/
|
|
39
|
+
async get(onProgress) {
|
|
34
40
|
if (this.#fresh(this.#current))
|
|
35
41
|
return this.#current;
|
|
36
|
-
return this.refresh();
|
|
42
|
+
return this.refresh(onProgress, this.#current ? 'expiring' : this.#invalidated ? 'invalidated' : 'initial');
|
|
37
43
|
}
|
|
38
|
-
/** Forces a new token (single-flight). */
|
|
39
|
-
refresh() {
|
|
44
|
+
/** Forces a new token (single-flight: concurrent callers share one request, and the first caller's trace). */
|
|
45
|
+
refresh(onProgress, reason = 'forced') {
|
|
40
46
|
this.#inflight ??= (async () => {
|
|
47
|
+
const trace = new Trace('token', onProgress, { workspaceId: this.workspaceId });
|
|
41
48
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
body
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
return await traced(trace, async () => {
|
|
50
|
+
trace.phase('request', reason);
|
|
51
|
+
const body = {};
|
|
52
|
+
if (this.agentLabel !== undefined)
|
|
53
|
+
body.agent_label = this.agentLabel;
|
|
54
|
+
if (this.tools !== undefined)
|
|
55
|
+
body.tools = [...this.tools];
|
|
56
|
+
const token = await this.#http.json('POST', `/v1/workspaces/${encodeURIComponent(this.workspaceId)}/tool-tokens`, { json: body, onRetry: trace.onRetry }, this.#authorization);
|
|
57
|
+
this.fetched += 1;
|
|
58
|
+
this.#current = token;
|
|
59
|
+
this.#invalidated = false;
|
|
60
|
+
return token;
|
|
61
|
+
});
|
|
51
62
|
}
|
|
52
63
|
finally {
|
|
53
64
|
this.#inflight = undefined;
|
|
@@ -57,5 +68,6 @@ export class ToolTokenManager {
|
|
|
57
68
|
}
|
|
58
69
|
invalidate() {
|
|
59
70
|
this.#current = undefined;
|
|
71
|
+
this.#invalidated = true;
|
|
60
72
|
}
|
|
61
73
|
}
|
package/dist/tools.d.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-neutral agent tools for a workspace (TEMPLATES.md):
|
|
3
|
+
*
|
|
4
|
+
* await agent.run({ input, tools: workspaceTools(workspace) });
|
|
5
|
+
*
|
|
6
|
+
* Each tool is { name, description, parameters (JSON Schema 2020-12), execute }.
|
|
7
|
+
* `execute` validates its arguments against `parameters` (the same schema the
|
|
8
|
+
* model saw), then calls the cell gateway through the workspace's managed tool
|
|
9
|
+
* token. The customer's model and agent loop stay in the customer's
|
|
10
|
+
* application; toOpenAITools/toAnthropicTools export the definitions in those
|
|
11
|
+
* providers' formats and executeToolCall dispatches a model's tool call.
|
|
12
|
+
*/
|
|
13
|
+
import type { CellClientOptions } from './cell.js';
|
|
1
14
|
import type { ToolName } from './tokens.js';
|
|
2
15
|
import type { Workspace } from './workspace.js';
|
|
3
16
|
export interface JsonSchema {
|
|
@@ -46,6 +59,10 @@ export interface WorkspaceToolsOptions {
|
|
|
46
59
|
maxOutputBytes?: number;
|
|
47
60
|
/** Working directory for exec when the model gives none (default the guest user's home). */
|
|
48
61
|
defaultCwd?: string;
|
|
62
|
+
/** Wake on use for the tools' cell calls (CellClientOptions.wake): default `workspace.wake()`; `null` opts out. */
|
|
63
|
+
wake?: CellClientOptions['wake'];
|
|
64
|
+
/** Bound on lifecycle waits (busy waits plus wakes) per tool call (CellClientOptions.transitionTimeoutMs). */
|
|
65
|
+
transitionTimeoutMs?: number;
|
|
49
66
|
}
|
|
50
67
|
/** Builds the tool list for a workspace. Synchronous: tokens are fetched on first use. */
|
|
51
68
|
export declare function workspaceTools(workspace: Workspace, opts?: WorkspaceToolsOptions): WorkspaceTool[];
|
package/dist/tools.js
CHANGED
|
@@ -77,7 +77,11 @@ function clip(text, max) {
|
|
|
77
77
|
}
|
|
78
78
|
/** Builds the tool list for a workspace. Synchronous: tokens are fetched on first use. */
|
|
79
79
|
export function workspaceTools(workspace, opts = {}) {
|
|
80
|
-
const cell = () => workspace.cell(
|
|
80
|
+
const cell = () => workspace.cell({
|
|
81
|
+
...(opts.agentLabel !== undefined ? { agentLabel: opts.agentLabel } : {}),
|
|
82
|
+
...(opts.wake !== undefined ? { wake: opts.wake } : {}),
|
|
83
|
+
...(opts.transitionTimeoutMs !== undefined ? { transitionTimeoutMs: opts.transitionTimeoutMs } : {}),
|
|
84
|
+
});
|
|
81
85
|
const max = opts.maxOutputBytes ?? 65_536;
|
|
82
86
|
const prefix = opts.prefix ?? '';
|
|
83
87
|
const allowed = new Set(opts.tools ?? workspace.grantedTools ?? ALL);
|
package/dist/workspace.d.ts
CHANGED
|
@@ -2,18 +2,37 @@
|
|
|
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 { ClientContext,
|
|
5
|
+
import type { ClientContext, DiskLayout, ForkTarget, Operation, WaitOptions, WorkspaceLifetime, WorkspaceOrigin, WorkspacePurpose, WorkspaceView } from './client.js';
|
|
6
6
|
import { CellClient } from './cell.js';
|
|
7
|
-
import type {
|
|
7
|
+
import type { FinishedOperation, LifecycleOptions, WaitedLifecycleOptions } from './lifecycle.js';
|
|
8
|
+
import { Trace } from './progress.js';
|
|
9
|
+
import type { LifecycleTiming, ProgressListener } from './progress.js';
|
|
10
|
+
import { WorkspaceSecrets } from './secrets.js';
|
|
11
|
+
import type { CellClientOptions, WorkspaceChangesPage, WorkspaceChangesParams } from './cell.js';
|
|
12
|
+
import type { SaveAsTemplateParams, SaveAsTemplateResponse } from './templates.js';
|
|
8
13
|
import { ToolTokenManager } from './tokens.js';
|
|
9
14
|
import type { ToolName, ToolToken } from './tokens.js';
|
|
15
|
+
export interface WakeOptions {
|
|
16
|
+
/** One deadline for the whole wake, across its retries (default 120 000 ms). */
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
/** Progress of the wake: the resume request, observed states, conflicts retried, and `done` with the timing. */
|
|
20
|
+
onProgress?: ProgressListener;
|
|
21
|
+
}
|
|
10
22
|
export declare class Workspace {
|
|
11
23
|
#private;
|
|
12
24
|
constructor(ctx: ClientContext, view: WorkspaceView, opts?: {
|
|
13
25
|
agentLabel?: string | undefined;
|
|
14
26
|
tools?: ToolName[] | undefined;
|
|
15
27
|
token?: ToolToken | null;
|
|
28
|
+
trace?: Trace;
|
|
16
29
|
});
|
|
30
|
+
/**
|
|
31
|
+
* Where the time went in the last lifecycle call made through this handle: open(), wake() (also when a tool call
|
|
32
|
+
* woke the workspace), or a lifecycle call with `wait`. Null for handles from get()/list() until such a call.
|
|
33
|
+
* `formatTiming(workspace.lastTiming)` prints it.
|
|
34
|
+
*/
|
|
35
|
+
get lastTiming(): LifecycleTiming | null;
|
|
17
36
|
get id(): string;
|
|
18
37
|
get key(): string;
|
|
19
38
|
/** Observed lifecycle state reported by the cell. */
|
|
@@ -28,43 +47,116 @@ export declare class Workspace {
|
|
|
28
47
|
get template(): WorkspaceView['template'];
|
|
29
48
|
get pendingReason(): string | null;
|
|
30
49
|
get activeOperation(): WorkspaceView['active_operation'];
|
|
50
|
+
/** persistent or session (contracts §19.11); immutable. A view without the field (older API) is persistent. */
|
|
51
|
+
get lifetime(): WorkspaceLifetime;
|
|
52
|
+
/** standard, template_draft or template_test (contracts §19.9). */
|
|
53
|
+
get purpose(): WorkspacePurpose;
|
|
54
|
+
/** legacy or layered (contracts §19.2); reset, save-as-template and changes need layered. */
|
|
55
|
+
get diskLayout(): DiskLayout;
|
|
56
|
+
/** Sessions: seconds without activity after which the session ends (null for persistent workspaces). */
|
|
57
|
+
get idleTimeoutSeconds(): number | null;
|
|
58
|
+
/** Where the disk came from: null, `{kind: "fork"}` or `{kind: "draft_state"}` (test instances). */
|
|
59
|
+
get origin(): WorkspaceOrigin | null;
|
|
60
|
+
/** How a session ended (closed, idle_timeout, draft_discarded); null while live or for a plain delete. */
|
|
61
|
+
get endedReason(): WorkspaceView['ended_reason'];
|
|
31
62
|
/** The raw view (GET /v1/workspaces/{id}). */
|
|
32
63
|
get data(): WorkspaceView;
|
|
64
|
+
/** Secret names bound to this workspace (injected into every exec/PTY start): `get()`, `set(names)`. */
|
|
65
|
+
get secrets(): WorkspaceSecrets;
|
|
33
66
|
refresh(): Promise<this>;
|
|
34
67
|
/** Waits for the active operation (if any) and refreshes. */
|
|
35
68
|
waitUntilReady(opts?: WaitOptions): Promise<this>;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Deletes the workspace (tool access ends at once; keys are never reused). Resolves when the delete is REQUESTED;
|
|
71
|
+
* with `{ wait: true }`, once it has FINISHED.
|
|
72
|
+
*/
|
|
73
|
+
delete(opts: WaitedLifecycleOptions): Promise<FinishedOperation>;
|
|
74
|
+
delete(opts?: LifecycleOptions): Promise<Operation>;
|
|
75
|
+
/**
|
|
76
|
+
* Suspends the workspace: memory and processes are checkpointed, compute stops. Resolves when the suspend is
|
|
77
|
+
* REQUESTED (the operation is usually still `queued`, and the workspace still running); pass `{ wait: true }` to
|
|
78
|
+
* resolve once it has FINISHED, with `workspace.state` then `suspended`.
|
|
79
|
+
*
|
|
80
|
+
* await workspace.suspend({ wait: true });
|
|
81
|
+
*/
|
|
82
|
+
suspend(opts: WaitedLifecycleOptions): Promise<FinishedOperation>;
|
|
83
|
+
suspend(opts?: LifecycleOptions): Promise<Operation>;
|
|
84
|
+
/**
|
|
85
|
+
* Resumes a suspended workspace. Resolves when the resume is REQUESTED; with `{ wait: true }`, once the workspace runs.
|
|
86
|
+
* Tool calls wake a suspended workspace by themselves, so this is rarely needed.
|
|
87
|
+
*/
|
|
88
|
+
resume(opts: WaitedLifecycleOptions): Promise<FinishedOperation>;
|
|
89
|
+
resume(opts?: LifecycleOptions): Promise<Operation>;
|
|
90
|
+
/** Takes a snapshot. Resolves when it is REQUESTED; with `{ wait: true }`, once it is taken. */
|
|
91
|
+
snapshot(opts: WaitedLifecycleOptions & {
|
|
92
|
+
label?: string;
|
|
93
|
+
}): Promise<FinishedOperation>;
|
|
94
|
+
snapshot(opts?: LifecycleOptions & {
|
|
46
95
|
label?: string;
|
|
47
|
-
idempotencyKey?: string;
|
|
48
96
|
}): Promise<Operation>;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Forks into a new key. Resolves when the fork is REQUESTED (the copy's handle is returned at once); with
|
|
99
|
+
* `{ wait: true }`, once the copy exists, with its handle refreshed.
|
|
100
|
+
*/
|
|
101
|
+
fork(target: ForkTarget, opts: WaitedLifecycleOptions): Promise<{
|
|
102
|
+
operation: FinishedOperation;
|
|
103
|
+
workspace: Workspace;
|
|
104
|
+
}>;
|
|
105
|
+
fork(target: ForkTarget, opts?: LifecycleOptions): Promise<{
|
|
55
106
|
operation: Operation;
|
|
56
107
|
workspace: Workspace;
|
|
57
108
|
}>;
|
|
109
|
+
/**
|
|
110
|
+
* Safe in a `finally` block for any workspace. It always closes this handle's local streams (in-flight cell requests,
|
|
111
|
+
* exec output streams and PTY reads started through `cell()` are aborted; later `cell()` calls get fresh clients).
|
|
112
|
+
* For a **session** workspace it also ends the session (POST /v1/workspaces/{id}/close): the workspace is deleted and
|
|
113
|
+
* the `delete` operation is returned (input.reason session_closed). For a **persistent** workspace no request is made
|
|
114
|
+
* and null is returned: the workspace keeps running (suspend() or delete() it explicitly). With `{ wait: true }`, a
|
|
115
|
+
* session's close resolves once the delete has finished.
|
|
116
|
+
*/
|
|
117
|
+
close(opts: WaitedLifecycleOptions): Promise<FinishedOperation | null>;
|
|
118
|
+
close(opts?: LifecycleOptions): Promise<Operation | null>;
|
|
119
|
+
/**
|
|
120
|
+
* Wipes every change in this layered workspace and restarts it on its template (contracts §19.12; sends
|
|
121
|
+
* confirm_destructive). Returns the `reset` operation: requested, or with `{ wait: true }` finished. Tool tokens of
|
|
122
|
+
* the old epoch are dropped.
|
|
123
|
+
*/
|
|
124
|
+
reset(opts: WaitedLifecycleOptions): Promise<FinishedOperation>;
|
|
125
|
+
reset(opts?: LifecycleOptions): Promise<Operation>;
|
|
126
|
+
/** Saves this layered workspace as the next version of an organization template (contracts §19.8). */
|
|
127
|
+
saveAsTemplate(params: SaveAsTemplateParams): Promise<SaveAsTemplateResponse>;
|
|
128
|
+
/**
|
|
129
|
+
* The workspace's changes against its template (cell gateway GET /v1/workspaces/{id}/changes; needs the `files` tool).
|
|
130
|
+
* 409 workspace_not_running, or conflict with details.reason legacy_disk_layout / guest_feature_unavailable.
|
|
131
|
+
*/
|
|
132
|
+
changes(opts?: WorkspaceChangesParams & {
|
|
133
|
+
agentLabel?: string;
|
|
134
|
+
tools?: ToolName[];
|
|
135
|
+
}): Promise<WorkspaceChangesPage>;
|
|
58
136
|
/** Token manager for an agent label / tool set (defaults: those given to open()). */
|
|
59
137
|
tokens(opts?: {
|
|
60
138
|
agentLabel?: string;
|
|
61
139
|
tools?: ToolName[];
|
|
62
140
|
}): ToolTokenManager;
|
|
63
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* Typed client for the cell gateway tools of this workspace. Calls wake the workspace on use (`wake()`, bounded by
|
|
143
|
+
* `transitionTimeoutMs`); pass `wake: null` to get `workspace_not_running` back instead. One client per agent label,
|
|
144
|
+
* tool set and transition settings; the other options apply when that client is first created.
|
|
145
|
+
*/
|
|
64
146
|
cell(opts?: {
|
|
65
147
|
agentLabel?: string;
|
|
66
148
|
tools?: ToolName[];
|
|
67
149
|
} & CellClientOptions): CellClient;
|
|
150
|
+
/**
|
|
151
|
+
* Makes a suspended (or suspending/resuming) workspace run again and resolves once it does (contracts §20.4): resume,
|
|
152
|
+
* or join the active resume/open; an active suspend (or other operation) is waited out first. Resolves `true` when it
|
|
153
|
+
* resumed or waited for a lifecycle operation, `false` when the workspace was already running. One deadline
|
|
154
|
+
* (`timeoutMs`, default 120 000 ms) covers every step. Throws OperationFailedError when the resume/open fails,
|
|
155
|
+
* OperationTimeoutError (naming the operation, its state and reason) when it is still queued, running or
|
|
156
|
+
* capacity_pending at the deadline, and any other API error (a conflict other than already_running /
|
|
157
|
+
* operation_in_progress) at once. Cell calls use it automatically when they meet `workspace_not_running`.
|
|
158
|
+
*/
|
|
159
|
+
wake(opts?: WakeOptions): Promise<boolean>;
|
|
68
160
|
/** Tools granted by the most recent token (null before one was issued). */
|
|
69
161
|
get grantedTools(): ToolName[] | null;
|
|
70
162
|
}
|
package/dist/workspace.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import { CellClient } from "./cell.js";
|
|
1
|
+
import { CellClient, DEFAULT_TRANSITION_TIMEOUT_MS } from "./cell.js";
|
|
2
|
+
import { OperationFailedError, ShardfluxApiError } from "./errors.js";
|
|
3
|
+
import { randomId } from "./http.js";
|
|
4
|
+
import { AFTER_WAIT, TRACE } from "./lifecycle.js";
|
|
5
|
+
import { Trace, combineListeners, traced } from "./progress.js";
|
|
6
|
+
import { WorkspaceSecrets } from "./secrets.js";
|
|
2
7
|
import { ToolTokenManager } from "./tokens.js";
|
|
3
8
|
export class Workspace {
|
|
4
9
|
#view;
|
|
@@ -6,13 +11,42 @@ export class Workspace {
|
|
|
6
11
|
#defaults;
|
|
7
12
|
#managers = new Map();
|
|
8
13
|
#cells = new Map();
|
|
14
|
+
/** The open() that produced this handle; its timing is final once open() has returned. */
|
|
15
|
+
#openTrace;
|
|
16
|
+
#lastTiming;
|
|
9
17
|
constructor(ctx, view, opts = {}) {
|
|
10
18
|
this.#ctx = ctx;
|
|
11
19
|
this.#view = view;
|
|
12
20
|
this.#defaults = { agentLabel: opts.agentLabel, tools: opts.tools };
|
|
21
|
+
this.#openTrace = opts.trace;
|
|
13
22
|
if (opts.token)
|
|
14
23
|
this.tokens().seed(opts.token);
|
|
15
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Where the time went in the last lifecycle call made through this handle: open(), wake() (also when a tool call
|
|
27
|
+
* woke the workspace), or a lifecycle call with `wait`. Null for handles from get()/list() until such a call.
|
|
28
|
+
* `formatTiming(workspace.lastTiming)` prints it.
|
|
29
|
+
*/
|
|
30
|
+
get lastTiming() {
|
|
31
|
+
return this.#lastTiming ?? this.#openTrace?.finished ?? null;
|
|
32
|
+
}
|
|
33
|
+
/** Adds this handle's timing capture to a call's listener. */
|
|
34
|
+
#tracked(opts) {
|
|
35
|
+
const capture = (e) => {
|
|
36
|
+
if (e.type === 'done' && e.action !== 'token')
|
|
37
|
+
this.#lastTiming = e.timing;
|
|
38
|
+
};
|
|
39
|
+
return { ...opts, onProgress: combineListeners(opts.onProgress, capture) };
|
|
40
|
+
}
|
|
41
|
+
/** Refreshes the view inside a waited call's trace (a deleted workspace may no longer be readable). */
|
|
42
|
+
#refreshAfterWait() {
|
|
43
|
+
return async (trace) => {
|
|
44
|
+
await trace.span('view', () => this.refresh()).catch((e) => {
|
|
45
|
+
if (!(e instanceof ShardfluxApiError && e.status === 404))
|
|
46
|
+
throw e;
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
}
|
|
16
50
|
get id() {
|
|
17
51
|
return this.#view.id;
|
|
18
52
|
}
|
|
@@ -49,10 +83,38 @@ export class Workspace {
|
|
|
49
83
|
get activeOperation() {
|
|
50
84
|
return this.#view.active_operation;
|
|
51
85
|
}
|
|
86
|
+
/** persistent or session (contracts §19.11); immutable. A view without the field (older API) is persistent. */
|
|
87
|
+
get lifetime() {
|
|
88
|
+
return this.#view.lifetime ?? 'persistent';
|
|
89
|
+
}
|
|
90
|
+
/** standard, template_draft or template_test (contracts §19.9). */
|
|
91
|
+
get purpose() {
|
|
92
|
+
return this.#view.purpose ?? 'standard';
|
|
93
|
+
}
|
|
94
|
+
/** legacy or layered (contracts §19.2); reset, save-as-template and changes need layered. */
|
|
95
|
+
get diskLayout() {
|
|
96
|
+
return this.#view.disk_layout ?? 'legacy';
|
|
97
|
+
}
|
|
98
|
+
/** Sessions: seconds without activity after which the session ends (null for persistent workspaces). */
|
|
99
|
+
get idleTimeoutSeconds() {
|
|
100
|
+
return this.#view.idle_timeout_seconds ?? null;
|
|
101
|
+
}
|
|
102
|
+
/** Where the disk came from: null, `{kind: "fork"}` or `{kind: "draft_state"}` (test instances). */
|
|
103
|
+
get origin() {
|
|
104
|
+
return this.#view.origin ?? null;
|
|
105
|
+
}
|
|
106
|
+
/** How a session ended (closed, idle_timeout, draft_discarded); null while live or for a plain delete. */
|
|
107
|
+
get endedReason() {
|
|
108
|
+
return this.#view.ended_reason ?? null;
|
|
109
|
+
}
|
|
52
110
|
/** The raw view (GET /v1/workspaces/{id}). */
|
|
53
111
|
get data() {
|
|
54
112
|
return this.#view;
|
|
55
113
|
}
|
|
114
|
+
/** Secret names bound to this workspace (injected into every exec/PTY start): `get()`, `set(names)`. */
|
|
115
|
+
get secrets() {
|
|
116
|
+
return new WorkspaceSecrets(this.#ctx, this.id);
|
|
117
|
+
}
|
|
56
118
|
async refresh() {
|
|
57
119
|
this.#view = await this.#ctx.http.json('GET', `/v1/workspaces/${encodeURIComponent(this.id)}`, {}, this.#ctx.authorization);
|
|
58
120
|
return this;
|
|
@@ -65,19 +127,49 @@ export class Workspace {
|
|
|
65
127
|
return this.refresh();
|
|
66
128
|
}
|
|
67
129
|
delete(opts = {}) {
|
|
68
|
-
return this.#ctx.workspaces.delete(this.id, opts);
|
|
130
|
+
return this.#ctx.workspaces.delete(this.id, { ...this.#tracked(opts), [AFTER_WAIT]: this.#refreshAfterWait() });
|
|
69
131
|
}
|
|
70
132
|
suspend(opts = {}) {
|
|
71
|
-
return this.#ctx.workspaces.suspend(this.id, opts);
|
|
133
|
+
return this.#ctx.workspaces.suspend(this.id, { ...this.#tracked(opts), [AFTER_WAIT]: this.#refreshAfterWait() });
|
|
72
134
|
}
|
|
73
135
|
resume(opts = {}) {
|
|
74
|
-
return this.#ctx.workspaces.resume(this.id, opts);
|
|
136
|
+
return this.#ctx.workspaces.resume(this.id, { ...this.#tracked(opts), [AFTER_WAIT]: this.#refreshAfterWait() });
|
|
75
137
|
}
|
|
76
138
|
snapshot(opts = {}) {
|
|
77
|
-
return this.#ctx.workspaces.snapshot(this.id, opts);
|
|
139
|
+
return this.#ctx.workspaces.snapshot(this.id, { ...this.#tracked(opts), [AFTER_WAIT]: this.#refreshAfterWait() });
|
|
78
140
|
}
|
|
79
141
|
fork(target, opts = {}) {
|
|
80
|
-
return this.#ctx.workspaces.fork(this.id, target, opts);
|
|
142
|
+
return this.#ctx.workspaces.fork(this.id, target, this.#tracked(opts));
|
|
143
|
+
}
|
|
144
|
+
async close(opts = {}) {
|
|
145
|
+
for (const c of this.#cells.values())
|
|
146
|
+
c.close();
|
|
147
|
+
this.#cells.clear();
|
|
148
|
+
if (this.lifetime !== 'session')
|
|
149
|
+
return null;
|
|
150
|
+
const out = await this.#ctx.workspaces.closeWithView(this.id, this.#tracked(opts));
|
|
151
|
+
this.#view = out.workspace;
|
|
152
|
+
for (const m of this.#managers.values())
|
|
153
|
+
m.invalidate();
|
|
154
|
+
return out.operation;
|
|
155
|
+
}
|
|
156
|
+
async reset(opts = {}) {
|
|
157
|
+
const op = await this.#ctx.workspaces.reset(this.id, { ...this.#tracked(opts), [AFTER_WAIT]: this.#refreshAfterWait() });
|
|
158
|
+
for (const m of this.#managers.values())
|
|
159
|
+
m.invalidate();
|
|
160
|
+
return op;
|
|
161
|
+
}
|
|
162
|
+
/** Saves this layered workspace as the next version of an organization template (contracts §19.8). */
|
|
163
|
+
saveAsTemplate(params) {
|
|
164
|
+
return this.#ctx.workspaces.saveAsTemplate(this.id, params);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The workspace's changes against its template (cell gateway GET /v1/workspaces/{id}/changes; needs the `files` tool).
|
|
168
|
+
* 409 workspace_not_running, or conflict with details.reason legacy_disk_layout / guest_feature_unavailable.
|
|
169
|
+
*/
|
|
170
|
+
changes(opts = {}) {
|
|
171
|
+
const { agentLabel, tools, ...params } = opts;
|
|
172
|
+
return this.cell({ ...(agentLabel !== undefined ? { agentLabel } : {}), ...(tools !== undefined ? { tools } : {}) }).changes(params);
|
|
81
173
|
}
|
|
82
174
|
/** Token manager for an agent label / tool set (defaults: those given to open()). */
|
|
83
175
|
tokens(opts = {}) {
|
|
@@ -91,18 +183,92 @@ export class Workspace {
|
|
|
91
183
|
}
|
|
92
184
|
return m;
|
|
93
185
|
}
|
|
94
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* Typed client for the cell gateway tools of this workspace. Calls wake the workspace on use (`wake()`, bounded by
|
|
188
|
+
* `transitionTimeoutMs`); pass `wake: null` to get `workspace_not_running` back instead. One client per agent label,
|
|
189
|
+
* tool set and transition settings; the other options apply when that client is first created.
|
|
190
|
+
*/
|
|
95
191
|
cell(opts = {}) {
|
|
96
|
-
const { agentLabel, tools, ...cellOpts } = opts;
|
|
192
|
+
const { agentLabel, tools, wake, onProgress, ...cellOpts } = opts;
|
|
97
193
|
const manager = this.tokens({ ...(agentLabel !== undefined ? { agentLabel } : {}), ...(tools !== undefined ? { tools } : {}) });
|
|
98
|
-
const
|
|
194
|
+
const wakeMode = wake === null ? 'off' : wake ? 'custom' : 'on';
|
|
195
|
+
const k = `${JSON.stringify([agentLabel ?? null, tools ?? null, wakeMode, cellOpts.transitionTimeoutMs ?? null])}`;
|
|
99
196
|
let c = this.#cells.get(k);
|
|
100
197
|
if (!c) {
|
|
101
|
-
|
|
198
|
+
// Tool-call events (token fetches, busy waits, retries) go to the client listener and this cell's own; a wake
|
|
199
|
+
// adds the client listener itself.
|
|
200
|
+
const listener = combineListeners(this.#ctx.onProgress, onProgress);
|
|
201
|
+
c = new CellClient(this.id, manager, {
|
|
202
|
+
fetch: this.#ctx.fetch,
|
|
203
|
+
userAgent: this.#ctx.userAgent,
|
|
204
|
+
sleep: this.#ctx.sleep,
|
|
205
|
+
...cellOpts,
|
|
206
|
+
...(listener ? { onProgress: listener } : {}),
|
|
207
|
+
wake: wake === undefined ? (timeoutMs, signal) => this.wake({ timeoutMs, ...(signal ? { signal } : {}), ...(onProgress ? { onProgress } : {}) }) : wake,
|
|
208
|
+
});
|
|
102
209
|
this.#cells.set(k, c);
|
|
103
210
|
}
|
|
104
211
|
return c;
|
|
105
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Makes a suspended (or suspending/resuming) workspace run again and resolves once it does (contracts §20.4): resume,
|
|
215
|
+
* or join the active resume/open; an active suspend (or other operation) is waited out first. Resolves `true` when it
|
|
216
|
+
* resumed or waited for a lifecycle operation, `false` when the workspace was already running. One deadline
|
|
217
|
+
* (`timeoutMs`, default 120 000 ms) covers every step. Throws OperationFailedError when the resume/open fails,
|
|
218
|
+
* OperationTimeoutError (naming the operation, its state and reason) when it is still queued, running or
|
|
219
|
+
* capacity_pending at the deadline, and any other API error (a conflict other than already_running /
|
|
220
|
+
* operation_in_progress) at once. Cell calls use it automatically when they meet `workspace_not_running`.
|
|
221
|
+
*/
|
|
222
|
+
async wake(opts = {}) {
|
|
223
|
+
const trace = new Trace('wake', combineListeners(this.#ctx.onProgress, this.#tracked(opts).onProgress), { workspaceId: this.id });
|
|
224
|
+
return traced(trace, () => this.#wake(opts, trace));
|
|
225
|
+
}
|
|
226
|
+
async #wake(opts, trace) {
|
|
227
|
+
const deadline = Date.now() + (opts.timeoutMs ?? DEFAULT_TRANSITION_TIMEOUT_MS);
|
|
228
|
+
const waitFor = (operationId) => this.#ctx.workspaces.waitForOperation(operationId, { timeoutMs: Math.max(1, deadline - Date.now()), ...(opts.signal ? { signal: opts.signal } : {}), [TRACE]: trace });
|
|
229
|
+
const resumePath = `/v1/workspaces/${encodeURIComponent(this.id)}/resume`;
|
|
230
|
+
for (let i = 0; i < 4; i += 1) {
|
|
231
|
+
if (opts.signal?.aborted)
|
|
232
|
+
throw opts.signal.reason;
|
|
233
|
+
let active;
|
|
234
|
+
try {
|
|
235
|
+
// Resume returns the active resume/open when there is one, so concurrent wakes join a single operation. The
|
|
236
|
+
// request is made here rather than through resume() so the wake is one trace.
|
|
237
|
+
trace.phase('request', i === 0 ? null : 'retry');
|
|
238
|
+
const { operation: op } = await this.#ctx.http.json('POST', resumePath, { idempotencyKey: randomId('op-'), onRetry: trace.onRetry }, this.#ctx.authorization);
|
|
239
|
+
trace.observe(op);
|
|
240
|
+
await waitFor(op.id);
|
|
241
|
+
await trace.span('view', () => this.refresh());
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
if (!(e instanceof ShardfluxApiError) || e.code !== 'conflict')
|
|
246
|
+
throw e;
|
|
247
|
+
if (e.reason === 'already_running') {
|
|
248
|
+
if (i > 0)
|
|
249
|
+
await trace.span('view', () => this.refresh());
|
|
250
|
+
return i > 0;
|
|
251
|
+
}
|
|
252
|
+
if (e.reason !== 'operation_in_progress')
|
|
253
|
+
throw e;
|
|
254
|
+
const id = e.operationId ?? e.details?.['active_operation_id'];
|
|
255
|
+
active = typeof id === 'string' ? id : undefined;
|
|
256
|
+
trace.retry({ request: `POST ${resumePath}`, attempt: i + 1, cause: `conflict operation_in_progress${active ? ` (joins ${active})` : ''}`, delayMs: 0 });
|
|
257
|
+
}
|
|
258
|
+
if (active === undefined)
|
|
259
|
+
continue; // a concurrent start raced ours: ask again
|
|
260
|
+
try {
|
|
261
|
+
await waitFor(active);
|
|
262
|
+
}
|
|
263
|
+
catch (e) {
|
|
264
|
+
// A failed open/resume means the workspace will not run: fail fast. Another kind that failed (a suspend,
|
|
265
|
+
// snapshot or fork) leaves the workspace as it was, so the next resume attempt decides.
|
|
266
|
+
if (!(e instanceof OperationFailedError) || e.operation.kind === 'open' || e.operation.kind === 'resume')
|
|
267
|
+
throw e;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
throw new Error(`workspace ${this.id} did not become runnable after repeated lifecycle conflicts`);
|
|
271
|
+
}
|
|
106
272
|
/** Tools granted by the most recent token (null before one was issued). */
|
|
107
273
|
get grantedTools() {
|
|
108
274
|
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.6.1",
|
|
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",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"files": [
|
|
31
31
|
"dist",
|
|
32
32
|
"README.md",
|
|
33
|
+
"CHANGELOG.md",
|
|
33
34
|
"LICENSE"
|
|
34
35
|
],
|
|
35
36
|
"publishConfig": {
|