@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 CHANGED
@@ -7,6 +7,312 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Removed
11
+
12
+ - **Breaking:** removed the `client.observability` surface (`spanProcessor`,
13
+ `logger`, `closeRun`, `closeLogger`, `flush`). Observability is no longer a
14
+ creator-facing `client` module: agent code logs through the handler-scoped
15
+ `ctx.logger` (already bound to the run) — plain `console.*` is captured and
16
+ correlated too — and the Studio run timeline is produced by the runtime, not
17
+ by code the agent registers. The platform primitives (`PlatformLogger`,
18
+ `RunStepsSpanProcessor`, `aggregateRunCost`) stay available on the
19
+ `@stackbone/sdk/observability` subpath, consumed by the runtime wrapper and
20
+ the agent emulator.
21
+
22
+ ### Changed
23
+
24
+ - The runtime now injects the SDK client into every handler context as
25
+ `ctx.client` — one `StackboneClient` per process, shared across invocations.
26
+ Agent code reads it off the invocation context (`async run({ input, client })
27
+ { ... }`) instead of constructing one at module scope, so a module-level
28
+ `const client = createClient()` is no longer required. `createClient()` stays
29
+ exported as an escape hatch for building a client outside a handler (scripts,
30
+ tests, one-off tasks) or with explicit config overrides.
31
+
32
+ - `client.queues` is now live (was a `notImplemented` stub). `publish`,
33
+ `schedule`, `unschedule` and `listSchedules` make authenticated calls to the
34
+ control-plane BullMQ dispatcher (`/api/v1/agent/queues/*`) using the existing
35
+ `STACKBONE_AGENT_JWT` channel — the agent never touches Redis.
36
+ **Breaking:** `publish` now takes `{ name, payload, retries?, delay?,
37
+ deduplicationId? }` and returns `{ messageId }` (was `{ url, body, retries?,
38
+ delay?, deduplicationId?, headers? }`). `name` is the opaque job name (e.g.
39
+ `send-email`), `payload` the opaque body, `delay` defers delivery in ms and
40
+ `retries` overrides the platform retry default per job. New public request
41
+ types `ScheduleRequest` / `UnscheduleRequest` are exported from the barrel.
42
+ Errors surface under the new `queues_*` prefix.
43
+
44
+ - `ResolvedConfig` is now an opaque, typed snapshot — the live `env`
45
+ channel is gone. Every consumer used to read environment variables with
46
+ the pattern `resolved.config.fieldX ?? resolved.env['FIELDX']`, which
47
+ spread two pieces of knowledge across ~20 call sites: the exact env-var
48
+ name and the precedence rule. The resolver now owns both. After this
49
+ change, modules read typed fields like `resolved.stackboneApiUrl`,
50
+ `resolved.s3Bucket`, `resolved.databaseUrl`, `resolved.requireContract`,
51
+ etc. — none of them touch `process.env` directly.
52
+
53
+ The resolver is the **single** seam that reads `process.env` in the
54
+ library. New env-driven settings go in two places: a typed field on
55
+ `ResolvedConfig` and the matching `config.xxx ?? env['XXX']` (or parsing
56
+ logic) inside `resolveConfig`. The compiler then flags every consumer
57
+ that needs the new field.
58
+
59
+ Runtime knobs previously read inline from `contract/contract-handshake.ts`
60
+ (`STACKBONE_REQUIRE_CONTRACT`, `STACKBONE_CONTRACT_TTL_MS`,
61
+ `STACKBONE_DEBUG`) are now resolved up-front and threaded into the per-
62
+ client `ContractStore`. The database singleton in
63
+ `surfaces/agent-local/database/shared-handle.ts` takes an explicit
64
+ `connectionString` parameter that `DatabaseModule` pulls off
65
+ `resolved.databaseUrl`; the only remaining read of
66
+ `STACKBONE_POSTGRES_URL` is inside `resolveConfig`.
67
+
68
+ Booleans (`STACKBONE_DEBUG=1`) and numbers (`STACKBONE_CONTRACT_TTL_MS=30000`)
69
+ get parsed at resolve time, so consumers receive `boolean` / `number`,
70
+ not raw strings — one parse, one place, no drift.
71
+
72
+ Behavioural change worth flagging: `ResolvedConfig` is a snapshot, not
73
+ a live view. Tests that mutated `process.env` AFTER `createClient()` to
74
+ exercise late-binding behaviour now need to rebuild the client (or pass
75
+ the new value through the config object). The new `src/config.spec.ts`
76
+ pins both the snapshot semantics and the deletion contract — `.env` is
77
+ no longer a property on `ResolvedConfig`, and reading it is both a type
78
+ error and a runtime `undefined`. Two integration / handshake tests
79
+ (`STACKBONE_REQUIRE_CONTRACT=0`, `STACKBONE_CONTRACT_TTL_MS`,
80
+ `STACKBONE_DEBUG=1`) now construct a fresh `ContractStore` per env-var
81
+ variation to keep the resolve-once contract explicit.
82
+
83
+ - `SdkError.code` is now a typed catalog (`SdkErrorCode`) declared in one
84
+ place — `src/errors/codes.ts`. Every literal `code` value that ships on a
85
+ `Result<T>` envelope (and the `errorMapping.prefix` slot on the transport)
86
+ is now compiler-checked against the catalog, so a future PR that adds
87
+ `secrets_not_foud` (typo) or `ai_provdier_down` (typo) fails to build
88
+ instead of silently shipping past the README contract. The catalog also
89
+ ties `HttpClient`'s `<prefix>_<reason>` projection to `SdkErrorPrefix`, so
90
+ a surface that wires `errorMapping: { prefix: '<unknown>' }` no longer
91
+ compiles. The wire-level shape of every code is unchanged: this is a
92
+ type-level constraint, not a rename.
93
+
94
+ Adding a new code is a single-file edit in `src/errors/codes.ts` (either
95
+ a new suffix under an existing prefix, or a freestanding code under
96
+ `SDK_ERROR_CODES_STANDALONE`). The compiler then flags every call site
97
+ that needs the new literal — and the new exhaustiveness test in
98
+ `src/lib/http-client.spec.ts` asserts every `<prefix>_<reason>` the
99
+ transport can emit is still a member of the catalog so README/code drift
100
+ fails loudly.
101
+
102
+ Two new symbols on the public barrel (`@stackbone/sdk`):
103
+ - `SdkErrorCode` — the union projection of the catalog. Use it as the
104
+ return type when you write a helper that produces an `SdkError`, or
105
+ as a discriminant in a `switch` over `result.error.code`.
106
+ - `isSdkErrorCode(raw: string)` — runtime narrowing for wire strings
107
+ that come from a decoded body. Returns `true` only when `raw` is a
108
+ catalog member; lets the SDK widen upstream errors safely.
109
+
110
+ RAG's `toRagError(cause, message, fallbackCode)` helper now constrains
111
+ `fallbackCode` to `SdkErrorCode` and checks `cause.code` against the
112
+ catalog before reusing it. The catalog and the helper are not folded
113
+ into one file — the catalog is the inventory, `toRagError` stays in the
114
+ RAG folder as a Postgres-tagged-error classifier.
115
+
116
+ - Contract gating membership is now an explicit rule, and the handshake
117
+ cache no longer lives in a process-wide static. Two things changed:
118
+ 1. **Membership rule (Option B from the audit)** — a surface gates iff
119
+ `MODULE_CAPABILITIES` (in `@stackbone/validators`) advertises a row
120
+ for its module id. `MODULE_CAPABILITIES` is now the single source of
121
+ truth: adding a surface to gating means adding a `Capability` and a
122
+ `<module>: '<capability>'` row first, then wiring
123
+ `createModuleGate('<module>', resolved, store)` in the surface
124
+ constructor. The new docstring at the top of
125
+ `contract/capability-registry.ts` spells out the rule and the steps.
126
+ Today's gated set stays the same (`database`, `rag`, `queues`,
127
+ `events`, `secrets`, `config`, `approval`, `storage`, `ai`). The
128
+ four ungated surfaces (`memory`, `connections`, `prompts`,
129
+ `observability`) now carry a one-paragraph comment on their class
130
+ header documenting **why** they are not gated — either the runtime
131
+ does not yet exist with a defined capability (`memory`,
132
+ `connections`, `prompts`) or the surface stays local / talks to
133
+ OpenTelemetry, not the Stackbone Agent Protocol (`observability`).
134
+
135
+ 2. **Per-client handshake store** — the `ContractHandshake` static
136
+ namespace is gone. In its place: `createContractStore()` returns a
137
+ `ContractStore` with three methods (`get`, `peek`, `gatingEnabled`)
138
+ that owns its own cache, single-flight map, TTL eviction and debug
139
+ bookkeeping. `StackboneClient` instantiates exactly one store per
140
+ instance and threads it through every gated surface via
141
+ `createModuleGate(module, resolved, store)`. Two clients
142
+ constructed in the same process no longer share handshake cache or
143
+ suppression-warning state — including `client.contract`, which now
144
+ reads from the per-client store's `peek()`.
145
+
146
+ Removed test globals (the visible sign that the cache had escaped its
147
+ owner): `ContractHandshake.__resetForTests` and
148
+ `__resetModuleGateWarningsForTests`. Specs that previously called them
149
+ in `beforeEach`/`afterEach` now simply construct a fresh
150
+ `createContractStore()` (or, for client-level coverage, a fresh
151
+ `createClient(...)`); state isolation is intrinsic to construction.
152
+ `module-gate.spec.ts` gained two coverage tests for the new shape:
153
+ "two gates pinned to the same store share suppression-warning state"
154
+ and "two gates pinned to different stores warn independently". The E2E
155
+ suite (`integration/handshake.spec.ts`) loses its global-reset
156
+ bookkeeping entirely.
157
+
158
+ Wire protocol is unchanged: `GET /api/contract` returns the same JSON
159
+ shape, `client.contract` exposes the same read-only `ContractResponse |
160
+ null` getter, gate-blocked calls still return the same `Result`
161
+ envelope with `capability_unavailable` / `contract_version_unsupported`
162
+ / `contract_unreachable` codes, and the `STACKBONE_REQUIRE_CONTRACT=0`
163
+ / `STACKBONE_CONTRACT_TTL_MS` / `STACKBONE_DEBUG=1` escape hatches
164
+ behave identically. Surface constructors keep the same `gate?:
165
+ ModuleGate` test seam — only the internal default (now backed by a
166
+ per-client store instead of a static singleton) changed.
167
+
168
+ - The cross-surface shared-handle seam is now an explicit, documented
169
+ pattern on the public surfaces. Two surfaces produce a reusable
170
+ resource and expose it through a canonical accessor:
171
+ - `client.database.shared(): DrizzleClient` — process-wide Drizzle
172
+ handle bound to `STACKBONE_POSTGRES_URL`. Same input → same
173
+ instance. `client.database.raw()` stays as the creator-facing
174
+ escape hatch and is now documented as an alias of `shared()`.
175
+ - `client.ai.shared(): Result<OpenAI>` — process-wide OpenRouter
176
+ `OpenAI` client used by every namespace under `client.ai`. Same
177
+ input → same instance; reflects the `clientOverride` injected at
178
+ construction time. The previous private `client()` accessor folded
179
+ into this public name.
180
+
181
+ These accessors are the **only** way cross-surface SDK consumers
182
+ should reach the shared pool / OpenAI client. RAG already consumed
183
+ the database pool via a module-private file (`database/internal.ts`)
184
+ — RAG now reaches it through `client.database.shared().$client`
185
+ instead. The renamed `database/shared-handle.ts` keeps the singleton
186
+ implementation but has zero sibling-relative importers outside its
187
+ own folder and its own spec; cross-surface friction (memory and
188
+ queues will need the same handles per the consolidation ADRs) now
189
+ has one named extension point instead of a per-consumer escape
190
+ hatch.
191
+
192
+ Public API of `client.database`, `client.ai`, and `client.rag` is
193
+ unchanged — method signatures and `Result` envelopes match the
194
+ previous release. `RagModule` constructor adds a
195
+ `() => DatabaseModule` accessor alongside the existing
196
+ `() => AiModule`, mirroring the wiring `client.ts` produces; tests
197
+ pass both verbatim.
198
+
199
+ - **BREAKING** — Control-plane facades (`client.secrets`, `client.config`,
200
+ `client.approval`) now surface HTTP failures with surface-specific error
201
+ codes instead of the generic `http_*` family. The transport (`HttpClient`)
202
+ absorbs response validation, status→domain remapping, and querystring
203
+ serialisation, so a facade method now reads as "POST to /path with body X,
204
+ validate against schema Y, error prefix Z" instead of a 30–80 line
205
+ hand-rolled pipeline per surface. Migration is mechanical: pattern-match
206
+ the surface prefix instead of `http_*`.
207
+ - 401 → `<prefix>_unauthorized` (was `http_unauthorized`).
208
+ - 403 → `<prefix>_forbidden` (was `http_forbidden`).
209
+ - 404 → `<prefix>_not_found` (was `http_not_found`).
210
+ - 408 → `<prefix>_timeout` (was `http_timeout`).
211
+ - 429 → `<prefix>_rate_limited` (was `http_rate_limited`).
212
+ - 5xx → `<prefix>_unavailable` (was `http_server_error`).
213
+ - other 4xx → `<prefix>_invalid_request` (was `http_client_error`).
214
+ - schema-drift on a 2xx body → `<prefix>_invalid_response`.
215
+
216
+ The raw HTTP status stays accessible at `result.error.meta.status` for
217
+ callers that still want to pattern-match on the wire. Endpoint-specific
218
+ overrides (e.g. a POST whose `409` means "already exists") are wired by
219
+ facades via the new `errorOverrides` option on the transport request.
220
+
221
+ Transport-level errors (`http_timeout`, `http_aborted`,
222
+ `http_network_error`, `http_parse_error`, `http_request_failed`,
223
+ `http_invalid_response`, `http_invalid_request`) keep the `http_*`
224
+ prefix the README documents — they are not surface-specific.
225
+
226
+ Internal cleanup: `src/lib/facade-helpers.ts` is gone (the two utilities
227
+ it held — `remapHttpNotFound` and `validateStringArray` — were either
228
+ absorbed by the transport or relocated to `lib/http-client.ts` as a
229
+ named export). The per-method bodies on the control-plane facades
230
+ shrank substantially: `secrets.get` and `config.get` lost their
231
+ hand-rolled `value` type-checks and remap blocks, batch endpoints
232
+ pushed array validation + comma-join down to the transport, and the
233
+ approval list endpoint dropped its `pickDefinedAsStrings` helper for
234
+ the transport's built-in scalar coercion. File-level line counts barely
235
+ move because the public type definitions dominate each facade (notably
236
+ on `approval.ts` where 80+ lines are interface exports), but the
237
+ per-method logic now reads like the design target: "POST to /path,
238
+ validate against schema, error prefix is X".
239
+
240
+ - **BREAKING** — Platform-side observability primitives moved off the main
241
+ `@stackbone/sdk` barrel and into a dedicated subpath entrypoint,
242
+ `@stackbone/sdk/observability`. The main barrel previously exposed both the
243
+ handler-scoped `createStructuredLogger` (the per-invocation `Logger` the
244
+ runtime injects into creator handlers) and the platform/run-scoped
245
+ `createPlatformLogger` factory side-by-side, with no signal about which
246
+ belonged to which audience. Creators only ever consume the run-scoped
247
+ surface through `client.observability.*` (the live accessors stay on
248
+ `StackboneClient` unchanged); only platform/runtime tooling and integration
249
+ tests need to construct the primitives directly. The symbols that moved:
250
+ - Per-run logger — `createPlatformLogger`, `LOG_LEVELS`, `defaultRunsDir`,
251
+ plus the types `LogLevelName`, `LogLevelNumber`, `LogRecord`, `PinoLike`,
252
+ `PlatformLogger`, `PlatformLoggerMode`, `PlatformLoggerOptions`.
253
+ - OTel span processor — `RunStepsSpanProcessor`, `RUN_STEP_TYPES`,
254
+ `RUN_ID_ATTRIBUTE`, `STEP_TYPE_ATTRIBUTE`, plus the types
255
+ `ReadableSpanLike`, `RunStepType`, `RunStepsSpanProcessorOptions`,
256
+ `SpanContextLike`, `PostgresLike`.
257
+ - Run-cost aggregator — `aggregateRunCost`, plus the types
258
+ `AggregateRunCostOptions`, `AggregateRunCostResult`.
259
+
260
+ Migration is mechanical:
261
+
262
+ ```ts
263
+ // Before
264
+ import { createPlatformLogger, RunStepsSpanProcessor } from '@stackbone/sdk';
265
+ // After
266
+ import { createPlatformLogger, RunStepsSpanProcessor } from '@stackbone/sdk/observability';
267
+ ```
268
+
269
+ The handler-scoped `createStructuredLogger`, `Logger`, `LoggerBindings`,
270
+ `LogSink` and `StructuredLoggerOptions` stay on the main `@stackbone/sdk`
271
+ barrel: those are the surface the runtime wrapper hands to creator
272
+ handlers via `InvokeContext.logger`. JSONL output shape, OTLP/HTTP/JSON
273
+ serialisation and `~/.stackbone/dev/runs/<runId>.jsonl` paths are
274
+ unchanged — only the import path moved.
275
+
276
+ - **BREAKING** — SDK source tree reorganised under `src/surfaces/` by **where
277
+ each surface's state lives** instead of "wraps SDK vs makes HTTP fetch". The
278
+ old `src/modules/` and `src/facades/` directories are gone. New layout:
279
+ - `src/surfaces/agent-local/` — state in the agent's own Postgres
280
+ (`database`, `rag`).
281
+ - `src/surfaces/external/` — managed partner SDKs the SDK wraps directly
282
+ (`ai`, `storage`, `observability`).
283
+ - `src/surfaces/control-plane/` — thin HTTP calls to the Stackbone control
284
+ plane (`approval`, `secrets`, `config`).
285
+ - `src/surfaces/pending/` — surface types are exported but the runtime is
286
+ not built yet (`queues`, `memory`, `prompts`, `connections`, `events`).
287
+
288
+ Subpath entrypoints (`@stackbone/sdk/db`, `@stackbone/sdk/db/testing`,
289
+ `@stackbone/sdk/queues/types`, `@stackbone/sdk/rag/migrations`,
290
+ `@stackbone/sdk/rag/schema`) keep working — only the underlying file
291
+ paths moved.
292
+
293
+ - The five pending surfaces (`queues`, `memory`, `prompts`, `connections`,
294
+ `events`) now live under `src/surfaces/pending/` and are exposed as live
295
+ `client.X` accessors that return `not_implemented` on every method. The
296
+ earlier draft of this refactor dropped the accessors entirely, but MVP
297
+ agent code needs to be authored against the eventual contract today and
298
+ switch over the day the backing runtime ships — keeping the getters as
299
+ placeholders makes that one-line change instead of a refactor. Public
300
+ types continue to ship from `@stackbone/sdk` (`PublishRequest`,
301
+ `MemoryItem`, `MemoryHit`, `Prompt`, `CreatePromptRequest`, …) so
302
+ creators can keep modelling integrations. `queues` and `events` go
303
+ through the contract gate (they are listed in `MODULE_CAPABILITIES`);
304
+ `memory`, `prompts` and `connections` are intentionally ungated.
305
+
306
+ - **BREAKING** — `@stackbone/sdk/db/testing` `createTestDatabase` now spins up
307
+ a real Postgres container via `@testcontainers/postgresql` instead of
308
+ carving a random schema out of a shared dev DB. Callers no longer pass a
309
+ `connectionString`; pass an optional `image` (defaults to
310
+ `pgvector/pgvector:pg17`) if you need extensions beyond pgvector. Tests are
311
+ self-contained: no `docker compose up` ceremony, no `STACKBONE_*_POSTGRES_URL`
312
+ env, no schema bookkeeping. Aligns the SDK helper with the rest of the
313
+ monorepo (see `apps/api/src/db/__tests__/setup-postgres.ts`). Docker must
314
+ be running on the host.
315
+
10
316
  ## [0.1.0-alpha.2] - 2026-05-18
11
317
 
12
318
  ### Fixed
@@ -14,7 +320,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
14
320
  - `client.rag.ingest` (batched mode, e.g. when an `onProgress` callback or a
15
321
  `jobWriter` is wired): every chunk batch was wrapping its INSERT in a
16
322
  transaction that first ran `DELETE FROM rag_chunks WHERE namespace=? AND
17
- doc_id=?`. With N batches per document, only the rows inserted by the LAST
323
+ doc_id=?`. With N batches per document, only the rows inserted by the LAST
18
324
  batch survived — every earlier slice was wiped by the next batch's DELETE.
19
325
  Split the DELETE into a single `clearDocument` call before the loop;
20
326
  `ingestDocument` now only INSERTs its slice.