@stackbone/sdk 0.1.0-alpha.2 → 0.1.0-alpha.4
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 +307 -1
- package/README.md +127 -167
- package/db/index.d.cts +1 -0
- package/db/index.d.ts +1 -0
- package/db/testing/index.cjs +12 -43
- package/db/testing/index.cjs.map +1 -1
- package/db/testing/index.d.cts +12 -21
- package/db/testing/index.d.ts +12 -21
- package/db/testing/index.js +12 -43
- package/db/testing/index.js.map +1 -1
- package/index.cjs +7876 -19124
- package/index.cjs.map +1 -1
- package/index.d.cts +1434 -768
- package/index.d.ts +1434 -768
- package/index.js +7844 -19105
- package/index.js.map +1 -1
- package/observability/index.cjs +610 -0
- package/observability/index.cjs.map +1 -0
- package/observability/index.d.cts +175 -0
- package/observability/index.d.ts +175 -0
- package/observability/index.js +601 -0
- package/observability/index.js.map +1 -0
- package/package.json +14 -12
- package/rag/schema.cjs +42 -82
- package/rag/schema.cjs.map +1 -1
- package/rag/schema.d.cts +1 -446
- package/rag/schema.d.ts +1 -446
- package/rag/schema.js +42 -61
- package/rag/schema.js.map +1 -1
- package/stackbone-sdk-0.1.0-alpha.4.tgz +0 -0
- package/rag/migrations/index.cjs +0 -71
- package/rag/migrations/index.cjs.map +0 -1
- package/rag/migrations/index.d.cts +0 -29
- package/rag/migrations/index.d.ts +0 -29
- package/rag/migrations/index.js +0 -66
- package/rag/migrations/index.js.map +0 -1
- package/stackbone-sdk-0.1.0-alpha.2.tgz +0 -0
package/README.md
CHANGED
|
@@ -13,17 +13,14 @@ Official TypeScript SDK for [Stackbone](https://stackbone.ai) — the marketplac
|
|
|
13
13
|
- **Database** — Direct Postgres access through a lazy Drizzle ORM wrapper bound to the agent's connection string, with full type-safe queries, transactions, and `sql` template literals — Drizzle is re-exported via `@stackbone/sdk/db` so the agent's `package.json` only depends on `@stackbone/sdk`
|
|
14
14
|
- **Storage** — S3-compatible object storage (Cloudflare R2 in prod, MinIO in dev) with automatic per-agent key prefixing and signed URLs
|
|
15
15
|
- **AI** — Wrapper around the official `openai` SDK pointed at OpenRouter, so 300+ chat, embedding and image models are reachable through a single OpenAI-compatible API
|
|
16
|
-
- **Queues** _(coming soon)_ — Cross-container HTTP push via QStash, with typed job payloads through module augmentation
|
|
17
16
|
- **RAG** — Document parsing, chunking and `pgvector`-backed retrieval through a flat, ceremony-free API
|
|
18
|
-
- **Memory** _(surface defined; runtime pending)_ — Long-term agent memory backed by mem0: ingest text or whole conversations, semantic search, per-user / per-session / per-agent scoping, GDPR delete-all, audit history and session lifecycle hooks
|
|
19
|
-
- **Prompts** _(surface defined; runtime pending)_ — Managed prompts stored in the Stackbone control plane: fetch by name with optional version pinning, compile Mustache `{{var}}` templates against a variables map, and full CRUD so prompts can evolve without rebuilding the container
|
|
20
17
|
- **Observability** — OpenTelemetry span processor + run-cost aggregator + per-run logger
|
|
21
18
|
- **Approval** — Human-in-the-loop inbox: fire-and-forget approval requests, HMAC-signed decision callbacks, and an LLM tool wrapper that gates execution behind human review
|
|
22
19
|
- **Secrets** — Read organization-encrypted secrets registered in the dashboard, no client-side cache so rotations propagate immediately
|
|
23
20
|
- **Config** — Typed reads of dynamic per-agent configuration the user set in the dashboard
|
|
24
|
-
- **
|
|
21
|
+
- **Coming soon (types only)** — `queues` (cross-container HTTP push via the BullMQ job dispatcher), `memory` (mem0-backed long-term memory), `prompts` (managed prompts with `{{var}}` templates), `connections` (OAuth integrations) and `events` (org-wide event bus). Public types are exported from `@stackbone/sdk` today; the runtime is not wired yet, so there is no live `client.X` accessor.
|
|
25
22
|
- **TypeScript-first** — Full type definitions and a uniform `Result<T>` envelope on every method (no thrown errors at the SDK boundary)
|
|
26
|
-
- **Lazy initialization** — Modules and partner SDKs are constructed on first access, so env vars rotated by the control plane after
|
|
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
|
|
27
24
|
|
|
28
25
|
## Installation
|
|
29
26
|
|
|
@@ -40,26 +37,44 @@ pnpm add @stackbone/sdk
|
|
|
40
37
|
|
|
41
38
|
> 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.
|
|
42
39
|
|
|
43
|
-
Need a copy-pasteable starting point? See the [`example/`](https://github.com/stackbone/stackbone/tree/main/libs/sdk/example) directory in the repository — one ESM and one CJS file showing the canonical agent shape (schema with `@stackbone/sdk/db`, `
|
|
40
|
+
Need a copy-pasteable starting point? See the [`example/`](https://github.com/stackbone/stackbone/tree/main/libs/sdk/example) directory in the repository — one ESM and one CJS file showing the canonical agent shape (schema with `@stackbone/sdk/db`, the runtime-injected `ctx.client`, async handler).
|
|
44
41
|
|
|
45
42
|
## Quick Start
|
|
46
43
|
|
|
47
|
-
###
|
|
44
|
+
### The Client
|
|
48
45
|
|
|
49
|
-
|
|
46
|
+
You don't construct the client. The runtime builds a single `StackboneClient` per process — wired from the env vars the platform injects at provisioning time — and hands it to every handler as `ctx.client`. Define your agent with `defineAgent` and destructure `client` (and `input`) off the invocation context:
|
|
50
47
|
|
|
51
48
|
```ts
|
|
52
|
-
import {
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
import { defineAgent, z } from '@stackbone/sdk';
|
|
50
|
+
|
|
51
|
+
export default defineAgent({
|
|
52
|
+
invoke: {
|
|
53
|
+
input: z.object({ email: z.string() }),
|
|
54
|
+
output: z.object({ id: z.number() }),
|
|
55
|
+
async run({ input, client }) {
|
|
56
|
+
const [row] = await client.database
|
|
57
|
+
.insert(leads)
|
|
58
|
+
.values({ email: input.email, status: 'new', score: 0 })
|
|
59
|
+
.returning();
|
|
60
|
+
return { id: row.id };
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
55
64
|
```
|
|
56
65
|
|
|
57
|
-
|
|
66
|
+
#### Escape hatch — `createClient()`
|
|
67
|
+
|
|
68
|
+
`createClient()` is still exported for when you need a client **outside** a handler (a script, a test, a one-off task) or want to override config explicitly — useful for local development against the emulator or when running outside a Stackbone container. In normal agent code you never need it; reach for `ctx.client` instead.
|
|
58
69
|
|
|
59
70
|
```ts
|
|
60
71
|
import { createClient } from '@stackbone/sdk';
|
|
61
72
|
|
|
62
|
-
|
|
73
|
+
// Zero-arg: every option falls back to an injected env var.
|
|
74
|
+
const stackbone = createClient();
|
|
75
|
+
|
|
76
|
+
// Or pass overrides explicitly.
|
|
77
|
+
const local = createClient({
|
|
63
78
|
agentJwt: process.env.STACKBONE_AGENT_JWT,
|
|
64
79
|
stackboneApiUrl: 'http://localhost:3000',
|
|
65
80
|
agentId: 'agent_local_dev',
|
|
@@ -72,7 +87,7 @@ const stackbone = createClient({
|
|
|
72
87
|
`client.database` is a lazy [Drizzle ORM](https://orm.drizzle.team/) instance bound to the agent's Postgres connection (`STACKBONE_POSTGRES_URL`). The full Drizzle surface (`select`, `insert`, `update`, `delete`, `transaction`, `execute`, `query`, `sql\`\``) is exposed verbatim, and the column builders live behind the `@stackbone/sdk/db`subpath so the creator never has to install`drizzle-orm`, `drizzle-orm/pg-core`or`postgres` directly.
|
|
73
88
|
|
|
74
89
|
```ts
|
|
75
|
-
import {
|
|
90
|
+
import { defineAgent, z } from '@stackbone/sdk';
|
|
76
91
|
import { eq, integer, pgTable, text } from '@stackbone/sdk/db';
|
|
77
92
|
|
|
78
93
|
// 1. Declare your tables — types flow end to end.
|
|
@@ -83,29 +98,39 @@ const leads = pgTable('leads', {
|
|
|
83
98
|
score: integer('score').notNull(),
|
|
84
99
|
});
|
|
85
100
|
|
|
86
|
-
|
|
101
|
+
export default defineAgent({
|
|
102
|
+
invoke: {
|
|
103
|
+
input: z.object({}),
|
|
104
|
+
output: z.object({}),
|
|
105
|
+
async run({ client }) {
|
|
106
|
+
// 2. Query — `data` is typed from the schema, no codegen needed.
|
|
107
|
+
const rows = await client.database
|
|
108
|
+
.select()
|
|
109
|
+
.from(leads)
|
|
110
|
+
.where(eq(leads.status, 'qualified'))
|
|
111
|
+
.limit(20);
|
|
87
112
|
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.where(eq(leads.status, 'qualified'))
|
|
93
|
-
.limit(20);
|
|
113
|
+
// 3. Mutate.
|
|
114
|
+
await client.database
|
|
115
|
+
.insert(leads)
|
|
116
|
+
.values({ id: 1, email: 'jane@example.com', status: 'new', score: 0 });
|
|
94
117
|
|
|
95
|
-
|
|
96
|
-
await stackbone.database
|
|
97
|
-
.insert(leads)
|
|
98
|
-
.values({ id: 1, email: 'jane@example.com', status: 'new', score: 0 });
|
|
118
|
+
await client.database.update(leads).set({ status: 'contacted' }).where(eq(leads.id, 1));
|
|
99
119
|
|
|
100
|
-
await
|
|
120
|
+
await client.database.delete(leads).where(eq(leads.id, 1));
|
|
101
121
|
|
|
102
|
-
|
|
122
|
+
return {};
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
});
|
|
103
126
|
```
|
|
104
127
|
|
|
128
|
+
The snippets below assume the `client` destructured off `ctx` inside a handler (`async run({ client }) { ... }`).
|
|
129
|
+
|
|
105
130
|
Interactive transactions yield a `tx` with the same Drizzle surface as `client.database`:
|
|
106
131
|
|
|
107
132
|
```ts
|
|
108
|
-
await
|
|
133
|
+
await client.database.transaction(async (tx) => {
|
|
109
134
|
await tx.insert(leads).values({ id: 2, email: 'b@x.com', status: 'new', score: 0 });
|
|
110
135
|
await tx.update(leads).set({ status: 'qualified' }).where(eq(leads.id, 2));
|
|
111
136
|
});
|
|
@@ -116,7 +141,7 @@ For raw SQL, reach for the `sql` tag re-exported from `@stackbone/sdk/db`:
|
|
|
116
141
|
```ts
|
|
117
142
|
import { sql } from '@stackbone/sdk/db';
|
|
118
143
|
|
|
119
|
-
const { rows } = await
|
|
144
|
+
const { rows } = await client.database.execute(sql`SELECT NOW()`);
|
|
120
145
|
```
|
|
121
146
|
|
|
122
147
|
> If `STACKBONE_POSTGRES_URL` is unset, the first call to `client.database` raises `database_not_configured` with a hint to run `stackbone dev`. Migrations are governed by the `stackbone db migrate *` CLI commands — `client.database` itself is just the runtime query surface.
|
|
@@ -127,7 +152,7 @@ const { rows } = await stackbone.database.execute(sql`SELECT NOW()`);
|
|
|
127
152
|
|
|
128
153
|
```ts
|
|
129
154
|
// Upload an object
|
|
130
|
-
const { data, error } = await
|
|
155
|
+
const { data, error } = await client.storage
|
|
131
156
|
.from('uploads')
|
|
132
157
|
.upload('reports/2026-04-29.pdf', pdfBlob, {
|
|
133
158
|
contentType: 'application/pdf',
|
|
@@ -135,22 +160,22 @@ const { data, error } = await stackbone.storage
|
|
|
135
160
|
});
|
|
136
161
|
|
|
137
162
|
// Download an object (returns a Blob)
|
|
138
|
-
const { data, error } = await
|
|
163
|
+
const { data, error } = await client.storage.from('uploads').download('reports/2026-04-29.pdf');
|
|
139
164
|
|
|
140
165
|
// List objects (paginated)
|
|
141
|
-
const { data, error } = await
|
|
166
|
+
const { data, error } = await client.storage
|
|
142
167
|
.from('uploads')
|
|
143
168
|
.list({ prefix: 'reports/', limit: 50 });
|
|
144
169
|
|
|
145
170
|
// Delete an object
|
|
146
|
-
const { data, error } = await
|
|
171
|
+
const { data, error } = await client.storage.from('uploads').remove('reports/2026-04-29.pdf');
|
|
147
172
|
```
|
|
148
173
|
|
|
149
174
|
Signed URLs and a public URL helper are exposed for direct browser uploads / downloads:
|
|
150
175
|
|
|
151
176
|
```ts
|
|
152
177
|
// Pre-signed upload URL (1h by default; clamp the contentType into the signature)
|
|
153
|
-
const { data: upload } = await
|
|
178
|
+
const { data: upload } = await client.storage
|
|
154
179
|
.from('uploads')
|
|
155
180
|
.getSignedUploadUrl('avatars/user-42.png', {
|
|
156
181
|
expiresIn: 600,
|
|
@@ -159,12 +184,12 @@ const { data: upload } = await stackbone.storage
|
|
|
159
184
|
|
|
160
185
|
// Pre-signed download URL — prefer this over `download()` for large objects,
|
|
161
186
|
// since `download()` materialises the entire body in memory as a Blob.
|
|
162
|
-
const { data: download } = await
|
|
187
|
+
const { data: download } = await client.storage
|
|
163
188
|
.from('uploads')
|
|
164
189
|
.getSignedDownloadUrl('reports/2026-04-29.pdf');
|
|
165
190
|
|
|
166
191
|
// Canonical public URL (only fetchable for public buckets)
|
|
167
|
-
const { data: url } =
|
|
192
|
+
const { data: url } = client.storage.from('uploads').getPublicUrl('avatars/user-42.png');
|
|
168
193
|
```
|
|
169
194
|
|
|
170
195
|
### AI
|
|
@@ -173,7 +198,7 @@ const { data: url } = stackbone.storage.from('uploads').getPublicUrl('avatars/us
|
|
|
173
198
|
|
|
174
199
|
```ts
|
|
175
200
|
// Chat completion
|
|
176
|
-
const { data, error } = await
|
|
201
|
+
const { data, error } = await client.ai.chat.completions.create({
|
|
177
202
|
model: 'anthropic/claude-sonnet-4.5',
|
|
178
203
|
messages: [
|
|
179
204
|
{ role: 'system', content: 'You are a sales qualification assistant.' },
|
|
@@ -189,7 +214,7 @@ if (!error) {
|
|
|
189
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`:
|
|
190
215
|
|
|
191
216
|
```ts
|
|
192
|
-
const { data: stream, error } = await
|
|
217
|
+
const { data: stream, error } = await client.ai.chat.completions.create({
|
|
193
218
|
model: 'anthropic/claude-sonnet-4.5',
|
|
194
219
|
messages: [{ role: 'user', content: 'Stream me a haiku.' }],
|
|
195
220
|
stream: true,
|
|
@@ -206,7 +231,7 @@ Embeddings, image generation and a model catalogue are first-class too:
|
|
|
206
231
|
|
|
207
232
|
```ts
|
|
208
233
|
// Embeddings — returns the standard OpenAI shape
|
|
209
|
-
const { data, error } = await
|
|
234
|
+
const { data, error } = await client.ai.embeddings.create({
|
|
210
235
|
model: 'openai/text-embedding-3-small',
|
|
211
236
|
input: 'A red Spanish sword from the 11th century.',
|
|
212
237
|
});
|
|
@@ -214,14 +239,14 @@ const { data, error } = await stackbone.ai.embeddings.create({
|
|
|
214
239
|
// Image generation — OpenRouter routes image models through chat completions;
|
|
215
240
|
// the SDK normalises the response so callers see the OpenAI-shaped payload.
|
|
216
241
|
// Returns `ai_no_image_generated` if the model produces zero images.
|
|
217
|
-
const { data, error } = await
|
|
242
|
+
const { data, error } = await client.ai.images.generate({
|
|
218
243
|
model: 'google/gemini-2.5-flash-image',
|
|
219
244
|
prompt: 'A medieval Spanish sword on a velvet cushion, museum lighting',
|
|
220
245
|
});
|
|
221
246
|
|
|
222
247
|
// List available models — preserves OpenRouter-specific fields like
|
|
223
248
|
// `pricing`, `context_length`, `supported_parameters`, `architecture`
|
|
224
|
-
const { data, error } = await
|
|
249
|
+
const { data, error } = await client.ai.models.list();
|
|
225
250
|
```
|
|
226
251
|
|
|
227
252
|
Cancellation works the way you'd expect — pass an `AbortSignal` and aborts surface as `ai_aborted` on non-streaming calls:
|
|
@@ -230,7 +255,7 @@ Cancellation works the way you'd expect — pass an `AbortSignal` and aborts sur
|
|
|
230
255
|
const controller = new AbortController();
|
|
231
256
|
setTimeout(() => controller.abort(), 5_000);
|
|
232
257
|
|
|
233
|
-
const { data, error } = await
|
|
258
|
+
const { data, error } = await client.ai.chat.completions.create(
|
|
234
259
|
{ model: 'anthropic/claude-sonnet-4.5', messages: [...] },
|
|
235
260
|
{ signal: controller.signal },
|
|
236
261
|
);
|
|
@@ -244,16 +269,16 @@ The fast path lets the SDK handle embeddings for you — pass an embedding `mode
|
|
|
244
269
|
|
|
245
270
|
```ts
|
|
246
271
|
// 1. Parse a document → plain text. Supports text/*, text/markdown and application/pdf.
|
|
247
|
-
const text = await
|
|
272
|
+
const text = await client.rag.parse(file);
|
|
248
273
|
|
|
249
274
|
// 2. Split into chunks. Pure utility, no DB call.
|
|
250
|
-
const chunks =
|
|
275
|
+
const chunks = client.rag.chunk(text, { size: 512, overlap: 64 });
|
|
251
276
|
|
|
252
277
|
// 3. Ingest. The SDK embeds the chunks for you in batches of 128 and
|
|
253
278
|
// provisions the schema on the first call (HNSW + cosine, dims inferred
|
|
254
279
|
// from the first batch). Re-ingesting the same `id` atomically replaces
|
|
255
280
|
// all of that document's chunks.
|
|
256
|
-
await
|
|
281
|
+
await client.rag.ingest({
|
|
257
282
|
id: 'doc-1',
|
|
258
283
|
chunks,
|
|
259
284
|
model: 'openai/text-embedding-3-small',
|
|
@@ -262,7 +287,7 @@ await stackbone.rag.ingest({
|
|
|
262
287
|
|
|
263
288
|
// 4. Retrieve. Pass the user's question and the same model — the SDK embeds
|
|
264
289
|
// it for you and returns hits scored in [0, 1] (cosine similarity).
|
|
265
|
-
const { data: hits } = await
|
|
290
|
+
const { data: hits } = await client.rag.retrieve({
|
|
266
291
|
text: 'how does X work?',
|
|
267
292
|
model: 'openai/text-embedding-3-small',
|
|
268
293
|
topK: 5,
|
|
@@ -273,12 +298,12 @@ const { data: hits } = await stackbone.rag.retrieve({
|
|
|
273
298
|
The escape hatch — pass embeddings precomputed yourself (different provider, custom dimensions, your own batching, deterministic offline tests):
|
|
274
299
|
|
|
275
300
|
```ts
|
|
276
|
-
const { data: embeddings } = await
|
|
301
|
+
const { data: embeddings } = await client.ai.embeddings.create({
|
|
277
302
|
model: 'openai/text-embedding-3-small',
|
|
278
303
|
input: chunks,
|
|
279
304
|
});
|
|
280
305
|
|
|
281
|
-
await
|
|
306
|
+
await client.rag.ingest({
|
|
282
307
|
id: 'doc-1',
|
|
283
308
|
chunks: chunks.map((content, i) => ({
|
|
284
309
|
content,
|
|
@@ -287,7 +312,7 @@ await stackbone.rag.ingest({
|
|
|
287
312
|
metadata: { source: 'manual-upload' },
|
|
288
313
|
});
|
|
289
314
|
|
|
290
|
-
const { data: hits } = await
|
|
315
|
+
const { data: hits } = await client.rag.retrieve({
|
|
291
316
|
embedding: queryEmbedding,
|
|
292
317
|
topK: 5,
|
|
293
318
|
});
|
|
@@ -296,29 +321,29 @@ const { data: hits } = await stackbone.rag.retrieve({
|
|
|
296
321
|
Delete by id (whole document) or by metadata predicate:
|
|
297
322
|
|
|
298
323
|
```ts
|
|
299
|
-
await
|
|
300
|
-
await
|
|
324
|
+
await client.rag.delete('doc-1');
|
|
325
|
+
await client.rag.delete(['doc-1', 'doc-2']);
|
|
301
326
|
|
|
302
|
-
await
|
|
327
|
+
await client.rag.deleteWhere({ source: 'manual-upload' });
|
|
303
328
|
```
|
|
304
329
|
|
|
305
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:
|
|
306
331
|
|
|
307
332
|
```ts
|
|
308
|
-
await
|
|
333
|
+
await client.rag.ingest({
|
|
309
334
|
id: 'faq-1',
|
|
310
335
|
chunks: [...],
|
|
311
336
|
namespace: 'faqs',
|
|
312
337
|
});
|
|
313
338
|
|
|
314
|
-
await
|
|
315
|
-
await
|
|
339
|
+
await client.rag.retrieve({ embedding, namespace: 'faqs' });
|
|
340
|
+
await client.rag.deleteWhere({}, { namespace: 'faqs' });
|
|
316
341
|
```
|
|
317
342
|
|
|
318
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:
|
|
319
344
|
|
|
320
345
|
```ts
|
|
321
|
-
await
|
|
346
|
+
await client.rag.reset();
|
|
322
347
|
```
|
|
323
348
|
|
|
324
349
|
> `parse()` is intentionally minimal: passthrough for text and markdown, `unpdf` for text-only PDFs (no OCR, no layout reconstruction). For complex documents (scanned PDFs, Office files, multi-column layouts) the recommended path is to parse upstream of the SDK and pass the resulting text into `chunk()` directly.
|
|
@@ -331,7 +356,7 @@ This shape decouples waiting from the container lifecycle — the agent does not
|
|
|
331
356
|
|
|
332
357
|
```ts
|
|
333
358
|
// 1. Issue an approval — returns immediately, the human reviews asynchronously.
|
|
334
|
-
const { data, error } = await
|
|
359
|
+
const { data, error } = await client.airoval.request({
|
|
335
360
|
topic: 'lead.qualify',
|
|
336
361
|
payload: { leadId: 42, score: 0.6 },
|
|
337
362
|
title: 'Approve lead qualification',
|
|
@@ -349,7 +374,7 @@ const { data, error } = await stackbone.airoval.request({
|
|
|
349
374
|
// returns a typed Decision<T>. Pass the raw `Request` (Fetch / Hono /
|
|
350
375
|
// undici) and switch on `decision.status`.
|
|
351
376
|
app.post('/approvals/lead-qualify', async (c) => {
|
|
352
|
-
const result = await
|
|
377
|
+
const result = await client.airoval.verify<{ leadId: number; score: number }>(c.req.raw);
|
|
353
378
|
if (result.error) return c.text('invalid', 401);
|
|
354
379
|
switch (result.data.status) {
|
|
355
380
|
case 'approved':
|
|
@@ -370,9 +395,9 @@ app.post('/approvals/lead-qualify', async (c) => {
|
|
|
370
395
|
Operations on existing approvals:
|
|
371
396
|
|
|
372
397
|
```ts
|
|
373
|
-
await
|
|
374
|
-
const { data: record } = await
|
|
375
|
-
const { data: page } = await
|
|
398
|
+
await client.airoval.cancel(approvalId, 'no longer needed');
|
|
399
|
+
const { data: record } = await client.airoval.get<LeadPayload>(approvalId);
|
|
400
|
+
const { data: page } = await client.airoval.list({
|
|
376
401
|
status: 'pending',
|
|
377
402
|
topic: 'lead.qualify',
|
|
378
403
|
limit: 50,
|
|
@@ -381,10 +406,10 @@ const { data: page } = await stackbone.airoval.list({
|
|
|
381
406
|
|
|
382
407
|
#### Approval-gated LLM tools
|
|
383
408
|
|
|
384
|
-
`
|
|
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.
|
|
385
410
|
|
|
386
411
|
```ts
|
|
387
|
-
const sendInvoice =
|
|
412
|
+
const sendInvoice = client.airoval.tool({
|
|
388
413
|
name: 'send_invoice',
|
|
389
414
|
description: 'Send an invoice to a recipient.',
|
|
390
415
|
parameters: {
|
|
@@ -405,7 +430,7 @@ const sendInvoice = stackbone.airoval.tool({
|
|
|
405
430
|
});
|
|
406
431
|
|
|
407
432
|
// Pass the tool to chat.completions.create — it speaks the OpenAI shape.
|
|
408
|
-
const completion = await
|
|
433
|
+
const completion = await client.ai.chat.completions.create({
|
|
409
434
|
model: 'anthropic/claude-sonnet-4.5',
|
|
410
435
|
messages,
|
|
411
436
|
tools: [sendInvoice.openaiSpec()],
|
|
@@ -436,13 +461,13 @@ The signing key for `verify()` is read from `STACKBONE_APPROVAL_SIGNING_KEY` (or
|
|
|
436
461
|
|
|
437
462
|
```ts
|
|
438
463
|
// Single secret — `secrets_not_found` error if the name is not registered.
|
|
439
|
-
const { data: apiKey, error } = await
|
|
464
|
+
const { data: apiKey, error } = await client.secrets.get('STRIPE_API_KEY');
|
|
440
465
|
if (!error) {
|
|
441
466
|
const stripe = new Stripe(apiKey);
|
|
442
467
|
}
|
|
443
468
|
|
|
444
469
|
// Bulk read at boot — one round-trip; missing names come back as omissions.
|
|
445
|
-
const { data: secrets } = await
|
|
470
|
+
const { data: secrets } = await client.secrets.getMany(['STRIPE_API_KEY', 'SLACK_BOT_TOKEN']);
|
|
446
471
|
// secrets: { STRIPE_API_KEY?: string; SLACK_BOT_TOKEN?: string }
|
|
447
472
|
```
|
|
448
473
|
|
|
@@ -452,17 +477,17 @@ const { data: secrets } = await stackbone.secrets.getMany(['STRIPE_API_KEY', 'SL
|
|
|
452
477
|
|
|
453
478
|
```ts
|
|
454
479
|
// Typed read — the generic narrows `data` to `'formal' | 'casual'`.
|
|
455
|
-
const { data: tone } = await
|
|
480
|
+
const { data: tone } = await client.config.get<'formal' | 'casual'>('reply_tone');
|
|
456
481
|
|
|
457
482
|
// Complex JSON — pass the full shape as the generic.
|
|
458
483
|
interface Preferences {
|
|
459
484
|
tone: 'formal' | 'casual';
|
|
460
485
|
maxEmailsPerDay: number;
|
|
461
486
|
}
|
|
462
|
-
const { data: prefs } = await
|
|
487
|
+
const { data: prefs } = await client.config.get<Preferences>('preferences');
|
|
463
488
|
|
|
464
489
|
// Bulk read — typed via the second generic; missing keys come back as omissions.
|
|
465
|
-
const { data: cfg } = await
|
|
490
|
+
const { data: cfg } = await client.config.getMany<{
|
|
466
491
|
reply_tone: 'formal' | 'casual';
|
|
467
492
|
max_emails: number;
|
|
468
493
|
}>(['reply_tone', 'max_emails']);
|
|
@@ -470,98 +495,29 @@ const { data: cfg } = await stackbone.config.getMany<{
|
|
|
470
495
|
|
|
471
496
|
`secrets.get` returns `secrets_not_found` (with the name in `meta`) when the secret is missing; `config.get` returns `config_not_found` symmetrically. Both `getMany` calls return whatever subset the organization has registered — no error for missing entries.
|
|
472
497
|
|
|
473
|
-
### Memory
|
|
474
|
-
|
|
475
|
-
`client.memory` is the agent's long-term memory surface. The first iteration is a **scaffolding placeholder** — every method returns `not_implemented`. The public types and signatures defined here are stable and the contract callers can already type their integrations against. The real backend will be [mem0](https://mem0.ai) (`mem0ApiKey` / `MEM0_API_KEY`).
|
|
476
|
-
|
|
477
|
-
Memories are scoped along three axes:
|
|
478
|
-
|
|
479
|
-
- `'user'` — long-term, persists across sessions of a given end user. Default.
|
|
480
|
-
- `'session'` — short-lived; collapsed (or dropped) when `endSession()` runs.
|
|
481
|
-
- `'agent'` — shared across every user of the agent (preferences, global knowledge).
|
|
482
|
-
|
|
483
|
-
```ts
|
|
484
|
-
// Ingest a fact (raw text or an OpenAI-shaped conversation).
|
|
485
|
-
const { data: memory } = await stackbone.memory.add(
|
|
486
|
-
'The user prefers replies in Spanish and addresses them by their last name.',
|
|
487
|
-
{ userId: 'user_42', scope: 'user', metadata: { source: 'onboarding' } },
|
|
488
|
-
);
|
|
489
|
-
|
|
490
|
-
// Or ingest a whole conversation — the backend summarises it into facts.
|
|
491
|
-
await stackbone.memory.add(
|
|
492
|
-
[
|
|
493
|
-
{ role: 'user', content: 'Call me Mr. Torres please.' },
|
|
494
|
-
{ role: 'assistant', content: 'Got it, Mr. Torres.' },
|
|
495
|
-
],
|
|
496
|
-
{ userId: 'user_42', sessionId: 'sess_abc', scope: 'session' },
|
|
497
|
-
);
|
|
498
|
-
|
|
499
|
-
// Semantic search across the user's memories.
|
|
500
|
-
const { data: hits } = await stackbone.memory.search('how should I address this user?', {
|
|
501
|
-
userId: 'user_42',
|
|
502
|
-
limit: 5,
|
|
503
|
-
threshold: 0.7,
|
|
504
|
-
includeScopes: ['user', 'agent'],
|
|
505
|
-
});
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
Management — read, paginate, edit and audit a single fact:
|
|
509
|
-
|
|
510
|
-
```ts
|
|
511
|
-
const { data: memory } = await stackbone.memory.get(memoryId);
|
|
512
|
-
|
|
513
|
-
const { data: page } = await stackbone.memory.list({
|
|
514
|
-
userId: 'user_42',
|
|
515
|
-
limit: 50,
|
|
516
|
-
cursor: previousCursor,
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
await stackbone.memory.update(memoryId, {
|
|
520
|
-
content: 'The user prefers replies in Spanish.',
|
|
521
|
-
metadata: { reviewed: true },
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
const { data: events } = await stackbone.memory.history(memoryId);
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
Deletion — single fact, all data for a user (GDPR), or end-of-session collapse:
|
|
528
|
-
|
|
529
|
-
```ts
|
|
530
|
-
// Forget one fact.
|
|
531
|
-
await stackbone.memory.delete(memoryId);
|
|
532
|
-
|
|
533
|
-
// GDPR: forget everything we have on this user.
|
|
534
|
-
await stackbone.memory.deleteAll({ userId: 'user_42' });
|
|
498
|
+
### Memory (pending)
|
|
535
499
|
|
|
536
|
-
|
|
537
|
-
const { data } = await stackbone.memory.endSession('sess_abc', { persist: true });
|
|
538
|
-
// data: { sessionId, persisted }
|
|
539
|
-
```
|
|
540
|
-
|
|
541
|
-
> Until the mem0 integration ships, every method returns `{ data: null, error: { code: 'not_implemented', ... } }`. The shape of the responses described here is the contract callers can already write code against.
|
|
500
|
+
Long-term memory is a pending surface: its public types (`AddMemoryRequest`, `MemoryItem`, `MemoryHit`, `MemoryScope`, …) are exported from `@stackbone/sdk` so creators can type their integrations today, but there is no live `client.memory` accessor yet. The mem0-backed runtime will be added in a future release and promoted into the Features list above. See the [Coming Soon](#coming-soon) section for the full pending set.
|
|
542
501
|
|
|
543
502
|
### Coming Soon
|
|
544
503
|
|
|
545
|
-
The following
|
|
504
|
+
The following surfaces have stable public **types** (so creators can write code against them today) but no runtime yet. They live under `src/surfaces/pending/` in the SDK source and do NOT appear as `client.X` accessors — importing the types directly from `@stackbone/sdk` is the supported path while the runtime is in flight:
|
|
546
505
|
|
|
547
506
|
```ts
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
// Emit an event to the organization event bus.
|
|
556
|
-
await stackbone.events.emit('lead.qualified', { leadId: 42 });
|
|
557
|
-
|
|
558
|
-
// List OAuth connections the user attached (Notion, GDrive, Slack…).
|
|
559
|
-
const { data: connections } = await stackbone.connections.list();
|
|
507
|
+
import type {
|
|
508
|
+
PublishRequest,
|
|
509
|
+
AddMemoryRequest,
|
|
510
|
+
MemoryHit,
|
|
511
|
+
Prompt,
|
|
512
|
+
CreatePromptRequest,
|
|
513
|
+
} from '@stackbone/sdk';
|
|
560
514
|
```
|
|
561
515
|
|
|
516
|
+
The pending surfaces are `queues` (cross-container HTTP push), `memory` (mem0-backed long-term memory), `prompts` (managed prompts with `{{var}}` templates), `connections` (OAuth integrations) and `events` (organisation event bus). When their runtime lands they will be promoted into the live `client.X` set described in the Features section above.
|
|
517
|
+
|
|
562
518
|
## Configuration
|
|
563
519
|
|
|
564
|
-
`createClient` accepts a single configuration object
|
|
520
|
+
The runtime-injected `ctx.client` is already configured from the env vars the platform injects, so agent code rarely touches these fields. They matter when you reach for the `createClient()` escape hatch: `createClient` accepts a single configuration object where every field is optional and falls back to an environment variable, which the platform injects into the agent container at boot:
|
|
565
521
|
|
|
566
522
|
```ts
|
|
567
523
|
const stackbone = createClient({
|
|
@@ -577,8 +533,6 @@ const stackbone = createClient({
|
|
|
577
533
|
// OpenRouter credentials → OPENROUTER_API_KEY / OPENROUTER_BASE_URL
|
|
578
534
|
openrouterKey: '...',
|
|
579
535
|
openrouterBaseUrl: 'https://openrouter.ai/api/v1',
|
|
580
|
-
// QStash credentials (queues) → QSTASH_TOKEN / QSTASH_CURRENT_SIGNING_KEY / QSTASH_NEXT_SIGNING_KEY
|
|
581
|
-
qstashToken: '...',
|
|
582
536
|
// RAG document parser → LLAMA_PARSE_API_KEY
|
|
583
537
|
llamaParseApiKey: '...',
|
|
584
538
|
// mem0 credentials (memory module) → MEM0_API_KEY / MEM0_BASE_URL
|
|
@@ -602,7 +556,7 @@ const stackbone = createClient({
|
|
|
602
556
|
});
|
|
603
557
|
```
|
|
604
558
|
|
|
605
|
-
Inside a Stackbone-hosted container all of these are pre-populated, so production agent code
|
|
559
|
+
Inside a Stackbone-hosted container all of these are pre-populated, so production agent code just reads `ctx.client` (a zero-arg `createClient()` would resolve the same values). For local development the `stackbone dev` emulator wires up the same env vars against MinIO, a local Postgres branch and your own OpenRouter key.
|
|
606
560
|
|
|
607
561
|
## Result Envelope and Error Handling
|
|
608
562
|
|
|
@@ -622,7 +576,7 @@ interface SdkError {
|
|
|
622
576
|
Narrowing on `error` automatically refines `data` to `T`, so the typical caller looks like:
|
|
623
577
|
|
|
624
578
|
```ts
|
|
625
|
-
const result = await
|
|
579
|
+
const result = await client.ai.chat.completions.create({ model, messages });
|
|
626
580
|
|
|
627
581
|
if (result.error) {
|
|
628
582
|
console.error(`[${result.error.code}] ${result.error.message}`, result.error.meta);
|
|
@@ -634,18 +588,24 @@ console.log(result.data.choices[0]?.message.content);
|
|
|
634
588
|
|
|
635
589
|
Each module ships its own stable code prefix so you can pattern-match without parsing the message:
|
|
636
590
|
|
|
637
|
-
| Prefix | Source
|
|
638
|
-
| ----------------- |
|
|
639
|
-
| `ai_*` | `client.ai`
|
|
640
|
-
| `s3_*` | `client.storage`
|
|
641
|
-
| `rag_*` | `client.rag`
|
|
642
|
-
| `approval_*` | `client.approval`
|
|
643
|
-
| `secrets_*` | `client.secrets`
|
|
644
|
-
| `config_*` | `client.config`
|
|
645
|
-
| `memory_*` | `client.memory`
|
|
646
|
-
| `http_*` |
|
|
647
|
-
|
|
|
648
|
-
| `
|
|
591
|
+
| Prefix | Source | Examples |
|
|
592
|
+
| ----------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
593
|
+
| `ai_*` | `client.ai` | `ai_unauthorized`, `ai_credits_exhausted`, `ai_rate_limited`, `ai_aborted`, `ai_no_image_generated` |
|
|
594
|
+
| `s3_*` | `client.storage` | `s3_credentials_missing`, `s3_invalid_key`, `s3_error` |
|
|
595
|
+
| `rag_*` | `client.rag` | `rag_invalid_request`, `rag_dim_mismatch`, `rag_embedding_failed`, `rag_error` |
|
|
596
|
+
| `approval_*` | `client.approval` | `approval_invalid_request`, `approval_invalid_signature`, `approval_signature_expired`, `approval_signing_key_missing`, `approval_invalid_payload`, `approval_tool_execute_failed`, `approval_unauthorized`, `approval_forbidden`, `approval_not_found`, `approval_rate_limited`, `approval_unavailable` |
|
|
597
|
+
| `secrets_*` | `client.secrets` | `secrets_invalid_request`, `secrets_not_found`, `secrets_invalid_response`, `secrets_unauthorized`, `secrets_forbidden`, `secrets_rate_limited`, `secrets_unavailable` |
|
|
598
|
+
| `config_*` | `client.config` | `config_invalid_request`, `config_not_found`, `config_invalid_response`, `config_unauthorized`, `config_forbidden`, `config_rate_limited`, `config_unavailable` |
|
|
599
|
+
| `memory_*` | `client.memory` | reserved for the future mem0-backed implementation; today every method returns `not_implemented` |
|
|
600
|
+
| `http_*` | transport-level errors | `http_timeout`, `http_aborted`, `http_network_error`, `http_parse_error`, `http_request_failed` — surface-level statuses (401/403/404/429/5xx) remap to `<surface>_*` codes |
|
|
601
|
+
| `database_*` | `client.database` | `database_not_configured` (raised when `STACKBONE_POSTGRES_URL` is unset) |
|
|
602
|
+
| `observability_*` | run-cost rollup (`@stackbone/sdk/observability`, runtime-wired) | `observability_close_run_failed` |
|
|
603
|
+
| `contract_*` | gated surfaces | `contract_malformed`, `contract_unreachable`, `contract_version_unsupported` — emitted by any gated call when the contract handshake fails |
|
|
604
|
+
| `capability_*` | gated surfaces | `capability_unavailable` — emitted when the negotiated contract advertises no support for the surface |
|
|
605
|
+
| `*_missing` | configuration | `agent_id_missing`, `openrouter_key_missing`, `database_url_missing`, `stackbone_api_url_missing` |
|
|
606
|
+
| `not_implemented` | stubbed module surface | returned by every "coming soon" method until it ships |
|
|
607
|
+
|
|
608
|
+
> The canonical inventory of every code lives in `src/errors/codes.ts` as a typed catalog (`SdkErrorCode`). The table above is the human-readable summary; adding or removing a code is a single-file edit there and the compiler refuses any literal `code` value not declared in the catalog. Pattern-match against `SdkErrorCode` (exported from `@stackbone/sdk`) for full type narrowing, or call `isSdkErrorCode(raw)` to widen a wire string back into the catalog.
|
|
649
609
|
|
|
650
610
|
## TypeScript Support
|
|
651
611
|
|
package/db/index.d.cts
CHANGED
package/db/index.d.ts
CHANGED