@stackbone/sdk 0.1.0-alpha.7 → 0.1.0-alpha.8

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
@@ -4,27 +4,26 @@
4
4
  [![Node](https://img.shields.io/badge/node-%3E%3D24-brightgreen.svg)](https://nodejs.org/)
5
5
  [![TypeScript](https://img.shields.io/badge/types-TypeScript-blue.svg)](https://www.typescriptlang.org/)
6
6
 
7
- Official TypeScript SDK for [Stackbone](https://stackbone.ai) — the marketplace and managed runtime for containerized AI agents. Stackbone provisions a Postgres branch, object storage, an LLM gateway and the rest of the platform primitives for every agent; this SDK is the in-container surface those agents call.
7
+ Official TypeScript SDK for [Stackbone](https://stackbone.ai) — the marketplace and managed runtime for AI agents. You author a **workspace** of durable agents and workflows; Stackbone provisions a Postgres branch (Neon), object storage, an LLM gateway and the rest of the platform primitives for it, and this SDK is the in-workspace surface your code calls through the ambient `stackbone` client.
8
8
 
9
9
  > "You write the agent. We connect it to the world."
10
10
 
11
11
  ## Features
12
12
 
13
- - **Database** — Direct Postgres access through a lazy Drizzle ORM wrapper bound to the agent's connection string, with full type-safe queries, transactions, and `sql` template literals Drizzle is re-exported via `@stackbone/sdk/db` so the agent's `package.json` only depends on `@stackbone/sdk`
13
+ - **Ambient client** — `import { stackbone } from '@stackbone/sdk'` and reach every platform surface from inside any tool's `execute()` or any workflow step. No `createClient()`, no credential wiringconfig is resolved lazily from the env the runtime injects
14
+ - **Database** — Native Drizzle ORM over the agent's own Neon Postgres, with type-safe queries, transactions, `pgvector` and `tsvector`. Drizzle is re-exported via `@stackbone/sdk/db` so your `package.json` only depends on `@stackbone/sdk`
14
15
  - **Storage** — S3-compatible object storage (Cloudflare R2 in prod, MinIO in dev) with automatic per-agent key prefixing and signed URLs
15
- - **AI** — Wrapper around the official `openai` SDK pointed at OpenRouter, so 300+ chat, embedding and image models are reachable through a single OpenAI-compatible API
16
- - **RAG** — Document parsing, chunking and `pgvector`-backed retrieval through a flat, ceremony-free API
17
- - **Observability** — OpenTelemetry span processor + run-cost aggregator + per-run logger
18
- - **Approval** — Human-in-the-loop inbox: fire-and-forget approval requests, HMAC-signed decision callbacks, and an LLM tool wrapper that gates execution behind human review
19
- - **Secrets** — Read organization-encrypted secrets registered in the dashboard, no client-side cache so rotations propagate immediately
20
- - **Config** — Typed reads of dynamic per-agent configuration the user set in the dashboard
21
- - **Coming soon (types only)** — `queues` (cross-container HTTP push via the BullMQ job dispatcher), `memory` (mem0-backed long-term memory), `prompts` (managed prompts with `{{var}}` templates), `connections` (OAuth integrations) and `events` (org-wide event bus). Public types are exported from `@stackbone/sdk` today; the runtime is not wired yet, so there is no live `client.X` accessor.
22
- - **TypeScript-first** — Full type definitions and a uniform `Result<T>` envelope on every method (no thrown errors at the SDK boundary)
23
- - **Lazy initialization** — Modules and partner SDKs are constructed on first access, so env vars rotated by the control plane after the client is built are still picked up
16
+ - **AI** — OpenAI-compatible client pointed at OpenRouter, so 300+ chat, embedding and image models are reachable through a single managed sub-key
17
+ - **RAG** — Document ingest and hybrid (`pgvector` + `tsvector`) retrieval through a flat, ceremony-free API; tables are platform-provisioned
18
+ - **Workflows** — Durable, replayable orchestration: start, wait on, and schedule workflows by name, and pause on human approval, via `@stackbone/sdk/workflow`
19
+ - **Human-in-the-loop** `requestApproval()` pauses a workflow durably until a person decides in the Studio inbox (or the timeout fires)
20
+ - **Connectors** — Call a brokered third-party integration (Slack, Gmail, …) with `stackbone.connection(id)` — no token ever enters the container
21
+ - **Config, secrets & prompts** — Typed reads of dynamic per-agent config, organization-encrypted secrets, and a versioned prompt catalog
22
+ - **TypeScript-first** — Full type definitions and a uniform `Result<{ data, error }>` envelope on every method (the two exceptions throw see below)
24
23
 
25
24
  ## Installation
26
25
 
27
- `@stackbone/sdk` is the SDK every published agent depends on. Inside the Stackbone monorepo it is available via the `@stackbone/sdk` workspace alias. From a generated agent project:
26
+ `@stackbone/sdk` is the SDK every Stackbone workspace depends on. From a generated workspace:
28
27
 
29
28
  ```bash
30
29
  # Alpha (today)
@@ -35,532 +34,343 @@ pnpm add @stackbone/sdk@alpha
35
34
  pnpm add @stackbone/sdk
36
35
  ```
37
36
 
37
+ `@stackbone/sdk` declares `eve` and `workflow` as **optional peer dependencies**. Install only the ones your workspace uses:
38
+
39
+ ```bash
40
+ pnpm add eve # to author an agent (agent.ts / tools)
41
+ pnpm add workflow # to author a workflow that pauses for approval
42
+ pnpm add @ai-sdk/openai-compatible # to bind the OpenRouter-compatible model in agent.ts
43
+ ```
44
+
45
+ A tool-only agent that never imports `@stackbone/sdk/workflow` or `@stackbone/sdk/connect` never loads those peers, so `import { stackbone } from '@stackbone/sdk'` stays peer-free.
46
+
38
47
  > Requires Node.js 24 or newer (the runtime your agent container ships with). The SDK is server-only — it is not built for the browser.
39
48
 
40
- Need a copy-pasteable starting point? See the [`example/`](https://github.com/stackbone/stackbone/tree/main/libs/sdk/example) directory in the repository one ESM and one CJS file showing the canonical agent shape (schema with `@stackbone/sdk/db`, the runtime-injected `ctx.client`, async handler).
49
+ > Scaffold a workspace with the `stackbone` CLI (`stackbone init`, `stackbone add agent|workflow`). The dev loop, publishing and migrations are CLI concerns see the Stackbone CLI docs.
41
50
 
42
- ## Quick Start
51
+ ## What you build — a workspace
43
52
 
44
- ### The Client
53
+ A **workspace** is a project that contains **agents** and **workflows**, discovered **by convention from the files on disk**:
45
54
 
46
- You don't construct the client. The runtime builds a single `StackboneClient` per process — wired from the env vars the platform injects at provisioning time — and hands it to every handler as `ctx.client`. Define your agent with `defineAgent` and destructure `client` (and `input`) off the invocation context:
55
+ ```
56
+ my-workspace/
57
+ agents/
58
+ support/
59
+ agent.yaml ← per-agent manifest; name: must equal the folder basename
60
+ schema.ts ← Drizzle tables for stackbone.database
61
+ agent/
62
+ agent.ts ← model + build config (default export = defineStackboneAgent)
63
+ instructions.md ← the system prompt
64
+ tools/
65
+ read_config.ts ← one tool per file (default export = defineTool)
66
+ escalate.ts
67
+ workflows/
68
+ onboarding.workflow.ts ← 'use workflow' fn + sibling inputSchema / outputSchema
69
+ config.schema.ts ← optional: typed dynamic config (Zod)
70
+ stackbone.config.ts ← OPTIONAL override of the convention scan
71
+ ```
72
+
73
+ - Every `agents/<name>/agent.yaml` (whose `name:` equals the folder basename) is an **agent**.
74
+ - Every `workflows/<name>.workflow.ts` (exporting `<camelCase(name)>Workflow`) is a **workflow**.
75
+ - `stackbone.config.ts` is an **optional** override — most workspaces need none. When present, `defineWorkspace({ agents, workflows })` wins over the scan:
47
76
 
48
77
  ```ts
49
- import { defineAgent, z } from '@stackbone/sdk';
50
-
51
- export default defineAgent({
52
- invoke: {
53
- input: z.object({ email: z.string() }),
54
- output: z.object({ id: z.number() }),
55
- async run({ input, client }) {
56
- const [row] = await client.database
57
- .insert(leads)
58
- .values({ email: input.email, status: 'new', score: 0 })
59
- .returning();
60
- return { id: row.id };
78
+ // stackbone.config.ts OPTIONAL; only when convention discovery isn't enough
79
+ import { defineWorkspace } from '@stackbone/sdk';
80
+
81
+ export default defineWorkspace({
82
+ agents: [{ name: 'support', dir: 'agents/support' }],
83
+ workflows: [
84
+ {
85
+ name: 'onboarding',
86
+ module: 'workflows/onboarding.workflow.ts',
87
+ export: 'onboardingWorkflow',
61
88
  },
62
- },
89
+ ],
63
90
  });
64
91
  ```
65
92
 
66
- #### Escape hatch `createClient()`
93
+ ### Authoring an eve agent
67
94
 
68
- `createClient()` is still exported for when you need a client **outside** a handler (a script, a test, a one-off task) or want to override config explicitly useful for local development against the emulator or when running outside a Stackbone container. In normal agent code you never need it; reach for `ctx.client` instead.
95
+ An agent is a durable [eve](https://eve.dev/docs/introduction) agent a model + instructions + tools that hold a conversation across turns. The runtime serves it over a signed session API; you write no HTTP code and no Dockerfile.
69
96
 
70
97
  ```ts
71
- import { createClient } from '@stackbone/sdk';
72
-
73
- // Zero-arg: every option falls back to an injected env var.
74
- const stackbone = createClient();
75
-
76
- // Or pass overrides explicitly.
77
- const local = createClient({
78
- agentJwt: process.env.STACKBONE_AGENT_JWT,
79
- stackboneApiUrl: 'http://localhost:3000',
80
- agentId: 'agent_local_dev',
81
- openrouterKey: process.env.OPENROUTER_API_KEY,
98
+ // agents/support/agent/agent.ts
99
+ import { defineStackboneAgent } from '@stackbone/sdk/eve';
100
+
101
+ // `defineStackboneAgent` wraps eve's `defineAgent` with the Stackbone defaults:
102
+ // the managed OpenRouter model bridge (the org's sub-key is injected at runtime
103
+ // as OPENROUTER_API_KEY), the context window eve uses to size compaction, and
104
+ // the `externalDependencies` (`@stackbone/sdk` + `eve*`) that keep the agent on
105
+ // ONE invocation context — inlining the SDK gives it a second copy of the SDK's
106
+ // AsyncLocalStorage and per-run logs arrive with no run id. Publishing enforces
107
+ // the externals, so the helper always injects them.
108
+ export default defineStackboneAgent({
109
+ model: 'anthropic/claude-haiku-4.5',
82
110
  });
83
111
  ```
84
112
 
85
- ### Database
113
+ > Pass a built `LanguageModel` instead of a string to use another provider, and
114
+ > add `modelContextWindowTokens` / `build` to override a default. The raw form —
115
+ > `defineAgent` from **`eve`** (not `@stackbone/sdk`) with an explicit
116
+ > `createOpenAICompatible` provider — still works if you want full control.
86
117
 
87
- `client.database` is a lazy [Drizzle ORM](https://orm.drizzle.team/) instance bound to the agent's Postgres connection (`STACKBONE_POSTGRES_URL`). The full Drizzle surface (`select`, `insert`, `update`, `delete`, `transaction`, `execute`, `query`, `sql\`\``) is exposed verbatim, and the column builders live behind the `@stackbone/sdk/db`subpath so the creator never has to install`drizzle-orm`, `drizzle-orm/pg-core`or`postgres` directly.
118
+ The agent's name lives in `agents/<name>/agent.yaml` and must equal the folder basename; the system prompt is `agent/instructions.md`. A **tool** is one file under `agent/tools/`, default-exporting `defineTool` from `eve/tools` — its `execute()` is where you reach the ambient `stackbone` client:
88
119
 
89
120
  ```ts
90
- import { defineAgent, z } from '@stackbone/sdk';
91
- import { eq, integer, pgTable, text } from '@stackbone/sdk/db';
92
-
93
- // 1. Declare your tables — types flow end to end.
94
- const leads = pgTable('leads', {
95
- id: integer('id').primaryKey(),
96
- email: text('email').notNull(),
97
- status: text('status').notNull(),
98
- score: integer('score').notNull(),
99
- });
100
-
101
- export default defineAgent({
102
- invoke: {
103
- input: z.object({}),
104
- output: z.object({}),
105
- async run({ client }) {
106
- // 2. Query — `data` is typed from the schema, no codegen needed.
107
- const rows = await client.database
108
- .select()
109
- .from(leads)
110
- .where(eq(leads.status, 'qualified'))
111
- .limit(20);
112
-
113
- // 3. Mutate.
114
- await client.database
115
- .insert(leads)
116
- .values({ id: 1, email: 'jane@example.com', status: 'new', score: 0 });
117
-
118
- await client.database.update(leads).set({ status: 'contacted' }).where(eq(leads.id, 1));
119
-
120
- await client.database.delete(leads).where(eq(leads.id, 1));
121
-
122
- return {};
123
- },
121
+ // agents/support/agent/tools/escalate.ts
122
+ import { defineTool } from 'eve/tools';
123
+ import { stackbone, z } from '@stackbone/sdk';
124
+ import { escalations } from '../../schema';
125
+
126
+ export default defineTool({
127
+ description: 'Escalate this lead to a human sales rep.',
128
+ inputSchema: z.object({
129
+ leadId: z.string().describe('CRM contact id'),
130
+ reason: z.string().describe('Short reason for the hand-off'),
131
+ }),
132
+ async execute({ leadId, reason }) {
133
+ await stackbone.database.insert(escalations).values({ leadId, reason });
134
+ return { leadId, tagged: 'needs-human' };
124
135
  },
125
136
  });
126
137
  ```
127
138
 
128
- The snippets below assume the `client` destructured off `ctx` inside a handler (`async run({ client }) { ... }`).
139
+ ### Authoring a durable workflow
129
140
 
130
- Interactive transactions yield a `tx` with the same Drizzle surface as `client.database`:
141
+ A workflow is a plain async function marked `'use workflow'`, with its side-effects in helpers marked `'use step'`. Its public contract is **sibling `inputSchema` / `outputSchema` exports** — there is no `defineWorkflow` wrapper. Each `'use step'` is a checkpoint: it runs once, is persisted, and resumes from the last completed step after a crash, so keep every step **idempotent**.
131
142
 
132
143
  ```ts
133
- await client.database.transaction(async (tx) => {
134
- await tx.insert(leads).values({ id: 2, email: 'b@x.com', status: 'new', score: 0 });
135
- await tx.update(leads).set({ status: 'qualified' }).where(eq(leads.id, 2));
144
+ // workflows/onboarding.workflow.ts
145
+ import { stackbone, z } from '@stackbone/sdk';
146
+ import { welcomes } from '../agents/support/schema';
147
+
148
+ // THE contract — sibling exports the build harvests into the manifest + a validator.
149
+ export const inputSchema = z.object({
150
+ userId: z.string(),
151
+ email: z.email(),
152
+ plan: z.enum(['free', 'pro', 'scale']),
153
+ });
154
+ export const outputSchema = z.object({
155
+ userId: z.string(),
156
+ tips: z.array(z.string()),
136
157
  });
137
- ```
138
158
 
139
- For raw SQL, reach for the `sql` tag re-exported from `@stackbone/sdk/db`:
159
+ export async function onboardingWorkflow(input: z.infer<typeof inputSchema>) {
160
+ 'use workflow'; // cheap, deterministic glue — it replays on resume; do I/O in steps
140
161
 
141
- ```ts
142
- import { sql } from '@stackbone/sdk/db';
162
+ const signup = await validateSignup(input);
163
+ const saved = await persistWelcome(signup.userId);
164
+ return saved;
165
+ }
166
+
167
+ async function validateSignup(input: z.infer<typeof inputSchema>) {
168
+ 'use step';
169
+ if (!input.email.includes('@')) throw new Error(`Invalid email: ${input.email}`);
170
+ return { userId: input.userId, plan: input.plan };
171
+ }
143
172
 
144
- const { rows } = await client.database.execute(sql`SELECT NOW()`);
173
+ async function persistWelcome(userId: string) {
174
+ 'use step'; // idempotent side effect on userId
175
+ await stackbone.database.insert(welcomes).values({ userId });
176
+ return { userId, tips: ['Connect your inbox', 'Invite a teammate'] };
177
+ }
145
178
  ```
146
179
 
147
- > If `STACKBONE_POSTGRES_URL` is unset, the first call to `client.database` raises `database_not_configured` with a hint to run `stackbone dev`. Migrations are governed by the `stackbone db migrate *` CLI commands — `client.database` itself is just the runtime query surface.
180
+ ## The ambient `stackbone` client
148
181
 
149
- ### Storage
182
+ `import { stackbone } from '@stackbone/sdk'` — the single handle for every platform surface, reachable from any tool `execute()` or workflow step. It resolves config from the injected environment lazily on first use; there is no `createClient()` in normal code and you never pass connection strings.
150
183
 
151
- `client.storage` is bucket-scoped via `from(bucket)` and transparently prefixes every key with `${agentId}/${bucket}/` before talking to S3 / R2 / MinIO, so your agent code never has to think about multi-tenancy:
184
+ | Surface | What it is | Returns |
185
+ | -------------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------- |
186
+ | `stackbone.database` | **Native Drizzle ORM** over the agent's Neon (`pgvector`, `tsvector`) | rows / **throws** (no `{ data, error }`) |
187
+ | `stackbone.storage` | File uploads / downloads / signed URLs (R2 in prod, MinIO in dev) | `{ data, error }` |
188
+ | `stackbone.ai` | LLM chat / embeddings / vision via OpenRouter (300+ models); stream long completions | `{ data, error }` |
189
+ | `stackbone.rag` | Ingest + hybrid (`pgvector` + `tsvector`) retrieval; tables are platform-provisioned | `{ data, error }` |
190
+ | `stackbone.config` | Read dynamic config the operator edits in Studio: `get(key)` / `getAll()` | `{ data, error }` |
191
+ | `stackbone.secrets` | Read operator-managed encrypted secrets: `get(name)` / `getMany(names)` | `{ data, error }` |
192
+ | `stackbone.prompts` | Versioned prompt catalog: `get()` / `compile(key, vars)` / `list()` / `create()` | `{ data, error }` |
193
+ | `stackbone.agent(id)` | Call a sibling agent by name — opens a session, sends a turn | `result()` → `{ data, status }` |
194
+ | `stackbone.connection(id)` | Call a third-party connector by id (Stackbone Connect) | operation output / **throws** `ConnectorCallError` |
152
195
 
153
- ```ts
154
- // Upload an object
155
- const { data, error } = await client.storage
156
- .from('uploads')
157
- .upload('reports/2026-04-29.pdf', pdfBlob, {
158
- contentType: 'application/pdf',
159
- metadata: { source: 'leads-agent' },
160
- });
196
+ The one rule across every surface: **destructure `{ data, error }` and handle both branches.** The only exceptions are `stackbone.database` (native Drizzle — returns rows, throws) and `stackbone.connection` (returns the operation output, throws `ConnectorCallError`).
161
197
 
162
- // Download an object (returns a Blob)
163
- const { data, error } = await client.storage.from('uploads').download('reports/2026-04-29.pdf');
198
+ ### Database
164
199
 
165
- // List objects (paginated)
166
- const { data, error } = await client.storage
167
- .from('uploads')
168
- .list({ prefix: 'reports/', limit: 50 });
200
+ Native Drizzle — awaiting a query returns the typed rows and **throws** on error (handle with `try/catch` or let it bubble). Tables are `pgTable` objects from `agents/<name>/schema.ts`; the column builders and helpers (`eq`, `and`, `sql`, …) all come from `@stackbone/sdk/db`, so you never install `drizzle-orm` directly.
169
201
 
170
- // Delete an object
171
- const { data, error } = await client.storage.from('uploads').remove('reports/2026-04-29.pdf');
202
+ ```ts
203
+ import { eq, desc } from '@stackbone/sdk/db';
204
+ import { leads } from '../../schema';
205
+
206
+ const rows = await stackbone.database
207
+ .select()
208
+ .from(leads)
209
+ .where(eq(leads.status, 'qualified'))
210
+ .orderBy(desc(leads.createdAt))
211
+ .limit(20); // a select() resolves to an ARRAY — const [first] = rows
212
+
213
+ const [created] = await stackbone.database
214
+ .insert(leads)
215
+ .values({ email: 'jane@example.com', status: 'new', score: 0 })
216
+ .returning();
217
+
218
+ await stackbone.database.transaction(async (tx) => {
219
+ await tx.update(leads).set({ status: 'contacted' }).where(eq(leads.id, created.id));
220
+ });
172
221
  ```
173
222
 
174
- Signed URLs and a public URL helper are exposed for direct browser uploads / downloads:
223
+ ### Storage
224
+
225
+ Bucket-scoped via `from(bucket)`; every key is transparently prefixed with the agent id so your code never thinks about multi-tenancy:
175
226
 
176
227
  ```ts
177
- // Pre-signed upload URL (1h by default; clamp the contentType into the signature)
178
- const { data: upload } = await client.storage
228
+ const { data, error } = await stackbone.storage
179
229
  .from('uploads')
180
- .getSignedUploadUrl('avatars/user-42.png', {
181
- expiresIn: 600,
182
- contentType: 'image/png',
183
- });
230
+ .upload('reports/2026-04-29.pdf', pdfBlob, { contentType: 'application/pdf' });
231
+ if (error) throw new Error(error.message);
184
232
 
185
- // Pre-signed download URL — prefer this over `download()` for large objects,
186
- // since `download()` materialises the entire body in memory as a Blob.
187
- const { data: download } = await client.storage
233
+ // Prefer a signed URL over download() for large objects (download materialises the body).
234
+ const { data: url } = await stackbone.storage
188
235
  .from('uploads')
189
236
  .getSignedDownloadUrl('reports/2026-04-29.pdf');
190
-
191
- // Canonical public URL (only fetchable for public buckets)
192
- const { data: url } = client.storage.from('uploads').getPublicUrl('avatars/user-42.png');
193
237
  ```
194
238
 
195
239
  ### AI
196
240
 
197
- `client.ai` mirrors the official `openai` SDK shape but every request is routed through OpenRouter, so you can pick from 300+ models with a single API key. The Stackbone platform attributes usage and billing back to the agent through OpenRouter Activity no proxy in the middle.
241
+ OpenAI-compatible shape, routed through OpenRouter (300+ models, one managed sub-key). The envelope covers the handshake; once a stream is open, mid-flight errors surface through the iterator (`try/catch` your `for await`).
198
242
 
199
243
  ```ts
200
- // Chat completion
201
- const { data, error } = await client.ai.chat.completions.create({
244
+ const { data, error } = await stackbone.ai.chat.completions.create({
202
245
  model: 'anthropic/claude-sonnet-4.5',
203
- messages: [
204
- { role: 'system', content: 'You are a sales qualification assistant.' },
205
- { role: 'user', content: 'Score this lead: ...' },
206
- ],
246
+ messages: [{ role: 'user', content: 'Score this lead: ...' }],
207
247
  });
248
+ if (!error) console.log(data.choices[0]?.message.content);
208
249
 
209
- if (!error) {
210
- console.log(data.choices[0]?.message.content);
211
- }
212
- ```
213
-
214
- Streaming uses the same method with `stream: true`. The `Result` envelope only covers the handshake — once the stream is open, mid-flight errors propagate through the iterator, so wrap your `for await` loop in `try/catch`:
215
-
216
- ```ts
217
- const { data: stream, error } = await client.ai.chat.completions.create({
218
- model: 'anthropic/claude-sonnet-4.5',
219
- messages: [{ role: 'user', content: 'Stream me a haiku.' }],
220
- stream: true,
221
- });
222
-
223
- if (!error) {
224
- for await (const chunk of stream) {
225
- process.stdout.write(chunk.choices[0]?.delta?.content ?? '');
226
- }
227
- }
228
- ```
229
-
230
- Embeddings, image generation and a model catalogue are first-class too:
231
-
232
- ```ts
233
- // Embeddings — returns the standard OpenAI shape
234
- const { data, error } = await client.ai.embeddings.create({
250
+ const { data: emb } = await stackbone.ai.embeddings.create({
235
251
  model: 'openai/text-embedding-3-small',
236
252
  input: 'A red Spanish sword from the 11th century.',
237
253
  });
238
-
239
- // Image generation — OpenRouter routes image models through chat completions;
240
- // the SDK normalises the response so callers see the OpenAI-shaped payload.
241
- // Returns `ai_no_image_generated` if the model produces zero images.
242
- const { data, error } = await client.ai.images.generate({
243
- model: 'google/gemini-2.5-flash-image',
244
- prompt: 'A medieval Spanish sword on a velvet cushion, museum lighting',
245
- });
246
-
247
- // List available models — preserves OpenRouter-specific fields like
248
- // `pricing`, `context_length`, `supported_parameters`, `architecture`
249
- const { data, error } = await client.ai.models.list();
250
- ```
251
-
252
- Cancellation works the way you'd expect — pass an `AbortSignal` and aborts surface as `ai_aborted` on non-streaming calls:
253
-
254
- ```ts
255
- const controller = new AbortController();
256
- setTimeout(() => controller.abort(), 5_000);
257
-
258
- const { data, error } = await client.ai.chat.completions.create(
259
- { model: 'anthropic/claude-sonnet-4.5', messages: [...] },
260
- { signal: controller.signal },
261
- );
262
254
  ```
263
255
 
264
256
  ### RAG
265
257
 
266
- `client.rag` exposes a flat, ceremony-free retrieval API on top of `pgvector` running in the agent's Postgres branch. There are no collections to create, no handles to thread through your code: the schema is auto-provisioned on the first `ingest()` and the embedding dimensions are inferred from the first chunk.
267
-
268
- The fast path lets the SDK handle embeddings for you — pass an embedding `model` and the chunks as plain strings, and `client.rag` calls `client.ai.embeddings.create` internally with batching:
258
+ Flat ingest + hybrid retrieval over the platform-provisioned tables no collections to create:
269
259
 
270
260
  ```ts
271
- // 1. Parse a document → plain text. Supports text/*, text/markdown and application/pdf.
272
- const text = await client.rag.parse(file);
273
-
274
- // 2. Split into chunks. Pure utility, no DB call.
275
- const chunks = client.rag.chunk(text, { size: 512, overlap: 64 });
276
-
277
- // 3. Ingest. The SDK embeds the chunks for you in batches of 128 and
278
- // provisions the schema on the first call (HNSW + cosine, dims inferred
279
- // from the first batch). Re-ingesting the same `id` atomically replaces
280
- // all of that document's chunks.
281
- await client.rag.ingest({
282
- id: 'doc-1',
283
- chunks,
284
- model: 'openai/text-embedding-3-small',
285
- metadata: { source: 'manual-upload' },
261
+ await stackbone.rag.ingest({
262
+ collection: 'docs',
263
+ content: markdown,
264
+ contentType: 'text/markdown',
286
265
  });
287
266
 
288
- // 4. Retrieve. Pass the user's question and the same model — the SDK embeds
289
- // it for you and returns hits scored in [0, 1] (cosine similarity).
290
- const { data: hits } = await client.rag.retrieve({
291
- text: 'how does X work?',
292
- model: 'openai/text-embedding-3-small',
267
+ const { data: hits } = await stackbone.rag.search({
268
+ collection: 'docs',
269
+ query: 'how does X work?',
293
270
  topK: 5,
294
- filter: { source: 'manual-upload' },
295
271
  });
296
272
  ```
297
273
 
298
- The escape hatch pass embeddings precomputed yourself (different provider, custom dimensions, your own batching, deterministic offline tests):
274
+ > For durable ingest of large documents, prefer `ingestDocuments()` from `@stackbone/sdk/workflow` (runs the reserved `rag-ingest` workflow).
299
275
 
300
- ```ts
301
- const { data: embeddings } = await client.ai.embeddings.create({
302
- model: 'openai/text-embedding-3-small',
303
- input: chunks,
304
- });
276
+ ### Config, secrets & prompts
305
277
 
306
- await client.rag.ingest({
307
- id: 'doc-1',
308
- chunks: chunks.map((content, i) => ({
309
- content,
310
- embedding: embeddings.data[i].embedding,
311
- })),
312
- metadata: { source: 'manual-upload' },
313
- });
314
-
315
- const { data: hits } = await client.rag.retrieve({
316
- embedding: queryEmbedding,
317
- topK: 5,
318
- });
319
- ```
320
-
321
- Delete by id (whole document) or by metadata predicate:
322
-
323
- ```ts
324
- await client.rag.delete('doc-1');
325
- await client.rag.delete(['doc-1', 'doc-2']);
326
-
327
- await client.rag.deleteWhere({ source: 'manual-upload' });
328
- ```
329
-
330
- Logical separation between document sets is via an optional `namespace` (default `'default'`). Backed by an indexed column in the same table — no extra physical collections to manage:
331
-
332
- ```ts
333
- await client.rag.ingest({
334
- id: 'faq-1',
335
- chunks: [...],
336
- namespace: 'faqs',
337
- });
338
-
339
- await client.rag.retrieve({ embedding, namespace: 'faqs' });
340
- await client.rag.deleteWhere({}, { namespace: 'faqs' });
341
- ```
342
-
343
- If you switch embedding models (e.g. `text-embedding-3-small` → `text-embedding-3-large`) the dimensions will not match what the table was provisioned with. The next `ingest()` returns `rag_dim_mismatch` with the exact stored vs received dims. Recreate the schema with:
278
+ All three are `{ data, error }`, read-only, and key-checked. `config.schema.ts` (a Zod object at the workspace root) generates types so `stackbone.config.get('greeting')` is typed and a typo is a compile error.
344
279
 
345
280
  ```ts
346
- await client.rag.reset();
281
+ const { data: tone } = await stackbone.config.get('replyTone'); // typed from config.schema.ts
282
+ const { data: apiKey, error } = await stackbone.secrets.get('STRIPE_API_KEY');
283
+ const { data: body } = await stackbone.prompts.compile('welcome-email', { name: 'Jane' });
347
284
  ```
348
285
 
349
- > `parse()` is intentionally minimal: passthrough for text and markdown, `unpdf` for text-only PDFs (no OCR, no layout reconstruction). For complex documents (scanned PDFs, Office files, multi-column layouts) the recommended path is to parse upstream of the SDK and pass the resulting text into `chunk()` directly.
286
+ ### Calling a sibling agent
350
287
 
351
- ### Approval (Human-In-The-Loop)
352
-
353
- `client.approval` exposes a **fire-and-forget** primitive: the agent issues an approval request, the call returns immediately with the `approvalId` and a `callbackUrl`, and the control plane POSTs the human's decision to the endpoint the agent registered in `onDecide`. The callback is HMAC-signed (`Stackbone-Signature: t=<unix>,v1=<hex>`) so the agent can authenticate it without storing any per-approval secret.
354
-
355
- This shape decouples waiting from the container lifecycle — the agent does not have to keep a long-poll connection alive while a human reviews, which means it survives idle scale-to-zero and arbitrary numbers of concurrent approvals without tying up sockets.
356
-
357
- ```ts
358
- // 1. Issue an approval — returns immediately, the human reviews asynchronously.
359
- const { data, error } = await client.airoval.request({
360
- topic: 'lead.qualify',
361
- payload: { leadId: 42, score: 0.6 },
362
- title: 'Approve lead qualification',
363
- description: 'Score is below the auto-qualify threshold (0.7).',
364
- onDecide: '/approvals/lead-qualify',
365
- timeout: '24h', // ISO duration or ms; default 24h
366
- onTimeout: 'reject', // 'reject' | 'approve' | 'ignore'
367
- approver: 'team:sales',
368
- idempotencyKey: 'qualify-42',
369
- metadata: { runId: 'r_123' },
370
- });
371
- // data: { approvalId, status: 'pending', callbackUrl, expiresAt }
372
-
373
- // 2. Receive the decision — `verify()` authenticates the inbound POST and
374
- // returns a typed Decision<T>. Pass the raw `Request` (Fetch / Hono /
375
- // undici) and switch on `decision.status`.
376
- app.post('/approvals/lead-qualify', async (c) => {
377
- const result = await client.airoval.verify<{ leadId: number; score: number }>(c.req.raw);
378
- if (result.error) return c.text('invalid', 401);
379
- switch (result.data.status) {
380
- case 'approved':
381
- await processLead(result.data.payload);
382
- break;
383
- case 'rejected':
384
- await archiveLead(result.data.payload);
385
- break;
386
- case 'timed_out':
387
- case 'cancelled':
388
- // No payload — only `decidedAt` (and an optional `reason` for cancelled).
389
- break;
390
- }
391
- return c.json({ ok: true });
392
- });
393
- ```
394
-
395
- Operations on existing approvals:
288
+ From a workflow step (or another agent's tool), open a session and send a turn; the structured reply is validated against the `outputSchema` you pass:
396
289
 
397
290
  ```ts
398
- await client.airoval.cancel(approvalId, 'no longer needed');
399
- const { data: record } = await client.airoval.get<LeadPayload>(approvalId);
400
- const { data: page } = await client.airoval.list({
401
- status: 'pending',
402
- topic: 'lead.qualify',
403
- limit: 50,
404
- });
405
- ```
406
-
407
- #### Approval-gated LLM tools
408
-
409
- `client.airoval.tool()` wraps an LLM tool so the agent loop pauses for human review before `execute()` runs. When `needsApproval` returns `true`, `invoke()` opens an approval and returns a `pending` marker — the caller persists agent state and breaks the loop; the eventual decision callback (handled via `verify()`) is the signal to resume.
410
-
411
- ```ts
412
- const sendInvoice = client.airoval.tool({
413
- name: 'send_invoice',
414
- description: 'Send an invoice to a recipient.',
415
- parameters: {
416
- type: 'object',
417
- properties: {
418
- amount: { type: 'number' },
419
- recipient: { type: 'string' },
420
- },
421
- required: ['amount', 'recipient'],
422
- },
423
- needsApproval: ({ amount }) => amount > 1000,
424
- toRequest: (input) => ({
425
- onDecide: '/approvals/send-invoice',
426
- title: `Send invoice for $${input.amount}`,
427
- description: `Recipient: ${input.recipient}`,
428
- }),
429
- execute: async ({ amount, recipient }) => sendInvoiceImpl(amount, recipient),
430
- });
431
-
432
- // Pass the tool to chat.completions.create — it speaks the OpenAI shape.
433
- const completion = await client.ai.chat.completions.create({
434
- model: 'anthropic/claude-sonnet-4.5',
435
- messages,
436
- tools: [sendInvoice.openaiSpec()],
437
- });
438
-
439
- // Drive the tool calls the LLM emitted.
440
- for (const call of completion.data?.choices[0]?.message.tool_calls ?? []) {
441
- const r = await sendInvoice.invoke(JSON.parse(call.function.arguments));
442
- if (r.error) return { error: r.error };
443
- if (r.data.status === 'pending') {
444
- // Approval was created. Persist agent state and break the loop —
445
- // the decision callback will resume it.
446
- return { pending: r.data.approvalId };
447
- }
448
- messages.push({
449
- role: 'tool',
450
- tool_call_id: call.id,
451
- content: JSON.stringify(r.data.result),
291
+ async function askSupportForTips(plan: string) {
292
+ 'use step';
293
+ const session = stackbone.agent('support').session();
294
+ const response = await session.send<{ tips: string[] }>({
295
+ message: `A customer joined the "${plan}" plan. Give up to 3 onboarding tips.`,
296
+ outputSchema: z.object({ tips: z.array(z.string()) }),
452
297
  });
298
+ const result = await response.result(); // { data, status }
299
+ return result.data ?? { tips: [] };
453
300
  }
454
301
  ```
455
302
 
456
- The signing key for `verify()` is read from `STACKBONE_APPROVAL_SIGNING_KEY` (or `approvalSigningKey` on `createClient`). Timestamp tolerance defaults to 300 seconds; tighten or override per call with `verify(req, { toleranceSeconds: 60 })`.
303
+ ### Calling a connector
457
304
 
458
- ### Secrets
459
-
460
- `client.secrets` reads organization-encrypted secrets the user registered in the dashboard. The SDK is **read-only** by design — secrets are managed in the control plane, never written from inside an agent container. There is no client-side cache: every `get()` is a network call so dashboard rotations propagate immediately.
305
+ Run a brokered third-party operation directly — no agent, no model, no secret in the container. The call returns the operation output and **throws** a `ConnectorCallError` on failure (match `err.code`, never `instanceof`):
461
306
 
462
307
  ```ts
463
- // Single secret `secrets_not_found` error if the name is not registered.
464
- const { data: apiKey, error } = await client.secrets.get('STRIPE_API_KEY');
465
- if (!error) {
466
- const stripe = new Stripe(apiKey);
308
+ async function sendMail(input: { to: string; subject: string; body: string }) {
309
+ 'use step';
310
+ // Typed after `stackbone dev` generates .stackbone/connect.d.ts; equivalent to
311
+ // .call('sendMail', input). Use .call('chat.postMessage', args) for dotted ids.
312
+ return stackbone.connection('stub-mail').sendMail(input);
467
313
  }
468
-
469
- // Bulk read at boot — one round-trip; missing names come back as omissions.
470
- const { data: secrets } = await client.secrets.getMany(['STRIPE_API_KEY', 'SLACK_BOT_TOKEN']);
471
- // secrets: { STRIPE_API_KEY?: string; SLACK_BOT_TOKEN?: string }
472
- ```
473
-
474
- ### Config
475
-
476
- `client.config` reads dynamic per-agent configuration the user set in the dashboard (tone, thresholds, feature flags, prompts…). Like `secrets`, it is read-only and uncached — and the value is fully typed via the generic on `get`.
477
-
478
- ```ts
479
- // Typed read — the generic narrows `data` to `'formal' | 'casual'`.
480
- const { data: tone } = await client.config.get<'formal' | 'casual'>('reply_tone');
481
-
482
- // Complex JSON — pass the full shape as the generic.
483
- interface Preferences {
484
- tone: 'formal' | 'casual';
485
- maxEmailsPerDay: number;
486
- }
487
- const { data: prefs } = await client.config.get<Preferences>('preferences');
488
-
489
- // Bulk read — typed via the second generic; missing keys come back as omissions.
490
- const { data: cfg } = await client.config.getMany<{
491
- reply_tone: 'formal' | 'casual';
492
- max_emails: number;
493
- }>(['reply_tone', 'max_emails']);
494
314
  ```
495
315
 
496
- `secrets.get` returns `secrets_not_found` (with the name in `meta`) when the secret is missing; `config.get` returns `config_not_found` symmetrically. Both `getMany` calls return whatever subset the organization has registered — no error for missing entries.
316
+ ## Subpath entrypoints
497
317
 
498
- ### Memory (pending)
318
+ Some surfaces ship behind subpaths so a tool-only agent never loads the `eve`/`workflow` peers it doesn't use:
499
319
 
500
- Long-term memory is a pending surface: its public types (`AddMemoryRequest`, `MemoryItem`, `MemoryHit`, `MemoryScope`, …) are exported from `@stackbone/sdk` so creators can type their integrations today, but there is no live `client.memory` accessor yet. The mem0-backed runtime will be added in a future release and promoted into the Features list above. See the [Coming Soon](#coming-soon) section for the full pending set.
320
+ | Entrypoint | Key exports |
321
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
322
+ | `@stackbone/sdk/workflow` | `requestApproval`, the raw `defineHook` / `sleep` escape hatch, `ingestDocuments` (the trigger/schedule helpers moved to `stackbone.workflows.*` on the main barrel; the loose `startWorkflow` / `scheduleWorkflow` / … exports here are deprecated aliases) |
323
+ | `@stackbone/sdk/connect` | `connect` (author an eve connection with brokered auth) |
324
+ | `@stackbone/sdk/db` | Drizzle re-exports — column builders (`pgTable`, `text`, `vector`, …) and helpers (`eq`, `and`, `sql`, …) |
325
+ | `@stackbone/sdk/db/testing` | `createTestDatabase` (spin up a throwaway Postgres for deterministic schema tests) |
501
326
 
502
- ### Coming Soon
327
+ ### Triggering & scheduling workflows
503
328
 
504
- The following surfaces have stable public **types** (so creators can write code against them today) but no runtime yet. They live under `src/surfaces/pending/` in the SDK source and do NOT appear as `client.X` accessors importing the types directly from `@stackbone/sdk` is the supported path while the runtime is in flight:
329
+ From inside a workflow, start or schedule another workflow **by name** through the ambient `stackbone.workflows` surface (these bind on dispatch, so call them from a workflow body or step, not a tool):
505
330
 
506
331
  ```ts
507
- import type {
508
- PublishRequest,
509
- AddMemoryRequest,
510
- MemoryHit,
511
- Prompt,
512
- CreatePromptRequest,
513
- } from '@stackbone/sdk';
332
+ import { stackbone } from '@stackbone/sdk';
333
+
334
+ const { runId } = await stackbone.workflows.start('reconcile', { invoiceId }); // fire-and-forget → its own run
335
+ const summary = await stackbone.workflows.startAndWait<Summary>('summarize', { docId }); // durably wait for the output
336
+ await stackbone.workflows.schedule('daily-digest', { scope: 'all' }, '0 9 * * *'); // dynamic cron, idempotent by name
514
337
  ```
515
338
 
516
- The pending surfaces are `queues` (cross-container HTTP push), `memory` (mem0-backed long-term memory), `prompts` (managed prompts with `{{var}}` templates), `connections` (OAuth integrations) and `events` (organisation event bus). When their runtime lands they will be promoted into the live `client.X` set described in the Features section above.
339
+ `name` is the `*.workflow.ts` convention name (narrowed to your declared workflows once `stackbone dev` generates `.stackbone/workflows.d.ts`); the input is validated against the target's `inputSchema` before anything is enqueued. There is no separate queue system — durable workflows _are_ the unit of background and scheduled work. For schedules that ship with the workspace, prefer the declarative `export const schedules = [{ cron, input }]` next to the workflow. (The loose `startWorkflow` / `startWorkflowAndWait` / `scheduleWorkflow` / `unschedule` / `listSchedules` exports on `@stackbone/sdk/workflow` still work but are deprecated aliases of these `stackbone.workflows.*` members.)
517
340
 
518
- ## Configuration
341
+ ### Human-in-the-loop — `requestApproval`
519
342
 
520
- The runtime-injected `ctx.client` is already configured from the env vars the platform injects, so agent code rarely touches these fields. They matter when you reach for the `createClient()` escape hatch: `createClient` accepts a single configuration object where every field is optional and falls back to an environment variable, which the platform injects into the agent container at boot:
343
+ A workflow that needs a person to decide pauses **durably** with `requestApproval()` from `@stackbone/sdk/workflow`. It writes a row the Studio inbox shows, then races the human decision against a timeout. **Call it from the workflow body, never inside a `'use step'`** the pause is a workflow primitive that suspends the run.
521
344
 
522
345
  ```ts
523
- const stackbone = createClient({
524
- // Ed25519 JWT signed by the control plane → STACKBONE_AGENT_JWT
525
- agentJwt: '...',
526
- // Stackbone control plane base URL (used by facades) → STACKBONE_API_URL
527
- stackboneApiUrl: 'https://api.stackbone.ai',
528
- // Stable agent identifier used as storage key prefix → STACKBONE_AGENT_ID
529
- agentId: 'agent_abc123',
530
- // Postgres connection string for `client.database`, `client.rag` and the
531
- // observability exporter → STACKBONE_POSTGRES_URL
532
- databaseUrl: 'postgresql://...',
533
- // OpenRouter credentials → OPENROUTER_API_KEY / OPENROUTER_BASE_URL
534
- openrouterKey: '...',
535
- openrouterBaseUrl: 'https://openrouter.ai/api/v1',
536
- // RAG document parser → LLAMA_PARSE_API_KEY
537
- llamaParseApiKey: '...',
538
- // mem0 credentials (memory module) MEM0_API_KEY / MEM0_BASE_URL
539
- mem0ApiKey: '...',
540
- mem0BaseUrl: 'https://api.mem0.ai',
541
- // HMAC key for verifying approval-decision callbacks → STACKBONE_APPROVAL_SIGNING_KEY
542
- approvalSigningKey: '...',
543
- // Object storage credentials → STACKBONE_S3_ACCESS_KEY / STACKBONE_S3_SECRET_KEY / STACKBONE_S3_ENDPOINT / STACKBONE_S3_BUCKET / STACKBONE_S3_REGION
544
- s3: {
545
- accessKeyId: '...',
546
- secretAccessKey: '...',
547
- endpoint: 'https://<account>.r2.cloudflarestorage.com',
548
- bucket: 'stackbone-prod',
549
- region: 'auto',
550
- },
551
- // OpenTelemetry exporter
552
- otel: {
553
- exporterOtlpEndpoint: 'https://otel.stackbone.ai',
554
- resourceAttributes: 'service.name=my-agent',
555
- },
556
- });
346
+ import { requestApproval } from '@stackbone/sdk/workflow';
347
+
348
+ export async function refundWorkflow(input: z.infer<typeof inputSchema>) {
349
+ 'use workflow';
350
+
351
+ const decision = await requestApproval({
352
+ token: `refund-${input.orderId}`, // resume key — unique per approval in the run
353
+ topic: 'refund',
354
+ payload: { orderId: input.orderId, amount: input.amount },
355
+ title: 'Approve refund',
356
+ timeout: '24h', // ISO-8601 duration or ms
357
+ fallback: 'reject', // applied if the timeout wins the race
358
+ });
359
+
360
+ // status === 'approved' is the ONLY green light; gate the side-effect on it.
361
+ if (decision.status !== 'approved') {
362
+ return { orderId: input.orderId, refunded: false, decision: decision.status };
363
+ }
364
+ await performRefund(input.orderId, input.amount); // a non-idempotent step, gated
365
+ return { orderId: input.orderId, refunded: true, decision: decision.status };
366
+ }
557
367
  ```
558
368
 
559
- Inside a Stackbone-hosted container all of these are pre-populated, so production agent code just reads `ctx.client` (a zero-arg `createClient()` would resolve the same values). For local development the `stackbone dev` emulator wires up the same env vars against MinIO, a local Postgres branch and your own OpenRouter key.
369
+ `requestApproval` resolves to `{ status: 'approved' | 'rejected', payload?, timedOut }` `timedOut` is `true` only when the `fallback` fired. A human resolves it from the Studio inbox or the `stackbone hitl` CLI.
560
370
 
561
371
  ## Result Envelope and Error Handling
562
372
 
563
- Every SDK method returns the same discriminated union:
373
+ Every SDK method (except `stackbone.database` and `stackbone.connection`, which throw) returns the same discriminated union:
564
374
 
565
375
  ```ts
566
376
  type Result<T> = { data: T; error: null } | { data: null; error: SdkError };
@@ -576,45 +386,39 @@ interface SdkError {
576
386
  Narrowing on `error` automatically refines `data` to `T`, so the typical caller looks like:
577
387
 
578
388
  ```ts
579
- const result = await client.ai.chat.completions.create({ model, messages });
389
+ const result = await stackbone.ai.chat.completions.create({ model, messages });
580
390
 
581
391
  if (result.error) {
582
392
  console.error(`[${result.error.code}] ${result.error.message}`, result.error.meta);
583
- return;
393
+ return; // or: throw new Error(result.error.message) to propagate
584
394
  }
585
395
 
586
396
  console.log(result.data.choices[0]?.message.content);
587
397
  ```
588
398
 
589
- Each module ships its own stable code prefix so you can pattern-match without parsing the message:
590
-
591
- | Prefix | Source | Examples |
592
- | ----------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
593
- | `ai_*` | `client.ai` | `ai_unauthorized`, `ai_credits_exhausted`, `ai_rate_limited`, `ai_aborted`, `ai_no_image_generated` |
594
- | `s3_*` | `client.storage` | `s3_credentials_missing`, `s3_invalid_key`, `s3_error` |
595
- | `rag_*` | `client.rag` | `rag_invalid_request`, `rag_dim_mismatch`, `rag_embedding_failed`, `rag_error` |
596
- | `approval_*` | `client.approval` | `approval_invalid_request`, `approval_invalid_signature`, `approval_signature_expired`, `approval_signing_key_missing`, `approval_invalid_payload`, `approval_tool_execute_failed`, `approval_unauthorized`, `approval_forbidden`, `approval_not_found`, `approval_rate_limited`, `approval_unavailable` |
597
- | `secrets_*` | `client.secrets` | `secrets_invalid_request`, `secrets_not_found`, `secrets_invalid_response`, `secrets_unauthorized`, `secrets_forbidden`, `secrets_rate_limited`, `secrets_unavailable` |
598
- | `config_*` | `client.config` | `config_invalid_request`, `config_not_found`, `config_invalid_response`, `config_unauthorized`, `config_forbidden`, `config_rate_limited`, `config_unavailable` |
599
- | `memory_*` | `client.memory` | reserved for the future mem0-backed implementation; today every method returns `not_implemented` |
600
- | `http_*` | transport-level errors | `http_timeout`, `http_aborted`, `http_network_error`, `http_parse_error`, `http_request_failed` — surface-level statuses (401/403/404/429/5xx) remap to `<surface>_*` codes |
601
- | `database_*` | `client.database` | `database_not_configured` (raised when `STACKBONE_POSTGRES_URL` is unset) |
602
- | `observability_*` | run-cost rollup (`@stackbone/sdk/observability`, runtime-wired) | `observability_close_run_failed` |
603
- | `contract_*` | gated surfaces | `contract_malformed`, `contract_unreachable`, `contract_version_unsupported` — emitted by any gated call when the contract handshake fails |
604
- | `capability_*` | gated surfaces | `capability_unavailable` emitted when the negotiated contract advertises no support for the surface |
605
- | `*_missing` | configuration | `agent_id_missing`, `openrouter_key_missing`, `database_url_missing`, `stackbone_api_url_missing` |
606
- | `not_implemented` | stubbed module surface | returned by every "coming soon" method until it ships |
607
-
608
- > The canonical inventory of every code lives in `src/errors/codes.ts` as a typed catalog (`SdkErrorCode`). The table above is the human-readable summary; adding or removing a code is a single-file edit there and the compiler refuses any literal `code` value not declared in the catalog. Pattern-match against `SdkErrorCode` (exported from `@stackbone/sdk`) for full type narrowing, or call `isSdkErrorCode(raw)` to widen a wire string back into the catalog.
399
+ `throw` to propagate a failure; branch on `error.code` to handle a known case and `return` instead. **Never swallow the `error` branch.** Each surface ships its own stable code prefix so you can pattern-match without parsing the message:
400
+
401
+ | Prefix | Source | Examples |
402
+ | ------------ | -------------------- | --------------------------------------------------------------------------------------------------- |
403
+ | `ai_*` | `stackbone.ai` | `ai_unauthorized`, `ai_credits_exhausted`, `ai_rate_limited`, `ai_aborted` |
404
+ | `s3_*` | `stackbone.storage` | `s3_credentials_missing`, `s3_invalid_key`, `s3_error` |
405
+ | `rag_*` | `stackbone.rag` | `rag_invalid_request`, `rag_dim_mismatch`, `rag_embedding_failed`, `rag_error` |
406
+ | `approval_*` | `stackbone.approval` | `approval_invalid_request`, `approval_persist_failed`, `approval_not_found`, `approval_unavailable` |
407
+ | `secrets_*` | `stackbone.secrets` | `secrets_invalid_request`, `secrets_not_found`, `secrets_unavailable` |
408
+ | `config_*` | `stackbone.config` | `config_invalid_request`, `config_not_found`, `config_unavailable` |
409
+ | `prompts_*` | `stackbone.prompts` | `prompts_not_found`, `prompts_invalid_request`, `prompts_unavailable` |
410
+ | `http_*` | transport-level | `http_timeout`, `http_aborted`, `http_network_error` — surface statuses remap to `<surface>_*` |
411
+
412
+ `stackbone.database` throws Postgres/Drizzle errors (no envelope); `stackbone.connection` throws a `ConnectorCallError` whose `err.code` follows the broker taxonomy (`invalid_args`, `credential_error`, `timeout`, `ambiguous`, `unauthorized`, `unavailable`).
413
+
414
+ > The canonical inventory of every envelope code lives in `src/errors/codes.ts` as a typed catalog (`SdkErrorCode`, exported from `@stackbone/sdk`). The compiler refuses any literal `code` not declared there; call `isSdkErrorCode(raw)` to widen a wire string back into the catalog.
609
415
 
610
416
  ## TypeScript Support
611
417
 
612
- The SDK is written in TypeScript and ships its own type definitions:
418
+ The SDK is written in TypeScript and ships its own type definitions. The ambient client, the `Result<T>` envelope and the surface types are all typed end to end:
613
419
 
614
420
  ```ts
615
- import { createClient, StackboneClient, type Result, type SdkError } from '@stackbone/sdk';
616
-
617
- const stackbone: StackboneClient = createClient();
421
+ import { stackbone, z, type Result, type SdkError } from '@stackbone/sdk';
618
422
 
619
423
  const result = await stackbone.ai.chat.completions.create({
620
424
  model: 'anthropic/claude-sonnet-4.5',
@@ -622,28 +426,19 @@ const result = await stackbone.ai.chat.completions.create({
622
426
  });
623
427
  ```
624
428
 
625
- Chat, embeddings and streaming types are re-exported directly from the `openai` packageby design, an agent author who already knows the OpenAI SDK does not have to relearn anything.
626
-
627
- For typed queue payloads, augment the `QueueJobs` registry once and both publisher and consumer pick up the shape:
429
+ Chat, embeddings and streaming types are re-exported directly from the OpenAI-compatible layer — an author who already knows the OpenAI SDK does not have to relearn anything. `stackbone.config.get(...)`, the workflow trigger names, and `stackbone.connection(id).<operation>` all narrow to your own workspace once `stackbone dev` has generated the `.stackbone/*.d.ts` ambient types.
628
430
 
629
- ```ts
630
- declare module '@stackbone/sdk/queues/types' {
631
- interface QueueJobs {
632
- 'lead.qualified': { leadId: number; score: number };
633
- 'send-report': { reportId: string; recipient: string };
634
- }
635
- }
636
- ```
431
+ > **Advanced escape hatch.** A `createClient()` is still exported for code that runs **outside** a tool/step (a script, a test, a one-off) or that must override config explicitly. In normal workspace code you never need it — reach for the ambient `stackbone` instead.
637
432
 
638
433
  ## Runtime
639
434
 
640
- The SDK targets Node.js 24+ inside the agent container. It is not designed for browser bundles — direct database access, S3 SDK usage and the agent JWT all assume a server-side runtime. For browser-side flows (uploads, public asset access), use the signed URLs returned by `client.storage` and let the browser hit S3 directly.
435
+ The SDK targets Node.js 24+ inside the agent container. It is not designed for browser bundles — direct database access, S3 SDK usage and the signed runtime calls all assume a server-side runtime. For browser-side flows (uploads, public asset access), use the signed URLs returned by `stackbone.storage` and let the browser hit S3 directly. Observability needs no configuration: the runtime auto-instruments outbound calls and correlates `console.*` output with the right run.
641
436
 
642
437
  ## Documentation
643
438
 
644
439
  - **[Stackbone TECH_STACK](../../docs/TECH_STACK.md)** — Normative stack and platform primitives
645
- - **[ADR — SDK monolítico del agente](../../docs/arquitectura/decisiones/2026-04-24-sdk-monolitico-agente.md)** — Why a single SDK, scope of every module, error model
646
- - **[Componente — SDK & Creator DX](../../docs/arquitectura/componentes/04-sdk-creator-dx.md)** — End-to-end developer experience for agent creators
440
+ - **[ADR — SDK monolítico del agente](../../docs/arquitectura/decisiones/2026-04-24-sdk-monolitico-agente.md)** — Why a single SDK, scope of every surface, error model
441
+ - **[Componente — SDK & Creator DX](../../docs/arquitectura/componentes/04-sdk-creator-dx.md)** — End-to-end developer experience for creators
647
442
 
648
443
  ## License
649
444