@pstdio/pocketcoder-sdk 0.3.1 → 0.4.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/README.md CHANGED
@@ -42,3 +42,57 @@ metadata-only terminal audit history.
42
42
  Use `client.raw(path, init)` for endpoints not yet represented by a typed
43
43
  resource client. API failures throw `PocketCoderError`; expired or deleted
44
44
  conversation history throws the more specific `ConversationGoneError`.
45
+
46
+ ## Resolve one user turn after preservation
47
+
48
+ Create one long-lived `WorkspaceTurnResolver` in a trusted backend. Call it
49
+ only when handling a user turn. A ready workspace is returned unchanged. A
50
+ preserved workspace is resumed through your callback, checked against the
51
+ source checkpoint, and returned only after it becomes ready.
52
+
53
+ ```ts
54
+ import {
55
+ PocketCoderClient,
56
+ WorkspaceTurnResolver,
57
+ } from "@pstdio/pocketcoder-sdk";
58
+
59
+ const client = new PocketCoderClient({
60
+ baseUrl: process.env.POCKETCODER_URL!,
61
+ apiKey: process.env.POCKETCODER_KEY!,
62
+ });
63
+
64
+ const resolver = new WorkspaceTurnResolver({
65
+ client,
66
+ resumeWorkspace: async ({ source, attemptId, signal }) => {
67
+ // Mint fresh workspace-scoped input. It must expire with the new workspace.
68
+ const launchInput = await issueWorkspaceBootstrap({ signal });
69
+ const result = await client.workspaces.resume(
70
+ source.id,
71
+ {
72
+ external_id: `turn-${attemptId}`,
73
+ launch_input: launchInput,
74
+ },
75
+ attemptId,
76
+ { signal },
77
+ );
78
+ return result.workspace;
79
+ },
80
+ });
81
+
82
+ export async function handleTurn(sourceWorkspaceId: string, prompt: string) {
83
+ const { workspace } = await resolver.resolve(sourceWorkspaceId);
84
+ await client.agent.sendMessage(workspace.id, { content: prompt });
85
+ return { workspace_id: workspace.id };
86
+ }
87
+ ```
88
+
89
+ The key needs `workspaces:read`; a callback that calls `resume()` also needs
90
+ `workspaces:restore`. Prompt relay and attachments need `services:relay` and
91
+ `attachments:write` respectively.
92
+
93
+ The resolver stores no A-to-B mapping. Return the resolved id through your
94
+ normal request or session state if a later request needs it. Canceling one
95
+ caller stops that caller's wait. It does not roll back a resume request already
96
+ accepted by the server, and another caller may join the same in-memory attempt.
97
+ Lifecycle failures use fixed messages and never expose callback errors or
98
+ launch input.
package/dist/index.d.ts CHANGED
@@ -1413,5 +1413,43 @@ declare class WorkspaceTerminalError extends Error {
1413
1413
  readonly workspace: WorkspaceResource;
1414
1414
  constructor(workspace: WorkspaceResource);
1415
1415
  }
1416
+ declare function isPocketCoderErrorCode(value: unknown): value is ErrorCode;
1416
1417
  //#endregion
1417
- export { AdministrationApi, AgentApi, type AgentMessageInput, type AttachmentDescriptor, type AttachmentUploadInput, AttachmentsApi, type CheckpointResource, type CheckpointState, CheckpointsApi, type ClientErrorCode, ConversationGoneError, type ConversationMessage, type ConversationPage, ConversationsApi, type CursorListQuery, type LogChunk, LogsApi, type NetworkEvent, NetworkEventsApi, type OperationKind, type OperationResource, type OperationState, OperationsApi, type OutputResource, OutputsApi, type Page, PocketCoderClient, type PocketCoderClientConfig, PocketCoderError, type PreserveRequest, type RequestOptions, type RestoreRequest, type ServerTerminalMessage, type StorageState, TERMINAL_WORKSPACE_STATES, type TemplateSummary, TemplatesApi, type TerminalConnectOptions, TerminalConnection, type TerminalSession, TerminalsApi, type WebSocketFactory, type WorkspaceCreateInput, type WorkspaceListQuery, type WorkspaceResource, type WorkspaceState, type WorkspaceSummary, WorkspaceTerminalError, WorkspacesApi, splitAttachmentManifest };
1418
+ //#region src/workspace-turn-resolver.d.ts
1419
+ type WorkspaceTurnResolutionErrorCode = "not_resumable" | "resume_handler_missing" | "resume_failed" | "invalid_resumed_workspace" | "readiness_failed";
1420
+ declare class WorkspaceTurnResolutionError extends Error {
1421
+ readonly code: WorkspaceTurnResolutionErrorCode;
1422
+ constructor(code: WorkspaceTurnResolutionErrorCode, cause?: unknown);
1423
+ }
1424
+ interface ResumeWorkspaceContext {
1425
+ source: WorkspaceResource;
1426
+ attemptId: string;
1427
+ signal: AbortSignal;
1428
+ }
1429
+ interface WorkspaceTurnResolverOptions {
1430
+ client: PocketCoderClient;
1431
+ resumeWorkspace?: (context: ResumeWorkspaceContext) => Promise<WorkspaceResource>;
1432
+ resumeTimeoutMs?: number;
1433
+ }
1434
+ interface ResolveWorkspaceTurnOptions {
1435
+ signal?: AbortSignal;
1436
+ }
1437
+ interface ResolvedWorkspaceTurn {
1438
+ workspace: WorkspaceResource;
1439
+ resumed: boolean;
1440
+ }
1441
+ declare class WorkspaceTurnResolver {
1442
+ private readonly client;
1443
+ private readonly resumeWorkspace;
1444
+ private readonly resumeTimeoutMs;
1445
+ private readonly attempts;
1446
+ constructor(options: WorkspaceTurnResolverOptions);
1447
+ resolve(sourceWorkspaceId: string, options?: ResolveWorkspaceTurnOptions): Promise<ResolvedWorkspaceTurn>;
1448
+ private start;
1449
+ private run;
1450
+ private waitForPreserved;
1451
+ private assertResumable;
1452
+ private assertLineage;
1453
+ }
1454
+ //#endregion
1455
+ export { AdministrationApi, AgentApi, type AgentMessageInput, type AttachmentDescriptor, type AttachmentUploadInput, AttachmentsApi, type CheckpointResource, type CheckpointState, CheckpointsApi, type ClientErrorCode, ConversationGoneError, type ConversationMessage, type ConversationPage, ConversationsApi, type CursorListQuery, type LogChunk, LogsApi, type NetworkEvent, NetworkEventsApi, type OperationKind, type OperationResource, type OperationState, OperationsApi, type OutputResource, OutputsApi, type Page, PocketCoderClient, type PocketCoderClientConfig, PocketCoderError, type PreserveRequest, type RequestOptions, type ResolveWorkspaceTurnOptions, type ResolvedWorkspaceTurn, type RestoreRequest, type ResumeWorkspaceContext, type ServerTerminalMessage, type StorageState, TERMINAL_WORKSPACE_STATES, type TemplateSummary, TemplatesApi, type TerminalConnectOptions, TerminalConnection, type TerminalSession, TerminalsApi, type WebSocketFactory, type WorkspaceCreateInput, type WorkspaceListQuery, type WorkspaceResource, type WorkspaceState, type WorkspaceSummary, WorkspaceTerminalError, WorkspaceTurnResolutionError, type WorkspaceTurnResolutionErrorCode, WorkspaceTurnResolver, type WorkspaceTurnResolverOptions, WorkspacesApi, isPocketCoderErrorCode, splitAttachmentManifest };
package/dist/index.js CHANGED
@@ -531,7 +531,9 @@ const AttachmentResolvedPayload = z.object({
531
531
  descriptors: z.array(AttachmentDescriptorSchema).optional(),
532
532
  missing_id: z.uuid().optional()
533
533
  });
534
- const errorCodes = Object.keys({
534
+ //#endregion
535
+ //#region ../contracts/src/errors.ts
536
+ const ERROR_CODES = {
535
537
  "auth.invalid_key": 401,
536
538
  "auth.missing_scope": 403,
537
539
  "auth.disabled_principal": 403,
@@ -583,7 +585,8 @@ const errorCodes = Object.keys({
583
585
  "attachment.unsupported": 409,
584
586
  "attachment.interrupted": 503,
585
587
  "internal.error": 500
586
- });
588
+ };
589
+ const errorCodes = Object.keys(ERROR_CODES);
587
590
  const ErrorEnvelopeSchema = z.object({ error: z.object({
588
591
  code: z.enum(errorCodes),
589
592
  message: z.string().min(1),
@@ -1779,6 +1782,9 @@ var WorkspaceTerminalError = class extends Error {
1779
1782
  this.workspace = workspace;
1780
1783
  }
1781
1784
  };
1785
+ function isPocketCoderErrorCode(value) {
1786
+ return typeof value === "string" && value in ERROR_CODES;
1787
+ }
1782
1788
  function responseError(response, body) {
1783
1789
  const parsed = ErrorEnvelopeSchema.safeParse(body);
1784
1790
  if (!parsed.success) return new PocketCoderError({
@@ -2332,4 +2338,148 @@ var PocketCoderClient = class {
2332
2338
  }
2333
2339
  };
2334
2340
  //#endregion
2335
- export { AdministrationApi, AgentApi, AttachmentsApi, CheckpointsApi, ConversationGoneError, ConversationsApi, LogsApi, NetworkEventsApi, OperationsApi, OutputsApi, PocketCoderClient, PocketCoderError, TERMINAL_WORKSPACE_STATES, TemplatesApi, TerminalConnection, TerminalsApi, WorkspaceTerminalError, WorkspacesApi, splitAttachmentManifest };
2341
+ //#region src/workspace-turn-resolver.ts
2342
+ const ERROR_MESSAGES = {
2343
+ not_resumable: "workspace cannot be resumed",
2344
+ resume_handler_missing: "workspace resume handler is not configured",
2345
+ resume_failed: "workspace resume failed",
2346
+ invalid_resumed_workspace: "workspace resume returned an invalid workspace",
2347
+ readiness_failed: "resumed workspace did not become ready"
2348
+ };
2349
+ var WorkspaceTurnResolutionError = class extends Error {
2350
+ code;
2351
+ constructor(code, cause) {
2352
+ super(ERROR_MESSAGES[code], cause === void 0 ? void 0 : { cause });
2353
+ this.name = "WorkspaceTurnResolutionError";
2354
+ this.code = code;
2355
+ }
2356
+ };
2357
+ const NON_RESUMABLE_STATES = /* @__PURE__ */ new Set([
2358
+ "failed",
2359
+ "canceled",
2360
+ "expired",
2361
+ "succeeded",
2362
+ "terminating"
2363
+ ]);
2364
+ function aborted(signal) {
2365
+ return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
2366
+ }
2367
+ function waitForCaller(promise, signal) {
2368
+ if (!signal) return promise;
2369
+ if (signal.aborted) return Promise.reject(aborted(signal));
2370
+ return new Promise((resolve, reject) => {
2371
+ const onAbort = () => reject(aborted(signal));
2372
+ signal.addEventListener("abort", onAbort, { once: true });
2373
+ promise.then((value) => {
2374
+ signal.removeEventListener("abort", onAbort);
2375
+ resolve(value);
2376
+ }, (error) => {
2377
+ signal.removeEventListener("abort", onAbort);
2378
+ reject(error);
2379
+ });
2380
+ });
2381
+ }
2382
+ function requireRemaining(deadline, failure) {
2383
+ const remaining = deadline - Date.now();
2384
+ if (remaining <= 0) throw new WorkspaceTurnResolutionError(failure);
2385
+ return remaining;
2386
+ }
2387
+ var WorkspaceTurnResolver = class {
2388
+ client;
2389
+ resumeWorkspace;
2390
+ resumeTimeoutMs;
2391
+ attempts = /* @__PURE__ */ new Map();
2392
+ constructor(options) {
2393
+ this.client = options.client;
2394
+ this.resumeWorkspace = options.resumeWorkspace;
2395
+ this.resumeTimeoutMs = options.resumeTimeoutMs ?? 3e5;
2396
+ }
2397
+ async resolve(sourceWorkspaceId, options = {}) {
2398
+ if (options.signal?.aborted) throw aborted(options.signal);
2399
+ const attempt = this.attempts.get(sourceWorkspaceId) ?? this.start(sourceWorkspaceId);
2400
+ attempt.waiters += 1;
2401
+ try {
2402
+ return await waitForCaller(attempt.promise, options.signal);
2403
+ } finally {
2404
+ attempt.waiters -= 1;
2405
+ if (attempt.waiters === 0 && !attempt.resumeStarted && !attempt.settled) attempt.controller.abort(new DOMException("No callers remain", "AbortError"));
2406
+ }
2407
+ }
2408
+ start(sourceWorkspaceId) {
2409
+ const attempt = {
2410
+ controller: new AbortController(),
2411
+ promise: Promise.resolve(void 0),
2412
+ waiters: 0,
2413
+ resumeStarted: false,
2414
+ settled: false
2415
+ };
2416
+ this.attempts.set(sourceWorkspaceId, attempt);
2417
+ attempt.promise = this.run(sourceWorkspaceId, attempt).finally(() => {
2418
+ attempt.settled = true;
2419
+ if (this.attempts.get(sourceWorkspaceId) === attempt) this.attempts.delete(sourceWorkspaceId);
2420
+ });
2421
+ attempt.promise.catch(() => {});
2422
+ return attempt;
2423
+ }
2424
+ async run(sourceWorkspaceId, attempt) {
2425
+ const deadline = Date.now() + this.resumeTimeoutMs;
2426
+ let source = await this.client.workspaces.get(sourceWorkspaceId, { signal: attempt.controller.signal });
2427
+ if (source.state === "ready") return {
2428
+ workspace: source,
2429
+ resumed: false
2430
+ };
2431
+ if (source.state === "preserving") source = await this.waitForPreserved(source, deadline, attempt.controller.signal);
2432
+ this.assertResumable(source);
2433
+ if (!this.resumeWorkspace) throw new WorkspaceTurnResolutionError("resume_handler_missing");
2434
+ attempt.resumeStarted = true;
2435
+ const attemptId = crypto.randomUUID();
2436
+ let allocated;
2437
+ try {
2438
+ allocated = await this.resumeWorkspace({
2439
+ source,
2440
+ attemptId,
2441
+ signal: attempt.controller.signal
2442
+ });
2443
+ } catch (error) {
2444
+ throw new WorkspaceTurnResolutionError("resume_failed", error);
2445
+ }
2446
+ let fetched;
2447
+ try {
2448
+ fetched = await this.client.workspaces.get(allocated.id, { signal: attempt.controller.signal });
2449
+ } catch (error) {
2450
+ throw new WorkspaceTurnResolutionError("invalid_resumed_workspace", error);
2451
+ }
2452
+ this.assertLineage(source, fetched);
2453
+ try {
2454
+ return {
2455
+ workspace: await this.client.workspaces.waitForReady(fetched, requireRemaining(deadline, "readiness_failed"), { signal: attempt.controller.signal }),
2456
+ resumed: true
2457
+ };
2458
+ } catch (error) {
2459
+ if (error instanceof WorkspaceTurnResolutionError) throw error;
2460
+ throw new WorkspaceTurnResolutionError("readiness_failed", error);
2461
+ }
2462
+ }
2463
+ async waitForPreserved(initial, deadline, signal) {
2464
+ let workspace = initial;
2465
+ while (workspace.state === "preserving") {
2466
+ const remaining = requireRemaining(deadline, "resume_failed");
2467
+ try {
2468
+ workspace = (await this.client.workspaces.change(workspace.id, workspace.change_cursor, Math.max(1, Math.min(30, Math.ceil(remaining / 1e3))), { signal })).workspace;
2469
+ } catch (error) {
2470
+ if (signal.aborted) throw error;
2471
+ throw new WorkspaceTurnResolutionError("resume_failed", error);
2472
+ }
2473
+ }
2474
+ if (workspace.state === "failed") throw new WorkspaceTurnResolutionError("resume_failed");
2475
+ return workspace;
2476
+ }
2477
+ assertResumable(workspace) {
2478
+ if (workspace.state !== "preserved" || NON_RESUMABLE_STATES.has(workspace.state) || workspace.persistence.conversation_resume.status !== "supported" || !workspace.persistence.latest_checkpoint_id) throw new WorkspaceTurnResolutionError("not_resumable");
2479
+ }
2480
+ assertLineage(source, resumedWorkspace) {
2481
+ if (resumedWorkspace.id === source.id || resumedWorkspace.origin_workspace_id !== source.id || resumedWorkspace.restored_from_checkpoint_id !== source.persistence.latest_checkpoint_id) throw new WorkspaceTurnResolutionError("invalid_resumed_workspace");
2482
+ }
2483
+ };
2484
+ //#endregion
2485
+ export { AdministrationApi, AgentApi, AttachmentsApi, CheckpointsApi, ConversationGoneError, ConversationsApi, LogsApi, NetworkEventsApi, OperationsApi, OutputsApi, PocketCoderClient, PocketCoderError, TERMINAL_WORKSPACE_STATES, TemplatesApi, TerminalConnection, TerminalsApi, WorkspaceTerminalError, WorkspaceTurnResolutionError, WorkspaceTurnResolver, WorkspacesApi, isPocketCoderErrorCode, splitAttachmentManifest };
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "type:library"
7
7
  ]
8
8
  },
9
- "version": "0.3.1",
9
+ "version": "0.4.0",
10
10
  "private": false,
11
11
  "description": "Runtime-validated TypeScript client for the PocketCoder control plane.",
12
12
  "type": "module",