@stackbone/sdk 0.1.0-alpha.1 → 0.1.0-alpha.3

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 CHANGED
@@ -7,6 +7,323 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Changed
11
+
12
+ - `ResolvedConfig` is now an opaque, typed snapshot — the live `env`
13
+ channel is gone. Every consumer used to read environment variables with
14
+ the pattern `resolved.config.fieldX ?? resolved.env['FIELDX']`, which
15
+ spread two pieces of knowledge across ~20 call sites: the exact env-var
16
+ name and the precedence rule. The resolver now owns both. After this
17
+ change, modules read typed fields like `resolved.stackboneApiUrl`,
18
+ `resolved.s3Bucket`, `resolved.databaseUrl`, `resolved.requireContract`,
19
+ etc. — none of them touch `process.env` directly.
20
+
21
+ The resolver is the **single** seam that reads `process.env` in the
22
+ library. New env-driven settings go in two places: a typed field on
23
+ `ResolvedConfig` and the matching `config.xxx ?? env['XXX']` (or parsing
24
+ logic) inside `resolveConfig`. The compiler then flags every consumer
25
+ that needs the new field.
26
+
27
+ Runtime knobs previously read inline from `contract/contract-handshake.ts`
28
+ (`STACKBONE_REQUIRE_CONTRACT`, `STACKBONE_CONTRACT_TTL_MS`,
29
+ `STACKBONE_DEBUG`) are now resolved up-front and threaded into the per-
30
+ client `ContractStore`. The database singleton in
31
+ `surfaces/agent-local/database/shared-handle.ts` takes an explicit
32
+ `connectionString` parameter that `DatabaseModule` pulls off
33
+ `resolved.databaseUrl`; the only remaining read of
34
+ `STACKBONE_POSTGRES_URL` is inside `resolveConfig`.
35
+
36
+ Booleans (`STACKBONE_DEBUG=1`) and numbers (`STACKBONE_CONTRACT_TTL_MS=30000`)
37
+ get parsed at resolve time, so consumers receive `boolean` / `number`,
38
+ not raw strings — one parse, one place, no drift.
39
+
40
+ Behavioural change worth flagging: `ResolvedConfig` is a snapshot, not
41
+ a live view. Tests that mutated `process.env` AFTER `createClient()` to
42
+ exercise late-binding behaviour now need to rebuild the client (or pass
43
+ the new value through the config object). The new `src/config.spec.ts`
44
+ pins both the snapshot semantics and the deletion contract — `.env` is
45
+ no longer a property on `ResolvedConfig`, and reading it is both a type
46
+ error and a runtime `undefined`. Two integration / handshake tests
47
+ (`STACKBONE_REQUIRE_CONTRACT=0`, `STACKBONE_CONTRACT_TTL_MS`,
48
+ `STACKBONE_DEBUG=1`) now construct a fresh `ContractStore` per env-var
49
+ variation to keep the resolve-once contract explicit.
50
+
51
+ - `SdkError.code` is now a typed catalog (`SdkErrorCode`) declared in one
52
+ place — `src/errors/codes.ts`. Every literal `code` value that ships on a
53
+ `Result<T>` envelope (and the `errorMapping.prefix` slot on the transport)
54
+ is now compiler-checked against the catalog, so a future PR that adds
55
+ `secrets_not_foud` (typo) or `ai_provdier_down` (typo) fails to build
56
+ instead of silently shipping past the README contract. The catalog also
57
+ ties `HttpClient`'s `<prefix>_<reason>` projection to `SdkErrorPrefix`, so
58
+ a surface that wires `errorMapping: { prefix: '<unknown>' }` no longer
59
+ compiles. The wire-level shape of every code is unchanged: this is a
60
+ type-level constraint, not a rename.
61
+
62
+ Adding a new code is a single-file edit in `src/errors/codes.ts` (either
63
+ a new suffix under an existing prefix, or a freestanding code under
64
+ `SDK_ERROR_CODES_STANDALONE`). The compiler then flags every call site
65
+ that needs the new literal — and the new exhaustiveness test in
66
+ `src/lib/http-client.spec.ts` asserts every `<prefix>_<reason>` the
67
+ transport can emit is still a member of the catalog so README/code drift
68
+ fails loudly.
69
+
70
+ Two new symbols on the public barrel (`@stackbone/sdk`):
71
+ - `SdkErrorCode` — the union projection of the catalog. Use it as the
72
+ return type when you write a helper that produces an `SdkError`, or
73
+ as a discriminant in a `switch` over `result.error.code`.
74
+ - `isSdkErrorCode(raw: string)` — runtime narrowing for wire strings
75
+ that come from a decoded body. Returns `true` only when `raw` is a
76
+ catalog member; lets the SDK widen upstream errors safely.
77
+
78
+ RAG's `toRagError(cause, message, fallbackCode)` helper now constrains
79
+ `fallbackCode` to `SdkErrorCode` and checks `cause.code` against the
80
+ catalog before reusing it. The catalog and the helper are not folded
81
+ into one file — the catalog is the inventory, `toRagError` stays in the
82
+ RAG folder as a Postgres-tagged-error classifier.
83
+
84
+ - Contract gating membership is now an explicit rule, and the handshake
85
+ cache no longer lives in a process-wide static. Two things changed:
86
+ 1. **Membership rule (Option B from the audit)** — a surface gates iff
87
+ `MODULE_CAPABILITIES` (in `@stackbone/validators`) advertises a row
88
+ for its module id. `MODULE_CAPABILITIES` is now the single source of
89
+ truth: adding a surface to gating means adding a `Capability` and a
90
+ `<module>: '<capability>'` row first, then wiring
91
+ `createModuleGate('<module>', resolved, store)` in the surface
92
+ constructor. The new docstring at the top of
93
+ `contract/capability-registry.ts` spells out the rule and the steps.
94
+ Today's gated set stays the same (`database`, `rag`, `queues`,
95
+ `events`, `secrets`, `config`, `approval`, `storage`, `ai`). The
96
+ four ungated surfaces (`memory`, `connections`, `prompts`,
97
+ `observability`) now carry a one-paragraph comment on their class
98
+ header documenting **why** they are not gated — either the runtime
99
+ does not yet exist with a defined capability (`memory`,
100
+ `connections`, `prompts`) or the surface stays local / talks to
101
+ OpenTelemetry, not the Stackbone Agent Protocol (`observability`).
102
+
103
+ 2. **Per-client handshake store** — the `ContractHandshake` static
104
+ namespace is gone. In its place: `createContractStore()` returns a
105
+ `ContractStore` with three methods (`get`, `peek`, `gatingEnabled`)
106
+ that owns its own cache, single-flight map, TTL eviction and debug
107
+ bookkeeping. `StackboneClient` instantiates exactly one store per
108
+ instance and threads it through every gated surface via
109
+ `createModuleGate(module, resolved, store)`. Two clients
110
+ constructed in the same process no longer share handshake cache or
111
+ suppression-warning state — including `client.contract`, which now
112
+ reads from the per-client store's `peek()`.
113
+
114
+ Removed test globals (the visible sign that the cache had escaped its
115
+ owner): `ContractHandshake.__resetForTests` and
116
+ `__resetModuleGateWarningsForTests`. Specs that previously called them
117
+ in `beforeEach`/`afterEach` now simply construct a fresh
118
+ `createContractStore()` (or, for client-level coverage, a fresh
119
+ `createClient(...)`); state isolation is intrinsic to construction.
120
+ `module-gate.spec.ts` gained two coverage tests for the new shape:
121
+ "two gates pinned to the same store share suppression-warning state"
122
+ and "two gates pinned to different stores warn independently". The E2E
123
+ suite (`integration/handshake.spec.ts`) loses its global-reset
124
+ bookkeeping entirely.
125
+
126
+ Wire protocol is unchanged: `GET /api/contract` returns the same JSON
127
+ shape, `client.contract` exposes the same read-only `ContractResponse |
128
+ null` getter, gate-blocked calls still return the same `Result`
129
+ envelope with `capability_unavailable` / `contract_version_unsupported`
130
+ / `contract_unreachable` codes, and the `STACKBONE_REQUIRE_CONTRACT=0`
131
+ / `STACKBONE_CONTRACT_TTL_MS` / `STACKBONE_DEBUG=1` escape hatches
132
+ behave identically. Surface constructors keep the same `gate?:
133
+ ModuleGate` test seam — only the internal default (now backed by a
134
+ per-client store instead of a static singleton) changed.
135
+
136
+ - The cross-surface shared-handle seam is now an explicit, documented
137
+ pattern on the public surfaces. Two surfaces produce a reusable
138
+ resource and expose it through a canonical accessor:
139
+ - `client.database.shared(): DrizzleClient` — process-wide Drizzle
140
+ handle bound to `STACKBONE_POSTGRES_URL`. Same input → same
141
+ instance. `client.database.raw()` stays as the creator-facing
142
+ escape hatch and is now documented as an alias of `shared()`.
143
+ - `client.ai.shared(): Result<OpenAI>` — process-wide OpenRouter
144
+ `OpenAI` client used by every namespace under `client.ai`. Same
145
+ input → same instance; reflects the `clientOverride` injected at
146
+ construction time. The previous private `client()` accessor folded
147
+ into this public name.
148
+
149
+ These accessors are the **only** way cross-surface SDK consumers
150
+ should reach the shared pool / OpenAI client. RAG already consumed
151
+ the database pool via a module-private file (`database/internal.ts`)
152
+ — RAG now reaches it through `client.database.shared().$client`
153
+ instead. The renamed `database/shared-handle.ts` keeps the singleton
154
+ implementation but has zero sibling-relative importers outside its
155
+ own folder and its own spec; cross-surface friction (memory and
156
+ queues will need the same handles per the consolidation ADRs) now
157
+ has one named extension point instead of a per-consumer escape
158
+ hatch.
159
+
160
+ Public API of `client.database`, `client.ai`, and `client.rag` is
161
+ unchanged — method signatures and `Result` envelopes match the
162
+ previous release. `RagModule` constructor adds a
163
+ `() => DatabaseModule` accessor alongside the existing
164
+ `() => AiModule`, mirroring the wiring `client.ts` produces; tests
165
+ pass both verbatim.
166
+
167
+ - **BREAKING** — Control-plane facades (`client.secrets`, `client.config`,
168
+ `client.approval`) now surface HTTP failures with surface-specific error
169
+ codes instead of the generic `http_*` family. The transport (`HttpClient`)
170
+ absorbs response validation, status→domain remapping, and querystring
171
+ serialisation, so a facade method now reads as "POST to /path with body X,
172
+ validate against schema Y, error prefix Z" instead of a 30–80 line
173
+ hand-rolled pipeline per surface. Migration is mechanical: pattern-match
174
+ the surface prefix instead of `http_*`.
175
+ - 401 → `<prefix>_unauthorized` (was `http_unauthorized`).
176
+ - 403 → `<prefix>_forbidden` (was `http_forbidden`).
177
+ - 404 → `<prefix>_not_found` (was `http_not_found`).
178
+ - 408 → `<prefix>_timeout` (was `http_timeout`).
179
+ - 429 → `<prefix>_rate_limited` (was `http_rate_limited`).
180
+ - 5xx → `<prefix>_unavailable` (was `http_server_error`).
181
+ - other 4xx → `<prefix>_invalid_request` (was `http_client_error`).
182
+ - schema-drift on a 2xx body → `<prefix>_invalid_response`.
183
+
184
+ The raw HTTP status stays accessible at `result.error.meta.status` for
185
+ callers that still want to pattern-match on the wire. Endpoint-specific
186
+ overrides (e.g. a POST whose `409` means "already exists") are wired by
187
+ facades via the new `errorOverrides` option on the transport request.
188
+
189
+ Transport-level errors (`http_timeout`, `http_aborted`,
190
+ `http_network_error`, `http_parse_error`, `http_request_failed`,
191
+ `http_invalid_response`, `http_invalid_request`) keep the `http_*`
192
+ prefix the README documents — they are not surface-specific.
193
+
194
+ Internal cleanup: `src/lib/facade-helpers.ts` is gone (the two utilities
195
+ it held — `remapHttpNotFound` and `validateStringArray` — were either
196
+ absorbed by the transport or relocated to `lib/http-client.ts` as a
197
+ named export). The per-method bodies on the control-plane facades
198
+ shrank substantially: `secrets.get` and `config.get` lost their
199
+ hand-rolled `value` type-checks and remap blocks, batch endpoints
200
+ pushed array validation + comma-join down to the transport, and the
201
+ approval list endpoint dropped its `pickDefinedAsStrings` helper for
202
+ the transport's built-in scalar coercion. File-level line counts barely
203
+ move because the public type definitions dominate each facade (notably
204
+ on `approval.ts` where 80+ lines are interface exports), but the
205
+ per-method logic now reads like the design target: "POST to /path,
206
+ validate against schema, error prefix is X".
207
+
208
+ - **BREAKING** — Platform-side observability primitives moved off the main
209
+ `@stackbone/sdk` barrel and into a dedicated subpath entrypoint,
210
+ `@stackbone/sdk/observability`. The main barrel previously exposed both the
211
+ handler-scoped `createStructuredLogger` (the per-invocation `Logger` the
212
+ runtime injects into creator handlers) and the platform/run-scoped
213
+ `createPlatformLogger` factory side-by-side, with no signal about which
214
+ belonged to which audience. Creators only ever consume the run-scoped
215
+ surface through `client.observability.*` (the live accessors stay on
216
+ `StackboneClient` unchanged); only platform/runtime tooling and integration
217
+ tests need to construct the primitives directly. The symbols that moved:
218
+ - Per-run logger — `createPlatformLogger`, `LOG_LEVELS`, `defaultRunsDir`,
219
+ plus the types `LogLevelName`, `LogLevelNumber`, `LogRecord`, `PinoLike`,
220
+ `PlatformLogger`, `PlatformLoggerMode`, `PlatformLoggerOptions`.
221
+ - OTel span processor — `RunStepsSpanProcessor`, `RUN_STEP_TYPES`,
222
+ `RUN_ID_ATTRIBUTE`, `STEP_TYPE_ATTRIBUTE`, plus the types
223
+ `ReadableSpanLike`, `RunStepType`, `RunStepsSpanProcessorOptions`,
224
+ `SpanContextLike`, `PostgresLike`.
225
+ - Run-cost aggregator — `aggregateRunCost`, plus the types
226
+ `AggregateRunCostOptions`, `AggregateRunCostResult`.
227
+
228
+ Migration is mechanical:
229
+
230
+ ```ts
231
+ // Before
232
+ import { createPlatformLogger, RunStepsSpanProcessor } from '@stackbone/sdk';
233
+ // After
234
+ import { createPlatformLogger, RunStepsSpanProcessor } from '@stackbone/sdk/observability';
235
+ ```
236
+
237
+ The handler-scoped `createStructuredLogger`, `Logger`, `LoggerBindings`,
238
+ `LogSink` and `StructuredLoggerOptions` stay on the main `@stackbone/sdk`
239
+ barrel: those are the surface the runtime wrapper hands to creator
240
+ handlers via `InvokeContext.logger`. JSONL output shape, OTLP/HTTP/JSON
241
+ serialisation and `~/.stackbone/dev/runs/<runId>.jsonl` paths are
242
+ unchanged — only the import path moved.
243
+
244
+ - **BREAKING** — SDK source tree reorganised under `src/surfaces/` by **where
245
+ each surface's state lives** instead of "wraps SDK vs makes HTTP fetch". The
246
+ old `src/modules/` and `src/facades/` directories are gone. New layout:
247
+ - `src/surfaces/agent-local/` — state in the agent's own Postgres
248
+ (`database`, `rag`).
249
+ - `src/surfaces/external/` — managed partner SDKs the SDK wraps directly
250
+ (`ai`, `storage`, `observability`).
251
+ - `src/surfaces/control-plane/` — thin HTTP calls to the Stackbone control
252
+ plane (`approval`, `secrets`, `config`).
253
+ - `src/surfaces/pending/` — surface types are exported but the runtime is
254
+ not built yet (`queues`, `memory`, `prompts`, `connections`, `events`).
255
+
256
+ Subpath entrypoints (`@stackbone/sdk/db`, `@stackbone/sdk/db/testing`,
257
+ `@stackbone/sdk/queues/types`, `@stackbone/sdk/rag/migrations`,
258
+ `@stackbone/sdk/rag/schema`) keep working — only the underlying file
259
+ paths moved.
260
+
261
+ - The five pending surfaces (`queues`, `memory`, `prompts`, `connections`,
262
+ `events`) now live under `src/surfaces/pending/` and are exposed as live
263
+ `client.X` accessors that return `not_implemented` on every method. The
264
+ earlier draft of this refactor dropped the accessors entirely, but MVP
265
+ agent code needs to be authored against the eventual contract today and
266
+ switch over the day the backing runtime ships — keeping the getters as
267
+ placeholders makes that one-line change instead of a refactor. Public
268
+ types continue to ship from `@stackbone/sdk` (`PublishRequest`,
269
+ `MemoryItem`, `MemoryHit`, `Prompt`, `CreatePromptRequest`, …) so
270
+ creators can keep modelling integrations. `queues` and `events` go
271
+ through the contract gate (they are listed in `MODULE_CAPABILITIES`);
272
+ `memory`, `prompts` and `connections` are intentionally ungated.
273
+
274
+ - **BREAKING** — `@stackbone/sdk/db/testing` `createTestDatabase` now spins up
275
+ a real Postgres container via `@testcontainers/postgresql` instead of
276
+ carving a random schema out of a shared dev DB. Callers no longer pass a
277
+ `connectionString`; pass an optional `image` (defaults to
278
+ `pgvector/pgvector:pg17`) if you need extensions beyond pgvector. Tests are
279
+ self-contained: no `docker compose up` ceremony, no `STACKBONE_*_POSTGRES_URL`
280
+ env, no schema bookkeeping. Aligns the SDK helper with the rest of the
281
+ monorepo (see `apps/api/src/db/__tests__/setup-postgres.ts`). Docker must
282
+ be running on the host.
283
+
284
+ ## [0.1.0-alpha.2] - 2026-05-18
285
+
286
+ ### Fixed
287
+
288
+ - `client.rag.ingest` (batched mode, e.g. when an `onProgress` callback or a
289
+ `jobWriter` is wired): every chunk batch was wrapping its INSERT in a
290
+ transaction that first ran `DELETE FROM rag_chunks WHERE namespace=? AND
291
+ doc_id=?`. With N batches per document, only the rows inserted by the LAST
292
+ batch survived — every earlier slice was wiped by the next batch's DELETE.
293
+ Split the DELETE into a single `clearDocument` call before the loop;
294
+ `ingestDocument` now only INSERTs its slice.
295
+ - `client.rag` error mapping: `42P01 relation does not exist` was only
296
+ translated to `SdkError('rag_schema_missing')` from the pipeline path.
297
+ `reset()`, the database-handle resolver, and `client.rag.ingestAsync`'s job
298
+ writer used a divergent local copy of `toRagError` that surfaced the raw
299
+ Postgres code instead. Consolidated all three call sites onto a shared
300
+ `modules/rag/errors.ts` so the schema-missing hint is consistent.
301
+ - `RunStepsSpanProcessor.stepIdBySpanId`: spans without a `stackbone.run.id`
302
+ attribute leaked their reservation in the map — `buildRow` returned early
303
+ before the cleanup line ran. Cleanup now happens unconditionally on every
304
+ `onEnd`, so dropped spans no longer accumulate.
305
+
306
+ ### Changed
307
+
308
+ - OTLP log records sent by `createPlatformLogger` now carry the actual
309
+ installed SDK version under `scope.version`. Previously every record was
310
+ tagged with a hardcoded `'0.0.1'` regardless of what was published.
311
+ - `client.rag.reset()` invalidates the cached schema fingerprint, so the
312
+ first ingest after a reset re-provisions the schema without serving a
313
+ stale `rag_dim_mismatch`.
314
+ - `client.rag.ingest`: `ensureSchema` now memoises the verified dimensions
315
+ per `Sql` instance, dropping the per-call `_rag_meta` SELECT on the hot
316
+ path. The `stackbone_rag_jobs` writer also collapses its
317
+ `ensureCollection` lookup to a single `INSERT … ON CONFLICT … RETURNING`
318
+ (was a `SELECT` followed by an upsert) and trims `isCancelled` to
319
+ `SELECT status … LIMIT 1`.
320
+ - `OtlpLogsBody` serialisation no longer iterates `Object.entries(LOG_LEVELS)`
321
+ per record to map level numbers to severity text — precomputed lookup
322
+ table replaces the linear scan.
323
+ - `RagModule` test seams (`sqlProvider`, `jobWriterFactory`) now flow
324
+ through a typed 4th constructor parameter (`RagModuleTestDeps`) instead of
325
+ private fields mutated via `as unknown as` casts.
326
+
10
327
  ## [0.1.0-alpha.1] - 2026-05-15
11
328
 
12
329
  ### BREAKING
package/README.md CHANGED
@@ -13,15 +13,12 @@ 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** _(coming soon)_ 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** _(coming soon)_ — 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
- - **Observability** _(coming soon)_ — OpenTelemetry traces and logs flushed through the platform collector
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
- - **Secrets** — Read workspace-encrypted secrets registered in the dashboard, no client-side cache so rotations propagate immediately
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
- - **Connections / Events** _(coming soon)_ OAuth integrations (Notion, GDrive, Slack…) and a workspace event bus
21
+ - **Coming soon (types only)** — `queues` (cross-container HTTP push via QStash), `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
23
  - **Lazy initialization** — Modules and partner SDKs are constructed on first access, so env vars rotated by the control plane after `createClient()` are still picked up
27
24
 
@@ -327,7 +324,7 @@ await stackbone.rag.reset();
327
324
 
328
325
  `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.
329
326
 
330
- 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 Neon scale-to-zero and arbitrary numbers of concurrent approvals without tying up sockets.
327
+ 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.
331
328
 
332
329
  ```ts
333
330
  // 1. Issue an approval — returns immediately, the human reviews asynchronously.
@@ -432,7 +429,7 @@ The signing key for `verify()` is read from `STACKBONE_APPROVAL_SIGNING_KEY` (or
432
429
 
433
430
  ### Secrets
434
431
 
435
- `client.secrets` reads workspace-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.
432
+ `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.
436
433
 
437
434
  ```ts
438
435
  // Single secret — `secrets_not_found` error if the name is not registered.
@@ -468,97 +465,28 @@ const { data: cfg } = await stackbone.config.getMany<{
468
465
  }>(['reply_tone', 'max_emails']);
469
466
  ```
470
467
 
471
- `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 workspace has registered — no error for missing entries.
468
+ `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
469
 
473
- ### Memory
470
+ ### Memory (pending)
474
471
 
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' });
535
-
536
- // Close a session and consolidate its facts into long-term memory (default).
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.
472
+ 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
473
 
543
474
  ### Coming Soon
544
475
 
545
- The following modules are part of the public surface but currently return a `not_implemented` error. They will land iteratively as the platform rolls out:
476
+ 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
477
 
547
478
  ```ts
548
- // Push an HTTP job to another agent (QStash-backed under the hood).
549
- await stackbone.queues.publish({
550
- url: 'https://agent-b.stackbone.ai/invoke',
551
- body: { leadId: 42 },
552
- retries: 3,
553
- });
554
-
555
- // Emit an event to the workspace 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();
479
+ import type {
480
+ PublishRequest,
481
+ AddMemoryRequest,
482
+ MemoryHit,
483
+ Prompt,
484
+ CreatePromptRequest,
485
+ } from '@stackbone/sdk';
560
486
  ```
561
487
 
488
+ 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.
489
+
562
490
  ## Configuration
563
491
 
564
492
  `createClient` accepts a single configuration object. Every field is optional and falls back to an environment variable, which the platform injects into the agent container at boot:
@@ -571,8 +499,8 @@ const stackbone = createClient({
571
499
  stackboneApiUrl: 'https://api.stackbone.ai',
572
500
  // Stable agent identifier used as storage key prefix → STACKBONE_AGENT_ID
573
501
  agentId: 'agent_abc123',
574
- // Postgres connection string for `client.rag` / observability (escape hatch).
575
- // Note: `client.database` reads `STACKBONE_POSTGRES_URL`, not this one.
502
+ // Postgres connection string for `client.database`, `client.rag` and the
503
+ // observability exporter → STACKBONE_POSTGRES_URL
576
504
  databaseUrl: 'postgresql://...',
577
505
  // OpenRouter credentials → OPENROUTER_API_KEY / OPENROUTER_BASE_URL
578
506
  openrouterKey: '...',
@@ -586,12 +514,13 @@ const stackbone = createClient({
586
514
  mem0BaseUrl: 'https://api.mem0.ai',
587
515
  // HMAC key for verifying approval-decision callbacks → STACKBONE_APPROVAL_SIGNING_KEY
588
516
  approvalSigningKey: '...',
589
- // Object storage credentials → AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / S3_ENDPOINT / S3_BUCKET
517
+ // Object storage credentials → STACKBONE_S3_ACCESS_KEY / STACKBONE_S3_SECRET_KEY / STACKBONE_S3_ENDPOINT / STACKBONE_S3_BUCKET / STACKBONE_S3_REGION
590
518
  s3: {
591
519
  accessKeyId: '...',
592
520
  secretAccessKey: '...',
593
521
  endpoint: 'https://<account>.r2.cloudflarestorage.com',
594
522
  bucket: 'stackbone-prod',
523
+ region: 'auto',
595
524
  },
596
525
  // OpenTelemetry exporter
597
526
  otel: {
@@ -633,18 +562,24 @@ console.log(result.data.choices[0]?.message.content);
633
562
 
634
563
  Each module ships its own stable code prefix so you can pattern-match without parsing the message:
635
564
 
636
- | Prefix | Source | Examples |
637
- | ----------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
638
- | `ai_*` | `client.ai` | `ai_unauthorized`, `ai_credits_exhausted`, `ai_rate_limited`, `ai_aborted`, `ai_no_image_generated` |
639
- | `s3_*` | `client.storage` | `s3_credentials_missing`, `s3_invalid_key`, `s3_error` |
640
- | `rag_*` | `client.rag` | `rag_invalid_request`, `rag_dim_mismatch`, `rag_embedding_failed`, `rag_error` |
641
- | `approval_*` | `client.approval` | `approval_invalid_request`, `approval_invalid_signature`, `approval_signature_expired`, `approval_signing_key_missing`, `approval_invalid_payload`, `approval_tool_execute_failed` |
642
- | `secrets_*` | `client.secrets` | `secrets_invalid_request`, `secrets_not_found`, `secrets_invalid_response` |
643
- | `config_*` | `client.config` | `config_invalid_request`, `config_not_found`, `config_invalid_response` |
644
- | `memory_*` | `client.memory` | reserved for the future mem0-backed implementation; today every method returns `not_implemented` |
645
- | `http_*` | facade HTTP client | `http_unauthorized`, `http_timeout`, `http_rate_limited`, `http_server_error` |
646
- | `*_missing` | configuration | `database_not_configured`, `agent_id_missing`, `openrouter_key_missing`, `database_url_missing` |
647
- | `not_implemented` | stubbed module surface | returned by every "coming soon" method until it ships |
565
+ | Prefix | Source | Examples |
566
+ | ----------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
567
+ | `ai_*` | `client.ai` | `ai_unauthorized`, `ai_credits_exhausted`, `ai_rate_limited`, `ai_aborted`, `ai_no_image_generated` |
568
+ | `s3_*` | `client.storage` | `s3_credentials_missing`, `s3_invalid_key`, `s3_error` |
569
+ | `rag_*` | `client.rag` | `rag_invalid_request`, `rag_dim_mismatch`, `rag_embedding_failed`, `rag_error` |
570
+ | `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` |
571
+ | `secrets_*` | `client.secrets` | `secrets_invalid_request`, `secrets_not_found`, `secrets_invalid_response`, `secrets_unauthorized`, `secrets_forbidden`, `secrets_rate_limited`, `secrets_unavailable` |
572
+ | `config_*` | `client.config` | `config_invalid_request`, `config_not_found`, `config_invalid_response`, `config_unauthorized`, `config_forbidden`, `config_rate_limited`, `config_unavailable` |
573
+ | `memory_*` | `client.memory` | reserved for the future mem0-backed implementation; today every method returns `not_implemented` |
574
+ | `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 |
575
+ | `database_*` | `client.database` | `database_not_configured` (raised when `STACKBONE_POSTGRES_URL` is unset) |
576
+ | `observability_*` | `client.observability` | `observability_close_run_failed` |
577
+ | `contract_*` | gated surfaces | `contract_malformed`, `contract_unreachable`, `contract_version_unsupported` — emitted by any gated call when the contract handshake fails |
578
+ | `capability_*` | gated surfaces | `capability_unavailable` — emitted when the negotiated contract advertises no support for the surface |
579
+ | `*_missing` | configuration | `agent_id_missing`, `openrouter_key_missing`, `database_url_missing`, `stackbone_api_url_missing` |
580
+ | `not_implemented` | stubbed module surface | returned by every "coming soon" method until it ships |
581
+
582
+ > 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.
648
583
 
649
584
  ## TypeScript Support
650
585
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var crypto = require('crypto');
3
+ var postgresql = require('@testcontainers/postgresql');
4
4
  var postgresJs = require('drizzle-orm/postgres-js');
5
5
  var migrator = require('drizzle-orm/postgres-js/migrator');
6
6
  var postgres = require('postgres');
@@ -11,46 +11,14 @@ var postgres__default = /*#__PURE__*/_interopDefault(postgres);
11
11
 
12
12
  var __defProp = Object.defineProperty;
13
13
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
14
- var resolveAdminUrl = /* @__PURE__ */ __name((override) => override ?? process.env["STACKBONE_TEST_POSTGRES_URL"] ?? process.env["STACKBONE_POSTGRES_URL"] ?? process.env["DATABASE_URL"] ?? "postgres://stackbone:stackbone@localhost:5432/stackbone", "resolveAdminUrl");
15
- var generateSchemaName = /* @__PURE__ */ __name(() => `stackbone_test_${crypto.randomBytes(8).toString("hex")}`, "generateSchemaName");
16
- var tagConnectionStringWithSchema = /* @__PURE__ */ __name((url, schemaName) => {
17
- const u = new URL(url);
18
- u.searchParams.set("schema", schemaName);
19
- return u.toString();
20
- }, "tagConnectionStringWithSchema");
21
- var dropSchema = /* @__PURE__ */ __name(async (adminUrl, schemaName) => {
22
- const admin = postgres__default.default(adminUrl, {
23
- max: 1,
24
- onnotice: /* @__PURE__ */ __name(() => void 0, "onnotice")
25
- });
26
- try {
27
- await admin.unsafe(`DROP SCHEMA "${schemaName.replace(/"/g, '""')}" CASCADE`);
28
- } finally {
29
- await admin.end({
30
- timeout: 5
31
- });
32
- }
33
- }, "dropSchema");
14
+ var DEFAULT_IMAGE = "pgvector/pgvector:pg17";
34
15
  var createTestDatabase = /* @__PURE__ */ __name(async (options) => {
35
- const adminUrl = resolveAdminUrl(options.connectionString);
36
- const schemaName = generateSchemaName();
37
- const admin = postgres__default.default(adminUrl, {
38
- max: 1,
39
- onnotice: /* @__PURE__ */ __name(() => void 0, "onnotice")
40
- });
41
- try {
42
- await admin`CREATE SCHEMA ${admin(schemaName)}`;
43
- } finally {
44
- await admin.end({
45
- timeout: 5
46
- });
47
- }
48
- const client = postgres__default.default(adminUrl, {
16
+ const image = options.image ?? DEFAULT_IMAGE;
17
+ const container = await new postgresql.PostgreSqlContainer(image).withDatabase("stackbone_test").withUsername("stackbone").withPassword("stackbone").start();
18
+ const connectionString = container.getConnectionUri();
19
+ const client = postgres__default.default(connectionString, {
49
20
  max: 5,
50
- onnotice: /* @__PURE__ */ __name(() => void 0, "onnotice"),
51
- connection: {
52
- search_path: schemaName
53
- }
21
+ onnotice: /* @__PURE__ */ __name(() => void 0, "onnotice")
54
22
  });
55
23
  const db = postgresJs.drizzle(client);
56
24
  let disposed = false;
@@ -60,19 +28,20 @@ var createTestDatabase = /* @__PURE__ */ __name(async (options) => {
60
28
  await client.end({
61
29
  timeout: 5
62
30
  }).catch(() => void 0);
63
- await dropSchema(adminUrl, schemaName).catch(() => void 0);
31
+ await container.stop({
32
+ timeout: 5e3
33
+ }).catch(() => void 0);
64
34
  }, "dispose");
65
35
  try {
66
36
  await migrator.migrate(db, {
67
- migrationsFolder: options.migrationsDir,
68
- migrationsSchema: schemaName
37
+ migrationsFolder: options.migrationsDir
69
38
  });
70
39
  } catch (err) {
71
40
  await dispose();
72
41
  throw err;
73
42
  }
74
43
  return {
75
- connectionString: tagConnectionStringWithSchema(adminUrl, schemaName),
44
+ connectionString,
76
45
  drizzle: db,
77
46
  dispose
78
47
  };