@stackbone/sdk 0.1.0-alpha.7 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +266 -476
- package/{workflow-scheduler-DXCNKDOS.d.cts → agent-registry-BNXuj88Q.d.cts} +137 -58
- package/{workflow-scheduler-DXCNKDOS.d.ts → agent-registry-BNXuj88Q.d.ts} +137 -58
- package/eve.cjs +52 -0
- package/eve.cjs.map +1 -0
- package/eve.d.cts +41 -0
- package/eve.d.ts +41 -0
- package/eve.js +47 -0
- package/eve.js.map +1 -0
- package/index.cjs +83 -28
- package/index.cjs.map +1 -1
- package/index.d.cts +53 -5
- package/index.d.ts +53 -5
- package/index.js +83 -28
- package/index.js.map +1 -1
- package/observability/index.cjs +1 -1
- package/observability/index.cjs.map +1 -1
- package/observability/index.js +1 -1
- package/observability/index.js.map +1 -1
- package/package.json +15 -1
- package/stackbone-sdk-0.1.0.tgz +0 -0
- package/workflow.cjs +67 -40
- package/workflow.cjs.map +1 -1
- package/workflow.d.cts +3 -82
- package/workflow.d.ts +3 -82
- package/workflow.js +67 -40
- package/workflow.js.map +1 -1
- package/stackbone-sdk-0.1.0-alpha.7.tgz +0 -0
package/README.md
CHANGED
|
@@ -4,563 +4,368 @@
|
|
|
4
4
|
[](https://nodejs.org/)
|
|
5
5
|
[](https://www.typescriptlang.org/)
|
|
6
6
|
|
|
7
|
-
Official TypeScript SDK for [Stackbone](https://stackbone.ai) — the marketplace and managed runtime for
|
|
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
|
-
- **
|
|
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 wiring — config 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** —
|
|
16
|
-
- **RAG** — Document
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **Config** — Typed reads of dynamic per-agent
|
|
21
|
-
- **
|
|
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
|
|
26
|
+
`@stackbone/sdk` is the SDK every Stackbone workspace depends on. From a generated workspace:
|
|
28
27
|
|
|
29
28
|
```bash
|
|
30
|
-
# Alpha (today)
|
|
31
|
-
pnpm add @stackbone/sdk@alpha
|
|
32
|
-
|
|
33
|
-
# Once we cut a stable release the unscoped tag will resolve to the latest
|
|
34
|
-
# minor:
|
|
35
29
|
pnpm add @stackbone/sdk
|
|
36
30
|
```
|
|
37
31
|
|
|
32
|
+
`@stackbone/sdk` declares `eve` and `workflow` as **optional peer dependencies**. Install only the ones your workspace uses:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pnpm add eve # to author an agent (agent.ts / tools)
|
|
36
|
+
pnpm add workflow # to author a workflow that pauses for approval
|
|
37
|
+
pnpm add @ai-sdk/openai-compatible # to bind the OpenRouter-compatible model in agent.ts
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
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.
|
|
41
|
+
|
|
38
42
|
> 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
43
|
|
|
40
|
-
|
|
44
|
+
> 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.
|
|
45
|
+
|
|
46
|
+
## What you build — a workspace
|
|
41
47
|
|
|
42
|
-
|
|
48
|
+
A **workspace** is a project that contains **agents** and **workflows**, discovered **by convention from the files on disk**:
|
|
43
49
|
|
|
44
|
-
|
|
50
|
+
```
|
|
51
|
+
my-workspace/
|
|
52
|
+
agents/
|
|
53
|
+
support/
|
|
54
|
+
agent.yaml ← per-agent manifest; name: must equal the folder basename
|
|
55
|
+
schema.ts ← Drizzle tables for stackbone.database
|
|
56
|
+
agent/
|
|
57
|
+
agent.ts ← model + build config (default export = defineStackboneAgent)
|
|
58
|
+
instructions.md ← the system prompt
|
|
59
|
+
tools/
|
|
60
|
+
read_config.ts ← one tool per file (default export = defineTool)
|
|
61
|
+
escalate.ts
|
|
62
|
+
workflows/
|
|
63
|
+
onboarding.workflow.ts ← 'use workflow' fn + sibling inputSchema / outputSchema
|
|
64
|
+
config.schema.ts ← optional: typed dynamic config (Zod)
|
|
65
|
+
stackbone.config.ts ← OPTIONAL override of the convention scan
|
|
66
|
+
```
|
|
45
67
|
|
|
46
|
-
|
|
68
|
+
- Every `agents/<name>/agent.yaml` (whose `name:` equals the folder basename) is an **agent**.
|
|
69
|
+
- Every `workflows/<name>.workflow.ts` (exporting `<camelCase(name)>Workflow`) is a **workflow**.
|
|
70
|
+
- `stackbone.config.ts` is an **optional** override — most workspaces need none. When present, `defineWorkspace({ agents, workflows })` wins over the scan:
|
|
47
71
|
|
|
48
72
|
```ts
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
.returning();
|
|
60
|
-
return { id: row.id };
|
|
73
|
+
// stackbone.config.ts — OPTIONAL; only when convention discovery isn't enough
|
|
74
|
+
import { defineWorkspace } from '@stackbone/sdk';
|
|
75
|
+
|
|
76
|
+
export default defineWorkspace({
|
|
77
|
+
agents: [{ name: 'support', dir: 'agents/support' }],
|
|
78
|
+
workflows: [
|
|
79
|
+
{
|
|
80
|
+
name: 'onboarding',
|
|
81
|
+
module: 'workflows/onboarding.workflow.ts',
|
|
82
|
+
export: 'onboardingWorkflow',
|
|
61
83
|
},
|
|
62
|
-
|
|
84
|
+
],
|
|
63
85
|
});
|
|
64
86
|
```
|
|
65
87
|
|
|
66
|
-
|
|
88
|
+
### Authoring an eve agent
|
|
67
89
|
|
|
68
|
-
|
|
90
|
+
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
91
|
|
|
70
92
|
```ts
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
// agents/support/agent/agent.ts
|
|
94
|
+
import { defineStackboneAgent } from '@stackbone/sdk/eve';
|
|
95
|
+
|
|
96
|
+
// `defineStackboneAgent` wraps eve's `defineAgent` with the Stackbone defaults:
|
|
97
|
+
// the managed OpenRouter model bridge (the org's sub-key is injected at runtime
|
|
98
|
+
// as OPENROUTER_API_KEY), the context window eve uses to size compaction, and
|
|
99
|
+
// the `externalDependencies` (`@stackbone/sdk` + `eve*`) that keep the agent on
|
|
100
|
+
// ONE invocation context — inlining the SDK gives it a second copy of the SDK's
|
|
101
|
+
// AsyncLocalStorage and per-run logs arrive with no run id. Publishing enforces
|
|
102
|
+
// the externals, so the helper always injects them.
|
|
103
|
+
export default defineStackboneAgent({
|
|
104
|
+
model: 'anthropic/claude-haiku-4.5',
|
|
82
105
|
});
|
|
83
106
|
```
|
|
84
107
|
|
|
85
|
-
|
|
108
|
+
> Pass a built `LanguageModel` instead of a string to use another provider, and
|
|
109
|
+
> add `modelContextWindowTokens` / `build` to override a default. The raw form —
|
|
110
|
+
> `defineAgent` from **`eve`** (not `@stackbone/sdk`) with an explicit
|
|
111
|
+
> `createOpenAICompatible` provider — still works if you want full control.
|
|
86
112
|
|
|
87
|
-
|
|
113
|
+
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
114
|
|
|
89
115
|
```ts
|
|
90
|
-
|
|
91
|
-
import {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
},
|
|
116
|
+
// agents/support/agent/tools/escalate.ts
|
|
117
|
+
import { defineTool } from 'eve/tools';
|
|
118
|
+
import { stackbone, z } from '@stackbone/sdk';
|
|
119
|
+
import { escalations } from '../../schema';
|
|
120
|
+
|
|
121
|
+
export default defineTool({
|
|
122
|
+
description: 'Escalate this lead to a human sales rep.',
|
|
123
|
+
inputSchema: z.object({
|
|
124
|
+
leadId: z.string().describe('CRM contact id'),
|
|
125
|
+
reason: z.string().describe('Short reason for the hand-off'),
|
|
126
|
+
}),
|
|
127
|
+
async execute({ leadId, reason }) {
|
|
128
|
+
await stackbone.database.insert(escalations).values({ leadId, reason });
|
|
129
|
+
return { leadId, tagged: 'needs-human' };
|
|
124
130
|
},
|
|
125
131
|
});
|
|
126
132
|
```
|
|
127
133
|
|
|
128
|
-
|
|
134
|
+
### Authoring a durable workflow
|
|
129
135
|
|
|
130
|
-
|
|
136
|
+
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
137
|
|
|
132
138
|
```ts
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
// workflows/onboarding.workflow.ts
|
|
140
|
+
import { stackbone, z } from '@stackbone/sdk';
|
|
141
|
+
import { welcomes } from '../agents/support/schema';
|
|
142
|
+
|
|
143
|
+
// THE contract — sibling exports the build harvests into the manifest + a validator.
|
|
144
|
+
export const inputSchema = z.object({
|
|
145
|
+
userId: z.string(),
|
|
146
|
+
email: z.email(),
|
|
147
|
+
plan: z.enum(['free', 'pro', 'scale']),
|
|
148
|
+
});
|
|
149
|
+
export const outputSchema = z.object({
|
|
150
|
+
userId: z.string(),
|
|
151
|
+
tips: z.array(z.string()),
|
|
136
152
|
});
|
|
137
|
-
```
|
|
138
153
|
|
|
139
|
-
|
|
154
|
+
export async function onboardingWorkflow(input: z.infer<typeof inputSchema>) {
|
|
155
|
+
'use workflow'; // cheap, deterministic glue — it replays on resume; do I/O in steps
|
|
140
156
|
|
|
141
|
-
|
|
142
|
-
|
|
157
|
+
const signup = await validateSignup(input);
|
|
158
|
+
const saved = await persistWelcome(signup.userId);
|
|
159
|
+
return saved;
|
|
160
|
+
}
|
|
143
161
|
|
|
144
|
-
|
|
162
|
+
async function validateSignup(input: z.infer<typeof inputSchema>) {
|
|
163
|
+
'use step';
|
|
164
|
+
if (!input.email.includes('@')) throw new Error(`Invalid email: ${input.email}`);
|
|
165
|
+
return { userId: input.userId, plan: input.plan };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function persistWelcome(userId: string) {
|
|
169
|
+
'use step'; // idempotent side effect on userId
|
|
170
|
+
await stackbone.database.insert(welcomes).values({ userId });
|
|
171
|
+
return { userId, tips: ['Connect your inbox', 'Invite a teammate'] };
|
|
172
|
+
}
|
|
145
173
|
```
|
|
146
174
|
|
|
147
|
-
|
|
175
|
+
## The ambient `stackbone` client
|
|
148
176
|
|
|
149
|
-
|
|
177
|
+
`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
178
|
|
|
151
|
-
|
|
179
|
+
| Surface | What it is | Returns |
|
|
180
|
+
| -------------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------- |
|
|
181
|
+
| `stackbone.database` | **Native Drizzle ORM** over the agent's Neon (`pgvector`, `tsvector`) | rows / **throws** (no `{ data, error }`) |
|
|
182
|
+
| `stackbone.storage` | File uploads / downloads / signed URLs (R2 in prod, MinIO in dev) | `{ data, error }` |
|
|
183
|
+
| `stackbone.ai` | LLM chat / embeddings / vision via OpenRouter (300+ models); stream long completions | `{ data, error }` |
|
|
184
|
+
| `stackbone.rag` | Ingest + hybrid (`pgvector` + `tsvector`) retrieval; tables are platform-provisioned | `{ data, error }` |
|
|
185
|
+
| `stackbone.config` | Read dynamic config the operator edits in Studio: `get(key)` / `getAll()` | `{ data, error }` |
|
|
186
|
+
| `stackbone.secrets` | Read operator-managed encrypted secrets: `get(name)` / `getMany(names)` | `{ data, error }` |
|
|
187
|
+
| `stackbone.prompts` | Versioned prompt catalog: `get()` / `compile(key, vars)` / `list()` / `create()` | `{ data, error }` |
|
|
188
|
+
| `stackbone.agent(id)` | Call a sibling agent by name — opens a session, sends a turn | `result()` → `{ data, status }` |
|
|
189
|
+
| `stackbone.connection(id)` | Call a third-party connector by id (Stackbone Connect) | operation output / **throws** `ConnectorCallError` |
|
|
152
190
|
|
|
153
|
-
|
|
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
|
-
});
|
|
191
|
+
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
192
|
|
|
162
|
-
|
|
163
|
-
const { data, error } = await client.storage.from('uploads').download('reports/2026-04-29.pdf');
|
|
193
|
+
### Database
|
|
164
194
|
|
|
165
|
-
|
|
166
|
-
const { data, error } = await client.storage
|
|
167
|
-
.from('uploads')
|
|
168
|
-
.list({ prefix: 'reports/', limit: 50 });
|
|
195
|
+
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
196
|
|
|
170
|
-
|
|
171
|
-
|
|
197
|
+
```ts
|
|
198
|
+
import { eq, desc } from '@stackbone/sdk/db';
|
|
199
|
+
import { leads } from '../../schema';
|
|
200
|
+
|
|
201
|
+
const rows = await stackbone.database
|
|
202
|
+
.select()
|
|
203
|
+
.from(leads)
|
|
204
|
+
.where(eq(leads.status, 'qualified'))
|
|
205
|
+
.orderBy(desc(leads.createdAt))
|
|
206
|
+
.limit(20); // a select() resolves to an ARRAY — const [first] = rows
|
|
207
|
+
|
|
208
|
+
const [created] = await stackbone.database
|
|
209
|
+
.insert(leads)
|
|
210
|
+
.values({ email: 'jane@example.com', status: 'new', score: 0 })
|
|
211
|
+
.returning();
|
|
212
|
+
|
|
213
|
+
await stackbone.database.transaction(async (tx) => {
|
|
214
|
+
await tx.update(leads).set({ status: 'contacted' }).where(eq(leads.id, created.id));
|
|
215
|
+
});
|
|
172
216
|
```
|
|
173
217
|
|
|
174
|
-
|
|
218
|
+
### Storage
|
|
219
|
+
|
|
220
|
+
Bucket-scoped via `from(bucket)`; every key is transparently prefixed with the agent id so your code never thinks about multi-tenancy:
|
|
175
221
|
|
|
176
222
|
```ts
|
|
177
|
-
|
|
178
|
-
const { data: upload } = await client.storage
|
|
223
|
+
const { data, error } = await stackbone.storage
|
|
179
224
|
.from('uploads')
|
|
180
|
-
.
|
|
181
|
-
|
|
182
|
-
contentType: 'image/png',
|
|
183
|
-
});
|
|
225
|
+
.upload('reports/2026-04-29.pdf', pdfBlob, { contentType: 'application/pdf' });
|
|
226
|
+
if (error) throw new Error(error.message);
|
|
184
227
|
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
const { data: download } = await client.storage
|
|
228
|
+
// Prefer a signed URL over download() for large objects (download materialises the body).
|
|
229
|
+
const { data: url } = await stackbone.storage
|
|
188
230
|
.from('uploads')
|
|
189
231
|
.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
232
|
```
|
|
194
233
|
|
|
195
234
|
### AI
|
|
196
235
|
|
|
197
|
-
|
|
236
|
+
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
237
|
|
|
199
238
|
```ts
|
|
200
|
-
|
|
201
|
-
const { data, error } = await client.ai.chat.completions.create({
|
|
239
|
+
const { data, error } = await stackbone.ai.chat.completions.create({
|
|
202
240
|
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
|
-
],
|
|
241
|
+
messages: [{ role: 'user', content: 'Score this lead: ...' }],
|
|
207
242
|
});
|
|
243
|
+
if (!error) console.log(data.choices[0]?.message.content);
|
|
208
244
|
|
|
209
|
-
|
|
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({
|
|
245
|
+
const { data: emb } = await stackbone.ai.embeddings.create({
|
|
235
246
|
model: 'openai/text-embedding-3-small',
|
|
236
247
|
input: 'A red Spanish sword from the 11th century.',
|
|
237
248
|
});
|
|
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
249
|
```
|
|
263
250
|
|
|
264
251
|
### RAG
|
|
265
252
|
|
|
266
|
-
|
|
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:
|
|
269
|
-
|
|
270
|
-
```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' },
|
|
286
|
-
});
|
|
287
|
-
|
|
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',
|
|
293
|
-
topK: 5,
|
|
294
|
-
filter: { source: 'manual-upload' },
|
|
295
|
-
});
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
The escape hatch — pass embeddings precomputed yourself (different provider, custom dimensions, your own batching, deterministic offline tests):
|
|
253
|
+
Flat ingest + hybrid retrieval over the platform-provisioned tables — no collections to create:
|
|
299
254
|
|
|
300
255
|
```ts
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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' },
|
|
256
|
+
await stackbone.rag.ingest({
|
|
257
|
+
collection: 'docs',
|
|
258
|
+
content: markdown,
|
|
259
|
+
contentType: 'text/markdown',
|
|
313
260
|
});
|
|
314
261
|
|
|
315
|
-
const { data: hits } = await
|
|
316
|
-
|
|
262
|
+
const { data: hits } = await stackbone.rag.search({
|
|
263
|
+
collection: 'docs',
|
|
264
|
+
query: 'how does X work?',
|
|
317
265
|
topK: 5,
|
|
318
266
|
});
|
|
319
267
|
```
|
|
320
268
|
|
|
321
|
-
|
|
269
|
+
> For durable ingest of large documents, prefer `ingestDocuments()` from `@stackbone/sdk/workflow` (runs the reserved `rag-ingest` workflow).
|
|
322
270
|
|
|
323
|
-
|
|
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
|
-
```
|
|
271
|
+
### Config, secrets & prompts
|
|
342
272
|
|
|
343
|
-
|
|
273
|
+
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
274
|
|
|
345
275
|
```ts
|
|
346
|
-
await
|
|
276
|
+
const { data: tone } = await stackbone.config.get('replyTone'); // typed from config.schema.ts
|
|
277
|
+
const { data: apiKey, error } = await stackbone.secrets.get('STRIPE_API_KEY');
|
|
278
|
+
const { data: body } = await stackbone.prompts.compile('welcome-email', { name: 'Jane' });
|
|
347
279
|
```
|
|
348
280
|
|
|
349
|
-
|
|
281
|
+
### Calling a sibling agent
|
|
350
282
|
|
|
351
|
-
|
|
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:
|
|
283
|
+
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
284
|
|
|
397
285
|
```ts
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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),
|
|
286
|
+
async function askSupportForTips(plan: string) {
|
|
287
|
+
'use step';
|
|
288
|
+
const session = stackbone.agent('support').session();
|
|
289
|
+
const response = await session.send<{ tips: string[] }>({
|
|
290
|
+
message: `A customer joined the "${plan}" plan. Give up to 3 onboarding tips.`,
|
|
291
|
+
outputSchema: z.object({ tips: z.array(z.string()) }),
|
|
452
292
|
});
|
|
293
|
+
const result = await response.result(); // { data, status }
|
|
294
|
+
return result.data ?? { tips: [] };
|
|
453
295
|
}
|
|
454
296
|
```
|
|
455
297
|
|
|
456
|
-
|
|
298
|
+
### Calling a connector
|
|
457
299
|
|
|
458
|
-
|
|
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.
|
|
300
|
+
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
301
|
|
|
462
302
|
```ts
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
303
|
+
async function sendMail(input: { to: string; subject: string; body: string }) {
|
|
304
|
+
'use step';
|
|
305
|
+
// Typed after `stackbone dev` generates .stackbone/connect.d.ts; equivalent to
|
|
306
|
+
// .call('sendMail', input). Use .call('chat.postMessage', args) for dotted ids.
|
|
307
|
+
return stackbone.connection('stub-mail').sendMail(input);
|
|
467
308
|
}
|
|
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
309
|
```
|
|
495
310
|
|
|
496
|
-
|
|
311
|
+
## Subpath entrypoints
|
|
497
312
|
|
|
498
|
-
|
|
313
|
+
Some surfaces ship behind subpaths so a tool-only agent never loads the `eve`/`workflow` peers it doesn't use:
|
|
499
314
|
|
|
500
|
-
|
|
315
|
+
| Entrypoint | Key exports |
|
|
316
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
317
|
+
| `@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) |
|
|
318
|
+
| `@stackbone/sdk/connect` | `connect` (author an eve connection with brokered auth) |
|
|
319
|
+
| `@stackbone/sdk/db` | Drizzle re-exports — column builders (`pgTable`, `text`, `vector`, …) and helpers (`eq`, `and`, `sql`, …) |
|
|
320
|
+
| `@stackbone/sdk/db/testing` | `createTestDatabase` (spin up a throwaway Postgres for deterministic schema tests) |
|
|
501
321
|
|
|
502
|
-
###
|
|
322
|
+
### Triggering & scheduling workflows
|
|
503
323
|
|
|
504
|
-
|
|
324
|
+
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
325
|
|
|
506
326
|
```ts
|
|
507
|
-
import
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
CreatePromptRequest,
|
|
513
|
-
} from '@stackbone/sdk';
|
|
327
|
+
import { stackbone } from '@stackbone/sdk';
|
|
328
|
+
|
|
329
|
+
const { runId } = await stackbone.workflows.start('reconcile', { invoiceId }); // fire-and-forget → its own run
|
|
330
|
+
const summary = await stackbone.workflows.startAndWait<Summary>('summarize', { docId }); // durably wait for the output
|
|
331
|
+
await stackbone.workflows.schedule('daily-digest', { scope: 'all' }, '0 9 * * *'); // dynamic cron, idempotent by name
|
|
514
332
|
```
|
|
515
333
|
|
|
516
|
-
|
|
334
|
+
`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
335
|
|
|
518
|
-
|
|
336
|
+
### Human-in-the-loop — `requestApproval`
|
|
519
337
|
|
|
520
|
-
|
|
338
|
+
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
339
|
|
|
522
340
|
```ts
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
});
|
|
341
|
+
import { requestApproval } from '@stackbone/sdk/workflow';
|
|
342
|
+
|
|
343
|
+
export async function refundWorkflow(input: z.infer<typeof inputSchema>) {
|
|
344
|
+
'use workflow';
|
|
345
|
+
|
|
346
|
+
const decision = await requestApproval({
|
|
347
|
+
token: `refund-${input.orderId}`, // resume key — unique per approval in the run
|
|
348
|
+
topic: 'refund',
|
|
349
|
+
payload: { orderId: input.orderId, amount: input.amount },
|
|
350
|
+
title: 'Approve refund',
|
|
351
|
+
timeout: '24h', // ISO-8601 duration or ms
|
|
352
|
+
fallback: 'reject', // applied if the timeout wins the race
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// status === 'approved' is the ONLY green light; gate the side-effect on it.
|
|
356
|
+
if (decision.status !== 'approved') {
|
|
357
|
+
return { orderId: input.orderId, refunded: false, decision: decision.status };
|
|
358
|
+
}
|
|
359
|
+
await performRefund(input.orderId, input.amount); // a non-idempotent step, gated
|
|
360
|
+
return { orderId: input.orderId, refunded: true, decision: decision.status };
|
|
361
|
+
}
|
|
557
362
|
```
|
|
558
363
|
|
|
559
|
-
|
|
364
|
+
`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
365
|
|
|
561
366
|
## Result Envelope and Error Handling
|
|
562
367
|
|
|
563
|
-
Every SDK method returns the same discriminated union:
|
|
368
|
+
Every SDK method (except `stackbone.database` and `stackbone.connection`, which throw) returns the same discriminated union:
|
|
564
369
|
|
|
565
370
|
```ts
|
|
566
371
|
type Result<T> = { data: T; error: null } | { data: null; error: SdkError };
|
|
@@ -576,45 +381,39 @@ interface SdkError {
|
|
|
576
381
|
Narrowing on `error` automatically refines `data` to `T`, so the typical caller looks like:
|
|
577
382
|
|
|
578
383
|
```ts
|
|
579
|
-
const result = await
|
|
384
|
+
const result = await stackbone.ai.chat.completions.create({ model, messages });
|
|
580
385
|
|
|
581
386
|
if (result.error) {
|
|
582
387
|
console.error(`[${result.error.code}] ${result.error.message}`, result.error.meta);
|
|
583
|
-
return;
|
|
388
|
+
return; // or: throw new Error(result.error.message) to propagate
|
|
584
389
|
}
|
|
585
390
|
|
|
586
391
|
console.log(result.data.choices[0]?.message.content);
|
|
587
392
|
```
|
|
588
393
|
|
|
589
|
-
Each
|
|
590
|
-
|
|
591
|
-
| Prefix
|
|
592
|
-
|
|
|
593
|
-
| `ai_*`
|
|
594
|
-
| `s3_*`
|
|
595
|
-
| `rag_*`
|
|
596
|
-
| `approval_*`
|
|
597
|
-
| `secrets_*`
|
|
598
|
-
| `config_*`
|
|
599
|
-
| `
|
|
600
|
-
| `http_*`
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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.
|
|
394
|
+
`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:
|
|
395
|
+
|
|
396
|
+
| Prefix | Source | Examples |
|
|
397
|
+
| ------------ | -------------------- | --------------------------------------------------------------------------------------------------- |
|
|
398
|
+
| `ai_*` | `stackbone.ai` | `ai_unauthorized`, `ai_credits_exhausted`, `ai_rate_limited`, `ai_aborted` |
|
|
399
|
+
| `s3_*` | `stackbone.storage` | `s3_credentials_missing`, `s3_invalid_key`, `s3_error` |
|
|
400
|
+
| `rag_*` | `stackbone.rag` | `rag_invalid_request`, `rag_dim_mismatch`, `rag_embedding_failed`, `rag_error` |
|
|
401
|
+
| `approval_*` | `stackbone.approval` | `approval_invalid_request`, `approval_persist_failed`, `approval_not_found`, `approval_unavailable` |
|
|
402
|
+
| `secrets_*` | `stackbone.secrets` | `secrets_invalid_request`, `secrets_not_found`, `secrets_unavailable` |
|
|
403
|
+
| `config_*` | `stackbone.config` | `config_invalid_request`, `config_not_found`, `config_unavailable` |
|
|
404
|
+
| `prompts_*` | `stackbone.prompts` | `prompts_not_found`, `prompts_invalid_request`, `prompts_unavailable` |
|
|
405
|
+
| `http_*` | transport-level | `http_timeout`, `http_aborted`, `http_network_error` — surface statuses remap to `<surface>_*` |
|
|
406
|
+
|
|
407
|
+
`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`).
|
|
408
|
+
|
|
409
|
+
> 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
410
|
|
|
610
411
|
## TypeScript Support
|
|
611
412
|
|
|
612
|
-
The SDK is written in TypeScript and ships its own type definitions:
|
|
413
|
+
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
414
|
|
|
614
415
|
```ts
|
|
615
|
-
import {
|
|
616
|
-
|
|
617
|
-
const stackbone: StackboneClient = createClient();
|
|
416
|
+
import { stackbone, z, type Result, type SdkError } from '@stackbone/sdk';
|
|
618
417
|
|
|
619
418
|
const result = await stackbone.ai.chat.completions.create({
|
|
620
419
|
model: 'anthropic/claude-sonnet-4.5',
|
|
@@ -622,28 +421,19 @@ const result = await stackbone.ai.chat.completions.create({
|
|
|
622
421
|
});
|
|
623
422
|
```
|
|
624
423
|
|
|
625
|
-
Chat, embeddings and streaming types are re-exported directly from the
|
|
626
|
-
|
|
627
|
-
For typed queue payloads, augment the `QueueJobs` registry once and both publisher and consumer pick up the shape:
|
|
424
|
+
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
425
|
|
|
629
|
-
|
|
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
|
-
```
|
|
426
|
+
> **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
427
|
|
|
638
428
|
## Runtime
|
|
639
429
|
|
|
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
|
|
430
|
+
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
431
|
|
|
642
432
|
## Documentation
|
|
643
433
|
|
|
644
434
|
- **[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
|
|
646
|
-
- **[Componente — SDK & Creator DX](../../docs/arquitectura/componentes/04-sdk-creator-dx.md)** — End-to-end developer experience for
|
|
435
|
+
- **[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
|
|
436
|
+
- **[Componente — SDK & Creator DX](../../docs/arquitectura/componentes/04-sdk-creator-dx.md)** — End-to-end developer experience for creators
|
|
647
437
|
|
|
648
438
|
## License
|
|
649
439
|
|