@stackbone/sdk 0.1.0-alpha.3 → 0.1.0-alpha.5
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 +42 -0
- package/README.md +113 -87
- package/db/index.d.cts +1 -0
- package/db/index.d.ts +1 -0
- package/index.cjs +16462 -3519
- package/index.cjs.map +1 -1
- package/index.d.cts +863 -711
- package/index.d.ts +863 -711
- package/index.js +16446 -3511
- package/index.js.map +1 -1
- package/observability/index.cjs +14 -0
- package/observability/index.cjs.map +1 -1
- package/observability/index.d.cts +79 -79
- package/observability/index.d.ts +79 -79
- package/observability/index.js +14 -0
- package/observability/index.js.map +1 -1
- package/package.json +3 -13
- 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.5.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.3.tgz +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,8 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`client.connections` surface (gated by the new `connections.actions`
|
|
13
|
+
capability, contract v13).** Agents can list the connectors wired to their
|
|
14
|
+
installation and invoke a connector action through a control-plane proxy —
|
|
15
|
+
credentials never enter the agent container. `client.connections.list()`
|
|
16
|
+
returns the available connectors and their connections;
|
|
17
|
+
`client.connections.invoke(connector, action, args)` runs an action,
|
|
18
|
+
optionally pinned to a specific connection id.
|
|
19
|
+
|
|
20
|
+
### Removed
|
|
21
|
+
|
|
22
|
+
- **Breaking:** removed the `client.observability` surface (`spanProcessor`,
|
|
23
|
+
`logger`, `closeRun`, `closeLogger`, `flush`). Observability is no longer a
|
|
24
|
+
creator-facing `client` module: agent code logs through the handler-scoped
|
|
25
|
+
`ctx.logger` (already bound to the run) — plain `console.*` is captured and
|
|
26
|
+
correlated too — and the Studio run timeline is produced by the runtime, not
|
|
27
|
+
by code the agent registers. The platform primitives (`PlatformLogger`,
|
|
28
|
+
`RunStepsSpanProcessor`, `aggregateRunCost`) stay available on the
|
|
29
|
+
`@stackbone/sdk/observability` subpath, consumed by the runtime wrapper and
|
|
30
|
+
the agent emulator.
|
|
31
|
+
|
|
10
32
|
### Changed
|
|
11
33
|
|
|
34
|
+
- The runtime now injects the SDK client into every handler context as
|
|
35
|
+
`ctx.client` — one `StackboneClient` per process, shared across invocations.
|
|
36
|
+
Agent code reads it off the invocation context (`async run({ input, client })
|
|
37
|
+
{ ... }`) instead of constructing one at module scope, so a module-level
|
|
38
|
+
`const client = createClient()` is no longer required. `createClient()` stays
|
|
39
|
+
exported as an escape hatch for building a client outside a handler (scripts,
|
|
40
|
+
tests, one-off tasks) or with explicit config overrides.
|
|
41
|
+
|
|
42
|
+
- `client.queues` is now live (was a `notImplemented` stub). `publish`,
|
|
43
|
+
`schedule`, `unschedule` and `listSchedules` make authenticated calls to the
|
|
44
|
+
control-plane BullMQ dispatcher (`/api/v1/agent/queues/*`) using the existing
|
|
45
|
+
`STACKBONE_AGENT_JWT` channel — the agent never touches Redis.
|
|
46
|
+
**Breaking:** `publish` now takes `{ name, payload, retries?, delay?,
|
|
47
|
+
deduplicationId? }` and returns `{ messageId }` (was `{ url, body, retries?,
|
|
48
|
+
delay?, deduplicationId?, headers? }`). `name` is the opaque job name (e.g.
|
|
49
|
+
`send-email`), `payload` the opaque body, `delay` defers delivery in ms and
|
|
50
|
+
`retries` overrides the platform retry default per job. New public request
|
|
51
|
+
types `ScheduleRequest` / `UnscheduleRequest` are exported from the barrel.
|
|
52
|
+
Errors surface under the new `queues_*` prefix.
|
|
53
|
+
|
|
12
54
|
- `ResolvedConfig` is now an opaque, typed snapshot — the live `env`
|
|
13
55
|
channel is gone. Every consumer used to read environment variables with
|
|
14
56
|
the pattern `resolved.config.fieldX ?? resolved.env['FIELDX']`, which
|
package/README.md
CHANGED
|
@@ -18,9 +18,9 @@ Official TypeScript SDK for [Stackbone](https://stackbone.ai) — the marketplac
|
|
|
18
18
|
- **Approval** — Human-in-the-loop inbox: fire-and-forget approval requests, HMAC-signed decision callbacks, and an LLM tool wrapper that gates execution behind human review
|
|
19
19
|
- **Secrets** — Read organization-encrypted secrets registered in the dashboard, no client-side cache so rotations propagate immediately
|
|
20
20
|
- **Config** — Typed reads of dynamic per-agent configuration the user set in the dashboard
|
|
21
|
-
- **Coming soon (types only)** — `queues` (cross-container HTTP push via
|
|
21
|
+
- **Coming soon (types only)** — `queues` (cross-container HTTP push via the BullMQ job dispatcher), `memory` (mem0-backed long-term memory), `prompts` (managed prompts with `{{var}}` templates), `connections` (OAuth integrations) and `events` (org-wide event bus). Public types are exported from `@stackbone/sdk` today; the runtime is not wired yet, so there is no live `client.X` accessor.
|
|
22
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
|
|
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
|
|
24
24
|
|
|
25
25
|
## Installation
|
|
26
26
|
|
|
@@ -37,26 +37,44 @@ pnpm add @stackbone/sdk
|
|
|
37
37
|
|
|
38
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.
|
|
39
39
|
|
|
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`, `
|
|
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).
|
|
41
41
|
|
|
42
42
|
## Quick Start
|
|
43
43
|
|
|
44
|
-
###
|
|
44
|
+
### The Client
|
|
45
45
|
|
|
46
|
-
|
|
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:
|
|
47
47
|
|
|
48
48
|
```ts
|
|
49
|
-
import {
|
|
50
|
-
|
|
51
|
-
|
|
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
|
+
});
|
|
52
64
|
```
|
|
53
65
|
|
|
54
|
-
|
|
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.
|
|
55
69
|
|
|
56
70
|
```ts
|
|
57
71
|
import { createClient } from '@stackbone/sdk';
|
|
58
72
|
|
|
59
|
-
|
|
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({
|
|
60
78
|
agentJwt: process.env.STACKBONE_AGENT_JWT,
|
|
61
79
|
stackboneApiUrl: 'http://localhost:3000',
|
|
62
80
|
agentId: 'agent_local_dev',
|
|
@@ -69,7 +87,7 @@ const stackbone = createClient({
|
|
|
69
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.
|
|
70
88
|
|
|
71
89
|
```ts
|
|
72
|
-
import {
|
|
90
|
+
import { defineAgent, z } from '@stackbone/sdk';
|
|
73
91
|
import { eq, integer, pgTable, text } from '@stackbone/sdk/db';
|
|
74
92
|
|
|
75
93
|
// 1. Declare your tables — types flow end to end.
|
|
@@ -80,29 +98,39 @@ const leads = pgTable('leads', {
|
|
|
80
98
|
score: integer('score').notNull(),
|
|
81
99
|
});
|
|
82
100
|
|
|
83
|
-
|
|
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);
|
|
84
112
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.where(eq(leads.status, 'qualified'))
|
|
90
|
-
.limit(20);
|
|
113
|
+
// 3. Mutate.
|
|
114
|
+
await client.database
|
|
115
|
+
.insert(leads)
|
|
116
|
+
.values({ id: 1, email: 'jane@example.com', status: 'new', score: 0 });
|
|
91
117
|
|
|
92
|
-
|
|
93
|
-
await stackbone.database
|
|
94
|
-
.insert(leads)
|
|
95
|
-
.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));
|
|
96
119
|
|
|
97
|
-
await
|
|
120
|
+
await client.database.delete(leads).where(eq(leads.id, 1));
|
|
98
121
|
|
|
99
|
-
|
|
122
|
+
return {};
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
});
|
|
100
126
|
```
|
|
101
127
|
|
|
128
|
+
The snippets below assume the `client` destructured off `ctx` inside a handler (`async run({ client }) { ... }`).
|
|
129
|
+
|
|
102
130
|
Interactive transactions yield a `tx` with the same Drizzle surface as `client.database`:
|
|
103
131
|
|
|
104
132
|
```ts
|
|
105
|
-
await
|
|
133
|
+
await client.database.transaction(async (tx) => {
|
|
106
134
|
await tx.insert(leads).values({ id: 2, email: 'b@x.com', status: 'new', score: 0 });
|
|
107
135
|
await tx.update(leads).set({ status: 'qualified' }).where(eq(leads.id, 2));
|
|
108
136
|
});
|
|
@@ -113,7 +141,7 @@ For raw SQL, reach for the `sql` tag re-exported from `@stackbone/sdk/db`:
|
|
|
113
141
|
```ts
|
|
114
142
|
import { sql } from '@stackbone/sdk/db';
|
|
115
143
|
|
|
116
|
-
const { rows } = await
|
|
144
|
+
const { rows } = await client.database.execute(sql`SELECT NOW()`);
|
|
117
145
|
```
|
|
118
146
|
|
|
119
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.
|
|
@@ -124,7 +152,7 @@ const { rows } = await stackbone.database.execute(sql`SELECT NOW()`);
|
|
|
124
152
|
|
|
125
153
|
```ts
|
|
126
154
|
// Upload an object
|
|
127
|
-
const { data, error } = await
|
|
155
|
+
const { data, error } = await client.storage
|
|
128
156
|
.from('uploads')
|
|
129
157
|
.upload('reports/2026-04-29.pdf', pdfBlob, {
|
|
130
158
|
contentType: 'application/pdf',
|
|
@@ -132,22 +160,22 @@ const { data, error } = await stackbone.storage
|
|
|
132
160
|
});
|
|
133
161
|
|
|
134
162
|
// Download an object (returns a Blob)
|
|
135
|
-
const { data, error } = await
|
|
163
|
+
const { data, error } = await client.storage.from('uploads').download('reports/2026-04-29.pdf');
|
|
136
164
|
|
|
137
165
|
// List objects (paginated)
|
|
138
|
-
const { data, error } = await
|
|
166
|
+
const { data, error } = await client.storage
|
|
139
167
|
.from('uploads')
|
|
140
168
|
.list({ prefix: 'reports/', limit: 50 });
|
|
141
169
|
|
|
142
170
|
// Delete an object
|
|
143
|
-
const { data, error } = await
|
|
171
|
+
const { data, error } = await client.storage.from('uploads').remove('reports/2026-04-29.pdf');
|
|
144
172
|
```
|
|
145
173
|
|
|
146
174
|
Signed URLs and a public URL helper are exposed for direct browser uploads / downloads:
|
|
147
175
|
|
|
148
176
|
```ts
|
|
149
177
|
// Pre-signed upload URL (1h by default; clamp the contentType into the signature)
|
|
150
|
-
const { data: upload } = await
|
|
178
|
+
const { data: upload } = await client.storage
|
|
151
179
|
.from('uploads')
|
|
152
180
|
.getSignedUploadUrl('avatars/user-42.png', {
|
|
153
181
|
expiresIn: 600,
|
|
@@ -156,12 +184,12 @@ const { data: upload } = await stackbone.storage
|
|
|
156
184
|
|
|
157
185
|
// Pre-signed download URL — prefer this over `download()` for large objects,
|
|
158
186
|
// since `download()` materialises the entire body in memory as a Blob.
|
|
159
|
-
const { data: download } = await
|
|
187
|
+
const { data: download } = await client.storage
|
|
160
188
|
.from('uploads')
|
|
161
189
|
.getSignedDownloadUrl('reports/2026-04-29.pdf');
|
|
162
190
|
|
|
163
191
|
// Canonical public URL (only fetchable for public buckets)
|
|
164
|
-
const { data: url } =
|
|
192
|
+
const { data: url } = client.storage.from('uploads').getPublicUrl('avatars/user-42.png');
|
|
165
193
|
```
|
|
166
194
|
|
|
167
195
|
### AI
|
|
@@ -170,7 +198,7 @@ const { data: url } = stackbone.storage.from('uploads').getPublicUrl('avatars/us
|
|
|
170
198
|
|
|
171
199
|
```ts
|
|
172
200
|
// Chat completion
|
|
173
|
-
const { data, error } = await
|
|
201
|
+
const { data, error } = await client.ai.chat.completions.create({
|
|
174
202
|
model: 'anthropic/claude-sonnet-4.5',
|
|
175
203
|
messages: [
|
|
176
204
|
{ role: 'system', content: 'You are a sales qualification assistant.' },
|
|
@@ -186,7 +214,7 @@ if (!error) {
|
|
|
186
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`:
|
|
187
215
|
|
|
188
216
|
```ts
|
|
189
|
-
const { data: stream, error } = await
|
|
217
|
+
const { data: stream, error } = await client.ai.chat.completions.create({
|
|
190
218
|
model: 'anthropic/claude-sonnet-4.5',
|
|
191
219
|
messages: [{ role: 'user', content: 'Stream me a haiku.' }],
|
|
192
220
|
stream: true,
|
|
@@ -203,7 +231,7 @@ Embeddings, image generation and a model catalogue are first-class too:
|
|
|
203
231
|
|
|
204
232
|
```ts
|
|
205
233
|
// Embeddings — returns the standard OpenAI shape
|
|
206
|
-
const { data, error } = await
|
|
234
|
+
const { data, error } = await client.ai.embeddings.create({
|
|
207
235
|
model: 'openai/text-embedding-3-small',
|
|
208
236
|
input: 'A red Spanish sword from the 11th century.',
|
|
209
237
|
});
|
|
@@ -211,14 +239,14 @@ const { data, error } = await stackbone.ai.embeddings.create({
|
|
|
211
239
|
// Image generation — OpenRouter routes image models through chat completions;
|
|
212
240
|
// the SDK normalises the response so callers see the OpenAI-shaped payload.
|
|
213
241
|
// Returns `ai_no_image_generated` if the model produces zero images.
|
|
214
|
-
const { data, error } = await
|
|
242
|
+
const { data, error } = await client.ai.images.generate({
|
|
215
243
|
model: 'google/gemini-2.5-flash-image',
|
|
216
244
|
prompt: 'A medieval Spanish sword on a velvet cushion, museum lighting',
|
|
217
245
|
});
|
|
218
246
|
|
|
219
247
|
// List available models — preserves OpenRouter-specific fields like
|
|
220
248
|
// `pricing`, `context_length`, `supported_parameters`, `architecture`
|
|
221
|
-
const { data, error } = await
|
|
249
|
+
const { data, error } = await client.ai.models.list();
|
|
222
250
|
```
|
|
223
251
|
|
|
224
252
|
Cancellation works the way you'd expect — pass an `AbortSignal` and aborts surface as `ai_aborted` on non-streaming calls:
|
|
@@ -227,7 +255,7 @@ Cancellation works the way you'd expect — pass an `AbortSignal` and aborts sur
|
|
|
227
255
|
const controller = new AbortController();
|
|
228
256
|
setTimeout(() => controller.abort(), 5_000);
|
|
229
257
|
|
|
230
|
-
const { data, error } = await
|
|
258
|
+
const { data, error } = await client.ai.chat.completions.create(
|
|
231
259
|
{ model: 'anthropic/claude-sonnet-4.5', messages: [...] },
|
|
232
260
|
{ signal: controller.signal },
|
|
233
261
|
);
|
|
@@ -241,16 +269,16 @@ The fast path lets the SDK handle embeddings for you — pass an embedding `mode
|
|
|
241
269
|
|
|
242
270
|
```ts
|
|
243
271
|
// 1. Parse a document → plain text. Supports text/*, text/markdown and application/pdf.
|
|
244
|
-
const text = await
|
|
272
|
+
const text = await client.rag.parse(file);
|
|
245
273
|
|
|
246
274
|
// 2. Split into chunks. Pure utility, no DB call.
|
|
247
|
-
const chunks =
|
|
275
|
+
const chunks = client.rag.chunk(text, { size: 512, overlap: 64 });
|
|
248
276
|
|
|
249
277
|
// 3. Ingest. The SDK embeds the chunks for you in batches of 128 and
|
|
250
278
|
// provisions the schema on the first call (HNSW + cosine, dims inferred
|
|
251
279
|
// from the first batch). Re-ingesting the same `id` atomically replaces
|
|
252
280
|
// all of that document's chunks.
|
|
253
|
-
await
|
|
281
|
+
await client.rag.ingest({
|
|
254
282
|
id: 'doc-1',
|
|
255
283
|
chunks,
|
|
256
284
|
model: 'openai/text-embedding-3-small',
|
|
@@ -259,7 +287,7 @@ await stackbone.rag.ingest({
|
|
|
259
287
|
|
|
260
288
|
// 4. Retrieve. Pass the user's question and the same model — the SDK embeds
|
|
261
289
|
// it for you and returns hits scored in [0, 1] (cosine similarity).
|
|
262
|
-
const { data: hits } = await
|
|
290
|
+
const { data: hits } = await client.rag.retrieve({
|
|
263
291
|
text: 'how does X work?',
|
|
264
292
|
model: 'openai/text-embedding-3-small',
|
|
265
293
|
topK: 5,
|
|
@@ -270,12 +298,12 @@ const { data: hits } = await stackbone.rag.retrieve({
|
|
|
270
298
|
The escape hatch — pass embeddings precomputed yourself (different provider, custom dimensions, your own batching, deterministic offline tests):
|
|
271
299
|
|
|
272
300
|
```ts
|
|
273
|
-
const { data: embeddings } = await
|
|
301
|
+
const { data: embeddings } = await client.ai.embeddings.create({
|
|
274
302
|
model: 'openai/text-embedding-3-small',
|
|
275
303
|
input: chunks,
|
|
276
304
|
});
|
|
277
305
|
|
|
278
|
-
await
|
|
306
|
+
await client.rag.ingest({
|
|
279
307
|
id: 'doc-1',
|
|
280
308
|
chunks: chunks.map((content, i) => ({
|
|
281
309
|
content,
|
|
@@ -284,7 +312,7 @@ await stackbone.rag.ingest({
|
|
|
284
312
|
metadata: { source: 'manual-upload' },
|
|
285
313
|
});
|
|
286
314
|
|
|
287
|
-
const { data: hits } = await
|
|
315
|
+
const { data: hits } = await client.rag.retrieve({
|
|
288
316
|
embedding: queryEmbedding,
|
|
289
317
|
topK: 5,
|
|
290
318
|
});
|
|
@@ -293,29 +321,29 @@ const { data: hits } = await stackbone.rag.retrieve({
|
|
|
293
321
|
Delete by id (whole document) or by metadata predicate:
|
|
294
322
|
|
|
295
323
|
```ts
|
|
296
|
-
await
|
|
297
|
-
await
|
|
324
|
+
await client.rag.delete('doc-1');
|
|
325
|
+
await client.rag.delete(['doc-1', 'doc-2']);
|
|
298
326
|
|
|
299
|
-
await
|
|
327
|
+
await client.rag.deleteWhere({ source: 'manual-upload' });
|
|
300
328
|
```
|
|
301
329
|
|
|
302
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:
|
|
303
331
|
|
|
304
332
|
```ts
|
|
305
|
-
await
|
|
333
|
+
await client.rag.ingest({
|
|
306
334
|
id: 'faq-1',
|
|
307
335
|
chunks: [...],
|
|
308
336
|
namespace: 'faqs',
|
|
309
337
|
});
|
|
310
338
|
|
|
311
|
-
await
|
|
312
|
-
await
|
|
339
|
+
await client.rag.retrieve({ embedding, namespace: 'faqs' });
|
|
340
|
+
await client.rag.deleteWhere({}, { namespace: 'faqs' });
|
|
313
341
|
```
|
|
314
342
|
|
|
315
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:
|
|
316
344
|
|
|
317
345
|
```ts
|
|
318
|
-
await
|
|
346
|
+
await client.rag.reset();
|
|
319
347
|
```
|
|
320
348
|
|
|
321
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.
|
|
@@ -328,7 +356,7 @@ This shape decouples waiting from the container lifecycle — the agent does not
|
|
|
328
356
|
|
|
329
357
|
```ts
|
|
330
358
|
// 1. Issue an approval — returns immediately, the human reviews asynchronously.
|
|
331
|
-
const { data, error } = await
|
|
359
|
+
const { data, error } = await client.airoval.request({
|
|
332
360
|
topic: 'lead.qualify',
|
|
333
361
|
payload: { leadId: 42, score: 0.6 },
|
|
334
362
|
title: 'Approve lead qualification',
|
|
@@ -346,7 +374,7 @@ const { data, error } = await stackbone.airoval.request({
|
|
|
346
374
|
// returns a typed Decision<T>. Pass the raw `Request` (Fetch / Hono /
|
|
347
375
|
// undici) and switch on `decision.status`.
|
|
348
376
|
app.post('/approvals/lead-qualify', async (c) => {
|
|
349
|
-
const result = await
|
|
377
|
+
const result = await client.airoval.verify<{ leadId: number; score: number }>(c.req.raw);
|
|
350
378
|
if (result.error) return c.text('invalid', 401);
|
|
351
379
|
switch (result.data.status) {
|
|
352
380
|
case 'approved':
|
|
@@ -367,9 +395,9 @@ app.post('/approvals/lead-qualify', async (c) => {
|
|
|
367
395
|
Operations on existing approvals:
|
|
368
396
|
|
|
369
397
|
```ts
|
|
370
|
-
await
|
|
371
|
-
const { data: record } = await
|
|
372
|
-
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({
|
|
373
401
|
status: 'pending',
|
|
374
402
|
topic: 'lead.qualify',
|
|
375
403
|
limit: 50,
|
|
@@ -378,10 +406,10 @@ const { data: page } = await stackbone.airoval.list({
|
|
|
378
406
|
|
|
379
407
|
#### Approval-gated LLM tools
|
|
380
408
|
|
|
381
|
-
`
|
|
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.
|
|
382
410
|
|
|
383
411
|
```ts
|
|
384
|
-
const sendInvoice =
|
|
412
|
+
const sendInvoice = client.airoval.tool({
|
|
385
413
|
name: 'send_invoice',
|
|
386
414
|
description: 'Send an invoice to a recipient.',
|
|
387
415
|
parameters: {
|
|
@@ -402,7 +430,7 @@ const sendInvoice = stackbone.airoval.tool({
|
|
|
402
430
|
});
|
|
403
431
|
|
|
404
432
|
// Pass the tool to chat.completions.create — it speaks the OpenAI shape.
|
|
405
|
-
const completion = await
|
|
433
|
+
const completion = await client.ai.chat.completions.create({
|
|
406
434
|
model: 'anthropic/claude-sonnet-4.5',
|
|
407
435
|
messages,
|
|
408
436
|
tools: [sendInvoice.openaiSpec()],
|
|
@@ -433,13 +461,13 @@ The signing key for `verify()` is read from `STACKBONE_APPROVAL_SIGNING_KEY` (or
|
|
|
433
461
|
|
|
434
462
|
```ts
|
|
435
463
|
// Single secret — `secrets_not_found` error if the name is not registered.
|
|
436
|
-
const { data: apiKey, error } = await
|
|
464
|
+
const { data: apiKey, error } = await client.secrets.get('STRIPE_API_KEY');
|
|
437
465
|
if (!error) {
|
|
438
466
|
const stripe = new Stripe(apiKey);
|
|
439
467
|
}
|
|
440
468
|
|
|
441
469
|
// Bulk read at boot — one round-trip; missing names come back as omissions.
|
|
442
|
-
const { data: secrets } = await
|
|
470
|
+
const { data: secrets } = await client.secrets.getMany(['STRIPE_API_KEY', 'SLACK_BOT_TOKEN']);
|
|
443
471
|
// secrets: { STRIPE_API_KEY?: string; SLACK_BOT_TOKEN?: string }
|
|
444
472
|
```
|
|
445
473
|
|
|
@@ -449,17 +477,17 @@ const { data: secrets } = await stackbone.secrets.getMany(['STRIPE_API_KEY', 'SL
|
|
|
449
477
|
|
|
450
478
|
```ts
|
|
451
479
|
// Typed read — the generic narrows `data` to `'formal' | 'casual'`.
|
|
452
|
-
const { data: tone } = await
|
|
480
|
+
const { data: tone } = await client.config.get<'formal' | 'casual'>('reply_tone');
|
|
453
481
|
|
|
454
482
|
// Complex JSON — pass the full shape as the generic.
|
|
455
483
|
interface Preferences {
|
|
456
484
|
tone: 'formal' | 'casual';
|
|
457
485
|
maxEmailsPerDay: number;
|
|
458
486
|
}
|
|
459
|
-
const { data: prefs } = await
|
|
487
|
+
const { data: prefs } = await client.config.get<Preferences>('preferences');
|
|
460
488
|
|
|
461
489
|
// Bulk read — typed via the second generic; missing keys come back as omissions.
|
|
462
|
-
const { data: cfg } = await
|
|
490
|
+
const { data: cfg } = await client.config.getMany<{
|
|
463
491
|
reply_tone: 'formal' | 'casual';
|
|
464
492
|
max_emails: number;
|
|
465
493
|
}>(['reply_tone', 'max_emails']);
|
|
@@ -489,7 +517,7 @@ The pending surfaces are `queues` (cross-container HTTP push), `memory` (mem0-ba
|
|
|
489
517
|
|
|
490
518
|
## Configuration
|
|
491
519
|
|
|
492
|
-
`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:
|
|
493
521
|
|
|
494
522
|
```ts
|
|
495
523
|
const stackbone = createClient({
|
|
@@ -505,8 +533,6 @@ const stackbone = createClient({
|
|
|
505
533
|
// OpenRouter credentials → OPENROUTER_API_KEY / OPENROUTER_BASE_URL
|
|
506
534
|
openrouterKey: '...',
|
|
507
535
|
openrouterBaseUrl: 'https://openrouter.ai/api/v1',
|
|
508
|
-
// QStash credentials (queues) → QSTASH_TOKEN / QSTASH_CURRENT_SIGNING_KEY / QSTASH_NEXT_SIGNING_KEY
|
|
509
|
-
qstashToken: '...',
|
|
510
536
|
// RAG document parser → LLAMA_PARSE_API_KEY
|
|
511
537
|
llamaParseApiKey: '...',
|
|
512
538
|
// mem0 credentials (memory module) → MEM0_API_KEY / MEM0_BASE_URL
|
|
@@ -530,7 +556,7 @@ const stackbone = createClient({
|
|
|
530
556
|
});
|
|
531
557
|
```
|
|
532
558
|
|
|
533
|
-
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.
|
|
534
560
|
|
|
535
561
|
## Result Envelope and Error Handling
|
|
536
562
|
|
|
@@ -550,7 +576,7 @@ interface SdkError {
|
|
|
550
576
|
Narrowing on `error` automatically refines `data` to `T`, so the typical caller looks like:
|
|
551
577
|
|
|
552
578
|
```ts
|
|
553
|
-
const result = await
|
|
579
|
+
const result = await client.ai.chat.completions.create({ model, messages });
|
|
554
580
|
|
|
555
581
|
if (result.error) {
|
|
556
582
|
console.error(`[${result.error.code}] ${result.error.message}`, result.error.meta);
|
|
@@ -562,22 +588,22 @@ console.log(result.data.choices[0]?.message.content);
|
|
|
562
588
|
|
|
563
589
|
Each module ships its own stable code prefix so you can pattern-match without parsing the message:
|
|
564
590
|
|
|
565
|
-
| Prefix | Source
|
|
566
|
-
| ----------------- |
|
|
567
|
-
| `ai_*` | `client.ai`
|
|
568
|
-
| `s3_*` | `client.storage`
|
|
569
|
-
| `rag_*` | `client.rag`
|
|
570
|
-
| `approval_*` | `client.approval`
|
|
571
|
-
| `secrets_*` | `client.secrets`
|
|
572
|
-
| `config_*` | `client.config`
|
|
573
|
-
| `memory_*` | `client.memory`
|
|
574
|
-
| `http_*` | transport-level errors
|
|
575
|
-
| `database_*` | `client.database`
|
|
576
|
-
| `observability_*` |
|
|
577
|
-
| `contract_*` | gated surfaces
|
|
578
|
-
| `capability_*` | gated surfaces
|
|
579
|
-
| `*_missing` | configuration
|
|
580
|
-
| `not_implemented` | stubbed module surface
|
|
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 |
|
|
581
607
|
|
|
582
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.
|
|
583
609
|
|
package/db/index.d.cts
CHANGED
package/db/index.d.ts
CHANGED