@sapiom/agent-core 0.9.1 → 0.9.3

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.
@@ -0,0 +1,408 @@
1
+ ---
2
+ name: sapiom-agent-authoring
3
+ description: Build, test, and deploy a Sapiom agent — a controlled, multi-step,
4
+ deployable automation defined in TypeScript. Use when the user wants to
5
+ automate a multi-step, scheduled, recurring, or deployable task ("build an
6
+ agent that checks competitor prices every morning", "automate our weekly
7
+ report", "make a bot that reviews PRs"), or names Sapiom, defineAgent,
8
+ @sapiom/agent, or the sapiom-dev MCP. Also use to run, inspect, or resume an
9
+ existing Sapiom agent. Do NOT use for a single one-off capability call
10
+ (a web search, one scrape, one image) with no automation to keep — use
11
+ Sapiom's remote MCP or SDK for that.
12
+ ---
13
+
14
+ # Building Sapiom Agents
15
+
16
+ A Sapiom **agent** is a small TypeScript project you author with your coding agent: a
17
+ `defineAgent({ name, entry, steps })` where each step's `run(input, ctx)` does work — calling
18
+ paid Sapiom capabilities through the typed `ctx.sapiom.*` client — and returns a directive.
19
+ You test it locally for free, then deploy it to run on Sapiom's cloud: on demand, on a
20
+ schedule, or resumed by signals. All from the terminal; no dashboard required.
21
+
22
+ **Load this skill before scaffolding — it drives the whole lifecycle from zero.** Inside a
23
+ scaffolded project, `AGENTS.md` is the quick reference; this skill is the deep guide.
24
+
25
+ ## If the Sapiom dev MCP isn't connected yet
26
+
27
+ The lifecycle below runs through the **sapiom-dev** MCP server (`@sapiom/mcp`). If its tools
28
+ (`sapiom_authenticate`, `sapiom_dev_agents_*`) aren't available, add the server first:
29
+
30
+ ```bash
31
+ claude mcp add sapiom -- npx -y @sapiom/mcp
32
+ ```
33
+
34
+ Other clients: run `npx -y @sapiom/mcp` as a local (stdio) MCP server — see
35
+ [docs.sapiom.ai](https://docs.sapiom.ai/integration/mcp-servers/setup) for per-client config.
36
+
37
+ ## Lifecycle from Zero
38
+
39
+ ### 1. Authenticate (required before deploy/run)
40
+
41
+ Run `sapiom_authenticate` — it opens a browser login and caches an API key in
42
+ `~/.sapiom/credentials.json`. Confirm with `sapiom_status`. This makes your coding agent an
43
+ API-key principal; deployed agents inherit that authority to call paid capabilities.
44
+
45
+ ### 2. Scaffold
46
+
47
+ Call `sapiom_dev_agents_scaffold` with a target directory. The scaffold writes:
48
+
49
+ ```
50
+ my-agent/
51
+ ├── index.ts # your agent definition (edit this)
52
+ ├── AGENTS.md # quick in-project reference
53
+ ├── CLAUDE.md # points your coding agent at AGENTS.md
54
+ ├── .claude/skills/ # this skill, locally — the deep guide
55
+ ├── .sapiom-dev/stubs.json
56
+ ├── package.json / tsconfig.json / ...
57
+ ```
58
+
59
+ Install deps: `npm install`. Templates: `"default"` (minimal two-step starter) or
60
+ `"coding-pause"` (launch + pause/resume coding-model pattern).
61
+
62
+ ### 3. Write steps → typecheck → check → run_local → deploy
63
+
64
+ | Command | What it does |
65
+ |---|---|
66
+ | `npm run typecheck` | Confirms types compile and every `ctx.sapiom.*` call exists |
67
+ | `sapiom_dev_agents_check` | Bundles `index.ts` + validates the step graph (offline, instant) |
68
+ | `sapiom_dev_agents_run_local` | Runs real step code with all capabilities stubbed — free, no spend |
69
+
70
+ Then ship:
71
+
72
+ | Command | What it does |
73
+ |---|---|
74
+ | `sapiom_dev_agents_link` | Registers the agent under your tenant |
75
+ | `sapiom_dev_agents_deploy` | Builds and deploys to Sapiom's cloud |
76
+ | `sapiom_dev_agents_run` | Starts a real (billed) execution |
77
+ | `sapiom_dev_agents_inspect` | Watch an execution's status, steps, and spend |
78
+
79
+ ## The Step Model — Hard Rules
80
+
81
+ ### Canonical API
82
+
83
+ | Import | From |
84
+ |---|---|
85
+ | `defineAgent` | `@sapiom/agent` |
86
+ | `defineStep` | `@sapiom/agent` |
87
+ | `goto / terminate / fail / retry / pauseUntilSignal` | `@sapiom/agent` |
88
+ | `AgentExecutionContext` | `@sapiom/agent` |
89
+ | `CODING_RESULT_SIGNAL / CodingResultPayload` | `@sapiom/tools` |
90
+
91
+ `@sapiom/agent` is the only authoring package.
92
+
93
+ ### `defineAgent` shape
94
+
95
+ ```typescript
96
+ export const agent = defineAgent({
97
+ name: "my-agent", // string — used for logging and inspect
98
+ entry: "start", // must name a key in steps
99
+ steps: { start, finish },
100
+ });
101
+ ```
102
+
103
+ Export exactly one `defineAgent(...)` from `index.ts`.
104
+
105
+ ### `defineStep` fields
106
+
107
+ | Field | Type | Required | Notes |
108
+ |---|---|---|---|
109
+ | `name` | `string` | yes | Step's id; must match its key in the steps object |
110
+ | `next` | `readonly string[]` | yes | Step names this step may `goto`. Empty array if terminal |
111
+ | `terminal` | `boolean` | no | `true` if this step ends the agent's execution |
112
+ | `canFail` | `boolean` | no | Must be `true` to return `fail()` |
113
+ | `pause` | `{ signal, resumeStep }` | no | Required when returning `pauseUntilSignal(...)` |
114
+ | `inputSchema` | `ZodType` | no | Zod schema validating this step's input |
115
+ | `timeoutMs` | `number` | no | Per-step timeout; no automatic retry cap |
116
+ | `run(input, ctx)` | `async function` | yes | Returns a directive |
117
+
118
+ Import Zod via the `zod/v4` subpath — `import { z } from "zod/v4"` — to match the SDK's
119
+ schema types. Don't import from bare `"zod"` or add a second zod dependency.
120
+
121
+ ### Directives — what `run` must return
122
+
123
+ | Directive | Function | Constraint |
124
+ |---|---|---|
125
+ | `goto(target, output?)` | Advance to another step | `target` must be in `next[]` |
126
+ | `terminate(output?, opts?)` | End the execution successfully | Step must have `terminal: true` |
127
+ | `fail(reason?, opts?)` | End the execution as failed | Step must have `canFail: true` |
128
+ | `retry(opts?)` | Re-run this step | Bound with `ctx.attempts` — no automatic cap |
129
+ | `pauseUntilSignal(handle, opts?)` | Suspend until a signal fires | Step must declare `pause: { signal, resumeStep }` |
130
+
131
+ TypeScript enforces these constraints at compile time — a `terminate` in a non-terminal step,
132
+ or a `fail` without `canFail: true`, is a type error.
133
+
134
+ ### Minimal two-step example
135
+
136
+ ```typescript
137
+ import { defineAgent, defineStep, goto, terminate } from "@sapiom/agent";
138
+
139
+ const start = defineStep({
140
+ name: "start",
141
+ next: ["finish"],
142
+ async run(input: { name: string }, ctx) {
143
+ ctx.logger.info("got input", { name: input.name });
144
+ return goto("finish", { greeting: `Hello, ${input.name}` });
145
+ },
146
+ });
147
+
148
+ const finish = defineStep({
149
+ name: "finish",
150
+ next: [],
151
+ terminal: true,
152
+ async run(input: { greeting: string }, ctx) {
153
+ return terminate({ result: input.greeting });
154
+ },
155
+ });
156
+
157
+ export const agent = defineAgent({
158
+ name: "greet",
159
+ entry: "start",
160
+ steps: { start, finish },
161
+ });
162
+ ```
163
+
164
+ ## Cross-Step State with `ctx.shared`
165
+
166
+ `goto(target, payload)` passes data to the next step's `input`. For data multiple downstream
167
+ steps need, use `ctx.shared` — a typed key/value store that persists across the whole execution.
168
+
169
+ ```typescript
170
+ interface Shared extends Record<string, unknown> {
171
+ taskId: string;
172
+ }
173
+
174
+ // In a step:
175
+ ctx.shared.set("taskId", "abc-123");
176
+
177
+ // In a later step:
178
+ const taskId = ctx.shared.get("taskId"); // typed as string | undefined
179
+ ```
180
+
181
+ `ctx.shared` API: `get(key)`, `set(key, value)`, `has(key)`, `snapshot()`.
182
+
183
+ **A step's `run(input, ctx)` first argument is its inbound input** — the entry input at the
184
+ entry step, or the previous step's `goto(target, payload)` value at later steps. The entry
185
+ input reaches only the entry step's argument; to use it in later steps, write it into
186
+ `ctx.shared` from the entry step.
187
+
188
+ ## `ctx` Reference
189
+
190
+ | Field | Type | Notes |
191
+ |---|---|---|
192
+ | `ctx.executionId` | `string` | Unique id for this execution |
193
+ | `ctx.agentName` | `string` | The agent's `name` |
194
+ | `ctx.input` | `unknown` | The execution's entry input — same value the entry step's `run` arg receives. Use `ctx.shared` to carry it forward; don't rely on `ctx.input` downstream. |
195
+ | `ctx.shared` | `TypedContextStore<TShared>` | Cross-step key/value store |
196
+ | `ctx.history` | `readonly StepExecutionRecord[]` | Previous steps' records |
197
+ | `ctx.attempts` | `number` | How many times this step has run (0-indexed) |
198
+ | `ctx.logger` | `StepLogger` | `info / warn / error / debug(msg, meta?)` |
199
+ | `ctx.sapiom` | `Sapiom` | The typed capability client — the `Sapiom` interface from `@sapiom/tools`, installed in your `node_modules` (see "Capabilities" below) |
200
+ | `ctx.organizationId` | `string \| null` | Tenant org |
201
+ | `ctx.tenantId` | `string \| null` | Tenant id |
202
+
203
+ ## Capabilities from Steps
204
+
205
+ Steps call Sapiom's paid capabilities through `ctx.sapiom.*` — sandboxes, repositories,
206
+ coding models (`ctx.sapiom.models.coding`), file storage, content generation, search,
207
+ databases, email, domains, memory, and more as they land. **Do not memorize the catalog:
208
+ types are the source of truth.** The full surface is the `Sapiom` interface in `@sapiom/tools`
209
+ — installed in your project's `node_modules`, so its types match the exact version you're on.
210
+ `ctx.sapiom.` autocompletes what exists, `npm run typecheck` rejects what doesn't, and the full
211
+ catalog with pricing lives at [docs.sapiom.ai/capabilities](https://docs.sapiom.ai/capabilities).
212
+
213
+ ## Failure Handling & Retries
214
+
215
+ There is no automatic per-step retry. Express it explicitly — this keeps the graph readable.
216
+ The common pattern is a bounded loop that escalates:
217
+
218
+ ```typescript
219
+ const reconsider = defineStep({
220
+ name: "reconsider",
221
+ next: ["work", "escalate"],
222
+ async run(_input, ctx: AgentExecutionContext<{ attempt: number }>) {
223
+ const attempt = ctx.shared.get("attempt") ?? 0;
224
+ if (attempt >= 3) return goto("escalate", {});
225
+ ctx.shared.set("attempt", attempt + 1);
226
+ return goto("work", {});
227
+ },
228
+ });
229
+ ```
230
+
231
+ For a step's own retries (transient errors):
232
+
233
+ ```typescript
234
+ async run(input, ctx) {
235
+ try {
236
+ const result = await ctx.sapiom.sandboxes.create({ name: "demo" });
237
+ return terminate({ result });
238
+ } catch (err) {
239
+ // ctx.attempts is 0-indexed: `+ 1 < N` gives exactly N total attempts.
240
+ // (`ctx.attempts < N` would run N+1 times — a common off-by-one.)
241
+ if (ctx.attempts + 1 < 3) return retry({ delayMs: 1000 });
242
+ return fail("too many attempts"); // requires canFail: true
243
+ }
244
+ }
245
+ ```
246
+
247
+ `timeoutMs` on a step caps how long its `run` may take. There is no engine-level retry cap —
248
+ you own the bound.
249
+
250
+ ## Pause & Resume (Long-Running Dispatched Steps)
251
+
252
+ A step's `run` completes in one synchronous dispatch. For long-running capabilities (a
253
+ dispatched coding-model run), **launch fire-and-forget and pause** — the engine suspends the
254
+ execution until the result signal fires, then resumes into a designated step whose `input`
255
+ IS the result payload.
256
+
257
+ ```typescript
258
+ import {
259
+ defineAgent, defineStep, goto, pauseUntilSignal, terminate,
260
+ type AgentExecutionContext,
261
+ } from "@sapiom/agent";
262
+ import { CODING_RESULT_SIGNAL, type CodingResultPayload } from "@sapiom/tools";
263
+
264
+ interface Shared extends Record<string, unknown> {
265
+ repoSlug: string;
266
+ }
267
+
268
+ const launch = defineStep({
269
+ name: "launch",
270
+ next: ["collect"],
271
+ // Declare the signal and resume step so the engine knows what to wait for.
272
+ pause: { signal: CODING_RESULT_SIGNAL, resumeStep: "collect" },
273
+ async run(input: { task: string }, ctx: AgentExecutionContext<Shared>) {
274
+ const repo = await ctx.sapiom.repositories.create("my-repo");
275
+ ctx.shared.set("repoSlug", repo.slug); // stash before pausing
276
+ const run = await ctx.sapiom.models.coding.launch({ task: input.task, gitRepository: repo });
277
+ return pauseUntilSignal(run, { resumeStep: "collect" }); // pass the handle, not the signal name
278
+ },
279
+ });
280
+
281
+ const collect = defineStep({
282
+ name: "collect",
283
+ next: [],
284
+ terminal: true,
285
+ // input IS the CodingResultPayload delivered by the resume signal.
286
+ async run(result: CodingResultPayload, ctx: AgentExecutionContext<Shared>) {
287
+ if (result.status !== "completed") {
288
+ return terminate({ status: result.status, error: result.error });
289
+ }
290
+ // Re-attach the sandbox — the payload crossed a wire boundary, so there are no live handles.
291
+ if (result.executionEnvironment?.type === "blaxel_sandbox") {
292
+ const sandbox = ctx.sapiom.sandboxes.attach(result.executionEnvironment.id);
293
+ // … push from sandbox, read files, etc.
294
+ }
295
+ return terminate({ status: result.status, summary: result.summary });
296
+ },
297
+ });
298
+
299
+ export const agent = defineAgent<{ task: string }, Shared>({
300
+ name: "code-and-collect",
301
+ entry: "launch",
302
+ steps: { launch, collect },
303
+ });
304
+ ```
305
+
306
+ Key rules:
307
+
308
+ - `pause: { signal, resumeStep }` is **required** on the step that returns `pauseUntilSignal`.
309
+ Passing the handle to `pauseUntilSignal(handle, ...)` wires the signal automatically.
310
+ - The **resumed step's `input` is the run's result payload** (`CodingResultPayload`).
311
+ Annotate it explicitly — don't hand-roll the shape.
312
+ - The payload crossed a process boundary: **no live handles**. Re-attach a sandbox from
313
+ `result.executionEnvironment.id` if needed; stash everything else in `ctx.shared` before pausing.
314
+ - For a **manual human-gate** (no capability handle), use the object form and fire the signal
315
+ from your approval flow:
316
+
317
+ ```typescript
318
+ return pauseUntilSignal({
319
+ signal: "my.approval",
320
+ resumeStep: "finalize",
321
+ correlationId: ctx.executionId, // makes the awaited signal unique to this execution
322
+ });
323
+ ```
324
+
325
+ Under `run_local`, a dispatch pause auto-resumes with the stub result; a manual gate
326
+ auto-resumes with `{}` unless stubbed — type the resumed step's input with optional fields
327
+ accordingly.
328
+
329
+ ## Determinism
330
+
331
+ A step body runs **once** on the happy path. It re-runs only on retry (after a throw or
332
+ `retry()`). Do not rely on a value being recomputed identically across a pause/resume or a
333
+ retry. Capture non-deterministic values (timestamps, random ids) once and carry them forward
334
+ via `goto` input or `ctx.shared`.
335
+
336
+ ## Testing with `run_local` and Stubs
337
+
338
+ `run_local` works with **no stubs** — capabilities return sensible defaults. Add
339
+ `.sapiom-dev/stubs.json` overrides only when a step branches on a specific result:
340
+
341
+ ```jsonc
342
+ {
343
+ "version": 1,
344
+ "steps": {
345
+ // Stub the coding run under the LAUNCHING step (here `launch`), not the resume step.
346
+ "launch": {
347
+ "models.coding.run": { "status": "completed", "summary": "done", "result": null, "error": null, "executionEnvironment": null }
348
+ },
349
+ "check": {
350
+ "repositories.list": [{ "slug": "my-repo", "cloneUrl": "https://..." }]
351
+ }
352
+ }
353
+ }
354
+ ```
355
+
356
+ Stub naming rules:
357
+
358
+ - Namespace calls use the **plural/namespace path**: `repositories.list`, `models.coding.run`.
359
+ - Handle method calls use the **singular**: `repository.pushFromSandbox`, `sandbox.exec`.
360
+ - To stub a coding run's resume payload, override `models.coding.run` (or
361
+ `models.coding.launch`) in the **launching step** — that value is both the inline result
362
+ and the payload the paused step resumes with.
363
+ - `run_local` reports `unusedStubs` (key matched nothing — usually a typo or plural/singular
364
+ slip) and `stubWarnings` (key matched but wrong shape). A green run with either non-empty
365
+ means the stub silently didn't apply.
366
+ - **Local retry cap:** the `run_local` tool defaults to `maxAttemptsPerStep: 3`. If a step's
367
+ own retry bound allows ≥3 retries, pass a higher `maxAttemptsPerStep` so the local harness
368
+ doesn't stop the loop before your `fail()` fires. This cap is local-test only — production
369
+ has no engine-level retry cap.
370
+
371
+ Write each step the way it should run in production — never weaken logic to shape a local run.
372
+
373
+ ## Tips
374
+
375
+ - **Types are the source of truth.** What's on `ctx.sapiom` is defined by `@sapiom/tools`.
376
+ Use autocomplete and `npm run typecheck` rather than guessing — a wrong capability name is
377
+ a type error, not a runtime surprise.
378
+ - **`check` before deploy.** It validates the step graph (names, `next` references,
379
+ `terminal` consistency) — a misconfigured graph is caught here, not at runtime.
380
+ - **One `defineAgent` export per file.** The scaffold wraps a single `index.ts`.
381
+ - **`ctx.shared` for fanout.** When three steps all need the entry input, write it into
382
+ `ctx.shared` in the entry step — don't thread it through every `goto` payload.
383
+ - **One-off capability call, no automation to keep?** That's not an agent — use Sapiom's
384
+ [remote MCP](https://docs.sapiom.ai/integration/mcp-servers/remote) (`https://api.sapiom.ai/v1/mcp`,
385
+ direct `sapiom_*` tools, `tool_discover` to find the right one) or the typed SDK client
386
+ ([`@sapiom/tools`](https://www.npmjs.com/package/@sapiom/tools)) instead of scaffolding.
387
+
388
+ ## Troubleshooting
389
+
390
+ | Symptom | Cause | Fix |
391
+ |---|---|---|
392
+ | `Cannot find module '@sapiom/agent'` | Deps not installed | `npm install` inside the scaffolded dir |
393
+ | Type error: `fail(...)` not assignable | Step missing `canFail: true` | Add `canFail: true` to `defineStep` |
394
+ | Type error: `terminate(...)` not assignable | Step missing `terminal: true` | Add `terminal: true` to `defineStep` |
395
+ | `goto` target rejected by types | Target not in `next[]` | Add the target name to `next` |
396
+ | `check` fails: step missing from graph | `steps` object key doesn't match `name` field | Match the key in `steps: { start }` to `defineStep({ name: "start" })` |
397
+ | `run_local` reports `unusedStubs` | Stub path typo or namespace/handle mix-up | Namespace path for calls (`repositories.list`), singular for handles (`repository.pushFromSandbox`) |
398
+ | Paused step resumes with empty input | Manual gate; `run_local` auto-resumes with `{}` | Type the resumed step's input with optional fields |
399
+ | `sapiom_authenticate` → credential not found at deploy | Authenticated in a different shell | Re-run `sapiom_authenticate`; credential is per-machine in `~/.sapiom/credentials.json` |
400
+
401
+ ## References
402
+
403
+ | Resource | What it covers |
404
+ |---|---|
405
+ | [Authoring guide](https://docs.sapiom.ai/agents/authoring) | Full step model, failure patterns, pause/resume, determinism |
406
+ | [Quickstart](https://docs.sapiom.ai/agents/quick-start) | Scaffold → write → test → deploy walkthrough |
407
+ | [Capabilities](https://docs.sapiom.ai/capabilities) | The full `ctx.sapiom.*` catalog with pricing |
408
+ | `AGENTS.md` in your scaffold | The quick in-project reference |
@@ -0,0 +1,96 @@
1
+ ---
2
+ name: sapiom-sandbox-preview
3
+ description: Deploy a live preview of a web app from the current project to a
4
+ Sapiom sandbox. Use when the user wants to preview, host, or deploy a web
5
+ app, dev server, API, or static site to a live URL ("preview this app",
6
+ "give me a live link", "host this server"), or mentions sapiom.json sandbox
7
+ resources. Do NOT use for deploying Sapiom agents (that's
8
+ sapiom_dev_agents_deploy) or for one-off capability calls.
9
+ ---
10
+
11
+ # Sandbox Previews
12
+
13
+ Deploy the web app in the current project directory to a Sapiom **sandbox** and get a
14
+ **live public URL** — provisioned, uploaded, built, and started in one tool call. Driven
15
+ by the sapiom-dev MCP (`npx -y @sapiom/mcp`; see the [Get Started guide](https://docs.sapiom.ai/)
16
+ if it isn't connected).
17
+
18
+ **This is not agent deployment.** Sapiom *agents* deploy with `sapiom_dev_agents_deploy`;
19
+ sandbox previews host an ordinary app (Node server, static site, API) from your working
20
+ directory.
21
+
22
+ ## Prerequisite
23
+
24
+ Run `sapiom_authenticate` once (browser login; caches a key in `~/.sapiom/credentials.json`).
25
+ `sapiom_dev_sandbox_preview` returns a structured not-authenticated error otherwise;
26
+ `configure` and `check` only touch local files and work signed-out. Check with
27
+ `sapiom_status`.
28
+
29
+ ## The lifecycle
30
+
31
+ 1. **`sapiom_dev_sandbox_configure`** — creates or updates a preview resource in the
32
+ project's `sapiom.json`. Fill the typed arguments instead of hand-writing JSON — the
33
+ config is validated and written under `resources.<name>` (`type: "sandbox"`). Returns
34
+ the stored config.
35
+ 2. **`sapiom_dev_sandbox_check`** *(optional)* — statically validates the resources without
36
+ deploying. Returns `{ ok, sandboxes, issues }`; fix any `issues` before previewing.
37
+ 3. **`sapiom_dev_sandbox_preview`** — reads `sapiom.json`, provisions the sandbox if
38
+ needed, uploads the local code, builds, starts, and exposes a public URL. Returns
39
+ `{ name, url, status, logs }`. Pass `name` only when the project defines more than one
40
+ resource.
41
+
42
+ **A `failed` status is not an error** — it carries the build/start logs so you can fix the
43
+ app or the config and run `sapiom_dev_sandbox_preview` again. `unverified` means the app
44
+ started but didn't answer 2xx yet.
45
+
46
+ ## The `sapiom.json` resource
47
+
48
+ ```json
49
+ {
50
+ "version": 1,
51
+ "resources": {
52
+ "web": {
53
+ "type": "sandbox",
54
+ "source": { "kind": "upload" },
55
+ "start": "node server.js",
56
+ "port": 3000,
57
+ "ttl": "1h"
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ | Field | Required | Notes |
64
+ |---|---|---|
65
+ | `source` | yes | `{ "kind": "upload", "path"? }` (upload the local dir) or `{ "kind": "git", "slug", "path"? }` (server checks out a Sapiom repo) |
66
+ | `start` | yes | The server command (e.g. `node server.js`) |
67
+ | `port` | yes | 1–65535 — the port your app listens on |
68
+ | `build` | no | Build command run before start (e.g. `npm run build`) |
69
+ | `tier` | no | Sandbox size: `xs` \| `s` \| `m` \| `l` \| `xl` |
70
+ | `ttl` | no | Sandbox lifetime, e.g. `"1h"`, `"24h"`, `"7d"` |
71
+ | `env` | no | Environment variables (string map) |
72
+
73
+ Uploads skip `node_modules`, `.git`, and dotfiles — dependencies install in the sandbox at
74
+ build time; never upload them.
75
+
76
+ ## CLI alternative
77
+
78
+ ```bash
79
+ sapiom sandbox preview [name] # alias: sapiom sbx preview; add --json for machine output
80
+ ```
81
+
82
+ Defaults to the single resource when the project defines exactly one.
83
+
84
+ ## From inside a Sapiom agent step
85
+
86
+ Steps can deploy previews through the typed client. Get a `Sandbox` instance first —
87
+ `await ctx.sapiom.sandboxes.create({...})` or `.attach(name)` — then call
88
+ `sandbox.uploadDir(localDir)` to stage the code and
89
+ `sandbox.deployPreview({ start, port, build?, env? })` →
90
+ `{ url, status: "deployed" | "unverified" | "failed", logs }` (failures return `status:
91
+ "failed"` with logs, not a throw). `sandbox.createPublicUrl({ port })` is the low-level
92
+ primitive — the port must have been declared when the sandbox was created.
93
+
94
+ ## Reference
95
+
96
+ Full details: [Compute — Sandbox previews](https://docs.sapiom.ai/capabilities/compute#sandbox-previews).
@@ -28,7 +28,7 @@ When you've made a coherent change and want to validate it — the same point yo
28
28
  { "version": 1, "steps": { "<stepName>": { "<capability.path>": <response> } } }
29
29
  ```
30
30
 
31
- - Capability paths are namespace methods (`repositories.list`, `repositories.create`, `agent.coding.run`) or handle methods, which use the **singular** handle type (`repository.pushFromSandbox`, `sandbox.exec`) — not the plural namespace.
31
+ - Capability paths are namespace methods (`repositories.list`, `repositories.create`, `models.coding.run`) or handle methods, which use the **singular** handle type (`repository.pushFromSandbox`, `sandbox.exec`) — not the plural namespace.
32
32
  - `<response>` is returned **verbatim** — it is the value that call would return, so match its real shape. `repositories.list` takes the array `list()` returns: `[{ "slug": "...", "cloneUrl": "..." }]` (each element a repository — *not* `[[ … ]]`). `repositories.create`/`get`/`attach` take a single `{ "slug", "cloneUrl" }`.
33
33
  - `run_local` reports **`unusedStubs`** (a key that matched no call — usually a typo or the plural/singular mistake) and **`stubWarnings`** (a key matched but the value was the wrong shape). A green run with either non-empty means a stub silently didn't take effect — check them.
34
34
 
@@ -43,7 +43,7 @@ return pauseUntilSignal(run, { resumeStep: "finalize" });
43
43
 
44
44
  - **The resumed step's `input` IS the run's result signal payload.** Annotate it with `CodingResultPayload` (from `@sapiom/tools`) — you don't have to hand-roll the shape.
45
45
  - That payload crossed a wire boundary, so it carries **no live handles** — to act on the run's sandbox, re-attach one from **`executionEnvironment`** with `ctx.sapiom.sandboxes.attach(result.executionEnvironment.id)` (`executionEnvironment` is `null` when the run provisioned none, e.g. a launch failure). Anything else the resumed step needs, stash in `ctx.shared` before pausing.
46
- - **To stub the resume payload** (e.g. to exercise the failure branch), override `agent.coding.run` *in the launching step* — that one value is both the `run()` result and the payload the paused step resumes with. `agent.coding.launch` is accepted there too.
46
+ - **To stub the resume payload** (e.g. to exercise the failure branch), override `models.coding.run` *in the launching step* — that one value is both the `run()` result and the payload the paused step resumes with. `models.coding.launch` is accepted there too.
47
47
 
48
48
  ## Determinism
49
49
 
@@ -9,7 +9,7 @@ resumes once the run finishes — authored as code against
9
9
  prepare → kickoff ──pause(models.coding.result)──▶ finalize
10
10
  ```
11
11
 
12
- - **kickoff** calls `agent.coding.launch(...)` (which returns a handle, *not* a
12
+ - **kickoff** calls `models.coding.launch(...)` (which returns a handle, *not* a
13
13
  result) and returns `pauseUntilSignal(handle, { resumeStep: "finalize" })`. The
14
14
  workflow suspends — a long run holds no worker.
15
15
  - **finalize** is resumed by the engine when the run reaches a terminal state.
@@ -22,7 +22,7 @@ branch locally, stub a failed result under the *launching* step in
22
22
  `.sapiom-dev/stubs.json` — that value is also the resume payload:
23
23
 
24
24
  ```jsonc
25
- { "version": 1, "steps": { "kickoff": { "agent.coding.launch": { "status": "failed", "result": { "success": false }, "error": { "stage": "run", "message": "…" } } } } }
25
+ { "version": 1, "steps": { "kickoff": { "models.coding.launch": { "status": "failed", "result": { "success": false }, "error": { "stage": "run", "message": "…" } } } } }
26
26
  ```
27
27
 
28
28
  ## Getting started
@@ -28,7 +28,7 @@ When you've made a coherent change and want to validate it — the same point yo
28
28
  { "version": 1, "steps": { "<stepName>": { "<capability.path>": <response> } } }
29
29
  ```
30
30
 
31
- - Capability paths are namespace methods (`repositories.list`, `repositories.create`, `agent.coding.run`) or handle methods, which use the **singular** handle type (`repository.pushFromSandbox`, `sandbox.exec`) — not the plural namespace.
31
+ - Capability paths are namespace methods (`repositories.list`, `repositories.create`, `models.coding.run`) or handle methods, which use the **singular** handle type (`repository.pushFromSandbox`, `sandbox.exec`) — not the plural namespace.
32
32
  - `<response>` is returned **verbatim** — it is the value that call would return, so match its real shape. `repositories.list` takes the array `list()` returns: `[{ "slug": "...", "cloneUrl": "..." }]` (each element a repository — *not* `[[ … ]]`). `repositories.create`/`get`/`attach` take a single `{ "slug", "cloneUrl" }`.
33
33
  - `run_local` reports **`unusedStubs`** (a key that matched no call — usually a typo or the plural/singular mistake) and **`stubWarnings`** (a key matched but the value was the wrong shape). A green run with either non-empty means a stub silently didn't take effect — check them.
34
34
 
@@ -43,7 +43,7 @@ return pauseUntilSignal(run, { resumeStep: "finalize" });
43
43
 
44
44
  - **The resumed step's `input` IS the run's result signal payload.** Annotate it with `CodingResultPayload` (from `@sapiom/tools`) — you don't have to hand-roll the shape.
45
45
  - That payload crossed a wire boundary, so it carries **no live handles** — to act on the run's sandbox, re-attach one from **`executionEnvironment`** with `ctx.sapiom.sandboxes.attach(result.executionEnvironment.id)` (`executionEnvironment` is `null` when the run provisioned none, e.g. a launch failure). Anything else the resumed step needs, stash in `ctx.shared` before pausing.
46
- - **To stub the resume payload** (e.g. to exercise the failure branch), override `agent.coding.run` *in the launching step* — that one value is both the `run()` result and the payload the paused step resumes with. `agent.coding.launch` is accepted there too.
46
+ - **To stub the resume payload** (e.g. to exercise the failure branch), override `models.coding.run` *in the launching step* — that one value is both the `run()` result and the payload the paused step resumes with. `models.coding.launch` is accepted there too.
47
47
 
48
48
  ## Determinism
49
49