@sealant/sdk 0.5.2 → 0.7.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/dist/client.d.ts +8 -1
- package/dist/client.js +26 -1
- package/dist/effect/api-client.d.ts +618 -4
- package/dist/effect/operations.d.ts +199 -2
- package/dist/effect/operations.js +15 -0
- package/dist/effect/run-harness.js +32 -13
- package/dist/facade/session.d.ts +22 -0
- package/dist/facade/session.js +171 -0
- package/dist/facade/workspace.js +60 -2
- package/dist/internal/blueprint.js +21 -9
- package/dist/types.d.ts +121 -8
- package/package.json +2 -2
|
@@ -117,6 +117,14 @@ export declare const execWorkspaceOp: (workspaceId: string, payload: {
|
|
|
117
117
|
readonly mode: "interactive" | "one-shot";
|
|
118
118
|
readonly status: "cancelled" | "completed" | "failed" | "queued" | "running";
|
|
119
119
|
readonly prompt?: string | undefined;
|
|
120
|
+
readonly command?: {
|
|
121
|
+
readonly executable: string;
|
|
122
|
+
readonly args: readonly string[];
|
|
123
|
+
readonly cwd?: string | undefined;
|
|
124
|
+
} | undefined;
|
|
125
|
+
readonly metadata?: {
|
|
126
|
+
readonly [x: string]: unknown;
|
|
127
|
+
} | undefined;
|
|
120
128
|
readonly exitCode?: number | undefined;
|
|
121
129
|
readonly errorMessage?: string | undefined;
|
|
122
130
|
readonly startedAt?: string | undefined;
|
|
@@ -156,6 +164,9 @@ export declare const createRunOp: (payload: {
|
|
|
156
164
|
readonly args: readonly string[];
|
|
157
165
|
readonly cwd?: string | undefined;
|
|
158
166
|
} | undefined;
|
|
167
|
+
readonly metadata?: {
|
|
168
|
+
readonly [x: string]: unknown;
|
|
169
|
+
} | undefined;
|
|
159
170
|
}) => Effect.Effect<{
|
|
160
171
|
readonly runId: string;
|
|
161
172
|
readonly workspaceId: string;
|
|
@@ -165,6 +176,14 @@ export declare const createRunOp: (payload: {
|
|
|
165
176
|
readonly mode: "interactive" | "one-shot";
|
|
166
177
|
readonly status: "cancelled" | "completed" | "failed" | "queued" | "running";
|
|
167
178
|
readonly prompt?: string | undefined;
|
|
179
|
+
readonly command?: {
|
|
180
|
+
readonly executable: string;
|
|
181
|
+
readonly args: readonly string[];
|
|
182
|
+
readonly cwd?: string | undefined;
|
|
183
|
+
} | undefined;
|
|
184
|
+
readonly metadata?: {
|
|
185
|
+
readonly [x: string]: unknown;
|
|
186
|
+
} | undefined;
|
|
168
187
|
readonly exitCode?: number | undefined;
|
|
169
188
|
readonly errorMessage?: string | undefined;
|
|
170
189
|
readonly startedAt?: string | undefined;
|
|
@@ -181,6 +200,14 @@ export declare const getRunOp: (runId: string) => Effect.Effect<{
|
|
|
181
200
|
readonly mode: "interactive" | "one-shot";
|
|
182
201
|
readonly status: "cancelled" | "completed" | "failed" | "queued" | "running";
|
|
183
202
|
readonly prompt?: string | undefined;
|
|
203
|
+
readonly command?: {
|
|
204
|
+
readonly executable: string;
|
|
205
|
+
readonly args: readonly string[];
|
|
206
|
+
readonly cwd?: string | undefined;
|
|
207
|
+
} | undefined;
|
|
208
|
+
readonly metadata?: {
|
|
209
|
+
readonly [x: string]: unknown;
|
|
210
|
+
} | undefined;
|
|
184
211
|
readonly exitCode?: number | undefined;
|
|
185
212
|
readonly errorMessage?: string | undefined;
|
|
186
213
|
readonly startedAt?: string | undefined;
|
|
@@ -203,6 +230,14 @@ export declare const listRunsOp: (query: {
|
|
|
203
230
|
readonly mode: "interactive" | "one-shot";
|
|
204
231
|
readonly status: "cancelled" | "completed" | "failed" | "queued" | "running";
|
|
205
232
|
readonly prompt?: string | undefined;
|
|
233
|
+
readonly command?: {
|
|
234
|
+
readonly executable: string;
|
|
235
|
+
readonly args: readonly string[];
|
|
236
|
+
readonly cwd?: string | undefined;
|
|
237
|
+
} | undefined;
|
|
238
|
+
readonly metadata?: {
|
|
239
|
+
readonly [x: string]: unknown;
|
|
240
|
+
} | undefined;
|
|
206
241
|
readonly exitCode?: number | undefined;
|
|
207
242
|
readonly errorMessage?: string | undefined;
|
|
208
243
|
readonly startedAt?: string | undefined;
|
|
@@ -230,6 +265,14 @@ export declare const updateRunOp: (runId: string, payload: {
|
|
|
230
265
|
readonly mode: "interactive" | "one-shot";
|
|
231
266
|
readonly status: "cancelled" | "completed" | "failed" | "queued" | "running";
|
|
232
267
|
readonly prompt?: string | undefined;
|
|
268
|
+
readonly command?: {
|
|
269
|
+
readonly executable: string;
|
|
270
|
+
readonly args: readonly string[];
|
|
271
|
+
readonly cwd?: string | undefined;
|
|
272
|
+
} | undefined;
|
|
273
|
+
readonly metadata?: {
|
|
274
|
+
readonly [x: string]: unknown;
|
|
275
|
+
} | undefined;
|
|
233
276
|
readonly exitCode?: number | undefined;
|
|
234
277
|
readonly errorMessage?: string | undefined;
|
|
235
278
|
readonly startedAt?: string | undefined;
|
|
@@ -255,11 +298,13 @@ export declare const getRunTimelineOp: (runId: string, query: {
|
|
|
255
298
|
}[], import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RunBadRequestError | import("@sealant/api-contracts").RunInternalServerError | import("@sealant/api-contracts").RunNotFoundError | import("effect/Schema").SchemaError, SealantApiClient>;
|
|
256
299
|
export declare const getRunScrollbackOp: (runId: string, query: {
|
|
257
300
|
readonly processId: string;
|
|
258
|
-
readonly stream: "stderr" | "stdout";
|
|
301
|
+
readonly stream: "pty" | "stderr" | "stdout";
|
|
259
302
|
readonly atSequence?: string | undefined;
|
|
303
|
+
readonly fromSequence?: string | undefined;
|
|
304
|
+
readonly limit?: string | undefined;
|
|
260
305
|
}) => Effect.Effect<{
|
|
261
306
|
readonly processId: string;
|
|
262
|
-
readonly stream: "stderr" | "stdout";
|
|
307
|
+
readonly stream: "pty" | "stderr" | "stdout";
|
|
263
308
|
readonly byteCount: number;
|
|
264
309
|
readonly contentBase64: string;
|
|
265
310
|
}, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RunBadRequestError | import("@sealant/api-contracts").RunInternalServerError | import("@sealant/api-contracts").RunNotFoundError | import("effect/Schema").SchemaError, SealantApiClient>;
|
|
@@ -286,6 +331,158 @@ export declare const getRunChangesOp: (runId: string) => Effect.Effect<{
|
|
|
286
331
|
}[];
|
|
287
332
|
readonly diff: string;
|
|
288
333
|
}, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RunInternalServerError | import("@sealant/api-contracts").RunNotFoundError | import("effect/Schema").SchemaError, SealantApiClient>;
|
|
334
|
+
export declare const createSessionOp: (payload: {
|
|
335
|
+
readonly workspaceId: string;
|
|
336
|
+
readonly ownerUserId: string;
|
|
337
|
+
readonly argv: readonly string[];
|
|
338
|
+
readonly cwd?: string | undefined;
|
|
339
|
+
readonly env?: {
|
|
340
|
+
readonly [x: string]: string;
|
|
341
|
+
} | undefined;
|
|
342
|
+
readonly cols?: number | undefined;
|
|
343
|
+
readonly rows?: number | undefined;
|
|
344
|
+
readonly term?: string | undefined;
|
|
345
|
+
readonly metadata?: {
|
|
346
|
+
readonly [x: string]: unknown;
|
|
347
|
+
} | undefined;
|
|
348
|
+
}) => Effect.Effect<{
|
|
349
|
+
readonly sessionId: string;
|
|
350
|
+
readonly workspaceId: string;
|
|
351
|
+
readonly runId: string;
|
|
352
|
+
readonly ownerUserId: string;
|
|
353
|
+
readonly status: "exited" | "failed" | "running" | "starting";
|
|
354
|
+
readonly argv: readonly string[];
|
|
355
|
+
readonly cwd?: string | undefined;
|
|
356
|
+
readonly cols: number;
|
|
357
|
+
readonly rows: number;
|
|
358
|
+
readonly exitCode?: number | undefined;
|
|
359
|
+
readonly exitSignal?: number | undefined;
|
|
360
|
+
readonly errorMessage?: string | undefined;
|
|
361
|
+
readonly metadata?: {
|
|
362
|
+
readonly [x: string]: unknown;
|
|
363
|
+
} | undefined;
|
|
364
|
+
readonly outputHighWater: string;
|
|
365
|
+
readonly createdAt: string;
|
|
366
|
+
readonly endedAt?: string | undefined;
|
|
367
|
+
}, import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").SessionBadGatewayError | import("@sealant/api-contracts").SessionBadRequestError | import("@sealant/api-contracts").SessionConflictError | import("@sealant/api-contracts").SessionForbiddenError | import("@sealant/api-contracts").SessionInternalServerError | import("@sealant/api-contracts").SessionNotFoundError | import("@sealant/api-contracts").SessionUnauthorizedError, SealantApiClient>;
|
|
368
|
+
export declare const getSessionOp: (sessionId: string, ownerUserId?: string | undefined) => Effect.Effect<{
|
|
369
|
+
readonly sessionId: string;
|
|
370
|
+
readonly workspaceId: string;
|
|
371
|
+
readonly runId: string;
|
|
372
|
+
readonly ownerUserId: string;
|
|
373
|
+
readonly status: "exited" | "failed" | "running" | "starting";
|
|
374
|
+
readonly argv: readonly string[];
|
|
375
|
+
readonly cwd?: string | undefined;
|
|
376
|
+
readonly cols: number;
|
|
377
|
+
readonly rows: number;
|
|
378
|
+
readonly exitCode?: number | undefined;
|
|
379
|
+
readonly exitSignal?: number | undefined;
|
|
380
|
+
readonly errorMessage?: string | undefined;
|
|
381
|
+
readonly metadata?: {
|
|
382
|
+
readonly [x: string]: unknown;
|
|
383
|
+
} | undefined;
|
|
384
|
+
readonly outputHighWater: string;
|
|
385
|
+
readonly createdAt: string;
|
|
386
|
+
readonly endedAt?: string | undefined;
|
|
387
|
+
}, import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").SessionBadRequestError | import("@sealant/api-contracts").SessionForbiddenError | import("@sealant/api-contracts").SessionInternalServerError | import("@sealant/api-contracts").SessionNotFoundError | import("@sealant/api-contracts").SessionUnauthorizedError, SealantApiClient>;
|
|
388
|
+
export declare const listSessionsOp: (query: {
|
|
389
|
+
readonly ownerUserId: string;
|
|
390
|
+
readonly workspaceId?: string | undefined;
|
|
391
|
+
readonly status?: "exited" | "failed" | "running" | "starting" | undefined;
|
|
392
|
+
readonly limit?: string | undefined;
|
|
393
|
+
}) => Effect.Effect<{
|
|
394
|
+
readonly items: readonly {
|
|
395
|
+
readonly sessionId: string;
|
|
396
|
+
readonly workspaceId: string;
|
|
397
|
+
readonly runId: string;
|
|
398
|
+
readonly ownerUserId: string;
|
|
399
|
+
readonly status: "exited" | "failed" | "running" | "starting";
|
|
400
|
+
readonly argv: readonly string[];
|
|
401
|
+
readonly cwd?: string | undefined;
|
|
402
|
+
readonly cols: number;
|
|
403
|
+
readonly rows: number;
|
|
404
|
+
readonly exitCode?: number | undefined;
|
|
405
|
+
readonly exitSignal?: number | undefined;
|
|
406
|
+
readonly errorMessage?: string | undefined;
|
|
407
|
+
readonly metadata?: {
|
|
408
|
+
readonly [x: string]: unknown;
|
|
409
|
+
} | undefined;
|
|
410
|
+
readonly outputHighWater: string;
|
|
411
|
+
readonly createdAt: string;
|
|
412
|
+
readonly endedAt?: string | undefined;
|
|
413
|
+
}[];
|
|
414
|
+
}, import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").SessionBadRequestError | import("@sealant/api-contracts").SessionForbiddenError | import("@sealant/api-contracts").SessionInternalServerError | import("@sealant/api-contracts").SessionUnauthorizedError, SealantApiClient>;
|
|
415
|
+
export declare const getSessionOutputOp: (sessionId: string, query: {
|
|
416
|
+
readonly ownerUserId?: string | undefined;
|
|
417
|
+
readonly from?: string | undefined;
|
|
418
|
+
readonly limit?: string | undefined;
|
|
419
|
+
}) => Effect.Effect<{
|
|
420
|
+
readonly sessionId: string;
|
|
421
|
+
readonly chunks: readonly {
|
|
422
|
+
readonly sequence: string;
|
|
423
|
+
readonly dataBase64: string;
|
|
424
|
+
}[];
|
|
425
|
+
readonly nextFrom: string;
|
|
426
|
+
readonly status: "exited" | "failed" | "running" | "starting";
|
|
427
|
+
}, import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").SessionBadRequestError | import("@sealant/api-contracts").SessionForbiddenError | import("@sealant/api-contracts").SessionInternalServerError | import("@sealant/api-contracts").SessionNotFoundError | import("@sealant/api-contracts").SessionUnauthorizedError, SealantApiClient>;
|
|
428
|
+
export declare const sendSessionInputOp: (sessionId: string, payload: {
|
|
429
|
+
readonly ownerUserId?: string | undefined;
|
|
430
|
+
readonly dataBase64: string;
|
|
431
|
+
}) => Effect.Effect<{
|
|
432
|
+
readonly ok: boolean;
|
|
433
|
+
}, import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").SessionBadGatewayError | import("@sealant/api-contracts").SessionBadRequestError | import("@sealant/api-contracts").SessionConflictError | import("@sealant/api-contracts").SessionForbiddenError | import("@sealant/api-contracts").SessionInternalServerError | import("@sealant/api-contracts").SessionNotFoundError | import("@sealant/api-contracts").SessionUnauthorizedError, SealantApiClient>;
|
|
434
|
+
export declare const resizeSessionOp: (sessionId: string, payload: {
|
|
435
|
+
readonly ownerUserId?: string | undefined;
|
|
436
|
+
readonly cols: number;
|
|
437
|
+
readonly rows: number;
|
|
438
|
+
}) => Effect.Effect<{
|
|
439
|
+
readonly ok: boolean;
|
|
440
|
+
}, import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").SessionBadGatewayError | import("@sealant/api-contracts").SessionBadRequestError | import("@sealant/api-contracts").SessionConflictError | import("@sealant/api-contracts").SessionForbiddenError | import("@sealant/api-contracts").SessionInternalServerError | import("@sealant/api-contracts").SessionNotFoundError | import("@sealant/api-contracts").SessionUnauthorizedError, SealantApiClient>;
|
|
441
|
+
export declare const signalSessionOp: (sessionId: string, payload: {
|
|
442
|
+
readonly ownerUserId?: string | undefined;
|
|
443
|
+
readonly signal: number;
|
|
444
|
+
}) => Effect.Effect<{
|
|
445
|
+
readonly ok: boolean;
|
|
446
|
+
}, import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").SessionBadGatewayError | import("@sealant/api-contracts").SessionBadRequestError | import("@sealant/api-contracts").SessionConflictError | import("@sealant/api-contracts").SessionForbiddenError | import("@sealant/api-contracts").SessionInternalServerError | import("@sealant/api-contracts").SessionNotFoundError | import("@sealant/api-contracts").SessionUnauthorizedError, SealantApiClient>;
|
|
447
|
+
export declare const closeSessionOp: (sessionId: string, payload: {
|
|
448
|
+
readonly ownerUserId?: string | undefined;
|
|
449
|
+
}) => Effect.Effect<{
|
|
450
|
+
readonly sessionId: string;
|
|
451
|
+
readonly workspaceId: string;
|
|
452
|
+
readonly runId: string;
|
|
453
|
+
readonly ownerUserId: string;
|
|
454
|
+
readonly status: "exited" | "failed" | "running" | "starting";
|
|
455
|
+
readonly argv: readonly string[];
|
|
456
|
+
readonly cwd?: string | undefined;
|
|
457
|
+
readonly cols: number;
|
|
458
|
+
readonly rows: number;
|
|
459
|
+
readonly exitCode?: number | undefined;
|
|
460
|
+
readonly exitSignal?: number | undefined;
|
|
461
|
+
readonly errorMessage?: string | undefined;
|
|
462
|
+
readonly metadata?: {
|
|
463
|
+
readonly [x: string]: unknown;
|
|
464
|
+
} | undefined;
|
|
465
|
+
readonly outputHighWater: string;
|
|
466
|
+
readonly createdAt: string;
|
|
467
|
+
readonly endedAt?: string | undefined;
|
|
468
|
+
}, import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").SessionBadGatewayError | import("@sealant/api-contracts").SessionBadRequestError | import("@sealant/api-contracts").SessionForbiddenError | import("@sealant/api-contracts").SessionInternalServerError | import("@sealant/api-contracts").SessionNotFoundError | import("@sealant/api-contracts").SessionUnauthorizedError, SealantApiClient>;
|
|
469
|
+
export declare const createAccessTokenOp: (payload: {
|
|
470
|
+
readonly ownerUserId: string;
|
|
471
|
+
readonly scopes: readonly ("session:input" | "session:read" | "workspace:exec")[];
|
|
472
|
+
readonly name?: string | undefined;
|
|
473
|
+
readonly workspaceId?: string | undefined;
|
|
474
|
+
readonly ttlSeconds?: number | undefined;
|
|
475
|
+
}) => Effect.Effect<{
|
|
476
|
+
readonly tokenId: string;
|
|
477
|
+
readonly ownerUserId: string;
|
|
478
|
+
readonly name?: string | undefined;
|
|
479
|
+
readonly scopes: readonly ("session:input" | "session:read" | "workspace:exec")[];
|
|
480
|
+
readonly workspaceId?: string | undefined;
|
|
481
|
+
readonly expiresAt?: string | undefined;
|
|
482
|
+
readonly revokedAt?: string | undefined;
|
|
483
|
+
readonly createdAt: string;
|
|
484
|
+
readonly token: string;
|
|
485
|
+
}, import("@sealant/api-contracts").AccessTokenBadRequestError | import("@sealant/api-contracts").AccessTokenInternalServerError | import("@sealant/api-contracts").AccessTokenNotFoundError | import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError, SealantApiClient>;
|
|
289
486
|
export declare const inferenceRespondOp: (payload: {
|
|
290
487
|
readonly ownerUserId: string;
|
|
291
488
|
readonly credentials?: {
|
|
@@ -20,5 +20,20 @@ export const getRunTimelineOp = (runId, query) => Effect.flatMap(SealantApiClien
|
|
|
20
20
|
export const getRunScrollbackOp = (runId, query) => Effect.flatMap(SealantApiClient, (client) => client.runs.getRunScrollback({ params: { runId }, query }));
|
|
21
21
|
export const getRunLossOp = (runId) => Effect.flatMap(SealantApiClient, (client) => client.runs.getRunLoss({ params: { runId } }));
|
|
22
22
|
export const getRunChangesOp = (runId) => Effect.flatMap(SealantApiClient, (client) => client.runs.getRunChanges({ params: { runId } }));
|
|
23
|
+
// ---- sessions ----
|
|
24
|
+
export const createSessionOp = (payload) => Effect.flatMap(SealantApiClient, (client) => client.sessions.createSession({ payload, headers: {} }));
|
|
25
|
+
export const getSessionOp = (sessionId, ownerUserId) => Effect.flatMap(SealantApiClient, (client) => client.sessions.getSession({
|
|
26
|
+
params: { sessionId },
|
|
27
|
+
headers: {},
|
|
28
|
+
query: ownerUserId === undefined ? {} : { ownerUserId },
|
|
29
|
+
}));
|
|
30
|
+
export const listSessionsOp = (query) => Effect.flatMap(SealantApiClient, (client) => client.sessions.listSessions({ query, headers: {} }));
|
|
31
|
+
export const getSessionOutputOp = (sessionId, query) => Effect.flatMap(SealantApiClient, (client) => client.sessions.getSessionOutput({ params: { sessionId }, headers: {}, query }));
|
|
32
|
+
export const sendSessionInputOp = (sessionId, payload) => Effect.flatMap(SealantApiClient, (client) => client.sessions.sendSessionInput({ params: { sessionId }, headers: {}, payload }));
|
|
33
|
+
export const resizeSessionOp = (sessionId, payload) => Effect.flatMap(SealantApiClient, (client) => client.sessions.resizeSession({ params: { sessionId }, headers: {}, payload }));
|
|
34
|
+
export const signalSessionOp = (sessionId, payload) => Effect.flatMap(SealantApiClient, (client) => client.sessions.signalSession({ params: { sessionId }, headers: {}, payload }));
|
|
35
|
+
export const closeSessionOp = (sessionId, payload) => Effect.flatMap(SealantApiClient, (client) => client.sessions.closeSession({ params: { sessionId }, headers: {}, payload }));
|
|
36
|
+
// ---- access tokens ----
|
|
37
|
+
export const createAccessTokenOp = (payload) => Effect.flatMap(SealantApiClient, (client) => client.accessTokens.createAccessToken({ payload }));
|
|
23
38
|
// ---- inference ----
|
|
24
39
|
export const inferenceRespondOp = (payload) => Effect.flatMap(SealantApiClient, (client) => client.inference.respond({ payload }));
|
|
@@ -11,31 +11,50 @@
|
|
|
11
11
|
import { Effect } from "effect";
|
|
12
12
|
import { SealantError } from "../errors.js";
|
|
13
13
|
import { makeRun, toRunChangesData } from "../facade/run.js";
|
|
14
|
-
import { createRunOp, getRunChangesOp, getRunOp } from "./operations.js";
|
|
14
|
+
import { createRunOp, getRunChangesOp, getRunOp, getWorkspaceOp } from "./operations.js";
|
|
15
15
|
const TERMINAL_STATUSES = new Set(["completed", "failed", "cancelled"]);
|
|
16
16
|
const POLL_INTERVAL = "500 millis";
|
|
17
17
|
const RUN_TIMEOUT_MS = 30 * 60 * 1_000;
|
|
18
18
|
/**
|
|
19
|
-
* Registers the run
|
|
20
|
-
*
|
|
19
|
+
* Registers the run — the control plane executes it server-side (the worker docker-execs it and
|
|
20
|
+
* ingests telemetry). With a client harness value (the creating handle), the client-built command
|
|
21
|
+
* rides along — the escape hatch that keeps `customHarness()` working. WITHOUT one (a handle from
|
|
22
|
+
* `workspaces.get(id)`), the harness id is read from the workspace's own spec and NO command is
|
|
23
|
+
* sent: the control plane constructs the invocation server-side, so re-fetched handles are fully
|
|
24
|
+
* capable.
|
|
21
25
|
*/
|
|
22
|
-
const createHarnessRunEffect = (ctx, init, prompt) => Effect.gen(function* () {
|
|
26
|
+
const createHarnessRunEffect = (ctx, init, prompt, options) => Effect.gen(function* () {
|
|
27
|
+
const metadata = options?.metadata === undefined ? {} : { metadata: { ...options.metadata } };
|
|
23
28
|
const harness = init.harness;
|
|
24
|
-
if (harness
|
|
25
|
-
|
|
29
|
+
if (harness !== undefined) {
|
|
30
|
+
const command = harness.buildRunCommand(prompt);
|
|
31
|
+
return yield* createRunOp({
|
|
32
|
+
workspaceId: init.id,
|
|
33
|
+
ownerUserId: ctx.config.hostLocal.ownerUserId,
|
|
34
|
+
harnessId: harness.id,
|
|
35
|
+
mode: "one-shot",
|
|
36
|
+
prompt,
|
|
37
|
+
command: { executable: command.executable, args: [...command.args] },
|
|
38
|
+
...metadata,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const details = yield* getWorkspaceOp(init.id);
|
|
42
|
+
const spec = details.spec;
|
|
43
|
+
const harnessId = spec?.harness?.id;
|
|
44
|
+
if (harnessId === undefined) {
|
|
45
|
+
return yield* Effect.fail(new SealantError(`Workspace ${init.id} has no harness in its spec; pass a harness at workspaces.create() or use workspace.exec().`, { code: "harness_required" }));
|
|
26
46
|
}
|
|
27
|
-
const command = harness.buildRunCommand(prompt);
|
|
28
47
|
return yield* createRunOp({
|
|
29
48
|
workspaceId: init.id,
|
|
30
49
|
ownerUserId: ctx.config.hostLocal.ownerUserId,
|
|
31
|
-
harnessId
|
|
50
|
+
harnessId,
|
|
32
51
|
mode: "one-shot",
|
|
33
52
|
prompt,
|
|
34
|
-
|
|
53
|
+
...metadata,
|
|
35
54
|
});
|
|
36
55
|
});
|
|
37
|
-
const runHarnessEffect = (ctx, init, prompt) => Effect.gen(function* () {
|
|
38
|
-
const created = yield* createHarnessRunEffect(ctx, init, prompt);
|
|
56
|
+
const runHarnessEffect = (ctx, init, prompt, options) => Effect.gen(function* () {
|
|
57
|
+
const created = yield* createHarnessRunEffect(ctx, init, prompt, options);
|
|
39
58
|
const runId = created.runId;
|
|
40
59
|
// Block until the run is terminal, polling the control plane.
|
|
41
60
|
const deadline = Date.now() + RUN_TIMEOUT_MS;
|
|
@@ -54,10 +73,10 @@ const runHarnessEffect = (ctx, init, prompt) => Effect.gen(function* () {
|
|
|
54
73
|
return makeRun(ctx, { wire, changes });
|
|
55
74
|
});
|
|
56
75
|
/** The BLOCKING `harness.run()` implementation, registered into the Workspace facade by the client. */
|
|
57
|
-
export const runHarness = (ctx, init, prompt) => ctx.runtime.run(runHarnessEffect(ctx, init, prompt));
|
|
76
|
+
export const runHarness = (ctx, init, prompt, options) => ctx.runtime.run(runHarnessEffect(ctx, init, prompt, options));
|
|
58
77
|
/**
|
|
59
78
|
* The NON-BLOCKING `harness.start()` implementation: register the run and return the live handle
|
|
60
79
|
* immediately. Callers stream progress via `run.record.stream()` and settle via `run.wait()` (which
|
|
61
80
|
* fetches the captured changes once terminal).
|
|
62
81
|
*/
|
|
63
|
-
export const startHarness = (ctx, init, prompt) => ctx.runtime.run(Effect.map(createHarnessRunEffect(ctx, init, prompt), (created) => makeRun(ctx, { wire: created })));
|
|
82
|
+
export const startHarness = (ctx, init, prompt, options) => ctx.runtime.run(Effect.map(createHarnessRunEffect(ctx, init, prompt, options), (created) => makeRun(ctx, { wire: created })));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { InteractiveSession } from "../types.js";
|
|
2
|
+
import type { SdkContext } from "./context.js";
|
|
3
|
+
export declare const makeInteractiveSession: (ctx: SdkContext, wire: {
|
|
4
|
+
readonly sessionId: string;
|
|
5
|
+
readonly workspaceId: string;
|
|
6
|
+
readonly runId: string;
|
|
7
|
+
readonly ownerUserId: string;
|
|
8
|
+
readonly status: "exited" | "failed" | "running" | "starting";
|
|
9
|
+
readonly argv: readonly string[];
|
|
10
|
+
readonly cwd?: string | undefined;
|
|
11
|
+
readonly cols: number;
|
|
12
|
+
readonly rows: number;
|
|
13
|
+
readonly exitCode?: number | undefined;
|
|
14
|
+
readonly exitSignal?: number | undefined;
|
|
15
|
+
readonly errorMessage?: string | undefined;
|
|
16
|
+
readonly metadata?: {
|
|
17
|
+
readonly [x: string]: unknown;
|
|
18
|
+
} | undefined;
|
|
19
|
+
readonly outputHighWater: string;
|
|
20
|
+
readonly createdAt: string;
|
|
21
|
+
readonly endedAt?: string | undefined;
|
|
22
|
+
}) => InteractiveSession;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { closeSessionOp, getSessionOp, getSessionOutputOp, resizeSessionOp, sendSessionInputOp, signalSessionOp, } from "../effect/operations.js";
|
|
2
|
+
const OUTPUT_POLL_INTERVAL_MS = 250;
|
|
3
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4
|
+
const decodeBase64 = (value) => {
|
|
5
|
+
const buffer = Buffer.from(value, "base64");
|
|
6
|
+
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
7
|
+
};
|
|
8
|
+
/** Parse one SSE frame block ("event: ...\nid: ...\ndata: ...") into its fields. */
|
|
9
|
+
const parseSseFrame = (block) => {
|
|
10
|
+
let event = "message";
|
|
11
|
+
let id;
|
|
12
|
+
const dataLines = [];
|
|
13
|
+
for (const line of block.split("\n")) {
|
|
14
|
+
if (line.startsWith(":")) {
|
|
15
|
+
continue; // comment / keep-alive
|
|
16
|
+
}
|
|
17
|
+
if (line.startsWith("event:")) {
|
|
18
|
+
event = line.slice(6).trim();
|
|
19
|
+
}
|
|
20
|
+
else if (line.startsWith("id:")) {
|
|
21
|
+
id = line.slice(3).trim();
|
|
22
|
+
}
|
|
23
|
+
else if (line.startsWith("data:")) {
|
|
24
|
+
dataLines.push(line.slice(5).trimStart());
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (dataLines.length === 0 && id === undefined) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return { event, id, data: dataLines.join("\n") };
|
|
31
|
+
};
|
|
32
|
+
/** Stream the session's output over SSE. Throws on transport failure (caller falls back). */
|
|
33
|
+
async function* streamOverSse(ctx, sessionId, from, signal) {
|
|
34
|
+
const config = ctx.config;
|
|
35
|
+
const fetchImpl = config.fetch ?? fetch;
|
|
36
|
+
const url = new URL(`/v1/sessions/${sessionId}/output/stream`, config.baseUrl);
|
|
37
|
+
url.searchParams.set("from", from.toString());
|
|
38
|
+
if (config.apiKey === undefined) {
|
|
39
|
+
url.searchParams.set("ownerUserId", config.hostLocal.ownerUserId);
|
|
40
|
+
}
|
|
41
|
+
const response = await fetchImpl(url, {
|
|
42
|
+
headers: {
|
|
43
|
+
accept: "text/event-stream",
|
|
44
|
+
...(config.apiKey === undefined ? {} : { authorization: `Bearer ${config.apiKey}` }),
|
|
45
|
+
},
|
|
46
|
+
...(signal === undefined ? {} : { signal }),
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok || response.body === null) {
|
|
49
|
+
throw new Error(`session output stream failed: HTTP ${response.status}`);
|
|
50
|
+
}
|
|
51
|
+
const reader = response.body.getReader();
|
|
52
|
+
const decoder = new TextDecoder();
|
|
53
|
+
let buffered = "";
|
|
54
|
+
try {
|
|
55
|
+
for (;;) {
|
|
56
|
+
const { done, value } = await reader.read();
|
|
57
|
+
if (done) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
buffered += decoder.decode(value, { stream: true });
|
|
61
|
+
for (;;) {
|
|
62
|
+
const boundary = buffered.indexOf("\n\n");
|
|
63
|
+
if (boundary === -1) {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
const block = buffered.slice(0, boundary);
|
|
67
|
+
buffered = buffered.slice(boundary + 2);
|
|
68
|
+
const frame = parseSseFrame(block);
|
|
69
|
+
if (frame === undefined) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (frame.event === "end") {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (frame.event === "output" && frame.id !== undefined) {
|
|
76
|
+
yield { sequence: BigInt(frame.id), data: decodeBase64(frame.data) };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
await reader.cancel().catch(() => undefined);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Poll-backed fallback: page the range endpoint until the session settles. */
|
|
86
|
+
async function* streamByPolling(ctx, sessionId, from, signal) {
|
|
87
|
+
let cursor = from;
|
|
88
|
+
for (;;) {
|
|
89
|
+
if (signal?.aborted === true) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const page = await ctx.runtime.run(getSessionOutputOp(sessionId, {
|
|
93
|
+
ownerUserId: ctx.config.hostLocal.ownerUserId,
|
|
94
|
+
from: cursor.toString(),
|
|
95
|
+
}));
|
|
96
|
+
for (const chunk of page.chunks) {
|
|
97
|
+
yield { sequence: BigInt(chunk.sequence), data: decodeBase64(chunk.dataBase64) };
|
|
98
|
+
}
|
|
99
|
+
cursor = BigInt(page.nextFrom);
|
|
100
|
+
if (page.chunks.length === 0) {
|
|
101
|
+
if (page.status === "exited" || page.status === "failed") {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
await delay(OUTPUT_POLL_INTERVAL_MS);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export const makeInteractiveSession = (ctx, wire) => {
|
|
109
|
+
const sessionId = wire.sessionId;
|
|
110
|
+
return {
|
|
111
|
+
id: sessionId,
|
|
112
|
+
workspaceId: wire.workspaceId,
|
|
113
|
+
runId: wire.runId,
|
|
114
|
+
send: async (input) => {
|
|
115
|
+
const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
|
|
116
|
+
await ctx.runtime.run(sendSessionInputOp(sessionId, {
|
|
117
|
+
ownerUserId: ctx.config.hostLocal.ownerUserId,
|
|
118
|
+
dataBase64: Buffer.from(bytes).toString("base64"),
|
|
119
|
+
}));
|
|
120
|
+
},
|
|
121
|
+
output: (options) => {
|
|
122
|
+
const from = options?.from ?? 0n;
|
|
123
|
+
const signal = options?.signal;
|
|
124
|
+
async function* iterate() {
|
|
125
|
+
let cursor = from;
|
|
126
|
+
try {
|
|
127
|
+
for await (const chunk of streamOverSse(ctx, sessionId, cursor, signal)) {
|
|
128
|
+
yield chunk;
|
|
129
|
+
cursor = chunk.sequence + 1n;
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (signal?.aborted === true) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// SSE transport unavailable (proxy, fetch impl, older server) — fall back to polling
|
|
138
|
+
// from the cursor; the sequence keying makes the switch seamless.
|
|
139
|
+
void error;
|
|
140
|
+
}
|
|
141
|
+
yield* streamByPolling(ctx, sessionId, cursor, signal);
|
|
142
|
+
}
|
|
143
|
+
return iterate();
|
|
144
|
+
},
|
|
145
|
+
resize: async (cols, rows) => {
|
|
146
|
+
await ctx.runtime.run(resizeSessionOp(sessionId, {
|
|
147
|
+
ownerUserId: ctx.config.hostLocal.ownerUserId,
|
|
148
|
+
cols,
|
|
149
|
+
rows,
|
|
150
|
+
}));
|
|
151
|
+
},
|
|
152
|
+
signal: async (signalNumber) => {
|
|
153
|
+
await ctx.runtime.run(signalSessionOp(sessionId, {
|
|
154
|
+
ownerUserId: ctx.config.hostLocal.ownerUserId,
|
|
155
|
+
signal: signalNumber,
|
|
156
|
+
}));
|
|
157
|
+
},
|
|
158
|
+
status: async () => {
|
|
159
|
+
const current = await ctx.runtime.run(getSessionOp(sessionId, ctx.config.hostLocal.ownerUserId));
|
|
160
|
+
return {
|
|
161
|
+
status: current.status,
|
|
162
|
+
...(current.exitCode === undefined ? {} : { exitCode: current.exitCode }),
|
|
163
|
+
...(current.exitSignal === undefined ? {} : { exitSignal: current.exitSignal }),
|
|
164
|
+
outputHighWater: BigInt(current.outputHighWater),
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
close: async () => {
|
|
168
|
+
await ctx.runtime.run(closeSessionOp(sessionId, { ownerUserId: ctx.config.hostLocal.ownerUserId }));
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
};
|
package/dist/facade/workspace.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { execWorkspace } from "../effect/exec-workspace.js";
|
|
2
|
-
import { expireWorkspaceOp, getWorkspaceOp, restartWorkspaceOp, stopWorkspaceOp, } from "../effect/operations.js";
|
|
2
|
+
import { createSessionOp, expireWorkspaceOp, getSessionOp, getWorkspaceOp, listSessionsOp, restartWorkspaceOp, stopWorkspaceOp, } from "../effect/operations.js";
|
|
3
3
|
import { SealantError, SealantNotImplementedError } from "../errors.js";
|
|
4
4
|
import { parseTtlSeconds } from "../internal/duration.js";
|
|
5
|
+
import { makeInteractiveSession } from "./session.js";
|
|
5
6
|
// Terminal statuses a workspace can never leave: ready()/events() fail fast (or end the stream)
|
|
6
7
|
// on these instead of polling out their deadline. "stopped" is terminal too — a TTL expiry or a
|
|
7
8
|
// concurrent stop while ready() polls must surface immediately, not as a 10-minute timeout.
|
|
@@ -15,7 +16,60 @@ let harnessExecutors;
|
|
|
15
16
|
export const registerHarnessExecutors = (executors) => {
|
|
16
17
|
harnessExecutors = executors;
|
|
17
18
|
};
|
|
19
|
+
// Launch commands for the built-in harnesses — used when a RE-FETCHED handle (no client harness
|
|
20
|
+
// value) opens a harness session; the workspace's own spec names the harness id.
|
|
21
|
+
const BUILTIN_LAUNCH_COMMANDS = {
|
|
22
|
+
opencode: "opencode",
|
|
23
|
+
codex: "codex",
|
|
24
|
+
"claude-code": "claude",
|
|
25
|
+
};
|
|
18
26
|
export const makeWorkspace = (ctx, init) => {
|
|
27
|
+
const openSession = async (argv, options) => {
|
|
28
|
+
const created = await ctx.runtime.run(createSessionOp({
|
|
29
|
+
workspaceId: init.id,
|
|
30
|
+
ownerUserId: ctx.config.hostLocal.ownerUserId,
|
|
31
|
+
argv: [...argv],
|
|
32
|
+
...(options?.cwd === undefined ? {} : { cwd: options.cwd }),
|
|
33
|
+
...(options?.env === undefined ? {} : { env: options.env }),
|
|
34
|
+
...(options?.cols === undefined ? {} : { cols: options.cols }),
|
|
35
|
+
...(options?.rows === undefined ? {} : { rows: options.rows }),
|
|
36
|
+
...(options?.term === undefined ? {} : { term: options.term }),
|
|
37
|
+
...(options?.metadata === undefined ? {} : { metadata: { ...options.metadata } }),
|
|
38
|
+
}));
|
|
39
|
+
return makeInteractiveSession(ctx, created);
|
|
40
|
+
};
|
|
41
|
+
const sessions = {
|
|
42
|
+
open: (argv, options) => openSession(argv, options),
|
|
43
|
+
get: async (sessionId) => {
|
|
44
|
+
const wire = await ctx.runtime.run(getSessionOp(sessionId, ctx.config.hostLocal.ownerUserId));
|
|
45
|
+
if (wire.workspaceId !== init.id) {
|
|
46
|
+
throw new SealantError(`Session ${sessionId} does not belong to workspace ${init.id}.`, {
|
|
47
|
+
code: "session_not_found",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return makeInteractiveSession(ctx, wire);
|
|
51
|
+
},
|
|
52
|
+
list: async () => {
|
|
53
|
+
const response = await ctx.runtime.run(listSessionsOp({
|
|
54
|
+
ownerUserId: ctx.config.hostLocal.ownerUserId,
|
|
55
|
+
workspaceId: init.id,
|
|
56
|
+
}));
|
|
57
|
+
return response.items.map((item) => makeInteractiveSession(ctx, item));
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
/** The harness's interactive launch argv — client value when present, else from the spec. */
|
|
61
|
+
const resolveHarnessLaunchArgv = async () => {
|
|
62
|
+
if (init.harness !== undefined) {
|
|
63
|
+
return [init.harness.launchCommand ?? init.harness.id];
|
|
64
|
+
}
|
|
65
|
+
const details = await ctx.runtime.run(getWorkspaceOp(init.id));
|
|
66
|
+
const spec = details.spec;
|
|
67
|
+
const harnessId = spec?.harness?.id;
|
|
68
|
+
if (harnessId === undefined) {
|
|
69
|
+
throw new SealantError(`Workspace ${init.id} has no harness in its spec; open a session with workspace.sessions.open(argv) instead.`, { code: "harness_required" });
|
|
70
|
+
}
|
|
71
|
+
return [BUILTIN_LAUNCH_COMMANDS[harnessId] ?? harnessId];
|
|
72
|
+
};
|
|
19
73
|
const harness = {
|
|
20
74
|
run: (prompt, options) => {
|
|
21
75
|
if (harnessExecutors === undefined) {
|
|
@@ -29,7 +83,10 @@ export const makeWorkspace = (ctx, init) => {
|
|
|
29
83
|
}
|
|
30
84
|
return harnessExecutors.start(ctx, init, prompt, options);
|
|
31
85
|
},
|
|
32
|
-
session: () =>
|
|
86
|
+
session: async (options) => {
|
|
87
|
+
const argv = await resolveHarnessLaunchArgv();
|
|
88
|
+
return openSession(argv, options);
|
|
89
|
+
},
|
|
33
90
|
};
|
|
34
91
|
const workspace = {
|
|
35
92
|
id: init.id,
|
|
@@ -60,6 +117,7 @@ export const makeWorkspace = (ctx, init) => {
|
|
|
60
117
|
}
|
|
61
118
|
},
|
|
62
119
|
harness,
|
|
120
|
+
sessions,
|
|
63
121
|
exec: (argv, options) => execWorkspace(ctx, init, argv, options),
|
|
64
122
|
// Poll-backed lifecycle stream: emit a coarse event on each status transition until the workspace
|
|
65
123
|
// reaches a terminal/ready state. Swaps to SSE over Postgres LISTEN/NOTIFY in Stage 5 (same shape).
|