@stackbone/sdk 0.1.0-alpha.6 → 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/CHANGELOG.md +96 -0
- package/README.md +266 -471
- package/agent-registry-BNXuj88Q.d.cts +198 -0
- package/agent-registry-BNXuj88Q.d.ts +198 -0
- package/call-connector-CYDw_yG5.d.cts +118 -0
- package/call-connector-CYDw_yG5.d.ts +118 -0
- package/connect.cjs +270 -0
- package/connect.cjs.map +1 -0
- package/connect.d.cts +94 -0
- package/connect.d.ts +94 -0
- package/connect.js +257 -0
- package/connect.js.map +1 -0
- 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 +435 -60
- package/index.cjs.map +1 -1
- package/index.d.cts +389 -81
- package/index.d.ts +389 -81
- package/index.js +430 -62
- package/index.js.map +1 -1
- package/observability/index.cjs +1 -268
- package/observability/index.cjs.map +1 -1
- package/observability/index.d.cts +1 -96
- package/observability/index.d.ts +1 -96
- package/observability/index.js +2 -264
- package/observability/index.js.map +1 -1
- package/package.json +47 -1
- package/stackbone-sdk-0.1.0-alpha.8.tgz +0 -0
- package/workflow.cjs +17444 -0
- package/workflow.cjs.map +1 -0
- package/workflow.d.cts +128 -0
- package/workflow.d.ts +128 -0
- package/workflow.js +17419 -0
- package/workflow.js.map +1 -0
- package/stackbone-sdk-0.1.0-alpha.6.tgz +0 -0
package/README.md
CHANGED
|
@@ -4,27 +4,26 @@
|
|
|
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
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
|
-
|
|
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
|
-
##
|
|
51
|
+
## What you build — a workspace
|
|
43
52
|
|
|
44
|
-
|
|
53
|
+
A **workspace** is a project that contains **agents** and **workflows**, discovered **by convention from the files on disk**:
|
|
45
54
|
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
93
|
+
### Authoring an eve agent
|
|
67
94
|
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
139
|
+
### Authoring a durable workflow
|
|
129
140
|
|
|
130
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
+
## The ambient `stackbone` client
|
|
148
181
|
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
const { data, error } = await client.storage.from('uploads').download('reports/2026-04-29.pdf');
|
|
198
|
+
### Database
|
|
164
199
|
|
|
165
|
-
|
|
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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
const { data: upload } = await client.storage
|
|
228
|
+
const { data, error } = await stackbone.storage
|
|
179
229
|
.from('uploads')
|
|
180
|
-
.
|
|
181
|
-
|
|
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
|
-
//
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
274
|
+
> For durable ingest of large documents, prefer `ingestDocuments()` from `@stackbone/sdk/workflow` (runs the reserved `rag-ingest` workflow).
|
|
299
275
|
|
|
300
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
286
|
+
### Calling a sibling agent
|
|
350
287
|
|
|
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:
|
|
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
|
-
|
|
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),
|
|
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
|
-
|
|
303
|
+
### Calling a connector
|
|
457
304
|
|
|
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.
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
316
|
+
## Subpath entrypoints
|
|
497
317
|
|
|
498
|
-
|
|
318
|
+
Some surfaces ship behind subpaths so a tool-only agent never loads the `eve`/`workflow` peers it doesn't use:
|
|
499
319
|
|
|
500
|
-
|
|
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
|
-
###
|
|
327
|
+
### Triggering & scheduling workflows
|
|
503
328
|
|
|
504
|
-
|
|
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
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
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
|
-
|
|
341
|
+
### Human-in-the-loop — `requestApproval`
|
|
519
342
|
|
|
520
|
-
|
|
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
|
-
|
|
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
|
-
});
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
646
|
-
- **[Componente — SDK & Creator DX](../../docs/arquitectura/componentes/04-sdk-creator-dx.md)** — End-to-end developer experience for
|
|
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
|
|