@sapiom/agent-core 0.6.0 → 0.8.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.
@@ -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 |
@@ -1,6 +1,8 @@
1
- # Working in this orchestration
1
+ # Working in this agent project
2
2
 
3
- This project defines exactly one Sapiom orchestration in `index.ts`, authored against `@sapiom/agent`. Inside a step's `run`, Sapiom capabilities are on `ctx.sapiom` (e.g. `ctx.sapiom.repositories.list()`, `repo.pushFromSandbox(...)`).
3
+ This project defines exactly one Sapiom agent in `index.ts`, authored against `@sapiom/agent`. Inside a step's `run`, Sapiom capabilities are on `ctx.sapiom` (e.g. `ctx.sapiom.repositories.list()`, `repo.pushFromSandbox(...)`).
4
+
5
+ The full authoring guide ships inside this project at `.claude/skills/sapiom-agent-authoring/SKILL.md` — read it for the step model, directives, failure patterns, pause/resume, and stub rules.
4
6
 
5
7
  ## Authoring
6
8