@sealant/sdk 0.4.0 → 0.5.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
@@ -20,8 +20,23 @@ await run.record.replay();
20
20
  ## Design
21
21
 
22
22
  - **Plain-Promise facade over an Effect core.** The default export is ordinary `async`/`await`. The
23
- Effect-native core (services, `Stream`s, typed errors) will be reachable via the
24
- `@sealant/sdk/effect` subpath for power users.
23
+ Effect-native core is reachable via the `@sealant/sdk/effect` subpath for consumers that are
24
+ Effect end-to-end: the contract-derived client as a service, one operation effect per endpoint,
25
+ and the typed contract errors on the failure channel (no squashing) —
26
+
27
+ ```ts
28
+ import { Effect } from "effect";
29
+ import { getRunOp, resolveInternalConfig, sealantApiClientLayer } from "@sealant/sdk/effect";
30
+
31
+ const layer = sealantApiClientLayer(resolveInternalConfig({ baseUrl: "http://localhost:8080" }));
32
+
33
+ const status = getRunOp("run_123").pipe(
34
+ Effect.map((run) => run.status),
35
+ Effect.catchTag("RunNotFoundError", () => Effect.succeed("gone" as const)),
36
+ Effect.provide(layer),
37
+ );
38
+ ```
39
+
25
40
  - **Decoupled public types.** The types in [`src/types.ts`](src/types.ts) are hand-written and kept
26
41
  independent of the Effect-core and `@sealant/telemetry` internal shapes, so the public surface
27
42
  stays stable across internal change. The whole surface is typed now, including operations not yet
@@ -29,6 +44,46 @@ await run.record.replay();
29
44
  - **Harness-neutral.** `opencode()`, `codex()`, `claudeCode()`, and `customHarness()` are thin
30
45
  client values describing how to invoke a harness one-shot.
31
46
 
47
+ ## Deterministic exec
48
+
49
+ Run a command in the workspace with no agent in the loop — recorded into a run record like any other
50
+ process:
51
+
52
+ ```ts
53
+ const check = await workspace.exec(["pnpm", "test"], { cwd: "/workspace/repo" });
54
+ check.exitCode; // the check datum — a NONZERO exit RESOLVES (that's the point)
55
+ check.stdout; // full stdout, decoded
56
+ check.run.record; // the durable evidence
57
+
58
+ // A causal proof is three execs with three recorded exit codes:
59
+ const base = await workspace.exec(["pnpm", "test"]); // fails
60
+ // ...apply the fix...
61
+ const head = await workspace.exec(["pnpm", "test"]); // passes
62
+ ```
63
+
64
+ `exec()` rejects only when the execution machinery itself broke (workspace gone, transport dropped)
65
+ — i.e. when the exit code cannot be trusted. The underlying endpoint
66
+ (`POST /v1/workspaces/:id/exec`) accepts an ordered **list** of commands recorded as one check run;
67
+ the SDK surface starts with the single-command form.
68
+
69
+ ## Typed record events
70
+
71
+ Timeline reads are discriminated by `kind` — switch on it and `data` narrows to the event's typed
72
+ payload (all 12 recorded kinds: process, io, file, network, runtime, and loss events):
73
+
74
+ ```ts
75
+ for await (const entry of run.record.timeline()) {
76
+ if (entry.kind === "networkSourceObserved") {
77
+ entry.data.host; // typed — the raw material of a "sources the agent opened" trail
78
+ entry.data.status;
79
+ }
80
+ }
81
+ ```
82
+
83
+ Forward compatibility is a case, not an error: kinds newer than your SDK version (and payloads that
84
+ fail their schema) arrive as `{ kind: "unknown", rawKind, data }` with everything preserved. Wire
85
+ conventions carry through: uint64 fields are decimal strings, protocol enums are numbers.
86
+
32
87
  ## Connected-account credentials
33
88
 
34
89
  Attach the caller's connected Claude / Codex / GitHub accounts to a workspace so the harness
@@ -48,13 +103,53 @@ field wins over the profile's binding for that provider. Only account references
48
103
  surface — secret material never does; the control plane resolves references to encrypted credentials
49
104
  and injects them at launch.
50
105
 
106
+ ## Inference on connected accounts
107
+
108
+ Run short, tool-calling inference loops on the caller's own subscription — server-side, through the
109
+ official agent SDKs (never raw model-API calls on stored credentials), with the tool loop executed
110
+ on YOUR side:
111
+
112
+ ```ts
113
+ let response = await sealant.inference.respond({
114
+ prompt: "Compile a review brief from this run record.",
115
+ tools: [
116
+ {
117
+ name: "get_timeline",
118
+ inputSchema: { type: "object", properties: { runId: { type: "string" } } },
119
+ },
120
+ ],
121
+ responseFormat: { type: "json", schema: briefSchema },
122
+ credentials: { claude: true },
123
+ });
124
+
125
+ while (response.turn.type === "toolCalls") {
126
+ const toolResults = await Promise.all(
127
+ response.turn.calls.map(async (call) => ({
128
+ toolCallId: call.toolCallId,
129
+ content: await runTool(call.name, call.input),
130
+ })),
131
+ );
132
+ response = await sealant.inference.respond({ sessionId: response.sessionId, toolResults });
133
+ }
134
+
135
+ response.turn.json; // schema-constrained result
136
+ ```
137
+
138
+ Only account references cross the surface — the control plane resolves and decrypts server-side and
139
+ invokes the official Claude Agent SDK with the account's own subscription token. Claude accounts
140
+ only for now (Codex inference is a stated follow-up); sessions are held in memory by the control
141
+ plane and expire after a few idle minutes, so handle a 404 on continuation by restarting the
142
+ exchange.
143
+
51
144
  ## Status
52
145
 
53
146
  The core loop is real: `workspaces.create()`/`get()`/`list()`, `ready()`, blocking `harness.run()`
54
147
  and non-blocking `harness.start()` (run execution happens server-side; the SDK is a thin HTTP
55
- client), `runs.get()`, and the record read surface — `replay()`, `timeline()`, `scrollback()`,
56
- `commands()`, `transcript()`, `stream()` (poll-backed), `loss()`, `summary()`, plus captured
57
- `changes` (files + diff) settled by `run()`/`wait()`.
148
+ client), deterministic `workspace.exec()`, `inference.respond()` (connected-account inference with a
149
+ caller-executed tool loop), `runs.get()`, and the record read surface — `replay()`, `timeline()`
150
+ (typed, kind-discriminated entries), `scrollback()`, `commands()`, `transcript()`, `stream()`
151
+ (poll-backed), `loss()`, `summary()`, plus captured `changes` (files + diff) settled by
152
+ `run()`/`wait()`. The Effect-native core ships at `@sealant/sdk/effect`.
58
153
 
59
154
  Still typed stubs pending their read models / endpoints: `artifacts.get()` and the time-travel folds
60
155
  `fileTreeAt()`/`processTreeAt()` (Phase 1), and `harness.session()` + workspace lifecycle
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { CreateOptions, ListOptions, Run, Workspace, SealantConfig } from "./types.js";
1
+ import type { CreateOptions, InferenceNamespace, ListOptions, Run, Workspace, SealantConfig } from "./types.js";
2
2
  export declare class Sealant {
3
3
  #private;
4
4
  constructor(config: SealantConfig);
@@ -10,6 +10,11 @@ export declare class Sealant {
10
10
  get: (id: string) => Promise<Workspace>;
11
11
  list: (options?: ListOptions | undefined) => Promise<readonly Workspace[]>;
12
12
  };
13
+ /**
14
+ * Inference on connected accounts — server-side via the official agent SDKs, never raw model-API
15
+ * calls. Tool calls park server-side; execute them here and `respond()` with the results.
16
+ */
17
+ readonly inference: InferenceNamespace;
13
18
  /** Runs by id — so a record can be replayed long after its workspace is gone. */
14
19
  readonly runs: {
15
20
  get: (runId: string) => Promise<Run>;
package/dist/client.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * stable surface and reject with `SealantNotImplementedError` so callers can compile and wire
11
11
  * against the final shape today.
12
12
  */
13
- import { createWorkspaceOp, getRunOp, getWorkspaceOp, listWorkspacesOp, } from "./effect/operations.js";
13
+ import { createWorkspaceOp, getRunOp, getWorkspaceOp, inferenceRespondOp, listWorkspacesOp, } from "./effect/operations.js";
14
14
  import { runHarness, startHarness } from "./effect/run-harness.js";
15
15
  import { makeSdkRuntime } from "./effect/runtime.js";
16
16
  import { SealantError } from "./errors.js";
@@ -18,6 +18,7 @@ import { makeRun } from "./facade/run.js";
18
18
  import { makeWorkspace, registerHarnessExecutors } from "./facade/workspace.js";
19
19
  import { buildCreateWorkspaceRequest } from "./internal/blueprint.js";
20
20
  import { resolveInternalConfig } from "./internal/config.js";
21
+ import { buildInferenceRespondRequest, mapInferenceResponse } from "./internal/inference.js";
21
22
  // Wire the run-execution implementations into the Workspace facade (the injection point exists to
22
23
  // break the workspace <-> run-harness import cycle; the client is the composition root).
23
24
  registerHarnessExecutors({ run: runHarness, start: startHarness });
@@ -90,6 +91,17 @@ export class Sealant {
90
91
  }));
91
92
  },
92
93
  };
94
+ /**
95
+ * Inference on connected accounts — server-side via the official agent SDKs, never raw model-API
96
+ * calls. Tool calls park server-side; execute them here and `respond()` with the results.
97
+ */
98
+ inference = {
99
+ respond: async (options) => {
100
+ const payload = buildInferenceRespondRequest(options, this.#ctx.config.hostLocal.ownerUserId);
101
+ const wire = await this.#runtime.run(inferenceRespondOp(payload));
102
+ return mapInferenceResponse(wire);
103
+ },
104
+ };
93
105
  /** Runs by id — so a record can be replayed long after its workspace is gone. */
94
106
  runs = {
95
107
  get: async (runId) => {
@@ -183,6 +183,56 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
183
183
  readonly syncedAt: string;
184
184
  }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("@sealant/api-contracts").GitHubForbiddenError | import("@sealant/api-contracts").GitHubInternalServerError | import("@sealant/api-contracts").GitHubNotFoundError | import("@sealant/api-contracts").GitHubServiceUnavailableError | import("effect/Schema").SchemaError), [Mode] extends ["response-only"] ? never : never>;
185
185
  };
186
+ readonly inference: {
187
+ readonly respond: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
188
+ readonly payload: {
189
+ readonly ownerUserId: string;
190
+ readonly credentials?: {
191
+ readonly profileId?: string | undefined;
192
+ readonly claude?: string | undefined;
193
+ readonly codex?: string | undefined;
194
+ } | undefined;
195
+ readonly prompt?: string | undefined;
196
+ readonly system?: string | undefined;
197
+ readonly model?: string | undefined;
198
+ readonly maxTurns?: number | undefined;
199
+ readonly tools?: readonly {
200
+ readonly name: string;
201
+ readonly description?: string | undefined;
202
+ readonly inputSchema: unknown;
203
+ }[] | undefined;
204
+ readonly responseFormat?: {
205
+ readonly type: "json";
206
+ readonly schema?: unknown;
207
+ } | undefined;
208
+ readonly sessionId?: string | undefined;
209
+ readonly toolResults?: readonly {
210
+ readonly toolCallId: string;
211
+ readonly content: string;
212
+ readonly isError?: boolean | undefined;
213
+ }[] | undefined;
214
+ };
215
+ readonly responseMode?: Mode;
216
+ }) => Effect.Effect<HttpApiClient.Client.Response<{
217
+ readonly sessionId: string;
218
+ readonly turn: {
219
+ readonly type: "text";
220
+ readonly text: string;
221
+ readonly json?: unknown;
222
+ } | {
223
+ readonly type: "toolCalls";
224
+ readonly calls: readonly {
225
+ readonly toolCallId: string;
226
+ readonly name: string;
227
+ readonly input: unknown;
228
+ }[];
229
+ };
230
+ readonly usage?: {
231
+ readonly inputTokens: number;
232
+ readonly outputTokens: number;
233
+ } | undefined;
234
+ }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("@sealant/api-contracts").InferenceBadRequestError | import("@sealant/api-contracts").InferenceConflictError | import("@sealant/api-contracts").InferenceInternalServerError | import("@sealant/api-contracts").InferenceNotFoundError | import("@sealant/api-contracts").InferenceUnavailableError | import("effect/Schema").SchemaError), [Mode] extends ["response-only"] ? never : never>;
235
+ };
186
236
  readonly packages: {
187
237
  readonly resolvePackage: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
188
238
  readonly query: {
@@ -716,6 +766,35 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
716
766
  readonly repository: string;
717
767
  readonly tag: string;
718
768
  }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadGatewayError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceForbiddenError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError | import("@sealant/api-contracts").WorkspaceServiceUnavailableError), [Mode] extends ["response-only"] ? never : never>;
769
+ readonly execWorkspace: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
770
+ readonly params: {
771
+ readonly workspaceId: string;
772
+ };
773
+ readonly payload: {
774
+ readonly ownerUserId: string;
775
+ readonly commands: readonly {
776
+ readonly executable: string;
777
+ readonly args: readonly string[];
778
+ readonly cwd?: string | undefined;
779
+ }[];
780
+ };
781
+ readonly responseMode?: Mode;
782
+ }) => Effect.Effect<HttpApiClient.Client.Response<{
783
+ readonly runId: string;
784
+ readonly workspaceId: string;
785
+ readonly attemptId?: string | undefined;
786
+ readonly ownerUserId: string;
787
+ readonly harnessId: string;
788
+ readonly mode: "interactive" | "one-shot";
789
+ readonly status: "cancelled" | "completed" | "failed" | "queued" | "running";
790
+ readonly prompt?: string | undefined;
791
+ readonly exitCode?: number | undefined;
792
+ readonly errorMessage?: string | undefined;
793
+ readonly startedAt?: string | undefined;
794
+ readonly finishedAt?: string | undefined;
795
+ readonly createdAt: string;
796
+ readonly updatedAt: string;
797
+ }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError), [Mode] extends ["response-only"] ? never : never>;
719
798
  readonly getWorkspace: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
720
799
  readonly params: {
721
800
  readonly workspaceId: string;
@@ -1066,6 +1145,56 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
1066
1145
  readonly syncedAt: string;
1067
1146
  }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("@sealant/api-contracts").GitHubForbiddenError | import("@sealant/api-contracts").GitHubInternalServerError | import("@sealant/api-contracts").GitHubNotFoundError | import("@sealant/api-contracts").GitHubServiceUnavailableError | import("effect/Schema").SchemaError), [Mode] extends ["response-only"] ? never : never>;
1068
1147
  };
1148
+ readonly inference: {
1149
+ readonly respond: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
1150
+ readonly payload: {
1151
+ readonly ownerUserId: string;
1152
+ readonly credentials?: {
1153
+ readonly profileId?: string | undefined;
1154
+ readonly claude?: string | undefined;
1155
+ readonly codex?: string | undefined;
1156
+ } | undefined;
1157
+ readonly prompt?: string | undefined;
1158
+ readonly system?: string | undefined;
1159
+ readonly model?: string | undefined;
1160
+ readonly maxTurns?: number | undefined;
1161
+ readonly tools?: readonly {
1162
+ readonly name: string;
1163
+ readonly description?: string | undefined;
1164
+ readonly inputSchema: unknown;
1165
+ }[] | undefined;
1166
+ readonly responseFormat?: {
1167
+ readonly type: "json";
1168
+ readonly schema?: unknown;
1169
+ } | undefined;
1170
+ readonly sessionId?: string | undefined;
1171
+ readonly toolResults?: readonly {
1172
+ readonly toolCallId: string;
1173
+ readonly content: string;
1174
+ readonly isError?: boolean | undefined;
1175
+ }[] | undefined;
1176
+ };
1177
+ readonly responseMode?: Mode;
1178
+ }) => Effect.Effect<HttpApiClient.Client.Response<{
1179
+ readonly sessionId: string;
1180
+ readonly turn: {
1181
+ readonly type: "text";
1182
+ readonly text: string;
1183
+ readonly json?: unknown;
1184
+ } | {
1185
+ readonly type: "toolCalls";
1186
+ readonly calls: readonly {
1187
+ readonly toolCallId: string;
1188
+ readonly name: string;
1189
+ readonly input: unknown;
1190
+ }[];
1191
+ };
1192
+ readonly usage?: {
1193
+ readonly inputTokens: number;
1194
+ readonly outputTokens: number;
1195
+ } | undefined;
1196
+ }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("@sealant/api-contracts").InferenceBadRequestError | import("@sealant/api-contracts").InferenceConflictError | import("@sealant/api-contracts").InferenceInternalServerError | import("@sealant/api-contracts").InferenceNotFoundError | import("@sealant/api-contracts").InferenceUnavailableError | import("effect/Schema").SchemaError), [Mode] extends ["response-only"] ? never : never>;
1197
+ };
1069
1198
  readonly packages: {
1070
1199
  readonly resolvePackage: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
1071
1200
  readonly query: {
@@ -1599,6 +1728,35 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
1599
1728
  readonly repository: string;
1600
1729
  readonly tag: string;
1601
1730
  }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadGatewayError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceForbiddenError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError | import("@sealant/api-contracts").WorkspaceServiceUnavailableError), [Mode] extends ["response-only"] ? never : never>;
1731
+ readonly execWorkspace: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
1732
+ readonly params: {
1733
+ readonly workspaceId: string;
1734
+ };
1735
+ readonly payload: {
1736
+ readonly ownerUserId: string;
1737
+ readonly commands: readonly {
1738
+ readonly executable: string;
1739
+ readonly args: readonly string[];
1740
+ readonly cwd?: string | undefined;
1741
+ }[];
1742
+ };
1743
+ readonly responseMode?: Mode;
1744
+ }) => Effect.Effect<HttpApiClient.Client.Response<{
1745
+ readonly runId: string;
1746
+ readonly workspaceId: string;
1747
+ readonly attemptId?: string | undefined;
1748
+ readonly ownerUserId: string;
1749
+ readonly harnessId: string;
1750
+ readonly mode: "interactive" | "one-shot";
1751
+ readonly status: "cancelled" | "completed" | "failed" | "queued" | "running";
1752
+ readonly prompt?: string | undefined;
1753
+ readonly exitCode?: number | undefined;
1754
+ readonly errorMessage?: string | undefined;
1755
+ readonly startedAt?: string | undefined;
1756
+ readonly finishedAt?: string | undefined;
1757
+ readonly createdAt: string;
1758
+ readonly updatedAt: string;
1759
+ }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError), [Mode] extends ["response-only"] ? never : never>;
1602
1760
  readonly getWorkspace: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
1603
1761
  readonly params: {
1604
1762
  readonly workspaceId: string;
@@ -0,0 +1,5 @@
1
+ import type { SdkContext } from "../facade/context.js";
2
+ import type { WorkspaceInit } from "../facade/workspace.js";
3
+ import type { WorkspaceExecOptions, WorkspaceExecResult } from "../types.js";
4
+ /** The `workspace.exec()` implementation (Promise boundary over the Effect above). */
5
+ export declare const execWorkspace: (ctx: SdkContext, init: WorkspaceInit, argv: readonly string[], options?: WorkspaceExecOptions | undefined) => Promise<WorkspaceExecResult>;
@@ -0,0 +1,60 @@
1
+ import { Effect } from "effect";
2
+ import { SealantError } from "../errors.js";
3
+ import { makeRun, toRunChangesData } from "../facade/run.js";
4
+ import { execWorkspaceOp, getRunChangesOp, getRunOp, getRunScrollbackOp, getRunTimelineOp, } from "./operations.js";
5
+ const TERMINAL_STATUSES = new Set(["completed", "failed", "cancelled"]);
6
+ const POLL_INTERVAL = "500 millis";
7
+ const EXEC_TIMEOUT_MS = 30 * 60 * 1_000;
8
+ const isRecord = (value) => typeof value === "object" && value !== null;
9
+ /**
10
+ * The exec'd command's process: the `processStarted` entry whose recorded executable matches, or the
11
+ * first one when attribution is imprecise (the record contains only processes this run exec'd).
12
+ */
13
+ const findCommandProcessId = (entries, executable) => {
14
+ const match = entries.find((entry) => isRecord(entry.ref) && entry.ref["executable"] === executable);
15
+ return (match ?? entries[0])?.processId;
16
+ };
17
+ const readScrollback = (runId, processId, stream) => Effect.map(getRunScrollbackOp(runId, { processId, stream }), (response) => Buffer.from(response.contentBase64, "base64").toString("utf8"));
18
+ const execWorkspaceEffect = (ctx, init, argv, options) => Effect.gen(function* () {
19
+ const [executable, ...args] = argv;
20
+ if (executable === undefined || executable.length === 0) {
21
+ return yield* Effect.fail(new SealantError("exec requires argv with at least the executable.", {
22
+ code: "invalid_argv",
23
+ }));
24
+ }
25
+ const created = yield* execWorkspaceOp(init.id, {
26
+ ownerUserId: ctx.config.hostLocal.ownerUserId,
27
+ commands: [{ executable, args, ...(options?.cwd === undefined ? {} : { cwd: options.cwd }) }],
28
+ });
29
+ const runId = created.runId;
30
+ // Block until the check run is terminal, polling the control plane (same shape as harness.run()).
31
+ const deadline = Date.now() + EXEC_TIMEOUT_MS;
32
+ let wire = created;
33
+ while (!TERMINAL_STATUSES.has(wire.status)) {
34
+ if (Date.now() > deadline) {
35
+ return yield* Effect.fail(new SealantError(`Timed out waiting for exec run ${runId} to complete.`, {
36
+ code: "exec_timeout",
37
+ }));
38
+ }
39
+ yield* Effect.sleep(POLL_INTERVAL);
40
+ wire = yield* getRunOp(runId);
41
+ }
42
+ // Exec framing: "completed" means every command executed and was recorded — anything else means
43
+ // the machinery broke and the exit code cannot be trusted, which IS the error case.
44
+ if (wire.status !== "completed") {
45
+ return yield* Effect.fail(new SealantError(`Workspace exec did not complete: ${wire.errorMessage ?? `run ${runId} is ${wire.status}`}`, { code: "exec_failed" }));
46
+ }
47
+ const started = yield* getRunTimelineOp(runId, { kinds: "processStarted" });
48
+ const processId = findCommandProcessId(started, executable);
49
+ const stdout = processId === undefined ? "" : yield* readScrollback(runId, processId, "stdout");
50
+ const stderr = processId === undefined ? "" : yield* readScrollback(runId, processId, "stderr");
51
+ const changes = toRunChangesData(yield* getRunChangesOp(runId));
52
+ return {
53
+ exitCode: wire.exitCode ?? -1,
54
+ stdout,
55
+ stderr,
56
+ run: makeRun(ctx, { wire, changes }),
57
+ };
58
+ });
59
+ /** The `workspace.exec()` implementation (Promise boundary over the Effect above). */
60
+ export const execWorkspace = (ctx, init, argv, options) => ctx.runtime.run(execWorkspaceEffect(ctx, init, argv, options));
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @sealant/sdk/effect — the Effect-native core of the SDK, for consumers that are Effect end-to-end.
3
+ *
4
+ * The Promise facade (`@sealant/sdk`) is a thin wrapper over what this subpath exports directly:
5
+ *
6
+ * - `SealantApiClient` + `sealantApiClientLayer` — the contract-derived control-plane client as an
7
+ * Effect service. The client is generated from the `@sealant/api-contracts` `HttpApi`, so its
8
+ * request/response types and tagged error channel are the contract's, by construction.
9
+ * - The operation effects (`createWorkspaceOp`, `createRunOp`, `getRunTimelineOp`, …) — one per
10
+ * contract endpoint, returning WIRE types on a typed error channel.
11
+ * - `makeSdkRuntime` — the managed runtime the facade itself runs on, for consumers who want the
12
+ * memoized layer build + `SealantError` mapping without hand-rolling a scope.
13
+ * - The tagged contract errors (`WorkspaceNotFoundError`, `RunNotFoundError`, …) so failures can be
14
+ * matched with `Effect.catchTag` instead of string-matching a squashed `SealantError`.
15
+ *
16
+ * Errors: effects composed from this subpath fail with the TYPED contract errors (plus Effect HTTP
17
+ * client/schema errors) — nothing is squashed. The plain `SealantError` classes only enter play via
18
+ * `makeSdkRuntime`, which maps the typed channel at the Promise boundary exactly like the facade.
19
+ */
20
+ export { SealantApiClient, sealantApiClientLayer } from "./api-client.js";
21
+ export type { ControlPlaneClient } from "./api-client.js";
22
+ export * from "./operations.js";
23
+ export { makeSdkRuntime } from "./runtime.js";
24
+ export type { SdkRuntime, SdkServices } from "./runtime.js";
25
+ export { resolveInternalConfig } from "../internal/config.js";
26
+ export type { SealantHostLocalConfig, SealantInternalConfig } from "../internal/config.js";
27
+ export { InferenceBadRequestError, InferenceConflictError, InferenceInternalServerError, InferenceNotFoundError, InferenceUnavailableError, RunBadRequestError, RunInternalServerError, RunNotFoundError, WorkspaceBadGatewayError, WorkspaceBadRequestError, WorkspaceConflictError, WorkspaceForbiddenError, WorkspaceInternalServerError, WorkspaceNotFoundError, WorkspaceServiceUnavailableError, WorkspaceUnauthorizedError, } from "@sealant/api-contracts";
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @sealant/sdk/effect — the Effect-native core of the SDK, for consumers that are Effect end-to-end.
3
+ *
4
+ * The Promise facade (`@sealant/sdk`) is a thin wrapper over what this subpath exports directly:
5
+ *
6
+ * - `SealantApiClient` + `sealantApiClientLayer` — the contract-derived control-plane client as an
7
+ * Effect service. The client is generated from the `@sealant/api-contracts` `HttpApi`, so its
8
+ * request/response types and tagged error channel are the contract's, by construction.
9
+ * - The operation effects (`createWorkspaceOp`, `createRunOp`, `getRunTimelineOp`, …) — one per
10
+ * contract endpoint, returning WIRE types on a typed error channel.
11
+ * - `makeSdkRuntime` — the managed runtime the facade itself runs on, for consumers who want the
12
+ * memoized layer build + `SealantError` mapping without hand-rolling a scope.
13
+ * - The tagged contract errors (`WorkspaceNotFoundError`, `RunNotFoundError`, …) so failures can be
14
+ * matched with `Effect.catchTag` instead of string-matching a squashed `SealantError`.
15
+ *
16
+ * Errors: effects composed from this subpath fail with the TYPED contract errors (plus Effect HTTP
17
+ * client/schema errors) — nothing is squashed. The plain `SealantError` classes only enter play via
18
+ * `makeSdkRuntime`, which maps the typed channel at the Promise boundary exactly like the facade.
19
+ */
20
+ // The contract-derived client service + layer.
21
+ export { SealantApiClient, sealantApiClientLayer } from "./api-client.js";
22
+ // One operation effect per contract endpoint (wire types in, wire types out).
23
+ export * from "./operations.js";
24
+ // The managed runtime the Promise facade runs on.
25
+ export { makeSdkRuntime } from "./runtime.js";
26
+ // The config the layer/runtime constructors take, and the resolver from the public `SealantConfig`.
27
+ export { resolveInternalConfig } from "../internal/config.js";
28
+ // The typed contract errors carried on the client's failure channel (workspaces + runs +
29
+ // inference — the groups the operations above call). Re-exported so Effect consumers don't need to
30
+ // depend on the contracts package directly to `Effect.catchTag` a failure.
31
+ export { InferenceBadRequestError, InferenceConflictError, InferenceInternalServerError, InferenceNotFoundError, InferenceUnavailableError, RunBadRequestError, RunInternalServerError, RunNotFoundError, WorkspaceBadGatewayError, WorkspaceBadRequestError, WorkspaceConflictError, WorkspaceForbiddenError, WorkspaceInternalServerError, WorkspaceNotFoundError, WorkspaceServiceUnavailableError, WorkspaceUnauthorizedError, } from "@sealant/api-contracts";
@@ -62,7 +62,7 @@ export declare const getWorkspaceOp: (workspaceId: string) => Effect.Effect<{
62
62
  readonly startedAt?: string | undefined;
63
63
  readonly finishedAt?: string | undefined;
64
64
  readonly spec?: unknown;
65
- }, unknown, SealantApiClient>;
65
+ }, import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError, SealantApiClient>;
66
66
  export declare const listWorkspacesOp: (query: {
67
67
  readonly ownerUserId: string;
68
68
  readonly status?: "cancelled" | "failed" | "queued" | "ready" | "running" | undefined;
@@ -98,6 +98,29 @@ export declare const listWorkspacesOp: (query: {
98
98
  readonly finishedAt?: string | undefined;
99
99
  }[];
100
100
  }, import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceInternalServerError, SealantApiClient>;
101
+ export declare const execWorkspaceOp: (workspaceId: string, payload: {
102
+ readonly ownerUserId: string;
103
+ readonly commands: readonly {
104
+ readonly executable: string;
105
+ readonly args: readonly string[];
106
+ readonly cwd?: string | undefined;
107
+ }[];
108
+ }) => Effect.Effect<{
109
+ readonly runId: string;
110
+ readonly workspaceId: string;
111
+ readonly attemptId?: string | undefined;
112
+ readonly ownerUserId: string;
113
+ readonly harnessId: string;
114
+ readonly mode: "interactive" | "one-shot";
115
+ readonly status: "cancelled" | "completed" | "failed" | "queued" | "running";
116
+ readonly prompt?: string | undefined;
117
+ readonly exitCode?: number | undefined;
118
+ readonly errorMessage?: string | undefined;
119
+ readonly startedAt?: string | undefined;
120
+ readonly finishedAt?: string | undefined;
121
+ readonly createdAt: string;
122
+ readonly updatedAt: string;
123
+ }, import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError, SealantApiClient>;
101
124
  export declare const createRunOp: (payload: {
102
125
  readonly workspaceId: string;
103
126
  readonly harnessId: string;
@@ -125,7 +148,7 @@ export declare const createRunOp: (payload: {
125
148
  readonly finishedAt?: string | undefined;
126
149
  readonly createdAt: string;
127
150
  readonly updatedAt: string;
128
- }, unknown, SealantApiClient>;
151
+ }, 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>;
129
152
  export declare const getRunOp: (runId: string) => Effect.Effect<{
130
153
  readonly runId: string;
131
154
  readonly workspaceId: string;
@@ -141,7 +164,7 @@ export declare const getRunOp: (runId: string) => Effect.Effect<{
141
164
  readonly finishedAt?: string | undefined;
142
165
  readonly createdAt: string;
143
166
  readonly updatedAt: string;
144
- }, unknown, SealantApiClient>;
167
+ }, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RunInternalServerError | import("@sealant/api-contracts").RunNotFoundError | import("effect/Schema").SchemaError, SealantApiClient>;
145
168
  export declare const listRunsOp: (query: {
146
169
  readonly workspaceId?: string | undefined;
147
170
  readonly ownerUserId?: string | undefined;
@@ -190,7 +213,7 @@ export declare const updateRunOp: (runId: string, payload: {
190
213
  readonly finishedAt?: string | undefined;
191
214
  readonly createdAt: string;
192
215
  readonly updatedAt: string;
193
- }, unknown, SealantApiClient>;
216
+ }, 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>;
194
217
  export declare const getRunTimelineOp: (runId: string, query: {
195
218
  readonly fromSequence?: string | undefined;
196
219
  readonly toSequence?: string | undefined;
@@ -206,7 +229,7 @@ export declare const getRunTimelineOp: (runId: string, query: {
206
229
  readonly processId?: string | undefined;
207
230
  readonly captureMethod: number;
208
231
  readonly confidence: number;
209
- }[], unknown, SealantApiClient>;
232
+ }[], 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>;
210
233
  export declare const getRunScrollbackOp: (runId: string, query: {
211
234
  readonly processId: string;
212
235
  readonly stream: "stderr" | "stdout";
@@ -216,7 +239,7 @@ export declare const getRunScrollbackOp: (runId: string, query: {
216
239
  readonly stream: "stderr" | "stdout";
217
240
  readonly byteCount: number;
218
241
  readonly contentBase64: string;
219
- }, unknown, SealantApiClient>;
242
+ }, 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>;
220
243
  export declare const getRunLossOp: (runId: string) => Effect.Effect<{
221
244
  readonly runId: string;
222
245
  readonly droppedEventCount: string;
@@ -231,7 +254,7 @@ export declare const getRunLossOp: (runId: string) => Effect.Effect<{
231
254
  readonly detectedVia: "gap" | "marker";
232
255
  readonly reason?: string | undefined;
233
256
  }[];
234
- }, unknown, SealantApiClient>;
257
+ }, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RunInternalServerError | import("@sealant/api-contracts").RunNotFoundError | import("effect/Schema").SchemaError, SealantApiClient>;
235
258
  export declare const getRunChangesOp: (runId: string) => Effect.Effect<{
236
259
  readonly files: readonly {
237
260
  readonly path: string;
@@ -239,4 +262,49 @@ export declare const getRunChangesOp: (runId: string) => Effect.Effect<{
239
262
  readonly oldPath?: string | undefined;
240
263
  }[];
241
264
  readonly diff: string;
242
- }, unknown, SealantApiClient>;
265
+ }, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RunInternalServerError | import("@sealant/api-contracts").RunNotFoundError | import("effect/Schema").SchemaError, SealantApiClient>;
266
+ export declare const inferenceRespondOp: (payload: {
267
+ readonly ownerUserId: string;
268
+ readonly credentials?: {
269
+ readonly profileId?: string | undefined;
270
+ readonly claude?: string | undefined;
271
+ readonly codex?: string | undefined;
272
+ } | undefined;
273
+ readonly prompt?: string | undefined;
274
+ readonly system?: string | undefined;
275
+ readonly model?: string | undefined;
276
+ readonly maxTurns?: number | undefined;
277
+ readonly tools?: readonly {
278
+ readonly name: string;
279
+ readonly description?: string | undefined;
280
+ readonly inputSchema: unknown;
281
+ }[] | undefined;
282
+ readonly responseFormat?: {
283
+ readonly type: "json";
284
+ readonly schema?: unknown;
285
+ } | undefined;
286
+ readonly sessionId?: string | undefined;
287
+ readonly toolResults?: readonly {
288
+ readonly toolCallId: string;
289
+ readonly content: string;
290
+ readonly isError?: boolean | undefined;
291
+ }[] | undefined;
292
+ }) => Effect.Effect<{
293
+ readonly sessionId: string;
294
+ readonly turn: {
295
+ readonly type: "text";
296
+ readonly text: string;
297
+ readonly json?: unknown;
298
+ } | {
299
+ readonly type: "toolCalls";
300
+ readonly calls: readonly {
301
+ readonly toolCallId: string;
302
+ readonly name: string;
303
+ readonly input: unknown;
304
+ }[];
305
+ };
306
+ readonly usage?: {
307
+ readonly inputTokens: number;
308
+ readonly outputTokens: number;
309
+ } | undefined;
310
+ }, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").InferenceBadRequestError | import("@sealant/api-contracts").InferenceConflictError | import("@sealant/api-contracts").InferenceInternalServerError | import("@sealant/api-contracts").InferenceNotFoundError | import("@sealant/api-contracts").InferenceUnavailableError | import("effect/Schema").SchemaError, SealantApiClient>;
@@ -7,6 +7,7 @@ export const createWorkspaceOp = (payload, idempotencyKey) => Effect.flatMap(Sea
7
7
  }));
8
8
  export const getWorkspaceOp = (workspaceId) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.getWorkspace({ params: { workspaceId } }));
9
9
  export const listWorkspacesOp = (query) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.listWorkspaces({ query }));
10
+ export const execWorkspaceOp = (workspaceId, payload) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.execWorkspace({ params: { workspaceId }, payload }));
10
11
  // ---- runs ----
11
12
  export const createRunOp = (payload) => Effect.flatMap(SealantApiClient, (client) => client.runs.createRun({ payload }));
12
13
  export const getRunOp = (runId) => Effect.flatMap(SealantApiClient, (client) => client.runs.getRun({ params: { runId } }));
@@ -16,3 +17,5 @@ export const getRunTimelineOp = (runId, query) => Effect.flatMap(SealantApiClien
16
17
  export const getRunScrollbackOp = (runId, query) => Effect.flatMap(SealantApiClient, (client) => client.runs.getRunScrollback({ params: { runId }, query }));
17
18
  export const getRunLossOp = (runId) => Effect.flatMap(SealantApiClient, (client) => client.runs.getRunLoss({ params: { runId } }));
18
19
  export const getRunChangesOp = (runId) => Effect.flatMap(SealantApiClient, (client) => client.runs.getRunChanges({ params: { runId } }));
20
+ // ---- inference ----
21
+ export const inferenceRespondOp = (payload) => Effect.flatMap(SealantApiClient, (client) => client.inference.respond({ payload }));
@@ -1,5 +1,21 @@
1
- import type { RunCommand, RunRecord } from "../types.js";
1
+ import type { RunCommand, RunRecord, TimelineEntry } from "../types.js";
2
2
  import type { SdkContext } from "./context.js";
3
+ /**
4
+ * Maps a wire entry to the public DISCRIMINATED entry: the contract's payload schemas fold
5
+ * `(kind, ref)` into typed `data`, degrading to the `unknown` case (raw kind + payload preserved)
6
+ * for kinds newer than this SDK or payloads that fail their schema. Exported for tests.
7
+ */
8
+ export declare const toTimelineEntry: (wire: {
9
+ readonly eventId: string;
10
+ readonly sequence: string;
11
+ readonly kind: string;
12
+ readonly occurredAt: string;
13
+ readonly summary: string;
14
+ readonly ref?: unknown;
15
+ readonly processId?: string | undefined;
16
+ readonly captureMethod: number;
17
+ readonly confidence: number;
18
+ }) => TimelineEntry;
3
19
  /**
4
20
  * Folds the timeline into the ordered list of terminal commands. Process boundaries are
5
21
  * `processStarted`/`processExited`; `ioChunk` byte counts accrue to the current command. Daemon noise
@@ -1,3 +1,11 @@
1
+ /**
2
+ * The `RunRecord` facade — the execution record as the SDK exposes it. Backed by the run/record
3
+ * contract endpoints (`client.runs.*`), it maps the wire shapes to the public types: decimal-string
4
+ * sequences become `bigint`, base64 scrollback becomes `Uint8Array`. `replay()`/`timeline()`/
5
+ * `scrollback()`/`loss()`/`summary()`/`stream()` are live; the time-travel folds are typed but
6
+ * reject until their read models land (Phase 1).
7
+ */
8
+ import { decodeRecordEventPayload, } from "@sealant/api-contracts";
1
9
  import { getRunLossOp, getRunOp, getRunScrollbackOp, getRunTimelineOp, } from "../effect/operations.js";
2
10
  import { SealantNotImplementedError } from "../errors.js";
3
11
  // Live `stream()` is poll-backed for now: it tails the timeline endpoint until the run is terminal.
@@ -6,11 +14,17 @@ const TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "cancelled"]);
6
14
  const STREAM_POLL_INTERVAL_MS = 500;
7
15
  const STREAM_TIMEOUT_MS = 30 * 60 * 1_000;
8
16
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
9
- const toTimelineEntry = (wire) => ({
17
+ /**
18
+ * Maps a wire entry to the public DISCRIMINATED entry: the contract's payload schemas fold
19
+ * `(kind, ref)` into typed `data`, degrading to the `unknown` case (raw kind + payload preserved)
20
+ * for kinds newer than this SDK or payloads that fail their schema. Exported for tests.
21
+ */
22
+ export const toTimelineEntry = (wire) => ({
10
23
  sequence: BigInt(wire.sequence),
11
- kind: wire.kind,
12
24
  occurredAt: wire.occurredAt,
13
- data: { summary: wire.summary, ...(wire.ref === undefined ? {} : { ref: wire.ref }) },
25
+ summary: wire.summary,
26
+ ...(wire.processId === undefined ? {} : { processId: wire.processId }),
27
+ ...decodeRecordEventPayload(wire.kind, wire.ref),
14
28
  });
15
29
  const toLossReport = (wire) => ({
16
30
  complete: !wire.earlyClose && wire.droppedEventCount === "0" && wire.sequenceGapCount === 0,
@@ -1,3 +1,4 @@
1
+ import { execWorkspace } from "../effect/exec-workspace.js";
1
2
  import { getWorkspaceOp } from "../effect/operations.js";
2
3
  import { SealantError, SealantNotImplementedError } from "../errors.js";
3
4
  const FAILED_STATUSES = new Set(["failed", "cancelled"]);
@@ -53,6 +54,7 @@ export const makeWorkspace = (ctx, init) => {
53
54
  }
54
55
  },
55
56
  harness,
57
+ exec: (argv, options) => execWorkspace(ctx, init, argv, options),
56
58
  // Poll-backed lifecycle stream: emit a coarse event on each status transition until the workspace
57
59
  // reaches a terminal/ready state. Swaps to SSE over Postgres LISTEN/NOTIFY in Stage 5 (same shape).
58
60
  events: () => {
@@ -0,0 +1,47 @@
1
+ import type { InferenceContinueOptions, InferenceRespondOptions, InferenceResponse } from "../types.js";
2
+ export declare const buildInferenceRespondRequest: (options: InferenceContinueOptions | InferenceRespondOptions, ownerUserId: string) => {
3
+ readonly ownerUserId: string;
4
+ readonly credentials?: {
5
+ readonly profileId?: string | undefined;
6
+ readonly claude?: string | undefined;
7
+ readonly codex?: string | undefined;
8
+ } | undefined;
9
+ readonly prompt?: string | undefined;
10
+ readonly system?: string | undefined;
11
+ readonly model?: string | undefined;
12
+ readonly maxTurns?: number | undefined;
13
+ readonly tools?: readonly {
14
+ readonly name: string;
15
+ readonly description?: string | undefined;
16
+ readonly inputSchema: unknown;
17
+ }[] | undefined;
18
+ readonly responseFormat?: {
19
+ readonly type: "json";
20
+ readonly schema?: unknown;
21
+ } | undefined;
22
+ readonly sessionId?: string | undefined;
23
+ readonly toolResults?: readonly {
24
+ readonly toolCallId: string;
25
+ readonly content: string;
26
+ readonly isError?: boolean | undefined;
27
+ }[] | undefined;
28
+ };
29
+ export declare const mapInferenceResponse: (wire: {
30
+ readonly sessionId: string;
31
+ readonly turn: {
32
+ readonly type: "text";
33
+ readonly text: string;
34
+ readonly json?: unknown;
35
+ } | {
36
+ readonly type: "toolCalls";
37
+ readonly calls: readonly {
38
+ readonly toolCallId: string;
39
+ readonly name: string;
40
+ readonly input: unknown;
41
+ }[];
42
+ };
43
+ readonly usage?: {
44
+ readonly inputTokens: number;
45
+ readonly outputTokens: number;
46
+ } | undefined;
47
+ }) => InferenceResponse;
@@ -0,0 +1,78 @@
1
+ const DEFAULT_ACCOUNT_NAME = "default";
2
+ const mapAccountRef = (value) => {
3
+ if (value === undefined || value === false) {
4
+ return undefined;
5
+ }
6
+ return value === true ? DEFAULT_ACCOUNT_NAME : value;
7
+ };
8
+ export const buildInferenceRespondRequest = (options, ownerUserId) => {
9
+ if ("sessionId" in options) {
10
+ return {
11
+ ownerUserId,
12
+ sessionId: options.sessionId,
13
+ toolResults: options.toolResults.map((result) => ({
14
+ toolCallId: result.toolCallId,
15
+ content: result.content,
16
+ ...(result.isError === undefined ? {} : { isError: result.isError }),
17
+ })),
18
+ };
19
+ }
20
+ const claude = mapAccountRef(options.credentials.claude);
21
+ const codex = mapAccountRef(options.credentials.codex);
22
+ return {
23
+ ownerUserId,
24
+ credentials: {
25
+ ...(options.credentials.profile === undefined
26
+ ? {}
27
+ : { profileId: options.credentials.profile }),
28
+ ...(claude === undefined ? {} : { claude }),
29
+ ...(codex === undefined ? {} : { codex }),
30
+ },
31
+ prompt: options.prompt,
32
+ ...(options.system === undefined ? {} : { system: options.system }),
33
+ ...(options.model === undefined ? {} : { model: options.model }),
34
+ ...(options.maxTurns === undefined ? {} : { maxTurns: options.maxTurns }),
35
+ ...(options.tools === undefined
36
+ ? {}
37
+ : {
38
+ tools: options.tools.map((tool) => ({
39
+ name: tool.name,
40
+ ...(tool.description === undefined ? {} : { description: tool.description }),
41
+ inputSchema: tool.inputSchema,
42
+ })),
43
+ }),
44
+ ...(options.responseFormat === undefined
45
+ ? {}
46
+ : {
47
+ responseFormat: {
48
+ type: options.responseFormat.type,
49
+ ...(options.responseFormat.schema === undefined
50
+ ? {}
51
+ : { schema: options.responseFormat.schema }),
52
+ },
53
+ }),
54
+ };
55
+ };
56
+ export const mapInferenceResponse = (wire) => {
57
+ const turn = wire.turn.type === "text"
58
+ ? {
59
+ type: "text",
60
+ text: wire.turn.text,
61
+ ...(wire.turn.json === undefined ? {} : { json: wire.turn.json }),
62
+ }
63
+ : {
64
+ type: "toolCalls",
65
+ calls: wire.turn.calls.map((call) => ({
66
+ toolCallId: call.toolCallId,
67
+ name: call.name,
68
+ input: call.input,
69
+ })),
70
+ };
71
+ return {
72
+ sessionId: wire.sessionId,
73
+ turn,
74
+ ...(wire.usage === undefined
75
+ ? {}
76
+ : { usage: { inputTokens: wire.usage.inputTokens, outputTokens: wire.usage.outputTokens } }),
77
+ };
78
+ };
package/dist/types.d.ts CHANGED
@@ -114,6 +114,27 @@ export interface ListOptions {
114
114
  readonly status?: WorkspaceStatus;
115
115
  readonly limit?: number;
116
116
  }
117
+ /** Options for a deterministic `workspace.exec()`. */
118
+ export interface WorkspaceExecOptions {
119
+ /** Working directory inside the workspace (defaults to the repository root). */
120
+ readonly cwd?: string;
121
+ }
122
+ /**
123
+ * The settled result of a deterministic `workspace.exec()`. The exit code is a check DATUM — a
124
+ * nonzero exit resolves normally (that's the point: `base fails` is a recorded fact, not an error).
125
+ * `exec()` rejects only when the execution machinery itself broke, i.e. when the exit code cannot
126
+ * be trusted.
127
+ */
128
+ export interface WorkspaceExecResult {
129
+ /** Exit code of the executed command. */
130
+ readonly exitCode: number;
131
+ /** Everything the command wrote to stdout, decoded as UTF-8. */
132
+ readonly stdout: string;
133
+ /** Everything the command wrote to stderr, decoded as UTF-8. */
134
+ readonly stderr: string;
135
+ /** The run this exec was recorded as — its `record` is the durable, replayable evidence. */
136
+ readonly run: Run;
137
+ }
117
138
  /** A live, disposable development environment around a real repository. */
118
139
  export interface Workspace {
119
140
  readonly id: string;
@@ -124,6 +145,11 @@ export interface Workspace {
124
145
  ready(): Promise<this>;
125
146
  /** Run a harness in this workspace. */
126
147
  readonly harness: HarnessRunner;
148
+ /**
149
+ * Execute one command deterministically in the workspace — no agent in the loop — recorded into a
150
+ * run record like any other process. `argv[0]` is the executable, the rest its arguments.
151
+ */
152
+ exec(argv: readonly string[], options?: WorkspaceExecOptions): Promise<WorkspaceExecResult>;
127
153
  /** Lifecycle events as an async stream. */
128
154
  events(): AsyncIterable<WorkspaceEvent>;
129
155
  /** Stop the workspace now (Phase 3). */
@@ -185,13 +211,171 @@ export interface RunArtifacts {
185
211
  list(): Promise<readonly ArtifactRef[]>;
186
212
  get(name: string): Promise<Uint8Array>;
187
213
  }
188
- /** A single ordered entry in the execution record's timeline. */
189
- export interface TimelineEntry {
214
+ /** The runtime daemon's lifecycle state changed. `state` is a numeric `RuntimeState`. */
215
+ export interface RuntimeStateChangedEvent {
216
+ readonly state: number;
217
+ readonly reason?: string | undefined;
218
+ }
219
+ /** Periodic runtime liveness signal. `state` is a numeric `RuntimeState`. */
220
+ export interface RuntimeHeartbeatEvent {
221
+ readonly state: number;
222
+ }
223
+ /** A supervised process began executing. */
224
+ export interface ProcessStartedEvent {
225
+ readonly pid: number;
226
+ readonly pgid: number;
227
+ readonly pidfd: boolean;
228
+ readonly executable: string;
229
+ readonly args: readonly string[];
230
+ readonly cwd: string;
231
+ /** Wall clock at start, microseconds (decimal string). */
232
+ readonly startedAt: string;
233
+ }
234
+ /** A supervised process ended. `reason` is a numeric `ExitReason`. */
235
+ export interface ProcessExitedEvent {
236
+ readonly exitCode?: number | undefined;
237
+ readonly signal?: number | undefined;
238
+ readonly reason: number;
239
+ /** Wall-clock duration, microseconds (decimal string). */
240
+ readonly durationMicros: string;
241
+ }
242
+ /**
243
+ * A run of process output. Raw bytes live in the artifact store (fetch byte-exact text via
244
+ * `record.scrollback()`); the event carries counts and a content hash. `stream` is a numeric
245
+ * `StreamKind` (stdout = 2, stderr = 3).
246
+ */
247
+ export interface IoChunkEvent {
248
+ readonly stream: number;
249
+ readonly byteCount: string;
250
+ readonly streamOffset: string;
251
+ readonly contentAlgo?: string | undefined;
252
+ readonly contentHash?: string | undefined;
253
+ readonly transform?: {
254
+ readonly redacted: boolean;
255
+ readonly truncated: boolean;
256
+ readonly coalesced: boolean;
257
+ readonly originalByteCount?: string | undefined;
258
+ } | undefined;
259
+ }
260
+ /** The runtime dropped events under pressure. `priority` is a numeric `EventPriority`. */
261
+ export interface TelemetryDroppedEvent {
262
+ readonly reason: string;
263
+ readonly count: string;
264
+ readonly priority: number;
265
+ }
266
+ /** Filesystem entry metadata attached to a change. `fileType` is a numeric `FileType`. */
267
+ export interface FileEntryData {
268
+ readonly path: string;
269
+ readonly fileType: number;
270
+ readonly size: string;
271
+ readonly mtimeMicros: string;
272
+ readonly mode: number;
273
+ readonly hash?: string | undefined;
274
+ readonly symlinkTarget?: string | undefined;
275
+ }
276
+ /** A watched file changed. `kind` is a numeric `FileChangeKind`. */
277
+ export interface FileChangeEvent {
278
+ readonly kind: number;
279
+ readonly path: string;
280
+ readonly renameFrom?: string | undefined;
281
+ readonly entry?: FileEntryData | undefined;
282
+ readonly certain: boolean;
283
+ }
284
+ /** The file watcher overflowed — changes under `root` may have been missed. */
285
+ export interface FileWatchOverflowEvent {
286
+ readonly root: string;
287
+ }
288
+ /** A filesystem snapshot pass finished. */
289
+ export interface FileSnapshotCompletedEvent {
290
+ readonly root: string;
291
+ readonly fileCount: string;
292
+ }
293
+ /** Aggregate before/after diff counts became available. */
294
+ export interface FileDiffAvailableEvent {
295
+ readonly added: string;
296
+ readonly modified: string;
297
+ readonly deleted: string;
298
+ readonly renamed: string;
299
+ }
300
+ /** An outbound network request the run made. `scheme` is a numeric `NetworkScheme`. */
301
+ export interface NetworkRequestEvent {
302
+ readonly scheme: number;
303
+ readonly method?: string | undefined;
304
+ readonly host: string;
305
+ readonly port: number;
306
+ readonly path?: string | undefined;
307
+ readonly status?: number | undefined;
308
+ readonly bytesSent: string;
309
+ readonly bytesReceived: string;
310
+ readonly durationMicros: string;
311
+ }
312
+ /** A network source the run touched — the raw material of a "sources the agent opened" trail. */
313
+ export interface NetworkSourceObservedEvent {
314
+ readonly host: string;
315
+ readonly resolvedIps: readonly string[];
316
+ readonly port: number;
317
+ readonly scheme?: number | undefined;
318
+ readonly method?: string | undefined;
319
+ readonly path?: string | undefined;
320
+ readonly status?: number | undefined;
321
+ }
322
+ /** Fields shared by every timeline entry, independent of its kind. */
323
+ export interface TimelineEntryBase {
190
324
  readonly sequence: bigint;
191
- readonly kind: string;
192
325
  readonly occurredAt: string;
193
- readonly data: unknown;
326
+ /** One-line human summary of the event. */
327
+ readonly summary: string;
328
+ /** Correlation id of the producing process, when attributable. */
329
+ readonly processId?: string | undefined;
194
330
  }
331
+ /**
332
+ * A single ordered entry in the execution record's timeline, DISCRIMINATED by `kind`: switch on it
333
+ * and `data` narrows to the event's typed payload. The `"unknown"` case is the forward-compatibility
334
+ * path — it carries kinds newer than this SDK (or payloads that failed their schema) with the wire
335
+ * kind preserved in `rawKind` and the payload verbatim in `data`.
336
+ */
337
+ export type TimelineEntry = (TimelineEntryBase & {
338
+ readonly kind: "runtimeStateChanged";
339
+ readonly data: RuntimeStateChangedEvent;
340
+ }) | (TimelineEntryBase & {
341
+ readonly kind: "runtimeHeartbeat";
342
+ readonly data: RuntimeHeartbeatEvent;
343
+ }) | (TimelineEntryBase & {
344
+ readonly kind: "processStarted";
345
+ readonly data: ProcessStartedEvent;
346
+ }) | (TimelineEntryBase & {
347
+ readonly kind: "processExited";
348
+ readonly data: ProcessExitedEvent;
349
+ }) | (TimelineEntryBase & {
350
+ readonly kind: "ioChunk";
351
+ readonly data: IoChunkEvent;
352
+ }) | (TimelineEntryBase & {
353
+ readonly kind: "telemetryDropped";
354
+ readonly data: TelemetryDroppedEvent;
355
+ }) | (TimelineEntryBase & {
356
+ readonly kind: "fileChange";
357
+ readonly data: FileChangeEvent;
358
+ }) | (TimelineEntryBase & {
359
+ readonly kind: "fileWatchOverflow";
360
+ readonly data: FileWatchOverflowEvent;
361
+ }) | (TimelineEntryBase & {
362
+ readonly kind: "fileSnapshotCompleted";
363
+ readonly data: FileSnapshotCompletedEvent;
364
+ }) | (TimelineEntryBase & {
365
+ readonly kind: "fileDiffAvailable";
366
+ readonly data: FileDiffAvailableEvent;
367
+ }) | (TimelineEntryBase & {
368
+ readonly kind: "networkRequest";
369
+ readonly data: NetworkRequestEvent;
370
+ }) | (TimelineEntryBase & {
371
+ readonly kind: "networkSourceObserved";
372
+ readonly data: NetworkSourceObservedEvent;
373
+ }) | (TimelineEntryBase & {
374
+ readonly kind: "unknown";
375
+ /** The kind as received on the wire — set when this SDK version doesn't model it. */
376
+ readonly rawKind: string;
377
+ readonly data: unknown;
378
+ });
195
379
  /** A re-fold of the record up to some point — scrubable by sequence. */
196
380
  export interface RunReplay {
197
381
  readonly entries: readonly TimelineEntry[];
@@ -289,3 +473,90 @@ export interface InteractiveSession {
289
473
  output(): AsyncIterable<Uint8Array>;
290
474
  close(): Promise<void>;
291
475
  }
476
+ /**
477
+ * Connected-account selection for inference — the same reference shape as workspace creation,
478
+ * minus GitHub (not a model provider). `true` means "my default account"; a string names one.
479
+ * Only claude accounts are supported today; a codex selection is rejected until Codex inference
480
+ * ships. SECURITY: only account references cross this surface — never token material.
481
+ */
482
+ export interface InferenceCredentialsOptions {
483
+ /** Profile id whose claude binding applies when `claude` is not set explicitly. */
484
+ readonly profile?: string;
485
+ /** `true` for the caller's default claude account, or a string naming a specific one. */
486
+ readonly claude?: boolean | string;
487
+ /** Reserved — rejected until Codex inference ships. */
488
+ readonly codex?: boolean | string;
489
+ }
490
+ /** A caller-defined tool the model may call. `inputSchema` is a JSON Schema object, verbatim. */
491
+ export interface InferenceToolDefinition {
492
+ readonly name: string;
493
+ readonly description?: string;
494
+ readonly inputSchema: unknown;
495
+ }
496
+ /** A tool call the model made. Execute it YOUR side, then respond with an `InferenceToolResult`. */
497
+ export interface InferenceToolCall {
498
+ readonly toolCallId: string;
499
+ readonly name: string;
500
+ readonly input: unknown;
501
+ }
502
+ /** Your result for one tool call, keyed by its `toolCallId`. */
503
+ export interface InferenceToolResult {
504
+ readonly toolCallId: string;
505
+ readonly content: string;
506
+ readonly isError?: boolean;
507
+ }
508
+ /** The assistant turn: the final text (with parsed `json` when requested) or pending tool calls. */
509
+ export type InferenceTurn = {
510
+ readonly type: "text";
511
+ readonly text: string;
512
+ readonly json?: unknown;
513
+ } | {
514
+ readonly type: "toolCalls";
515
+ readonly calls: readonly InferenceToolCall[];
516
+ };
517
+ export interface InferenceUsage {
518
+ readonly inputTokens: number;
519
+ readonly outputTokens: number;
520
+ }
521
+ export interface InferenceResponse {
522
+ /** Continuation handle for the tool loop (held in memory server-side; expires after idle). */
523
+ readonly sessionId: string;
524
+ readonly turn: InferenceTurn;
525
+ /** Usage for the exchange, present on the final text turn. */
526
+ readonly usage?: InferenceUsage;
527
+ }
528
+ /** Starts a new inference exchange on a connected account. */
529
+ export interface InferenceRespondOptions {
530
+ readonly prompt: string;
531
+ readonly system?: string;
532
+ readonly model?: string;
533
+ /** Upper bound on agentic turns within the exchange (server default 16). */
534
+ readonly maxTurns?: number;
535
+ readonly tools?: readonly InferenceToolDefinition[];
536
+ /** Structured output: reply as JSON (schema-constrained when `schema` is given). */
537
+ readonly responseFormat?: {
538
+ readonly type: "json";
539
+ readonly schema?: unknown;
540
+ };
541
+ readonly credentials: InferenceCredentialsOptions;
542
+ }
543
+ /** Continues an exchange by posting the results of the previous turn's tool calls. */
544
+ export interface InferenceContinueOptions {
545
+ readonly sessionId: string;
546
+ readonly toolResults: readonly InferenceToolResult[];
547
+ }
548
+ /**
549
+ * Inference on connected accounts. The model call runs SERVER-SIDE through the official agent SDKs
550
+ * on the resolved account's credential (never raw model-API calls); the tool loop is CALLER-
551
+ * EXECUTED — a `toolCalls` turn parks server-side until you `respond()` with the results:
552
+ *
553
+ * let response = await sealant.inference.respond({ prompt, tools, credentials: { claude: true } })
554
+ * while (response.turn.type === "toolCalls") {
555
+ * const toolResults = await runTools(response.turn.calls)
556
+ * response = await sealant.inference.respond({ sessionId: response.sessionId, toolResults })
557
+ * }
558
+ * response.turn.text
559
+ */
560
+ export interface InferenceNamespace {
561
+ respond(options: InferenceRespondOptions | InferenceContinueOptions): Promise<InferenceResponse>;
562
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealant/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "The fluent public SDK for Sealant — create a workspace, run a harness, replay the record.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -16,6 +16,10 @@
16
16
  ".": {
17
17
  "types": "./dist/index.d.ts",
18
18
  "import": "./dist/index.js"
19
+ },
20
+ "./effect": {
21
+ "types": "./dist/effect/index.d.ts",
22
+ "import": "./dist/effect/index.js"
19
23
  }
20
24
  },
21
25
  "publishConfig": {
@@ -23,7 +27,7 @@
23
27
  },
24
28
  "dependencies": {
25
29
  "effect": "^4.0.0-beta.85",
26
- "@sealant/api-contracts": "^0.4.0"
30
+ "@sealant/api-contracts": "^0.5.0"
27
31
  },
28
32
  "devDependencies": {
29
33
  "@effect/vitest": "^4.0.0-beta.85",