|
| 1 | +import { z } from "zod"; |
| 2 | + |
| 3 | +/** |
| 4 | + * Sandbox state snapshot + restore. |
| 5 | + * |
| 6 | + * A `SandboxSnapshot` is a point-in-time capture of a warm sandbox's |
| 7 | + * external state — the workdir (tarball) + metadata + an optional reference |
| 8 | + * to a SessionStore where conversation memory lives. Engine subprocess state |
| 9 | + * isn't snapshotted byte-for-byte; restoration relies on conversation log |
| 10 | + * replay (via SessionStore) + workdir overlay (via attachments) to bring a |
| 11 | + * fresh ComputerAgent back to "where it left off". |
| 12 | + * |
| 13 | + * The shape deliberately mirrors `TaskStore` (Wedge 1.11): the interface |
| 14 | + * lives here; backends implement it; a `kind` wire descriptor lets clients |
| 15 | + * pick a backend per-request. |
| 16 | + */ |
| 17 | + |
| 18 | +// ── Wire descriptor (the only thing crossing HTTP) ──────────────────────── |
| 19 | + |
| 20 | +/** |
| 21 | + * Lightweight descriptor a client POSTs to choose where snapshots go. |
| 22 | + * `kind` matches a registered builder in the harness server's `stateStores` |
| 23 | + * registry. `options` is opaque — the builder shape decides what it accepts. |
| 24 | + */ |
| 25 | +export const StateStoreConfig = z.object({ |
| 26 | + kind: z.string().min(1), |
| 27 | + options: z.unknown().optional(), |
| 28 | +}); |
| 29 | +export type StateStoreConfig = z.infer<typeof StateStoreConfig>; |
| 30 | + |
| 31 | +// ── Per-snapshot shape ──────────────────────────────────────────────────── |
| 32 | + |
| 33 | +/** |
| 34 | + * Aggregated token + cost rollup, accumulated from every `ca_usage_snapshot` |
| 35 | + * during the sandbox's lifetime. Mirrors `TaskUsage` from `task-store.ts` so |
| 36 | + * dashboards can reuse the same renderer. Duplicated rather than re-imported |
| 37 | + * to keep the state-store module self-contained. |
| 38 | + */ |
| 39 | +export interface SandboxUsage { |
| 40 | + readonly inputTokens?: number; |
| 41 | + readonly outputTokens?: number; |
| 42 | + readonly cacheCreationInputTokens?: number; |
| 43 | + readonly cacheReadInputTokens?: number; |
| 44 | + readonly costUsd?: number; |
| 45 | +} |
| 46 | + |
| 47 | +/** |
| 48 | + * Optional pointer to where the conversation log lives. If set, restore |
| 49 | + * pins the new sandbox's `sessionStore` to the same kind+options+sessionId |
| 50 | + * so the engine replays prior turns on its first chat. If absent, restored |
| 51 | + * sandbox starts with a fresh conversation (workdir is still recovered). |
| 52 | + * |
| 53 | + * `options` may contain credentials (mongo URL with password). The CALLER |
| 54 | + * is responsible for redacting before save if they don't want them stored |
| 55 | + * — the protocol doesn't second-guess. Bucket-level encryption / scoped |
| 56 | + * IAM is the right defense. |
| 57 | + */ |
| 58 | +export interface SessionStoreRef { |
| 59 | + readonly kind: string; |
| 60 | + readonly options?: unknown; |
| 61 | + readonly sessionId: string; |
| 62 | +} |
| 63 | + |
| 64 | +/** |
| 65 | + * One complete snapshot. Backends serialize this however they like — |
| 66 | + * S3 splits it into `meta.json` + `workdir.tar.gz`; memory keeps it intact; |
| 67 | + * a future GCS backend could do the same as S3. |
| 68 | + */ |
| 69 | +export interface SandboxSnapshot { |
| 70 | + readonly snapshotId: string; |
| 71 | + readonly sourceSandboxId: string; |
| 72 | + readonly sourceSessionId: string; |
| 73 | + readonly takenAt: Date; |
| 74 | + /** Redacted POST /sandboxes body — used by restore to recreate the agent. */ |
| 75 | + readonly config: Record<string, unknown>; |
| 76 | + readonly turnCount: number; |
| 77 | + readonly usage: SandboxUsage; |
| 78 | + readonly sessionStoreRef?: SessionStoreRef; |
| 79 | + /** |
| 80 | + * gzipped tar of the workdir. Stored alongside the metadata; the wire |
| 81 | + * format is the backend's choice. The protocol passes through Buffer |
| 82 | + * — streams are a follow-up wedge if snapshots routinely exceed memory. |
| 83 | + */ |
| 84 | + readonly workdirTar: Buffer; |
| 85 | + /** Tarball byte size (compressed). */ |
| 86 | + readonly workdirBytes: number; |
| 87 | + /** Number of files captured (uncompressed entry count). For UX in `list`. */ |
| 88 | + readonly workdirFileCount: number; |
| 89 | +} |
| 90 | + |
| 91 | +/** Snapshot minus the workdir bytes — for cheap listings. */ |
| 92 | +export type SnapshotSummary = Omit<SandboxSnapshot, "workdirTar">; |
| 93 | + |
| 94 | +/** |
| 95 | + * Filter for `listSnapshots`. All fields optional; backends apply them as |
| 96 | + * additive predicates. Implementations that can't push filters into the |
| 97 | + * underlying store may post-filter (S3 falls back to a prefix scan). |
| 98 | + */ |
| 99 | +export interface SnapshotFilter { |
| 100 | + readonly sourceSandboxId?: string; |
| 101 | + readonly since?: Date; |
| 102 | + readonly limit?: number; |
| 103 | +} |
| 104 | + |
| 105 | +// ── The interface backends implement ────────────────────────────────────── |
| 106 | + |
| 107 | +/** |
| 108 | + * Abstract state storage adapter. Implementations live in their own |
| 109 | + * packages (`@computeragent/state-store-s3`, future redis/gcs/azure |
| 110 | + * backends). The harness server only depends on this interface. |
| 111 | + */ |
| 112 | +export interface StateStore { |
| 113 | + /** |
| 114 | + * Persist a snapshot. Returns the canonical id (in case the backend |
| 115 | + * normalizes it) + the on-the-wire size. MUST be atomic from the |
| 116 | + * client's perspective — partial uploads should not surface in `list()`. |
| 117 | + */ |
| 118 | + save(snap: SandboxSnapshot): Promise<{ snapshotId: string; sizeBytes: number }>; |
| 119 | + |
| 120 | + /** Full snapshot + tarball. Returns `null` if the id doesn't exist. */ |
| 121 | + load(snapshotId: string): Promise<SandboxSnapshot | null>; |
| 122 | + |
| 123 | + /** Snapshot summaries matching the filter, most-recent first. */ |
| 124 | + list(filter?: SnapshotFilter): Promise<readonly SnapshotSummary[]>; |
| 125 | + |
| 126 | + /** Hard delete. Idempotent — deleting a missing id MUST NOT throw. */ |
| 127 | + delete(snapshotId: string): Promise<void>; |
| 128 | +} |
0 commit comments