@sealant/sdk 0.3.1 → 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 +107 -12
- package/dist/client.d.ts +12 -7
- package/dist/client.js +33 -21
- package/dist/effect/api-client.d.ts +408 -250
- package/dist/effect/exec-workspace.d.ts +5 -0
- package/dist/effect/exec-workspace.js +60 -0
- package/dist/effect/index.d.ts +27 -0
- package/dist/effect/index.js +31 -0
- package/dist/effect/operations.d.ts +90 -22
- package/dist/effect/operations.js +7 -4
- package/dist/effect/run-harness.d.ts +2 -2
- package/dist/effect/run-harness.js +4 -4
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/facade/record.d.ts +17 -1
- package/dist/facade/record.js +17 -3
- package/dist/facade/run.d.ts +1 -1
- package/dist/facade/{sandbox.d.ts → workspace.d.ts} +6 -6
- package/dist/facade/{sandbox.js → workspace.js} +19 -17
- package/dist/harness.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/internal/blueprint.d.ts +1 -1
- package/dist/internal/blueprint.js +8 -8
- package/dist/internal/config.d.ts +2 -2
- package/dist/internal/credentials.d.ts +7 -7
- package/dist/internal/credentials.js +2 -2
- package/dist/internal/inference.d.ts +47 -0
- package/dist/internal/inference.js +78 -0
- package/dist/internal/map-error.d.ts +1 -1
- package/dist/internal/map-error.js +1 -1
- package/dist/types.d.ts +310 -39
- package/dist/types.js +2 -2
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# @sealant/sdk
|
|
2
2
|
|
|
3
|
-
The fluent public SDK for Sealant — **create a
|
|
3
|
+
The fluent public SDK for Sealant — **create a workspace, run a harness, replay the record.**
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
import { Sealant, opencode } from "@sealant/sdk";
|
|
7
7
|
|
|
8
8
|
const sealant = new Sealant({ baseUrl: "http://localhost:8080" });
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const workspace = await sealant.workspaces.create({
|
|
11
11
|
repository: "github.com/acme/billing-service",
|
|
12
12
|
harness: opencode(),
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
const run = await
|
|
15
|
+
const run = await workspace.harness.run("Round invoice totals once, after applying the discount.");
|
|
16
16
|
|
|
17
17
|
await run.record.replay();
|
|
18
18
|
```
|
|
@@ -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
|
|
24
|
-
|
|
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,13 +44,53 @@ 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
|
-
Attach the caller's connected Claude / Codex / GitHub accounts to a
|
|
89
|
+
Attach the caller's connected Claude / Codex / GitHub accounts to a workspace so the harness
|
|
35
90
|
authenticates as that identity instead of running unauthenticated:
|
|
36
91
|
|
|
37
92
|
```ts
|
|
38
|
-
const
|
|
93
|
+
const workspace = await sealant.workspaces.create({
|
|
39
94
|
repository: "github.com/acme/billing-service",
|
|
40
95
|
harness: claudeCode(),
|
|
41
96
|
credentials: { claude: true, github: "bot-account" },
|
|
@@ -48,14 +103,54 @@ 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
|
-
The core loop is real: `
|
|
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), `
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
`fileTreeAt()`/`processTreeAt()` (Phase 1), and `harness.session()` +
|
|
155
|
+
`fileTreeAt()`/`processTreeAt()` (Phase 1), and `harness.session()` + workspace lifecycle
|
|
61
156
|
`stop()`/`restart()`/`expire()` (Phase 3).
|
package/dist/client.d.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import type { CreateOptions, ListOptions, Run,
|
|
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);
|
|
5
5
|
/** The configured control-plane base URL. */
|
|
6
6
|
get baseUrl(): string;
|
|
7
|
-
/**
|
|
8
|
-
readonly
|
|
9
|
-
create: (options: CreateOptions) => Promise<
|
|
10
|
-
get: (id: string) => Promise<
|
|
11
|
-
list: (options?: ListOptions | undefined) => Promise<readonly
|
|
7
|
+
/** Workspace lifecycle: create, fetch, and list live environments. */
|
|
8
|
+
readonly workspaces: {
|
|
9
|
+
create: (options: CreateOptions) => Promise<Workspace>;
|
|
10
|
+
get: (id: string) => Promise<Workspace>;
|
|
11
|
+
list: (options?: ListOptions | undefined) => Promise<readonly Workspace[]>;
|
|
12
12
|
};
|
|
13
|
-
/**
|
|
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;
|
|
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>;
|
|
16
21
|
};
|
package/dist/client.js
CHANGED
|
@@ -10,16 +10,17 @@
|
|
|
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 {
|
|
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";
|
|
17
17
|
import { makeRun } from "./facade/run.js";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
18
|
+
import { makeWorkspace, registerHarnessExecutors } from "./facade/workspace.js";
|
|
19
|
+
import { buildCreateWorkspaceRequest } from "./internal/blueprint.js";
|
|
20
20
|
import { resolveInternalConfig } from "./internal/config.js";
|
|
21
|
-
|
|
22
|
-
//
|
|
21
|
+
import { buildInferenceRespondRequest, mapInferenceResponse } from "./internal/inference.js";
|
|
22
|
+
// Wire the run-execution implementations into the Workspace facade (the injection point exists to
|
|
23
|
+
// break the workspace <-> run-harness import cycle; the client is the composition root).
|
|
23
24
|
registerHarnessExecutors({ run: runHarness, start: startHarness });
|
|
24
25
|
export class Sealant {
|
|
25
26
|
#config;
|
|
@@ -38,19 +39,19 @@ export class Sealant {
|
|
|
38
39
|
get baseUrl() {
|
|
39
40
|
return this.#config.baseUrl;
|
|
40
41
|
}
|
|
41
|
-
/**
|
|
42
|
-
|
|
42
|
+
/** Workspace lifecycle: create, fetch, and list live environments. */
|
|
43
|
+
workspaces = {
|
|
43
44
|
create: async (options) => {
|
|
44
|
-
const { payload } =
|
|
45
|
-
const created = await this.#runtime.run(
|
|
46
|
-
const
|
|
47
|
-
id: created.
|
|
45
|
+
const { payload } = buildCreateWorkspaceRequest(options, this.#ctx.config);
|
|
46
|
+
const created = await this.#runtime.run(createWorkspaceOp(payload));
|
|
47
|
+
const workspace = makeWorkspace(this.#ctx, {
|
|
48
|
+
id: created.workspaceId,
|
|
48
49
|
name: created.name,
|
|
49
50
|
status: created.status,
|
|
50
51
|
harness: options.harness,
|
|
51
52
|
});
|
|
52
53
|
if (options.wait === false) {
|
|
53
|
-
return
|
|
54
|
+
return workspace;
|
|
54
55
|
}
|
|
55
56
|
// Pump provisioning events to onEvent (best-effort) while we wait for ready. Never let an
|
|
56
57
|
// event-stream hiccup fail create().
|
|
@@ -58,7 +59,7 @@ export class Sealant {
|
|
|
58
59
|
const onEvent = options.onEvent;
|
|
59
60
|
void (async () => {
|
|
60
61
|
try {
|
|
61
|
-
for await (const event of
|
|
62
|
+
for await (const event of workspace.events()) {
|
|
62
63
|
onEvent(event);
|
|
63
64
|
}
|
|
64
65
|
}
|
|
@@ -67,30 +68,41 @@ export class Sealant {
|
|
|
67
68
|
}
|
|
68
69
|
})();
|
|
69
70
|
}
|
|
70
|
-
return
|
|
71
|
+
return workspace.ready();
|
|
71
72
|
},
|
|
72
73
|
get: async (id) => {
|
|
73
|
-
const details = await this.#runtime.run(
|
|
74
|
-
return
|
|
75
|
-
id: details.
|
|
74
|
+
const details = await this.#runtime.run(getWorkspaceOp(id));
|
|
75
|
+
return makeWorkspace(this.#ctx, {
|
|
76
|
+
id: details.workspaceId,
|
|
76
77
|
name: details.name,
|
|
77
78
|
status: details.status,
|
|
78
79
|
});
|
|
79
80
|
},
|
|
80
81
|
list: async (options) => {
|
|
81
|
-
const response = await this.#runtime.run(
|
|
82
|
+
const response = await this.#runtime.run(listWorkspacesOp({
|
|
82
83
|
ownerUserId: this.#ctx.config.hostLocal.ownerUserId,
|
|
83
84
|
...(options?.status === undefined ? {} : { status: options.status }),
|
|
84
85
|
...(options?.limit === undefined ? {} : { limit: String(options.limit) }),
|
|
85
86
|
}));
|
|
86
|
-
return response.items.map((item) =>
|
|
87
|
-
id: item.
|
|
87
|
+
return response.items.map((item) => makeWorkspace(this.#ctx, {
|
|
88
|
+
id: item.workspaceId,
|
|
88
89
|
name: item.name,
|
|
89
90
|
status: item.status,
|
|
90
91
|
}));
|
|
91
92
|
},
|
|
92
93
|
};
|
|
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
|
+
};
|
|
105
|
+
/** Runs by id — so a record can be replayed long after its workspace is gone. */
|
|
94
106
|
runs = {
|
|
95
107
|
get: async (runId) => {
|
|
96
108
|
const wire = await this.#runtime.run(getRunOp(runId));
|