@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/index.d.cts CHANGED
@@ -1,14 +1,21 @@
1
- import { ContractResponse } from '@stackbone/validators';
1
+ import { PublishJobResponse, ScheduleJobResponse, UnscheduleJobResponse, ListSchedulesResponse, ContractResponse } from '@stackbone/validators';
2
+ import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
3
+ import { Sql } from 'postgres';
4
+ import { IngestJobWriter, IngestRequest, IngestResponse, RagIngestProgress, DeleteOptions, DeleteResponse, RetrieveRequest, RetrieveHit, ChunkOptions, ParseInput, ParseOptions } from '@stackbone/rag-core';
5
+ export { ChunkOptions, ChunkStrategy, DeleteOptions, DeleteResponse, IngestChunk, IngestRequest, IngestRequestAutoEmbed, IngestRequestPrecomputed, IngestResponse, ParseInput, ParseOptions, RagIngestProgress, RetrieveHit, RetrieveRequest, RetrieveRequestAutoEmbed, RetrieveRequestPrecomputed } from '@stackbone/rag-core';
2
6
  import OpenAI from 'openai';
3
7
  import { ChatCompletionCreateParamsStreaming, ChatCompletionChunk, ChatCompletionCreateParamsNonStreaming, ChatCompletion, EmbeddingCreateParams, CreateEmbeddingResponse, ChatCompletionMessageParam } from 'openai/resources';
4
8
  import { Stream } from 'openai/streaming';
5
- import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
6
- import { Sql } from 'postgres';
7
9
  import { S3Client } from '@aws-sdk/client-s3';
10
+ import { z } from 'zod';
11
+ export { z } from 'zod';
12
+ import { SecretCipher } from '@stackbone/crypto';
8
13
 
9
14
  /**
10
15
  * Optional overrides accepted by `createClient`. All fields are optional;
11
- * modules fall back to the corresponding upstream env var on first access.
16
+ * the resolver applies the precedence rule (`config.xxx` first, env var
17
+ * fallback) and produces a fully typed `ResolvedConfig` so downstream
18
+ * modules never have to know the env-var names or the order of precedence.
12
19
  */
13
20
  interface ClientConfig {
14
21
  /** Ed25519 JWT signed by the control plane. Falls back to `STACKBONE_AGENT_JWT`. */
@@ -24,11 +31,17 @@ interface ClientConfig {
24
31
  * every SDK consumer that talks to the agent's Postgres.
25
32
  */
26
33
  databaseUrl?: string;
34
+ /**
35
+ * Per-agent encryption key (base64, 32 bytes) used to decrypt the agent's
36
+ * own application secrets read directly from `stackbone_platform.secrets`.
37
+ * Falls back to `STACKBONE_SECRET_KEY` — the per-agent key the saga/harness
38
+ * inject into the runtime env. This is the ONLY new env channel the
39
+ * agent-owned-secrets split adds; the deliberate no-generic-env-channel
40
+ * stance is preserved.
41
+ */
42
+ secretKey?: string;
27
43
  openrouterKey?: string;
28
44
  openrouterBaseUrl?: string;
29
- qstashToken?: string;
30
- qstashCurrentSigningKey?: string;
31
- qstashNextSigningKey?: string;
32
45
  llamaParseApiKey?: string;
33
46
  /** mem0 API key for the long-term memory backend (`client.memory`). Falls back to `MEM0_API_KEY`. */
34
47
  mem0ApiKey?: string;
@@ -86,22 +99,277 @@ interface ClientConfig {
86
99
  protocolRequired?: number;
87
100
  }
88
101
  /**
89
- * `env` is a live reference to `process.env` (not a snapshot) so tests that
90
- * mutate the environment between `createClient()` and the first module access
91
- * still observe the new values via the lazy initialisers.
102
+ * Fully-resolved, typed snapshot the SDK consumes. The resolver is the **only**
103
+ * place in the library that touches `process.env`; every downstream module
104
+ * reads from typed fields here instead of consulting the environment itself.
105
+ *
106
+ * Precedence rule: `config.xxx` (passed to `createClient`) wins over
107
+ * `process.env['XXX']`. Required env vars surface as `string | undefined` —
108
+ * the resolver never throws when a var is missing; whether a missing value
109
+ * is fatal is a per-surface decision (some error at construction, others
110
+ * lazily on first use).
111
+ *
112
+ * Construction is a snapshot. Tests that mutated `process.env` between
113
+ * `createClient()` and the first module access used to observe the new value
114
+ * through the old `env` channel; they now pass overrides via `createClient`
115
+ * or rebuild a new `ResolvedConfig` after mutating `process.env`. Snapshot
116
+ * semantics make the SDK easier to reason about — one read, one source.
117
+ *
118
+ * Note: there is intentionally no `env` channel. Reading
119
+ * `resolved.env['SOMETHING']` is a type error. Adding a new env-driven
120
+ * setting means adding a typed field here and resolving it in
121
+ * `resolveConfig`.
92
122
  */
93
123
  interface ResolvedConfig {
94
- config: Readonly<ClientConfig>;
95
- env: NodeJS.ProcessEnv;
124
+ /** The frozen creator-supplied options (untouched, for debug introspection). */
125
+ readonly config: Readonly<ClientConfig>;
126
+ /** Resolved from `config.stackboneApiUrl` ?? `STACKBONE_API_URL`. */
127
+ readonly stackboneApiUrl: string | undefined;
128
+ /** Resolved from `config.agentJwt` ?? `STACKBONE_AGENT_JWT`. */
129
+ readonly agentJwt: string | undefined;
130
+ /** Resolved from `config.installationId` ?? `STACKBONE_INSTALLATION_ID`. */
131
+ readonly installationId: string | undefined;
132
+ /** Resolved from `config.approvalSigningKey` ?? `STACKBONE_APPROVAL_SIGNING_KEY`. */
133
+ readonly approvalSigningKey: string | undefined;
134
+ /** Resolved from `config.agentId` ?? `STACKBONE_AGENT_ID`. */
135
+ readonly agentId: string | undefined;
136
+ /** Resolved from `config.databaseUrl` ?? `STACKBONE_POSTGRES_URL`. Shared by `client.database`, `client.rag`, and the platform observability hooks in `@stackbone/sdk/observability`. */
137
+ readonly databaseUrl: string | undefined;
138
+ /**
139
+ * Resolved from `config.secretKey` ?? `STACKBONE_SECRET_KEY`. The per-agent
140
+ * base64 encryption key `client.secrets` uses to decrypt rows read straight
141
+ * out of `stackbone_platform.secrets`.
142
+ */
143
+ readonly secretKey: string | undefined;
144
+ /** Resolved from `config.openrouterKey` ?? `OPENROUTER_API_KEY`. */
145
+ readonly openrouterKey: string | undefined;
146
+ /** Resolved from `config.openrouterBaseUrl` ?? `OPENROUTER_BASE_URL`. `undefined` means use the OpenRouter default. */
147
+ readonly openrouterBaseUrl: string | undefined;
148
+ /** Resolved from `config.s3.accessKeyId` ?? `STACKBONE_S3_ACCESS_KEY`. */
149
+ readonly s3AccessKeyId: string | undefined;
150
+ /** Resolved from `config.s3.secretAccessKey` ?? `STACKBONE_S3_SECRET_KEY`. */
151
+ readonly s3SecretAccessKey: string | undefined;
152
+ /** Resolved from `config.s3.endpoint` ?? `STACKBONE_S3_ENDPOINT`. */
153
+ readonly s3Endpoint: string | undefined;
154
+ /** Resolved from `config.s3.bucket` ?? `STACKBONE_S3_BUCKET`. */
155
+ readonly s3Bucket: string | undefined;
156
+ /** Resolved from `config.s3.region` ?? `STACKBONE_S3_REGION` ?? `'auto'`. Always defined. */
157
+ readonly s3Region: string;
158
+ /** Resolved from `config.otel.exporterOtlpEndpoint` ?? `OTEL_EXPORTER_OTLP_ENDPOINT`. */
159
+ readonly otelExporterOtlpEndpoint: string | undefined;
160
+ /**
161
+ * `true` unless `STACKBONE_REQUIRE_CONTRACT=0` is set. Consulted by the
162
+ * contract gate to decide whether a `capability_unavailable` /
163
+ * `contract_version_unsupported` error becomes a hard failure or a logged
164
+ * warning (escape-hatch path).
165
+ */
166
+ readonly requireContract: boolean;
167
+ /**
168
+ * Parsed `STACKBONE_CONTRACT_TTL_MS`. `undefined` (default) means cached
169
+ * handshakes never expire within the process lifetime; a positive integer
170
+ * caps the cache entry's `expiresAt`.
171
+ */
172
+ readonly contractTtlMs: number | undefined;
173
+ /**
174
+ * `true` only when `STACKBONE_DEBUG=1` is set. Consulted by the handshake
175
+ * to emit a one-shot debug line per (store, baseUrl) tuple after a
176
+ * successful negotiation.
177
+ */
178
+ readonly debug: boolean;
179
+ /**
180
+ * Embedding model parsed by the CLI from `agent.yaml.rag.embeddingModel`
181
+ * and forwarded as `config.rag.embeddingModel`. Same value, surfaced
182
+ * directly so consumers don't reach through the nested object.
183
+ */
184
+ readonly ragEmbeddingModel: string | undefined;
185
+ /**
186
+ * Forwarded from `config.protocolRequired`. The CLI sources it from
187
+ * `agent.yaml.protocol.required`; there is no env-var fallback today.
188
+ */
189
+ readonly protocolRequired: number | undefined;
96
190
  }
97
191
 
192
+ /**
193
+ * The single source of truth for every `code` an `SdkError` may carry on the
194
+ * `Result` envelope returned from a public SDK method. The README contract
195
+ * (and every creator who pattern-matches on `result.error.code`) is driven by
196
+ * this catalog — adding a new code means editing this file, and **only** this
197
+ * file. The compiler then refuses to ship any `SdkError({ code: '<unknown>' })`
198
+ * downstream.
199
+ *
200
+ * Shape:
201
+ * - `SDK_ERROR_CODE_PREFIXES` declares each prefix family and the suffixes
202
+ * it owns. `<prefix>` + `_<reason>` is the wire-level code.
203
+ * - `SDK_ERROR_CODES_STANDALONE` declares the freestanding codes that don't
204
+ * follow the `<prefix>_<reason>` rule (`not_implemented`, the various
205
+ * `*_missing` setup-bug codes, the `capability_*` / `contract_*` shapes).
206
+ * - `SdkErrorCode` is the union projected from both. Pass it to `err({...})`
207
+ * and the constructor rejects any literal that isn't in this catalog.
208
+ *
209
+ * Stability contract: the wire-level shape of every code in this catalog is
210
+ * part of the README's public error table. Codes are added behind a minor;
211
+ * codes are removed behind a major and the CHANGELOG. Spelling tweaks (even
212
+ * typos) wait for a major — fix the catalog and the call site together.
213
+ */
214
+ /**
215
+ * Prefix families. Each entry maps a `<prefix>` to the tuple of `<reason>`
216
+ * suffixes the SDK can emit under it. Two-step structure (object of tuples)
217
+ * is what powers the typed projection at the bottom of this file: the
218
+ * `as const` makes both the prefix names and the suffix strings into literal
219
+ * types so `SdkErrorCode` collapses to a finite string union.
220
+ */
221
+ declare const SDK_ERROR_CODE_PREFIXES: {
222
+ /**
223
+ * `client.ai` — wraps the OpenAI SDK pointed at OpenRouter. Status codes
224
+ * come from `codeForStatus` in `ai.ts` plus the fetch-error mapper.
225
+ */
226
+ readonly ai: readonly ["aborted", "credits_exhausted", "forbidden", "moderation_blocked", "network_error", "no_image_generated", "provider_error", "rate_limited", "timeout", "unauthorized", "validation_error"];
227
+ /**
228
+ * `client.storage` — wraps `@aws-sdk/client-s3` against R2/MinIO. Most
229
+ * failures collapse into `s3_error` with the AWS metadata on `meta`; the
230
+ * specific codes below are the early validation paths.
231
+ */
232
+ readonly s3: readonly ["bucket_missing", "credentials_missing", "empty_response", "error", "invalid_argument", "invalid_key"];
233
+ /**
234
+ * `client.rag` — the agent-local retrieval pipeline. `invalid_request` is
235
+ * the validation rail; the rest map specific Postgres / embedder failures
236
+ * the README documents.
237
+ */
238
+ readonly rag: readonly ["dim_mismatch", "embedding_failed", "embedding_model_unsupported", "error", "ingest_cancelled", "invalid_request", "job_insert_failed", "jobs_error", "schema_missing"];
239
+ /**
240
+ * `client.approval` — AGENT-LOCAL DB facade plus the HMAC verify helper.
241
+ * The request/cancel/get/list codes (`persist_failed`, `cancel_failed`,
242
+ * `unavailable`) come from the direct write/read against
243
+ * `stackbone_platform.approvals`; the verify-specific codes
244
+ * (`invalid_signature`, etc.) are emitted locally.
245
+ */
246
+ readonly approval: readonly ["cancel_failed", "forbidden", "invalid_payload", "invalid_request", "invalid_response", "invalid_signature", "not_found", "persist_failed", "rate_limited", "signature_expired", "signing_key_missing", "timeout", "tool_execute_failed", "unauthorized", "unavailable"];
247
+ /**
248
+ * `client.secrets` — AGENT-LOCAL facade. Reads `stackbone_platform.secrets`
249
+ * over the shared `client.database` pool and decrypts with the per-agent
250
+ * key. `not_configured` covers a missing `STACKBONE_SECRET_KEY`;
251
+ * `decrypt_failed` covers a key/envelope mismatch; `unavailable` covers a
252
+ * failed DB read.
253
+ */
254
+ readonly secrets: readonly ["already_exists", "decrypt_failed", "forbidden", "invalid_request", "invalid_response", "not_configured", "not_found", "rate_limited", "timeout", "unauthorized", "unavailable"];
255
+ /**
256
+ * `client.config` — AGENT-LOCAL facade, reads the singleton
257
+ * `stackbone_platform.agent_config` row over the shared `client.database`
258
+ * pool.
259
+ */
260
+ readonly config: readonly ["forbidden", "invalid_request", "invalid_response", "not_found", "rate_limited", "timeout", "unauthorized", "unavailable"];
261
+ /**
262
+ * `client.queues` — the agent → control plane job-enqueue surface. Every
263
+ * method (`publish`, `schedule`, `unschedule`, `listSchedules`) POSTs/GETs
264
+ * the BullMQ dispatcher endpoints over `HttpClient`, so the full
265
+ * status→domain remap (`queues_unauthorized`, `queues_not_found`,
266
+ * `queues_unavailable`, …) is in play. The agent never touches Redis — it
267
+ * only calls these endpoints.
268
+ */
269
+ readonly queues: readonly ["forbidden", "invalid_request", "invalid_response", "not_found", "rate_limited", "timeout", "unauthorized", "unavailable"];
270
+ /**
271
+ * `client.prompts` — AGENT-LOCAL facade. Reads
272
+ * `stackbone_platform.prompts` / `prompt_versions` over the shared
273
+ * `client.database` pool and compiles templates with the local
274
+ * `@stackbone/prompt-compiler` (Mustache subset). `not_configured` covers a
275
+ * missing prompts schema (the `42P01` "run `stackbone db migrate up`" hint);
276
+ * `not_found` covers an absent / soft-deleted key; `missing_var` is the
277
+ * compile-time diagnostic when a `{{var}}` has no value; `unavailable`
278
+ * covers a failed DB read; `already_exists` covers a duplicate `create`.
279
+ */
280
+ readonly prompts: readonly ["already_exists", "invalid_request", "missing_var", "not_configured", "not_found", "unavailable"];
281
+ /**
282
+ * `client.memory` — reserved. The README documents the prefix; today the
283
+ * pending surface returns `not_implemented` for every method so no
284
+ * `memory_*` codes are emitted yet. Kept as an empty bucket so the
285
+ * catalog matches the README table verbatim.
286
+ */
287
+ readonly memory: readonly string[];
288
+ /**
289
+ * Transport-level error family — `HttpClient` emits these when no
290
+ * facade-specific `errorMapping.prefix` is configured, or when the
291
+ * failure is pre-/post-HTTP (DNS, abort, body parse).
292
+ */
293
+ readonly http: readonly ["aborted", "client_error", "forbidden", "invalid_request", "invalid_response", "network_error", "not_found", "parse_error", "rate_limited", "request_failed", "server_error", "timeout", "unauthorized"];
294
+ /**
295
+ * `client.database` (and its cross-surface consumers) — the setup-bug
296
+ * surface stays under one prefix so creators can pattern-match on
297
+ * `database_*` regardless of whether the failure came from the database
298
+ * module, RAG, or the observability run-cost hook.
299
+ */
300
+ readonly database: readonly ["not_configured"];
301
+ /**
302
+ * Observability — the run-cost rollup hook in `@stackbone/sdk/observability`
303
+ * (wired by the runtime, not a creator surface) emits this when its post-run
304
+ * Postgres write fails; everything else either returns ok or surfaces through
305
+ * the `database_*` family above.
306
+ */
307
+ readonly observability: readonly ["close_run_failed"];
308
+ /**
309
+ * Contract handshake / gating family — emitted by the per-client
310
+ * `ContractStore` and consumed by every gated facade as a hard error.
311
+ * Documented for creators in the gating section of the SDK ADRs.
312
+ */
313
+ readonly contract: readonly ["malformed", "unreachable", "version_unsupported"];
314
+ /**
315
+ * Capability gating family — the single code is the table-driven block
316
+ * the gate returns when the negotiated contract lacks the required
317
+ * capability. The shape lives in `@stackbone/validators`; the SDK widens
318
+ * it into `SdkError` verbatim.
319
+ */
320
+ readonly capability: readonly ["unavailable"];
321
+ };
322
+ /**
323
+ * Freestanding codes — they don't follow the `<prefix>_<reason>` rule, so
324
+ * they live as full literals here. Two families:
325
+ *
326
+ * - `not_implemented` for the pending surfaces' stubbed methods.
327
+ * - `*_missing` for the setup-bug rail every facade checks before its
328
+ * first network call (`STACKBONE_API_URL`, `STACKBONE_AGENT_ID`,
329
+ * `OPENROUTER_API_KEY`, `STACKBONE_POSTGRES_URL`).
330
+ */
331
+ declare const SDK_ERROR_CODES_STANDALONE: readonly ["agent_id_missing", "database_url_missing", "not_implemented", "openrouter_key_missing", "stackbone_api_url_missing"];
332
+ /**
333
+ * Type-level projection: `<prefix>_<reason>` for every entry of
334
+ * `SDK_ERROR_CODE_PREFIXES`, plus the standalone codes. The trick is the
335
+ * second mapped-tuple step: `(typeof SDK_ERROR_CODE_PREFIXES)[P][number]`
336
+ * collapses the tuple to the union of its suffixes, then template-string
337
+ * inference builds the full code.
338
+ */
339
+ type Prefix = keyof typeof SDK_ERROR_CODE_PREFIXES;
340
+ type PrefixedCode<P extends Prefix = Prefix> = {
341
+ [K in P]: `${K}_${(typeof SDK_ERROR_CODE_PREFIXES)[K][number]}`;
342
+ }[P];
343
+ type SdkErrorCode = PrefixedCode | (typeof SDK_ERROR_CODES_STANDALONE)[number];
344
+ /**
345
+ * Type guard for callers that receive a raw string from the wire (e.g. a
346
+ * decoded JSON body) and want to narrow it to the catalog. Returns false
347
+ * for any unknown code so the caller can decide whether to re-throw,
348
+ * fall back, or surface the raw string.
349
+ */
350
+ declare function isSdkErrorCode(code: string): code is SdkErrorCode;
351
+ /**
352
+ * Prefix names exposed as a tuple for the `errorMapping.prefix` slot on
353
+ * `HttpClient.request`. Statically tying the transport's prefix to the
354
+ * catalog keeps `<prefix>_<reason>` emissions inside `SdkErrorCode` —
355
+ * adding a new prefix means adding it here AND a `<prefix>_invalid_request`
356
+ * / `<prefix>_invalid_response` / standard status-mapped suffix to the
357
+ * catalog above.
358
+ */
359
+ type SdkErrorPrefix = Prefix;
360
+
98
361
  /**
99
362
  * Uniform error envelope returned by every SDK method. `cause` and `meta` are
100
363
  * optional escape hatches so wrappers can attach upstream errors / context
101
364
  * without breaking the public shape.
365
+ *
366
+ * `code` is typed against the catalog declared in `errors/codes.ts` — the
367
+ * compiler refuses any literal that isn't part of the documented inventory.
368
+ * Adding a new code is a single-file edit there; ALL throw sites then update
369
+ * automatically through the union.
102
370
  */
103
371
  interface SdkError {
104
- code: string;
372
+ code: SdkErrorCode;
105
373
  message: string;
106
374
  cause?: unknown;
107
375
  meta?: Record<string, unknown>;
@@ -126,55 +394,117 @@ type Result<T> = {
126
394
  * `Result<T>` of the method.
127
395
  *
128
396
  * Test seam — every gated module exposes an optional `gate` constructor
129
- * argument that defaults to `createModuleGate(<id>, resolved)`. Specs inject
130
- * a stub returning a synthetic `Result<undefined>` to exercise the gating
131
- * paths (ok / `contract_version_unsupported` / `capability_unavailable` /
132
- * `contract_unreachable`) without driving a real handshake.
397
+ * argument that defaults to `createModuleGate(<id>, resolved, store)`. Specs
398
+ * inject a stub returning a synthetic `Result<undefined>` to exercise the
399
+ * gating paths (ok / `contract_version_unsupported` / `capability_unavailable`
400
+ * / `contract_unreachable`) without driving a real handshake.
133
401
  */
134
402
  type ModuleGate = () => Promise<Result<undefined>>;
135
403
 
136
- /** Subset of `RequestInit['body']` we serialize without modification. */
137
- type SerializedBody = NonNullable<RequestInit['body']>;
138
- interface RequestOptions$1 extends Omit<RequestInit, 'body' | 'signal'> {
139
- params?: Record<string, string>;
140
- body?: SerializedBody | Record<string, unknown> | unknown[] | null;
141
- signal?: AbortSignal;
142
- /** Allow retrying non-idempotent requests (POST, PATCH). Off by default to prevent duplicate writes. */
143
- idempotent?: boolean;
144
- }
145
- interface HttpClientOptions {
146
- /** Default 30_000. Set to 0 to disable. */
147
- timeout?: number;
148
- /** Default 3. Set to 0 to disable. */
149
- retryCount?: number;
150
- /** Initial backoff in ms; doubles each attempt with ±15% jitter. Default 500. */
151
- retryDelay?: number;
152
- /** Override the global fetch (useful for tests). */
153
- fetch?: typeof fetch;
154
- }
155
404
  /**
156
- * Shared HTTP client used by the control-plane facades (approval, secrets,
157
- * connections, config, events). Wraps `fetch` with timeout, exponential
158
- * backoff with jitter, idempotent-only retries, and a uniform Result
159
- * envelope so facades never throw at the SDK boundary.
405
+ * Public surface a creator can use through `client.database`. We pin it to the
406
+ * `postgres-js` Drizzle wrapper because that is the only driver `@stackbone/sdk`
407
+ * ships and the only one `STACKBONE_POSTGRES_URL` is contracted to point at.
160
408
  *
161
- * Reads `STACKBONE_API_URL` and `STACKBONE_AGENT_JWT` lazily on every request so
162
- * env-var rotation is picked up without restarting the client.
409
+ * Re-exporting Drizzle's own type instead of inventing a structural alias keeps
410
+ * the surface 1:1 with what `drizzle-orm` documents and what `@stackbone/sdk/db`
411
+ * re-exports — so types flow end to end without translation.
163
412
  */
164
- declare class HttpClient {
165
- private readonly resolved;
166
- private readonly timeout;
167
- private readonly retryCount;
168
- private readonly retryDelay;
169
- private readonly fetchImpl;
170
- constructor(resolved: ResolvedConfig, options?: HttpClientOptions);
171
- get<T>(path: string, options?: RequestOptions$1): Promise<Result<T>>;
172
- post<T>(path: string, body?: RequestOptions$1['body'], options?: RequestOptions$1): Promise<Result<T>>;
173
- put<T>(path: string, body?: RequestOptions$1['body'], options?: RequestOptions$1): Promise<Result<T>>;
174
- patch<T>(path: string, body?: RequestOptions$1['body'], options?: RequestOptions$1): Promise<Result<T>>;
175
- delete<T>(path: string, options?: RequestOptions$1): Promise<Result<T>>;
176
- request<T>(method: string, path: string, options?: RequestOptions$1): Promise<Result<T>>;
177
- private computeBackoff;
413
+ type DrizzleClient = PostgresJsDatabase<Record<string, never>> & {
414
+ $client: Sql;
415
+ };
416
+
417
+ /**
418
+ * `client.database` is a lazy wrapper over a `drizzle()` instance bound to
419
+ * the resolved `databaseUrl` (config first, then `STACKBONE_POSTGRES_URL`
420
+ * at `resolveConfig` time). The underlying postgres-js connection and the
421
+ * Drizzle facade are constructed on the first method call so partner SDKs
422
+ * only pay the cost when the agent actually touches the database.
423
+ *
424
+ * Native Drizzle methods (`select`, `insert`, `update`, `delete`, `transaction`,
425
+ * `execute`) are exposed verbatim — we explicitly avoid wrapping them in a
426
+ * Result envelope here. Drizzle already throws on misuse and returns typed rows
427
+ * on success; mirroring its API keeps the contract identical to what the
428
+ * `@stackbone/sdk/db` re-exports document, so creators write idiomatic Drizzle.
429
+ *
430
+ * Missing `STACKBONE_POSTGRES_URL` raises `SdkError('database_not_configured')`
431
+ * with an actionable hint that points to `stackbone dev`. This is a creator
432
+ * setup bug, not something the agent's code can recover from at runtime.
433
+ *
434
+ * Methods are declared with their full Drizzle generic signatures (rather than
435
+ * `(...args) => Drizzle[k](...args)` arrow properties) so the inferred row
436
+ * types from `pgTable(...)` flow end-to-end into the caller, which is the
437
+ * promise the `@stackbone/sdk/db` re-export makes.
438
+ *
439
+ * The actual `postgres()` pool and the Drizzle handle are owned by the
440
+ * module-internal singleton in `./shared-handle.ts`. The shared handle is
441
+ * exposed to other SDK surfaces (RAG today, memory / queues tomorrow)
442
+ * through the `shared()` method below — the **canonical accessor** of
443
+ * the SDK's shared-handles pattern. Same instance every call → one pool
444
+ * per agent process. See `shared-handle.spec.ts` for the invariant.
445
+ */
446
+ declare class DatabaseModule {
447
+ private readonly _gate;
448
+ private readonly _databaseUrl;
449
+ constructor(resolved: ResolvedConfig,
450
+ /**
451
+ * Test seam — see `ModuleGate`. Defaults to the lazy contract gate.
452
+ * The gate is awaited at the terminal `await` of every Drizzle chain
453
+ * (`select/insert/update/delete/execute/transaction`); when it returns
454
+ * an error, a tagged `GateBlockedError` is thrown so callers see the
455
+ * same `Error & { code, message, meta }` surface as the existing
456
+ * `database_not_configured` setup-bug.
457
+ */
458
+ gate?: ModuleGate);
459
+ /**
460
+ * Escape hatch — returns Drizzle's relational query builder verbatim. The
461
+ * contract gate does NOT fire here because the surface is a synchronous
462
+ * accessor; the user can still bypass the gate via `client.database.raw()`
463
+ * for the same reason. Audit: `query` and `raw()` are intentionally
464
+ * ungated escape hatches; the standard `select/insert/update/delete/
465
+ * execute/transaction` path is fully gated.
466
+ */
467
+ get query(): DrizzleClient['query'];
468
+ select: DrizzleClient['select'];
469
+ insert: DrizzleClient['insert'];
470
+ update: DrizzleClient['update'];
471
+ delete: DrizzleClient['delete'];
472
+ execute: DrizzleClient['execute'];
473
+ transaction: DrizzleClient['transaction'];
474
+ /**
475
+ * Canonical accessor for the **shared-handles pattern**. Returns the
476
+ * process-wide Drizzle handle backing `client.database`. Cross-surface
477
+ * SDK consumers (RAG today, memory / queues tomorrow) call this — never
478
+ * the implementation-detail singleton in `./shared-handle.ts` — to
479
+ * ensure they share the one `postgres()` pool the agent process owns
480
+ * against `STACKBONE_POSTGRES_URL`.
481
+ *
482
+ * Contract: same input → same instance. Calling `shared()` twice in the
483
+ * same process returns the same reference; the underlying postgres-js
484
+ * `Sql` is reachable as `shared().$client` for callers (e.g. RAG) that
485
+ * need the template-tag function instead of the Drizzle facade.
486
+ *
487
+ * Like `raw()`, this is a synchronous accessor and intentionally does
488
+ * NOT consult the contract gate — surfaces that gate behaviour gate it
489
+ * at their own call sites (`pipeline.ingest`, etc.), not at handle
490
+ * acquisition. Configuration errors (`database_not_configured`) throw
491
+ * with the same tagged `Error` shape every other `client.database`
492
+ * surface uses.
493
+ */
494
+ shared(): DrizzleClient;
495
+ /**
496
+ * Escape hatch for **creator** callers that want the raw Drizzle handle
497
+ * (e.g. to pass it to a library that expects `PostgresJsDatabase`).
498
+ * Functionally identical to `shared()` — both return the same singleton
499
+ * and trigger the same lazy initialisation — but kept under a distinct
500
+ * name because the audiences differ: `shared()` is consumed by other
501
+ * SDK surfaces and documents the cross-surface contract; `raw()` is
502
+ * documented as the creator's escape hatch out of the structured
503
+ * `select/insert/...` surface. Reading either intentionally does NOT
504
+ * consult the contract gate; the caller is opting out of the SDK's
505
+ * structured surface.
506
+ */
507
+ raw(): DrizzleClient;
178
508
  }
179
509
 
180
510
  interface ApprovalToolSpec<I, O> {
@@ -289,147 +619,151 @@ interface ApprovalListResult<T = unknown> {
289
619
  items: ApprovalRecord<T>[];
290
620
  nextCursor?: string;
291
621
  }
622
+ /**
623
+ * `client.approval` — AGENT-LOCAL. The HITL pause is recorded directly in the
624
+ * agent's own `stackbone_platform.approvals` table over the shared
625
+ * `client.database` pool; there is no control-plane POST anymore. The RESUME
626
+ * half stays Studio-driven: a human decides in Studio, the control plane signs
627
+ * an HMAC callback to `callbackUrl`, and the agent reconciles it with
628
+ * `verify()` (local crypto — see `verify.ts`).
629
+ *
630
+ * The write populates only the columns the cloud `create()` used to set
631
+ * (topic/payload/callback_url/idempotency_key/fallback/metadata/timeout_at;
632
+ * `schema` was sourced from a DTO field the SDK options object does not carry,
633
+ * so it stays NULL). `workspace_id`/`agent_id`/`run_id` stay NULL exactly as
634
+ * they were on the control-plane path — the idempotency `ON CONFLICT` relies on
635
+ * `workspace_id` being NULL with `NULLS NOT DISTINCT` (migration 0008).
636
+ */
292
637
  declare class ApprovalFacade {
293
638
  private readonly _resolved;
294
- private readonly _http;
295
- private readonly _gate;
296
- constructor(_resolved: ResolvedConfig, _http: HttpClient,
297
- /** Test seam see `ModuleGate`. Defaults to the lazy contract gate. */
298
- gate?: ModuleGate);
639
+ /** Lazy accessor for `client.database` — reaches the shared `Sql` via `.shared().$client`. */
640
+ private readonly _getDatabase;
641
+ constructor(_resolved: ResolvedConfig,
642
+ /** Lazy accessor for `client.database` reaches the shared `Sql` via `.shared().$client`. */
643
+ _getDatabase: () => DatabaseModule);
299
644
  request<T = unknown>(options: ApprovalRequestOptions<T>): Promise<Result<ApprovalRequest>>;
300
645
  cancel(approvalId: string, reason?: string): Promise<Result<void>>;
301
646
  get<T = unknown>(approvalId: string): Promise<Result<ApprovalRecord<T>>>;
302
647
  list<T = unknown>(options?: ApprovalListOptions): Promise<Result<ApprovalListResult<T>>>;
303
648
  /**
304
- * Local crypto verification — does not touch the datapath, so it is NOT
305
- * gated by the contract handshake. Auditing rule: a method gates iff it
306
- * issues an HTTP request to the configured baseUrl.
649
+ * Local crypto verification — does not touch the database, so it has the same
650
+ * shape it had on the control-plane surface. Auditing rule: a method gates
651
+ * iff it issues a datapath call; this one never does.
307
652
  */
308
653
  verify<T = unknown>(request: Request, options?: VerifyOptions): Promise<Result<Decision<T>>>;
309
654
  /**
310
- * Pure factory — returns an `ApprovalTool` whose `invoke()` ultimately
311
- * calls back into `ApprovalFacade.request`, so the gate fires there. Not
312
- * gated here.
655
+ * Pure factory — returns an `ApprovalTool` whose `invoke()` ultimately calls
656
+ * back into `ApprovalFacade.request`.
313
657
  */
314
658
  tool<I, O>(spec: ApprovalToolSpec<I, O>): ApprovalTool<I, O>;
659
+ private sql;
315
660
  }
316
661
 
317
662
  declare class ConfigFacade {
318
- private readonly _http;
319
- private readonly _gate;
320
- constructor(resolved: ResolvedConfig, _http: HttpClient,
321
- /** Test seam — see `ModuleGate`. Defaults to the lazy contract gate. */
322
- gate?: ModuleGate);
663
+ private readonly _resolved;
664
+ /** Lazy accessor for `client.database` — see `SecretsFacade`. */
665
+ private readonly _getDatabase;
666
+ constructor(_resolved: ResolvedConfig,
667
+ /** Lazy accessor for `client.database` — see `SecretsFacade`. */
668
+ _getDatabase: () => DatabaseModule);
323
669
  get<T = unknown>(key: string): Promise<Result<T>>;
324
670
  /**
325
671
  * Keys absent from the agent's config come back as omissions in the
326
- * response — the returned map only contains entries the control plane
327
- * actually has, so callers must access fields with `?.` (the return type
328
- * is `Partial<T>`).
672
+ * response — the returned map only contains entries the agent DB actually
673
+ * has, so callers must access fields with `?.` (the return type is
674
+ * `Partial<T>`).
329
675
  */
330
676
  getMany<T extends Record<string, unknown> = Record<string, unknown>>(keys: string[]): Promise<Result<Partial<T>>>;
331
- }
332
-
333
- /** OAuth connections (Notion, GDrive, Slack…). */
334
- declare class ConnectionsFacade {
335
- private readonly _resolved;
336
- private readonly _http;
337
- constructor(_resolved: ResolvedConfig, _http: HttpClient);
338
- list(): Promise<Result<readonly never[]>>;
339
- }
340
-
341
- /** Emit events to the organization event bus. */
342
- declare class EventsFacade {
343
- private readonly _http;
344
- private readonly _gate;
345
- constructor(resolved: ResolvedConfig, _http: HttpClient,
346
- /** Test seam — see `ModuleGate`. Defaults to the lazy contract gate. */
347
- gate?: ModuleGate);
348
- emit(_name: string, _payload: unknown): Promise<Result<void>>;
677
+ private loadPayload;
678
+ private sql;
349
679
  }
350
680
 
351
681
  /**
352
- * A prompt managed by the Stackbone control plane. Templates are plain strings
353
- * with Mustache-style `{{var}}` placeholders no conditionals or loops.
682
+ * A prompt's current/active state. Key-based (workspace-unique). `template` is
683
+ * the current version's content; `version` is the live version number.
354
684
  */
355
685
  interface Prompt {
686
+ key: string;
356
687
  name: string;
688
+ description: string | null;
689
+ /** The current version's content. */
357
690
  template: string;
358
- /** Monotonically increasing per `name`. The first `create` produces version `1`. */
691
+ /** The live version number. The first `create` produces version `1`. */
359
692
  version: number;
360
- /** Variable names parsed from the template. */
361
- variables?: readonly string[];
362
- metadata?: Record<string, unknown>;
693
+ /** `{{var}}` names referenced by the current version. */
694
+ variables: readonly string[];
695
+ metadata: Record<string, unknown> | null;
363
696
  /** ISO 8601 UTC timestamp. */
364
697
  createdAt: string;
365
698
  updatedAt: string;
366
699
  }
367
700
  interface GetPromptOptions {
368
- /** Pin a specific version. Omitted -> latest. */
701
+ /** Pin a specific version. Omitted -> current/latest. */
369
702
  version?: number;
370
703
  }
371
704
  interface ListPromptsOptions {
372
705
  /** 1..100. Default 50. */
373
706
  limit?: number;
374
- cursor?: string;
375
707
  }
376
708
  interface ListPromptsResult {
377
- /** Latest version of each prompt. */
709
+ /** Current version of each (non-deleted) prompt. */
378
710
  items: readonly Prompt[];
379
- nextCursor?: string;
380
711
  }
381
712
  interface CreatePromptRequest {
713
+ key: string;
382
714
  name: string;
715
+ /** The version-1 content. */
383
716
  template: string;
717
+ description?: string;
384
718
  metadata?: Record<string, unknown>;
385
719
  }
386
720
  interface UpdatePromptOptions {
387
- /** New template. Bumps `version` by one. */
721
+ /** New content. Appends a new immutable version. */
388
722
  template?: string;
389
- /** Shallow-merged onto the existing metadata. */
723
+ name?: string;
724
+ description?: string | null;
725
+ /** Replaces the prompt-head metadata. */
390
726
  metadata?: Record<string, unknown>;
391
727
  }
392
728
  interface DeletePromptOptions {
393
- /** Delete a specific version. Omitted -> delete every version under `name`. */
729
+ /** Reserved for future per-version deletes; ignored today (soft-deletes the key). */
394
730
  version?: number;
395
731
  }
396
732
  interface DeletePromptResult {
397
- name: string;
398
- /** Number of versions actually removed. */
733
+ key: string;
734
+ /** Number of prompt heads soft-deleted (0 or 1). */
399
735
  deleted: number;
400
736
  }
401
- /**
402
- * `client.prompts` — managed prompts for the agent. Prompts live outside the
403
- * code so creators can edit, version and A/B test them without rebuilding the
404
- * container. The first iteration is a scaffolding placeholder: every method
405
- * returns `not_implemented`. The real backend will be the Stackbone control
406
- * plane and the public surface defined here is the contract callers can
407
- * already type against.
408
- */
409
- declare class PromptsFacade {
410
- private readonly _http;
411
- constructor(_resolved: ResolvedConfig, _http: HttpClient);
412
- get(_name: string, _options?: GetPromptOptions): Promise<Result<Prompt>>;
413
- compile(_name: string, _variables: Record<string, unknown>, _options?: GetPromptOptions): Promise<Result<string>>;
414
- list(_options?: ListPromptsOptions): Promise<Result<ListPromptsResult>>;
415
- create(_request: CreatePromptRequest): Promise<Result<Prompt>>;
416
- update(_name: string, _options: UpdatePromptOptions): Promise<Result<Prompt>>;
417
- delete(_name: string, _options?: DeletePromptOptions): Promise<Result<DeletePromptResult>>;
737
+ interface CompilePromptResult {
738
+ /** The rendered string. */
739
+ output: string;
740
+ /** The version the compile ran against. */
741
+ version: number;
418
742
  }
419
-
420
- declare class SecretsFacade {
421
- private readonly _http;
422
- private readonly _gate;
423
- constructor(resolved: ResolvedConfig, _http: HttpClient,
424
- /** Test seam — see `ModuleGate`. Defaults to the lazy contract gate. */
425
- gate?: ModuleGate);
426
- get(name: string): Promise<Result<string>>;
743
+ declare class PromptsFacade {
744
+ private readonly _resolved;
427
745
  /**
428
- * Names absent from the organization come back as omissions in the response —
429
- * the returned map only contains entries the control plane actually has.
430
- * Callers that need "all-or-nothing" semantics should diff the keys.
746
+ * Lazy accessor for `client.database`. The facade pulls the shared
747
+ * `postgres-js` `Sql` via `getDatabase().shared().$client` never
748
+ * reaching into a sibling module's implementation file.
431
749
  */
432
- getMany(names: string[]): Promise<Result<Record<string, string>>>;
750
+ private readonly _getDatabase;
751
+ constructor(_resolved: ResolvedConfig,
752
+ /**
753
+ * Lazy accessor for `client.database`. The facade pulls the shared
754
+ * `postgres-js` `Sql` via `getDatabase().shared().$client` — never
755
+ * reaching into a sibling module's implementation file.
756
+ */
757
+ _getDatabase: () => DatabaseModule);
758
+ get(key: string, options?: GetPromptOptions): Promise<Result<Prompt>>;
759
+ compile(key: string, variables: Record<string, unknown>, options?: GetPromptOptions): Promise<Result<CompilePromptResult>>;
760
+ list(options?: ListPromptsOptions): Promise<Result<ListPromptsResult>>;
761
+ create(request: CreatePromptRequest): Promise<Result<Prompt>>;
762
+ update(key: string, options: UpdatePromptOptions): Promise<Result<Prompt>>;
763
+ delete(key: string, _options?: DeletePromptOptions): Promise<Result<DeletePromptResult>>;
764
+ private sql;
765
+ private mapReadError;
766
+ private mapWriteError;
433
767
  }
434
768
 
435
769
  /**
@@ -451,7 +785,14 @@ declare class SecretsFacade {
451
785
  *
452
786
  * Test override: pass `clientOverride` to inject a pre-built `OpenAI`
453
787
  * instance (or a stand-in) instead of letting the module build one. The
454
- * override flows to all four namespaces — `models.list()` included.
788
+ * override flows to all four namespaces — `models.list()` included
789
+ * and to the cross-surface `shared()` accessor.
790
+ *
791
+ * Shared-handles pattern: `shared(): Result<OpenAI>` is the canonical
792
+ * accessor cross-surface SDK consumers (RAG today, memory tomorrow) use
793
+ * to reach the single OpenRouter client. Same instance every call; one
794
+ * credential, one base URL, one retry policy, one error shape. See
795
+ * README + CHANGELOG.
455
796
  */
456
797
  declare class AiModule {
457
798
  private readonly _resolved;
@@ -470,20 +811,40 @@ declare class AiModule {
470
811
  get embeddings(): EmbeddingsNamespace;
471
812
  get images(): ImagesNamespace;
472
813
  get models(): ModelsNamespace;
473
- private client;
814
+ /**
815
+ * Canonical accessor for the **shared-handles pattern** on the AI
816
+ * surface. Returns the lazily-constructed OpenRouter-flavoured `OpenAI`
817
+ * client backing `client.ai`. Cross-surface SDK consumers (RAG today,
818
+ * memory tomorrow) call this — never instantiate a parallel `OpenAI`
819
+ * client — to keep credential resolution, base-URL routing, retry /
820
+ * timeout policy, and token accounting on a single instance.
821
+ *
822
+ * Contract: same input → same instance. Calling `shared()` twice in
823
+ * the same process (with the same `ResolvedConfig`) returns the same
824
+ * reference. The Result envelope surfaces `openrouter_key_missing`
825
+ * when `OPENROUTER_API_KEY` is unset, matching the rest of the
826
+ * `client.ai.*` API; the gate is intentionally NOT consulted here
827
+ * (surfaces that gate behaviour gate it at their own call sites).
828
+ *
829
+ * The constructor's `clientOverride` flows through this accessor too,
830
+ * so tests that inject a fake `OpenAI` see consistent behaviour across
831
+ * `chat / embeddings / images / models` and any cross-surface
832
+ * consumer.
833
+ */
834
+ shared(): Result<OpenAI>;
474
835
  }
475
836
  declare class ChatCompletionsNamespace {
476
837
  private readonly _getClient;
477
838
  private readonly _gate;
478
839
  constructor(_getClient: () => Result<OpenAI>, _gate: ModuleGate);
479
- create(params: ChatCompletionCreateParamsStreaming, options?: RequestOptions): Promise<Result<Stream<ChatCompletionChunk>>>;
480
- create(params: ChatCompletionCreateParamsNonStreaming, options?: RequestOptions): Promise<Result<ChatCompletion>>;
840
+ create(params: ChatCompletionCreateParamsStreaming, options?: RequestOptions$1): Promise<Result<Stream<ChatCompletionChunk>>>;
841
+ create(params: ChatCompletionCreateParamsNonStreaming, options?: RequestOptions$1): Promise<Result<ChatCompletion>>;
481
842
  }
482
843
  declare class EmbeddingsNamespace {
483
844
  private readonly _getClient;
484
845
  private readonly _gate;
485
846
  constructor(_getClient: () => Result<OpenAI>, _gate: ModuleGate);
486
- create(params: EmbeddingCreateParams, options?: RequestOptions): Promise<Result<CreateEmbeddingResponse>>;
847
+ create(params: EmbeddingCreateParams, options?: RequestOptions$1): Promise<Result<CreateEmbeddingResponse>>;
487
848
  }
488
849
  interface ImageGenerateParams {
489
850
  model: string;
@@ -520,7 +881,7 @@ declare class ImagesNamespace {
520
881
  * empty success — surfacing the failure to the caller instead of
521
882
  * silently producing zero images.
522
883
  */
523
- generate(params: ImageGenerateParams, options?: RequestOptions): Promise<Result<ImagesResponse>>;
884
+ generate(params: ImageGenerateParams, options?: RequestOptions$1): Promise<Result<ImagesResponse>>;
524
885
  }
525
886
  interface OpenRouterModel {
526
887
  id: string;
@@ -550,87 +911,273 @@ declare class ModelsNamespace {
550
911
  * the namespaces — and any `clientOverride` injected for tests applies
551
912
  * here too.
552
913
  */
553
- list(options?: RequestOptions): Promise<Result<ModelsListResponse>>;
914
+ list(options?: RequestOptions$1): Promise<Result<ModelsListResponse>>;
554
915
  }
555
- interface RequestOptions {
916
+ interface RequestOptions$1 {
556
917
  signal?: AbortSignal;
557
918
  }
558
919
 
559
920
  /**
560
- * Public surface a creator can use through `client.database`. We pin it to the
561
- * `postgres-js` Drizzle wrapper because that is the only driver `@stackbone/sdk`
562
- * ships and the only one `STACKBONE_POSTGRES_URL` is contracted to point at.
563
- *
564
- * Re-exporting Drizzle's own type instead of inventing a structural alias keeps
565
- * the surface 1:1 with what `drizzle-orm` documents and what `@stackbone/sdk/db`
566
- * re-exports — so types flow end to end without translation.
921
+ * Handle returned by `client.rag.ingestAsync`. The job id is allocated
922
+ * synchronously against `stackbone_rag_jobs` so the caller can track / cancel
923
+ * the work via the `/api/rag/jobs/:jobId/*` surface; `events` is a streaming
924
+ * channel of progress events (ADR §D9 shape); `result` settles when the
925
+ * pipeline finishes (or fails / is cancelled).
567
926
  */
568
- type DrizzleClient = PostgresJsDatabase<Record<string, never>> & {
569
- $client: Sql;
570
- };
571
-
927
+ interface IngestAsyncHandle {
928
+ jobId: string;
929
+ events: AsyncIterable<RagIngestProgress>;
930
+ result: Promise<Result<IngestResponse>>;
931
+ }
572
932
  /**
573
- * `client.database` is a lazy wrapper over a `drizzle()` instance bound to
574
- * `STACKBONE_POSTGRES_URL`. The underlying postgres-js connection and the
575
- * Drizzle facade are constructed on the first method call so env vars rotated
576
- * post-`createClient()` are still honoured.
933
+ * `client.rag` `pgvector`-backed retrieval. Two shapes per write/read:
934
+ * pass `model` to let the SDK embed for you; pass embeddings precomputed for
935
+ * provider/dimension/batching control.
577
936
  *
578
- * Native Drizzle methods (`select`, `insert`, `update`, `delete`, `transaction`,
579
- * `execute`) are exposed verbatim we explicitly avoid wrapping them in a
580
- * Result envelope here. Drizzle already throws on misuse and returns typed rows
581
- * on success; mirroring its API keeps the contract identical to what the
582
- * `@stackbone/sdk/db` re-exports document, so creators write idiomatic Drizzle.
937
+ * Implementation note: `RagModule` is a thin facade over `RagPipeline`. The
938
+ * pipeline owns parse chunk embed persist; the facade resolves the
939
+ * shared `client.database` pool, builds an `Embedder` on demand, and maps
940
+ * configuration errors. See ADR `2026-05-10-rag-consolidation-on-client-database`.
583
941
  *
584
- * Missing `STACKBONE_POSTGRES_URL` raises `SdkError('database_not_configured')`
585
- * with an actionable hint that points to `stackbone dev`. This is a creator
586
- * setup bug, not something the agent's code can recover from at runtime.
587
- *
588
- * Methods are declared with their full Drizzle generic signatures (rather than
589
- * `(...args) => Drizzle[k](...args)` arrow properties) so the inferred row
590
- * types from `pgTable(...)` flow end-to-end into the caller, which is the
591
- * promise the `@stackbone/sdk/db` re-export makes.
942
+ * Connection ownership: this module never opens its own postgres pool. It
943
+ * reaches the underlying `postgres-js` `Sql` through the **shared-handles
944
+ * pattern** `client.database.shared().$client` so an agent that
945
+ * touches both surfaces still opens exactly one connection pool against
946
+ * `STACKBONE_POSTGRES_URL`. RAG is the canonical first cross-surface
947
+ * consumer; memory and queues will adopt the same accessor when they
948
+ * land.
592
949
  *
593
- * The actual `postgres()` pool and the Drizzle handle are owned by the
594
- * module-internal singleton in `./internal.ts`. `client.database` and any
595
- * other internal SDK module that imports `getDatabaseHandle` therefore share
596
- * one pool per agent process — see `internal.spec.ts` for the invariant.
950
+ * Schema readiness: the canonical schema (`stackbone_platform.rag_*`) is
951
+ * provisioned by the platform migrator, present on every install. When an
952
+ * operation still hits `42P01 relation does not exist`, the pipeline returns
953
+ * `SdkError('rag_schema_missing')` with an actionable hint.
597
954
  */
598
- declare class DatabaseModule {
955
+ /**
956
+ * Test-only seams accepted by `RagModule`. Production callers leave these
957
+ * unset — they are injected by `ingest-async.spec.ts` to substitute the
958
+ * shared-pool lookup and the SQL-backed job writer.
959
+ */
960
+ interface RagModuleTestDeps {
961
+ /**
962
+ * Replaces the `client.database.shared().$client` lookup used by
963
+ * `ingestAsync`. Tests can plug a fake `Sql` here without standing up
964
+ * the database surface.
965
+ */
966
+ sqlProvider?: () => Result<Sql>;
967
+ /** Builds an `IngestJobWriter` from the resolved SQL. Defaults to `createSqlJobWriter`. */
968
+ jobWriterFactory?: (sql: Sql) => IngestJobWriter;
969
+ }
970
+ declare class RagModule {
599
971
  private readonly _resolved;
972
+ /**
973
+ * Lazy accessor for `client.database`. RAG calls `getDatabase().shared()`
974
+ * to reach the process-wide Drizzle handle (and its `$client` Sql) via
975
+ * the shared-handles pattern — never reaches into a sibling module's
976
+ * implementation file.
977
+ */
978
+ private readonly _getDatabase;
979
+ /**
980
+ * Lazy accessor for `client.ai`. RAG calls `getAi()` for the embeddings
981
+ * namespace; the `Embedder` adapter reaches the shared OpenAI client
982
+ * through the same `client.ai.embeddings.create` public surface, so
983
+ * RAG never instantiates a parallel `OpenAI` client.
984
+ */
985
+ private readonly _getAi;
600
986
  private readonly _gate;
987
+ private readonly _testSqlOverride?;
988
+ private readonly _testJobWriterFactory?;
601
989
  constructor(_resolved: ResolvedConfig,
602
990
  /**
603
- * Test seam see `ModuleGate`. Defaults to the lazy contract gate.
604
- * The gate is awaited at the terminal `await` of every Drizzle chain
605
- * (`select/insert/update/delete/execute/transaction`); when it returns
606
- * an error, a tagged `GateBlockedError` is thrown so callers see the
607
- * same `Error & { code, message, meta }` surface as the existing
608
- * `database_not_configured` setup-bug.
991
+ * Lazy accessor for `client.database`. RAG calls `getDatabase().shared()`
992
+ * to reach the process-wide Drizzle handle (and its `$client` Sql) via
993
+ * the shared-handles pattern — never reaches into a sibling module's
994
+ * implementation file.
995
+ */
996
+ _getDatabase: () => DatabaseModule,
997
+ /**
998
+ * Lazy accessor for `client.ai`. RAG calls `getAi()` for the embeddings
999
+ * namespace; the `Embedder` adapter reaches the shared OpenAI client
1000
+ * through the same `client.ai.embeddings.create` public surface, so
1001
+ * RAG never instantiates a parallel `OpenAI` client.
1002
+ */
1003
+ _getAi: () => AiModule,
1004
+ /** Test seam — see `ModuleGate`. Defaults to the lazy contract gate. */
1005
+ gate?: ModuleGate, testDeps?: RagModuleTestDeps);
1006
+ ingest(request: IngestRequest): Promise<Result<IngestResponse>>;
1007
+ /**
1008
+ * Asynchronous ingest. Allocates a `stackbone_rag_jobs` row, returns the
1009
+ * job id immediately, and exposes both an `AsyncIterable` of streaming
1010
+ * progress events and the final `Result` so callers can `await` either
1011
+ * surface — typical webhook handlers consume `events` and ignore `result`,
1012
+ * tests `await result` directly. Cancellation is observed by flipping the
1013
+ * row's status (e.g. via `POST /api/rag/jobs/:jobId/cancel`); the worker
1014
+ * polls between chunk batches and bails out with `rag_ingest_cancelled`.
1015
+ *
1016
+ * Connection ownership: the `IngestJobWriter` is bound to the same SQL
1017
+ * pulled off the shared `client.database.shared()` handle, so a single
1018
+ * agent that issues `ingestAsync` and `client.database.select` in
1019
+ * flight still opens exactly one pool against
1020
+ * `STACKBONE_POSTGRES_URL`.
1021
+ */
1022
+ ingestAsync(request: IngestRequest): Promise<Result<IngestAsyncHandle>>;
1023
+ delete(ids: string | string[], options?: DeleteOptions): Promise<Result<DeleteResponse>>;
1024
+ deleteWhere(filter: Record<string, unknown>, options?: DeleteOptions): Promise<Result<DeleteResponse>>;
1025
+ retrieve(request: RetrieveRequest): Promise<Result<RetrieveHit[]>>;
1026
+ /**
1027
+ * Drops the legacy ad-hoc RAG tables (`rag_chunks` / `_rag_meta`) that
1028
+ * pre-feature-30 agents provisioned on first ingest. Kept for backwards
1029
+ * compatibility so an old agent can clean those up after upgrading. The
1030
+ * canonical RAG schema (`stackbone_platform.rag_*`) is owned by the platform
1031
+ * migrator — this method never touches it.
1032
+ */
1033
+ reset(): Promise<Result<void>>;
1034
+ /**
1035
+ * Pure helper — exposed verbatim from the chunker. No DB, no AI, no
1036
+ * datapath traffic, so it is intentionally NOT gated.
1037
+ */
1038
+ chunk(text: string, options?: ChunkOptions): string[];
1039
+ /**
1040
+ * Pure helper — exposed verbatim from the parser. No DB, no AI, no
1041
+ * datapath traffic, so it is intentionally NOT gated.
1042
+ */
1043
+ parse(input: ParseInput, options?: ParseOptions): Promise<string>;
1044
+ /**
1045
+ * Resolves the postgres-js `Sql` to use for this operation. Goes through
1046
+ * the **shared-handles pattern** — `client.database.shared().$client` —
1047
+ * to pull the postgres-js template-tag function off the same Drizzle
1048
+ * handle the rest of the SDK uses. Single pool per agent process.
1049
+ * Future cross-module transaction propagation plugs a tx-bound `Sql`
1050
+ * into the pipeline at the same call site, replacing the pool-bound
1051
+ * one without touching the rest of RAG.
1052
+ *
1053
+ * Configuration errors (`STACKBONE_POSTGRES_URL` unset) flow up as a
1054
+ * `Result.err` with the same `database_not_configured` shape
1055
+ * `client.database` raises, instead of throwing through the public
1056
+ * surface.
1057
+ */
1058
+ private sql;
1059
+ private withPipeline;
1060
+ }
1061
+
1062
+ declare class SecretsFacade {
1063
+ private readonly _resolved;
1064
+ /**
1065
+ * Lazy accessor for `client.database`. The facade pulls the shared
1066
+ * `postgres-js` `Sql` via `getDatabase().shared().$client` — never
1067
+ * reaching into a sibling module's implementation file.
1068
+ */
1069
+ private readonly _getDatabase;
1070
+ private _cipher?;
1071
+ constructor(_resolved: ResolvedConfig,
1072
+ /**
1073
+ * Lazy accessor for `client.database`. The facade pulls the shared
1074
+ * `postgres-js` `Sql` via `getDatabase().shared().$client` — never
1075
+ * reaching into a sibling module's implementation file.
1076
+ */
1077
+ _getDatabase: () => DatabaseModule);
1078
+ get(name: string): Promise<Result<string>>;
1079
+ /**
1080
+ * Names absent from the agent come back as omissions in the response — the
1081
+ * returned map only contains entries the agent DB actually has. Callers that
1082
+ * need "all-or-nothing" semantics should diff the keys.
609
1083
  */
1084
+ getMany(names: string[]): Promise<Result<Record<string, string>>>;
1085
+ private cipher;
1086
+ private sql;
1087
+ }
1088
+
1089
+ interface StorageObject {
1090
+ key: string;
1091
+ size: number;
1092
+ lastModified?: Date;
1093
+ etag?: string;
1094
+ }
1095
+ interface UploadOptions {
1096
+ contentType?: string;
1097
+ metadata?: Record<string, string>;
1098
+ }
1099
+ interface ListOptions {
1100
+ prefix?: string;
1101
+ /** Maximum number of objects to return per page. Must be > 0. */
1102
+ limit?: number;
1103
+ cursor?: string;
1104
+ }
1105
+ interface SignedUrlOptions {
1106
+ /** Seconds until the URL expires. Defaults to 3600 (1h). */
1107
+ expiresIn?: number;
1108
+ /** Only honoured by `getSignedUploadUrl` — pinned into the signature so the uploader must send a matching `Content-Type`. */
1109
+ contentType?: string;
1110
+ }
1111
+ interface SignedUrl {
1112
+ url: string;
1113
+ expiresAt: Date;
1114
+ }
1115
+ type StorageBody = Blob | Uint8Array | string;
1116
+ interface S3Settings {
1117
+ client: S3Client;
1118
+ endpoint: string;
1119
+ bucket: string;
1120
+ agentId: string;
1121
+ }
1122
+ /**
1123
+ * Public surface behind `client.storage`. Wraps `@aws-sdk/client-s3` against
1124
+ * R2 (prod) or MinIO (dev). Exposes a bucket-scoped API via `from(bucket)`.
1125
+ *
1126
+ * Multi-tenancy: every key is prefixed with `agentId/bucket/` before hitting
1127
+ * S3. Credentials are scoped to the agent's physical bucket (env `S3_BUCKET`),
1128
+ * and the `bucket` argument is a logical namespace the agent uses to organise
1129
+ * its own objects. The full prefixed key is an internal detail — public
1130
+ * methods accept and return the user-facing key (without prefix). Metadata
1131
+ * tracking in `_storage_objects` (Neon) is out-of-scope for this iteration.
1132
+ *
1133
+ * Memory note: `download()` materialises the whole object as a `Blob` in
1134
+ * memory. For agents that need to stream large objects, prefer
1135
+ * `getSignedDownloadUrl()` and `fetch()` the URL directly to consume the
1136
+ * stream incrementally.
1137
+ */
1138
+ declare class StorageModule {
1139
+ private readonly _resolved;
1140
+ private _s3;
1141
+ private readonly _gate;
1142
+ constructor(_resolved: ResolvedConfig,
1143
+ /** Test seam — see `ModuleGate`. Defaults to the lazy contract gate. */
610
1144
  gate?: ModuleGate);
1145
+ from(bucket: string): StorageBucket;
1146
+ private settings;
1147
+ }
1148
+ declare class StorageBucket {
1149
+ private readonly bucketName;
1150
+ private readonly resolveSettings;
1151
+ private readonly gate;
1152
+ constructor(bucketName: string, resolveSettings: () => Result<S3Settings>, gate: ModuleGate);
1153
+ upload(key: string, body: StorageBody, options?: UploadOptions): Promise<Result<{
1154
+ key: string;
1155
+ etag?: string;
1156
+ }>>;
1157
+ download(key: string): Promise<Result<Blob>>;
1158
+ list(options?: ListOptions): Promise<Result<{
1159
+ objects: readonly StorageObject[];
1160
+ nextCursor?: string;
1161
+ }>>;
1162
+ remove(key: string): Promise<Result<{
1163
+ key: string;
1164
+ }>>;
611
1165
  /**
612
- * Escape hatch returns Drizzle's relational query builder verbatim. The
613
- * contract gate does NOT fire here because the surface is a synchronous
614
- * accessor; the user can still bypass the gate via `client.database.raw()`
615
- * for the same reason. Audit: `query` and `raw()` are intentionally
616
- * ungated escape hatches; the standard `select/insert/update/delete/
617
- * execute/transaction` path is fully gated.
1166
+ * Returns the canonical S3-style URL for an object. Pure URL-builder no
1167
+ * S3 round-trip so this method intentionally bypasses the contract gate.
1168
+ * Whether the URL is publicly fetchable depends on the bucket policy; for
1169
+ * private buckets, use `getSignedDownloadUrl` instead.
618
1170
  */
619
- get query(): DrizzleClient['query'];
620
- select: DrizzleClient['select'];
621
- insert: DrizzleClient['insert'];
622
- update: DrizzleClient['update'];
623
- delete: DrizzleClient['delete'];
624
- execute: DrizzleClient['execute'];
625
- transaction: DrizzleClient['transaction'];
1171
+ getPublicUrl(key: string): Result<string>;
1172
+ getSignedUploadUrl(key: string, options?: SignedUrlOptions): Promise<Result<SignedUrl>>;
1173
+ getSignedDownloadUrl(key: string, options?: SignedUrlOptions): Promise<Result<SignedUrl>>;
1174
+ private signUrl;
626
1175
  /**
627
- * Escape hatch for callers that want the raw Drizzle handle (e.g. to pass
628
- * it to a library that expects `PostgresJsDatabase`). Reading this property
629
- * triggers the same lazy initialisation as any other method but
630
- * intentionally does NOT consult the contract gate — the surface is sync
631
- * and the caller is opting out of the SDK's structured surface.
1176
+ * Resolves S3 settings, validates the user key, and prefixes it with
1177
+ * `${agentId}/${bucketName}/`. Rejects path-traversal segments (`..`) so a
1178
+ * caller-controlled key cannot escape the agent's namespace.
632
1179
  */
633
- raw(): DrizzleClient;
1180
+ private resolve;
634
1181
  }
635
1182
 
636
1183
  /**
@@ -727,9 +1274,14 @@ interface EndSessionResult {
727
1274
  * scaffolding placeholder: every method returns `not_implemented`. The real
728
1275
  * backend will be mem0 (`mem0ApiKey` / `MEM0_API_KEY`) and the public surface
729
1276
  * defined here is the contract callers can already type against.
1277
+ *
1278
+ * Contract gating: not gated. Memory targets mem0 (a non-Stackbone partner),
1279
+ * so there is no Stackbone Agent Protocol capability to assert against. No
1280
+ * entry in `MODULE_CAPABILITIES` per the rule documented in
1281
+ * `contract/capability-registry.ts`. When/if the runtime moves to a
1282
+ * Stackbone-served backend we add the capability and wire the gate.
730
1283
  */
731
1284
  declare class MemoryModule {
732
- private readonly _resolved;
733
1285
  constructor(_resolved: ResolvedConfig);
734
1286
  add(_content: MemoryContent, _request: AddMemoryRequest): Promise<Result<MemoryItem>>;
735
1287
  search(_query: string, _options?: SearchMemoryOptions): Promise<Result<readonly MemoryHit[]>>;
@@ -746,628 +1298,285 @@ declare class MemoryModule {
746
1298
  endSession(_sessionId: string, _options?: EndSessionOptions): Promise<Result<EndSessionResult>>;
747
1299
  }
748
1300
 
749
- declare const RUN_STEP_TYPES: readonly ["agent", "llm_call", "db_query", "http_fetch", "queue_publish", "hitl_pause", "tool_call", "rag_query", "storage_op"];
750
- type RunStepType = (typeof RUN_STEP_TYPES)[number];
751
- declare const STEP_TYPE_ATTRIBUTE = "stackbone.step.type";
752
- declare const RUN_ID_ATTRIBUTE = "stackbone.run.id";
1301
+ /** Subset of `RequestInit['body']` we serialize without modification. */
1302
+ type SerializedBody = NonNullable<RequestInit['body']>;
753
1303
  /**
754
- * Minimal duck-typed view of an OpenTelemetry `ReadableSpan`. We mirror the
755
- * shape the OTel SDK exposes on `@opentelemetry/sdk-trace-base` rather than
756
- * importing the package so `@stackbone/sdk` doesn't pull the OTel runtime into
757
- * every agent that doesn't enable Studio. Agents wire the processor with
758
- * `provider.addSpanProcessor(new RunStepsSpanProcessor(...))` and the
759
- * structural match satisfies TypeScript.
1304
+ * Per-status overrides facades attach when an HTTP status carries a
1305
+ * non-canonical meaning for the endpoint e.g. a `POST` whose `409` means
1306
+ * "already exists" instead of the default "conflict". Keys are HTTP statuses,
1307
+ * values are domain codes the transport should surface verbatim (no prefix
1308
+ * concatenation the override is the final code).
1309
+ *
1310
+ * The value is typed against the catalog (`SdkErrorCode`) so an endpoint
1311
+ * cannot smuggle a wire code that isn't in the documented inventory.
760
1312
  */
761
- interface SpanContextLike {
762
- spanId: string;
763
- traceId: string;
764
- }
765
- interface ReadableSpanLike {
766
- spanContext(): SpanContextLike;
767
- parentSpanContext?: SpanContextLike | undefined;
768
- parentSpanId?: string | undefined;
769
- name: string;
770
- attributes: Record<string, unknown>;
771
- status?: {
772
- code: number;
773
- message?: string;
774
- } | undefined;
775
- startTime?: [number, number] | undefined;
776
- endTime?: [number, number] | undefined;
777
- duration?: [number, number] | undefined;
778
- }
779
- interface PostgresLike {
780
- unsafe(query: string, params?: unknown[]): Promise<unknown>;
781
- end?(): Promise<void>;
782
- }
783
- interface RunStepsSpanProcessorOptions {
1313
+ type ErrorOverrides = Record<number, SdkErrorCode>;
1314
+ /**
1315
+ * Tells the transport how to remap HTTP status codes onto the surface's
1316
+ * domain code prefix. `404 → '<prefix>_not_found'`, `401 → '<prefix>_unauthorized'`,
1317
+ * etc. Endpoint-specific overrides win over the canonical mapping.
1318
+ *
1319
+ * `prefix` is constrained to `SdkErrorPrefix` (the catalog's prefix tuple) so
1320
+ * an unknown surface name doesn't compile — keeps the wire codes the
1321
+ * transport emits inside the typed inventory.
1322
+ */
1323
+ interface ErrorMapping {
1324
+ /** Prefix concatenated with `_not_found`, `_unauthorized`, …. Required for any domain remap. */
1325
+ prefix: SdkErrorPrefix;
1326
+ /** Per-status final code that overrides the canonical mapping. */
1327
+ overrides?: ErrorOverrides;
1328
+ }
1329
+ interface RequestOptions extends Omit<RequestInit, 'body' | 'signal'> {
1330
+ /** Scalar query params. `undefined` values are dropped, others get coerced via `String()`. */
1331
+ params?: Record<string, string | number | boolean | undefined>;
784
1332
  /**
785
- * Postgres connection string of the install (local emulator at :5433 in
786
- * dev, Neon TCP/HTTP URL in cloud). Falls back to `DATABASE_URL`.
1333
+ * Repeated string-array params validated and serialised by the transport.
1334
+ * Empty arrays / blank elements reject upstream with `<prefix>_invalid_request`
1335
+ * (falls back to `http_invalid_request` when no `errorMapping` is set). Each
1336
+ * array is joined with `,` to match the canonical control-plane shape.
787
1337
  */
788
- connectionString?: string | undefined;
789
- /** Flush trigger by buffer size. Defaults to 50. */
790
- flushBatchSize?: number | undefined;
791
- /** Flush trigger by elapsed time (ms). Defaults to 500. */
792
- flushIntervalMs?: number | undefined;
793
- /** Override how a span maps to a step type. */
794
- resolveStepType?: ((span: ReadableSpanLike) => RunStepType) | undefined;
795
- /** Test seam — inject a postgres-js-compatible client. */
796
- sql?: PostgresLike | undefined;
797
- /** Test seam — deterministic UUID generator. */
798
- newId?: (() => string) | undefined;
1338
+ arrayParams?: Record<string, readonly string[]>;
1339
+ body?: SerializedBody | Record<string, unknown> | unknown[] | null;
1340
+ signal?: AbortSignal;
1341
+ /** Allow retrying non-idempotent requests (POST, PATCH). Off by default to prevent duplicate writes. */
1342
+ idempotent?: boolean;
799
1343
  }
800
1344
  /**
801
- * Materialises every OTel span produced by the agent into a row of
802
- * `stackbone_platform.run_steps`. Designed to coexist with the OTLP exporter:
803
- * cloud builds register both processors so Postgres feeds Studio (paridad
804
- * con local) and Tempo feeds Platform Ops with retention/aggregation. If a
805
- * write fails the processor logs to stderr and keeps accepting spans —
806
- * observability is never load-bearing for agent execution.
1345
+ * Shape every facade method ideally collapses into: "POST to /path with this
1346
+ * body, validate against this schema, surface errors as `<prefix>_*`". The
1347
+ * transport handles validation, HTTP→domain remapping, querystring assembly
1348
+ * and the `Result` envelope so facades stay tiny orchestrators.
807
1349
  *
808
- * Spec: docs/arquitectura/specs/stackbone-agent-protocol-v1.md §6.3
809
- * ADR: docs/arquitectura/decisiones/2026-05-03-stackbone-studio-datapath-y-scope-mvp.md §D9
1350
+ * `arrayParams` keys default to `errorMapping.prefix` for the validation
1351
+ * error code (e.g. an empty `names: []` becomes `secrets_invalid_request`).
810
1352
  */
811
- declare class RunStepsSpanProcessor {
812
- private readonly buffer;
813
- private readonly stepIdBySpanId;
814
- private readonly batchSize;
815
- private readonly intervalMs;
816
- private readonly resolveStepType;
817
- private readonly newId;
818
- private readonly connectionString;
819
- private readonly ownsSql;
820
- private sql;
821
- private timer;
822
- private inFlight;
823
- constructor(options?: RunStepsSpanProcessorOptions);
824
- onStart(span: ReadableSpanLike): void;
825
- onEnd(span: ReadableSpanLike): void;
826
- forceFlush(): Promise<void>;
827
- shutdown(): Promise<void>;
828
- private buildRow;
829
- private armTimer;
830
- private disarmTimer;
831
- private flush;
832
- private ensureSql;
833
- private writeBatch;
834
- }
835
-
836
- interface AggregateRunCostOptions {
837
- sql: PostgresLike;
838
- }
839
- interface AggregateRunCostResult {
840
- costEstimatedUsd: number;
841
- }
842
- declare function aggregateRunCost(runId: string, options: AggregateRunCostOptions): Promise<AggregateRunCostResult>;
843
-
844
- declare const LOG_LEVELS: {
845
- readonly trace: 10;
846
- readonly debug: 20;
847
- readonly info: 30;
848
- readonly warn: 40;
849
- readonly error: 50;
850
- readonly fatal: 60;
851
- };
852
- type LogLevelName = keyof typeof LOG_LEVELS;
853
- type LogLevelNumber = (typeof LOG_LEVELS)[LogLevelName];
854
- interface LogRecord {
855
- /** Pino-style numeric level. */
856
- level: LogLevelNumber;
857
- /** Epoch milliseconds. Pino uses `time` as a number. */
858
- time: number;
859
- msg: string;
860
- run_id: string;
861
- trace_id?: string;
862
- installation_id?: string;
863
- agent_id?: string;
864
- [key: string]: unknown;
865
- }
866
- interface PlatformLoggerOptions {
867
- runId: string;
868
- installationId?: string | undefined;
869
- agentId?: string | undefined;
1353
+ interface TransportRequest<T> extends RequestOptions {
1354
+ method: string;
1355
+ path: string;
870
1356
  /**
871
- * When set (cloud), logs are batched and POSTed to `<endpoint>/v1/logs` as
872
- * OTLP/HTTP/JSON. When unset (local), logs are appended to
873
- * `<runsDir>/<runId>.jsonl`. Falls back to `OTEL_EXPORTER_OTLP_ENDPOINT`.
1357
+ * Zod schema (or any object with a `.safeParse()` method) the parsed body
1358
+ * must validate against. On failure the transport surfaces
1359
+ * `<prefix>_invalid_response` (or `http_invalid_response` if no prefix is
1360
+ * configured) with the schema issues attached to `meta.issues`.
874
1361
  */
875
- otelEndpoint?: string | undefined;
876
- /** Local-only: directory where JSONL files live. Defaults to `~/.stackbone/dev/runs`. */
877
- runsDir?: string | undefined;
878
- /** Cloud-only: extra OTel resource attributes to send with every batch. */
879
- resourceAttributes?: Record<string, string> | undefined;
880
- /** Cloud-only: HTTP timeout per OTLP POST in ms. Defaults to 5_000. */
881
- httpTimeoutMs?: number | undefined;
882
- /** Flush trigger by buffer size. Defaults to 50. */
883
- flushBatchSize?: number | undefined;
884
- /** Flush trigger by elapsed time (ms). Defaults to 1_000. */
885
- flushIntervalMs?: number | undefined;
886
- /** Test seam — fetch implementation. Defaults to global `fetch`. */
887
- fetchImpl?: typeof fetch | undefined;
888
- /** Test seam — `fs.appendFile`-shaped sink the local destination uses. */
889
- appendFileImpl?: ((path: string, data: string) => Promise<void>) | undefined;
890
- /** Test seam — `fs.mkdir`-shaped helper the local destination uses. */
891
- mkdirImpl?: ((path: string) => Promise<void>) | undefined;
892
- /** Test seam — clock for `time` field. Defaults to `Date.now`. */
893
- now?: (() => number) | undefined;
894
- /** Override mode resolution (skips the env detection). */
895
- mode?: PlatformLoggerMode | undefined;
896
- }
897
- type PlatformLoggerMode = 'local' | 'cloud';
898
- interface PinoLike {
899
- trace(msgOrObj: unknown, msg?: string): void;
900
- debug(msgOrObj: unknown, msg?: string): void;
901
- info(msgOrObj: unknown, msg?: string): void;
902
- warn(msgOrObj: unknown, msg?: string): void;
903
- error(msgOrObj: unknown, msg?: string): void;
904
- fatal(msgOrObj: unknown, msg?: string): void;
905
- }
906
- interface PlatformLogger extends PinoLike {
907
- readonly mode: PlatformLoggerMode;
908
- /** Bind extra fields and return a child logger sharing the same destination. */
909
- child(bindings: Record<string, unknown>): PlatformLogger;
910
- /** Force-flush pending writes (close file handle or POST OTLP batch). */
911
- flush(): Promise<void>;
912
- /** Stop accepting writes and release resources. */
913
- close(): Promise<void>;
1362
+ responseSchema?: ResponseSchema<T>;
1363
+ /** HTTP→domain remapping. Omit to let `http_*` codes flow through unchanged. */
1364
+ errorMapping?: ErrorMapping;
914
1365
  }
915
1366
  /**
916
- * Creates a Pino-aligned logger that writes JSONL to disk in local mode and
917
- * batches OTLP/HTTP/JSON log records to an OTel collector in cloud mode. Mode
918
- * is resolved from `OTEL_EXPORTER_OTLP_ENDPOINT` or the explicit override.
1367
+ * Minimal contract the transport needs from a schema `safeParse`. Compatible
1368
+ * with Zod (`z.object(…).safeParse(value)`) and any hand-rolled validator that
1369
+ * follows the same shape. Avoids leaking Zod's full surface into the transport
1370
+ * signature.
919
1371
  */
920
- declare function createPlatformLogger(options: PlatformLoggerOptions): PlatformLogger;
921
- declare function defaultRunsDir(): string;
922
-
923
- declare class ObservabilityModule {
924
- private readonly _resolved;
925
- private _processor?;
926
- private readonly _loggers;
927
- private _sql;
928
- private _sqlResolved;
929
- constructor(_resolved: ResolvedConfig);
930
- /**
931
- * Singleton SpanProcessor that the agent registers in its OTel
932
- * TracerProvider. Coexists with the OTLP exporter — cloud builds wire
933
- * both so Postgres feeds Studio (paridad con local) and Tempo feeds
934
- * Platform Ops.
935
- */
936
- spanProcessor(options?: RunStepsSpanProcessorOptions): RunStepsSpanProcessor;
937
- flush(): Promise<Result<void>>;
1372
+ interface ResponseSchema<T> {
1373
+ safeParse(input: unknown): {
1374
+ success: true;
1375
+ data: T;
1376
+ } | {
1377
+ success: false;
1378
+ error: {
1379
+ issues: unknown;
1380
+ };
1381
+ };
1382
+ }
1383
+ interface HttpClientOptions {
1384
+ /** Default 30_000. Set to 0 to disable. */
1385
+ timeout?: number;
1386
+ /** Default 3. Set to 0 to disable. */
1387
+ retryCount?: number;
1388
+ /** Initial backoff in ms; doubles each attempt with ±15% jitter. Default 500. */
1389
+ retryDelay?: number;
1390
+ /** Override the global fetch (useful for tests). */
1391
+ fetch?: typeof fetch;
1392
+ }
1393
+ /**
1394
+ * Shared HTTP client used by every facade that talks to the Stackbone
1395
+ * control plane. Wraps `fetch` with timeout, exponential backoff with jitter,
1396
+ * idempotent-only retries, response validation (Zod-compatible schemas) and
1397
+ * a uniform `Result` envelope so facades never throw at the SDK boundary.
1398
+ *
1399
+ * Each facade tells the transport its error prefix (`'secrets'`, `'config'`,
1400
+ * `'approval'`) so 404/401/403/429/5xx remap to surface-specific codes —
1401
+ * `secrets_not_found`, `approval_unauthorized`, etc. Endpoint-specific
1402
+ * remaps (e.g. `409 → 'secrets_already_exists'`) flow through
1403
+ * `errorMapping.overrides`. Transport-level failures (network, timeout,
1404
+ * parse) keep the `http_*` prefix the README contract documents.
1405
+ *
1406
+ * Reads `STACKBONE_API_URL` and `STACKBONE_AGENT_JWT` lazily on every request so
1407
+ * env-var rotation is picked up without restarting the client.
1408
+ */
1409
+ declare class HttpClient {
1410
+ private readonly resolved;
1411
+ private readonly timeout;
1412
+ private readonly retryCount;
1413
+ private readonly retryDelay;
1414
+ private readonly fetchImpl;
1415
+ constructor(resolved: ResolvedConfig, options?: HttpClientOptions);
938
1416
  /**
939
- * Per-run structured logger. In local mode (no `OTEL_EXPORTER_OTLP_ENDPOINT`)
940
- * appends JSONL to `~/.stackbone/dev/runs/<runId>.jsonl`; in cloud mode batches
941
- * OTLP/HTTP/JSON log records to the collector. Cached by `runId` so the
942
- * file handle / buffered timer stays stable across repeated calls inside
943
- * the same run.
1417
+ * Convenience for `request({ method: 'GET', path, ... })`. Kept so legacy
1418
+ * callers (tests, pending facades) keep compiling the deep work happens
1419
+ * in `request()`.
944
1420
  */
945
- logger(options: {
946
- runId: string;
947
- } & Partial<PlatformLoggerOptions>): PlatformLogger;
948
- /** Closes the per-run logger (flushes the JSONL file or the OTLP buffer). */
949
- closeLogger(runId: string): Promise<void>;
1421
+ get<T>(path: string, options?: RequestOptions): Promise<Result<T>>;
1422
+ post<T>(path: string, body?: RequestOptions['body'], options?: RequestOptions): Promise<Result<T>>;
1423
+ put<T>(path: string, body?: RequestOptions['body'], options?: RequestOptions): Promise<Result<T>>;
1424
+ patch<T>(path: string, body?: RequestOptions['body'], options?: RequestOptions): Promise<Result<T>>;
1425
+ delete<T>(path: string, options?: RequestOptions): Promise<Result<T>>;
950
1426
  /**
951
- * Hook the agent calls when a run finishes. Sums `cost_usd` from every
952
- * `llm_call` span of the run and writes the total to
953
- * `stackbone_platform.runs.cost_estimated_usd`. Errors are folded into the
954
- * Result envelope so the agent's run cleanup never aborts on a flaky
955
- * Postgres call — the cost is decorative metadata, not load-bearing.
1427
+ * The transport entry point every facade should target. Handles base-URL
1428
+ * resolution, header injection, body/querystring serialisation, retry +
1429
+ * timeout, response validation and HTTP→domain error remapping.
956
1430
  */
957
- closeRun(runId: string): Promise<Result<AggregateRunCostResult>>;
958
- private resolveSql;
1431
+ request<T>(req: TransportRequest<T>): Promise<Result<T>>;
1432
+ private computeBackoff;
959
1433
  }
960
1434
 
1435
+ /**
1436
+ * Enqueue a one-shot (or deferred) job. The opaque `payload` is whatever the
1437
+ * agent's `receive` handler expects — the core never inspects it.
1438
+ */
961
1439
  interface PublishRequest {
962
- url: string;
963
- body: unknown;
1440
+ /** Opaque job name (e.g. `send-email`). Surfaced in the Studio inspector. */
1441
+ name: string;
1442
+ /** Opaque job payload — JSON-serialisable; the core forwards it untouched. */
1443
+ payload: unknown;
1444
+ /** Per-job override of the platform retry default. Omitted → platform default. */
964
1445
  retries?: number;
1446
+ /** Deferred delivery in milliseconds. Omitted / 0 → immediate. */
965
1447
  delay?: number;
1448
+ /** Idempotency hint — a duplicate publish with the same key collapses to one job. */
966
1449
  deduplicationId?: string;
967
- headers?: Record<string, string>;
968
1450
  }
969
- declare class QueuesModule {
970
- private readonly _gate;
971
- constructor(resolved: ResolvedConfig,
972
- /** Test seam — see `ModuleGate`. Defaults to the lazy contract gate. */
973
- gate?: ModuleGate);
974
- publish(_request: PublishRequest): Promise<Result<{
975
- messageId: string;
976
- }>>;
977
- }
978
-
979
- type ChunkStrategy = 'recursive' | 'sentence';
980
- interface ChunkOptions {
981
- /** Splitter algorithm. Default `recursive`. */
982
- strategy?: ChunkStrategy;
983
- /** Target characters per chunk. Default 512. */
984
- size?: number;
985
- /** Characters of overlap between consecutive chunks. Default 64. */
986
- overlap?: number;
987
- }
988
-
989
- type RagIngestProgress = {
990
- type: 'started';
991
- jobId: string;
992
- totalChunks: number;
993
- } | {
994
- type: 'progress';
995
- jobId: string;
996
- processedChunks: number;
997
- totalChunks: number;
998
- currentDocument?: {
999
- source: string;
1000
- ordinal: number;
1001
- };
1002
- } | {
1003
- type: 'completed';
1004
- jobId: string;
1005
- totalChunks: number;
1006
- } | {
1007
- type: 'failed';
1008
- jobId: string;
1009
- error: string;
1010
- };
1011
- interface IngestJobStartArgs {
1012
- collection: string;
1013
- source: string;
1014
- totalChunks: number;
1015
- }
1016
- interface IngestJobProgressArgs {
1017
- jobId: string;
1018
- processedChunks: number;
1019
- totalChunks: number;
1020
- currentDocument?: {
1021
- source: string;
1022
- ordinal: number;
1023
- };
1024
- }
1025
- interface IngestJobCompleteArgs {
1026
- jobId: string;
1027
- totalChunks: number;
1028
- }
1029
- interface IngestJobFailArgs {
1030
- jobId: string;
1031
- error: string;
1032
- }
1033
- interface IngestJobWriter {
1034
- /**
1035
- * Allocates a `stackbone_rag_jobs` row in `running` state and returns its
1036
- * id. Caller is expected to surface this id to the user before any
1037
- * progress event fires so a `client.rag.ingestAsync` consumer can
1038
- * immediately address the job (e.g. `POST /api/rag/jobs/:jobId/cancel`).
1039
- */
1040
- start(args: IngestJobStartArgs): Promise<Result<{
1041
- jobId: string;
1042
- }>>;
1043
- /** Idempotent — last write wins on `progress` jsonb. */
1044
- progress(args: IngestJobProgressArgs): Promise<void>;
1045
- /** Marks the row terminal (status='succeeded'). */
1046
- complete(args: IngestJobCompleteArgs): Promise<void>;
1047
- /** Marks the row terminal (status='failed') and stores the error message. */
1048
- fail(args: IngestJobFailArgs): Promise<void>;
1049
- /**
1050
- * Polled between chunk batches. Returning `true` makes the pipeline abort
1051
- * with `rag_ingest_cancelled`, leaving the row in whatever terminal state
1052
- * the writer transitions it to next (typically `cancelled`).
1053
- */
1054
- isCancelled(args: {
1055
- jobId: string;
1056
- }): Promise<boolean>;
1057
- }
1058
-
1059
- interface ParseOptions {
1060
- /** MIME type override. If omitted, the parser sniffs the input (Blob.type or PDF magic bytes). */
1061
- mime?: string;
1062
- }
1063
- type ParseInput = Blob | string | Uint8Array | ArrayBuffer;
1064
-
1065
- interface IngestChunk {
1066
- content: string;
1067
- embedding: number[];
1068
- }
1069
- interface IngestRequestBase {
1070
- /** Document identifier — re-ingesting with the same `id` replaces all its chunks atomically. */
1071
- id: string;
1072
- metadata?: Record<string, unknown>;
1073
- /** Logical namespace for separation. Default `'default'`. */
1074
- namespace?: string;
1075
- /**
1076
- * Canonical collection name used by the `stackbone_rag_jobs` writer (F07)
1077
- * to bind the row to a `stackbone_rag_collections` parent. Optional for
1078
- * back-compat with pre-F07 callers — when absent, no job row is written
1079
- * and progress is delivered only via `onProgress` (if any).
1080
- */
1081
- collection?: string;
1082
- /**
1083
- * Optional sink for streaming progress events. Fired in addition to (and
1084
- * before) the matching `IngestJobWriter` call so synchronous `ingest`
1085
- * consumers can observe progress without paying the SQL writer cost.
1086
- */
1087
- onProgress?: (event: RagIngestProgress) => void | Promise<void>;
1088
- }
1089
- interface IngestRequestPrecomputed extends IngestRequestBase {
1090
- chunks: IngestChunk[];
1091
- }
1092
- interface IngestRequestAutoEmbed extends IngestRequestBase {
1093
- chunks: string[];
1094
- /** Embedding model id. When set, the pipeline calls the configured Embedder. */
1095
- model: string;
1096
- /**
1097
- * Items per embeddings request. Consumed by the facade when it builds the
1098
- * `Embedder`; ignored by the pipeline itself (the embedder is fully
1099
- * configured at construction time).
1100
- */
1101
- batchSize?: number;
1102
- }
1103
- type IngestRequest = IngestRequestPrecomputed | IngestRequestAutoEmbed;
1104
- interface IngestResponse {
1105
- id: string;
1106
- chunks: number;
1107
- }
1108
- interface DeleteOptions {
1109
- namespace?: string;
1110
- }
1111
- interface DeleteResponse {
1112
- deleted: number;
1113
- }
1114
- interface RetrieveRequestBase {
1115
- topK?: number;
1116
- filter?: Record<string, unknown>;
1117
- namespace?: string;
1118
- includeContent?: boolean;
1119
- includeMetadata?: boolean;
1120
- }
1121
- interface RetrieveRequestPrecomputed extends RetrieveRequestBase {
1122
- embedding: number[];
1123
- }
1124
- interface RetrieveRequestAutoEmbed extends RetrieveRequestBase {
1125
- text: string;
1126
- /** Must match the model used at ingest time. */
1127
- model: string;
1128
- }
1129
- type RetrieveRequest = RetrieveRequestPrecomputed | RetrieveRequestAutoEmbed;
1130
- interface RetrieveHit {
1131
- id: string;
1132
- chunkIdx: number;
1133
- content?: string;
1134
- metadata?: Record<string, unknown>;
1135
- score: number;
1451
+ /** Register / update a dynamic per-installation cron schedule. */
1452
+ interface ScheduleRequest {
1453
+ /** Schedule name, unique per install. Re-scheduling the same name updates it in place. */
1454
+ name: string;
1455
+ /** Opaque payload delivered on every fire. */
1456
+ payload: unknown;
1457
+ /** Cron expression evaluated by the dispatcher. */
1458
+ cron: string;
1459
+ /** IANA timezone the cron pattern runs in. Defaults to UTC. */
1460
+ tz?: string;
1461
+ /** Per-job override of the platform retry default. */
1462
+ retries?: number;
1136
1463
  }
1137
-
1138
- /**
1139
- * Handle returned by `client.rag.ingestAsync`. The job id is allocated
1140
- * synchronously against `stackbone_rag_jobs` so the caller can track / cancel
1141
- * the work via the `/api/rag/jobs/:jobId/*` surface; `events` is a streaming
1142
- * channel of progress events (ADR §D9 shape); `result` settles when the
1143
- * pipeline finishes (or fails / is cancelled).
1144
- */
1145
- interface IngestAsyncHandle {
1146
- jobId: string;
1147
- events: AsyncIterable<RagIngestProgress>;
1148
- result: Promise<Result<IngestResponse>>;
1464
+ /** Cancel a previously registered schedule by name. */
1465
+ interface UnscheduleRequest {
1466
+ name: string;
1149
1467
  }
1150
1468
  /**
1151
- * `client.rag` — `pgvector`-backed retrieval. Two shapes per write/read:
1152
- * pass `model` to let the SDK embed for you; pass embeddings precomputed for
1153
- * provider/dimension/batching control.
1469
+ * `client.queues` — the agent's handle on the BullMQ-backed job system. The
1470
+ * agent never touches Redis: each method makes an authenticated call to the
1471
+ * control plane (the dispatcher owns the queue + the clock).
1154
1472
  *
1155
- * Implementation note: `RagModule` is a thin facade over `RagPipeline`. The
1156
- * pipeline owns parse chunk → embed → persist; the facade resolves the
1157
- * shared `client.database` pool, builds an `Embedder` on demand, and maps
1158
- * configuration errors. See ADR `2026-05-10-rag-consolidation-on-client-database`.
1473
+ * - `publish` enqueues a one-shot job (optionally deferred via `delay`, with a
1474
+ * per-job `retries` override and an optional `deduplicationId`).
1475
+ * - `schedule` / `unschedule` manage dynamic per-user cron schedules.
1476
+ * - `listSchedules` lists the install's active schedules.
1159
1477
  *
1160
- * Connection ownership: this module never opens its own postgres pool. It
1161
- * pulls the underlying `postgres-js` `Sql` from the shared
1162
- * `getDatabaseHandle()` exposed by `client.database`, so an agent that
1163
- * touches both surfaces still opens exactly one connection pool against
1164
- * `STACKBONE_POSTGRES_URL`.
1478
+ * Authentication reuses the existing `STACKBONE_AGENT_JWT` channel injected at
1479
+ * provisioning (the `HttpClient` attaches the bearer + install header), so the
1480
+ * core scopes every enqueue to the agent's own install.
1165
1481
  *
1166
- * Schema readiness: starting with feature 30, the canonical schema is
1167
- * installed by the CLI (`stackbone db migrate add-rag` + `migrate up`). When
1168
- * an operation hits `42P01 relation does not exist`, the pipeline returns
1169
- * `SdkError('rag_schema_missing')` with an actionable hint.
1482
+ * Contract gating: every method awaits the `queues.jobs` module gate before
1483
+ * touching the network, so a stale datapath / missing capability surfaces as
1484
+ * a gating error rather than a failed POST.
1170
1485
  */
1171
- /**
1172
- * Test-only seams accepted by `RagModule`. Production callers leave these
1173
- * unset — they are injected by `ingest-async.spec.ts` to substitute the
1174
- * shared-pool lookup and the SQL-backed job writer.
1175
- */
1176
- interface RagModuleTestDeps {
1177
- /** Replaces the lazy `getDatabaseHandle()` lookup used by `ingestAsync`. */
1178
- sqlProvider?: () => Result<Sql>;
1179
- /** Builds an `IngestJobWriter` from the resolved SQL. Defaults to `createSqlJobWriter`. */
1180
- jobWriterFactory?: (sql: Sql) => IngestJobWriter;
1181
- }
1182
- declare class RagModule {
1183
- private readonly _resolved;
1184
- private readonly _getAi;
1486
+ declare class QueuesModule {
1487
+ private readonly _http;
1185
1488
  private readonly _gate;
1186
- private readonly _testSqlOverride?;
1187
- private readonly _testJobWriterFactory?;
1188
- constructor(_resolved: ResolvedConfig, _getAi: () => AiModule,
1489
+ constructor(resolved: ResolvedConfig, _http: HttpClient,
1189
1490
  /** Test seam — see `ModuleGate`. Defaults to the lazy contract gate. */
1190
- gate?: ModuleGate, testDeps?: RagModuleTestDeps);
1191
- ingest(request: IngestRequest): Promise<Result<IngestResponse>>;
1192
- /**
1193
- * Asynchronous ingest. Allocates a `stackbone_rag_jobs` row, returns the
1194
- * job id immediately, and exposes both an `AsyncIterable` of streaming
1195
- * progress events and the final `Result` so callers can `await` either
1196
- * surface — typical webhook handlers consume `events` and ignore `result`,
1197
- * tests `await result` directly. Cancellation is observed by flipping the
1198
- * row's status (e.g. via `POST /api/rag/jobs/:jobId/cancel`); the worker
1199
- * polls between chunk batches and bails out with `rag_ingest_cancelled`.
1200
- *
1201
- * Connection ownership: the `IngestJobWriter` is bound to the same SQL
1202
- * pulled off the shared `client.database` handle, so a single agent that
1203
- * issues `ingestAsync` and `client.database.select` in flight still opens
1204
- * exactly one pool against `STACKBONE_POSTGRES_URL`.
1205
- */
1206
- ingestAsync(request: IngestRequest): Promise<Result<IngestAsyncHandle>>;
1207
- delete(ids: string | string[], options?: DeleteOptions): Promise<Result<DeleteResponse>>;
1208
- deleteWhere(filter: Record<string, unknown>, options?: DeleteOptions): Promise<Result<DeleteResponse>>;
1209
- retrieve(request: RetrieveRequest): Promise<Result<RetrieveHit[]>>;
1210
- /**
1211
- * Drops the legacy ad-hoc RAG schema. Kept for backwards compatibility with
1212
- * pre-feature-30 agents whose tables were provisioned by `ensureSchema` on
1213
- * first call. The canonical RAG schema (feature 30) is owned by the CLI and
1214
- * removed via `stackbone db migrate` flows, not by this method.
1215
- */
1216
- reset(): Promise<Result<void>>;
1217
- /**
1218
- * Pure helper — exposed verbatim from the chunker. No DB, no AI, no
1219
- * datapath traffic, so it is intentionally NOT gated.
1220
- */
1221
- chunk(text: string, options?: ChunkOptions): string[];
1222
- /**
1223
- * Pure helper — exposed verbatim from the parser. No DB, no AI, no
1224
- * datapath traffic, so it is intentionally NOT gated.
1225
- */
1226
- parse(input: ParseInput, options?: ParseOptions): Promise<string>;
1227
- /**
1228
- * Resolves the postgres-js `Sql` to use for this operation. Pulls `$client`
1229
- * off the shared Drizzle handle (single pool per agent process). Slice F03's
1230
- * NOTE about cross-module transaction propagation lives here: this is the
1231
- * seam where a future `database.transaction` integration plugs a tx-bound
1232
- * `Sql` into the pipeline instead of the pool-bound one.
1233
- *
1234
- * Configuration errors (`STACKBONE_POSTGRES_URL` unset) flow up as a
1235
- * `Result.err` with the same `database_not_configured` shape `client.database`
1236
- * raises, instead of throwing through the public surface.
1237
- */
1238
- private sql;
1239
- private withPipeline;
1491
+ gate?: ModuleGate);
1492
+ publish(request: PublishRequest): Promise<Result<PublishJobResponse>>;
1493
+ schedule(request: ScheduleRequest): Promise<Result<ScheduleJobResponse>>;
1494
+ unschedule(request: UnscheduleRequest): Promise<Result<UnscheduleJobResponse>>;
1495
+ listSchedules(): Promise<Result<ListSchedulesResponse>>;
1240
1496
  }
1241
1497
 
1242
- interface StorageObject {
1243
- key: string;
1244
- size: number;
1245
- lastModified?: Date;
1246
- etag?: string;
1247
- }
1248
- interface UploadOptions {
1249
- contentType?: string;
1250
- metadata?: Record<string, string>;
1251
- }
1252
- interface ListOptions {
1253
- prefix?: string;
1254
- /** Maximum number of objects to return per page. Must be > 0. */
1255
- limit?: number;
1256
- cursor?: string;
1257
- }
1258
- interface SignedUrlOptions {
1259
- /** Seconds until the URL expires. Defaults to 3600 (1h). */
1260
- expiresIn?: number;
1261
- /** Only honoured by `getSignedUploadUrl` — pinned into the signature so the uploader must send a matching `Content-Type`. */
1262
- contentType?: string;
1263
- }
1264
- interface SignedUrl {
1265
- url: string;
1266
- expiresAt: Date;
1267
- }
1268
- type StorageBody = Blob | Uint8Array | string;
1269
- interface S3Settings {
1270
- client: S3Client;
1271
- endpoint: string;
1272
- bucket: string;
1273
- agentId: string;
1274
- }
1275
1498
  /**
1276
- * Public surface behind `client.storage`. Wraps `@aws-sdk/client-s3` against
1277
- * R2 (prod) or MinIO (dev). Exposes a bucket-scoped API via `from(bucket)`.
1499
+ * The live agent surfaces exposed to the creator's code through a single
1500
+ * `createClient()` entry point. The folder layout under `src/surfaces/`
1501
+ * groups each surface by where its state lives:
1278
1502
  *
1279
- * Multi-tenancy: every key is prefixed with `agentId/bucket/` before hitting
1280
- * S3. Credentials are scoped to the agent's physical bucket (env `S3_BUCKET`),
1281
- * and the `bucket` argument is a logical namespace the agent uses to organise
1282
- * its own objects. The full prefixed key is an internal detail — public
1283
- * methods accept and return the user-facing key (without prefix). Metadata
1284
- * tracking in `_storage_objects` (Neon) is out-of-scope for this iteration.
1503
+ * - `surfaces/agent-local/` Postgres branch owned by the agent
1504
+ * (`database`, `rag`, `secrets`, `config`, `approval`). `secrets`,
1505
+ * `config` and `approval` moved here from the control plane: the agent now
1506
+ * reads its secrets/config and records HITL pauses directly against its own
1507
+ * `stackbone_platform.*` tables (decrypting secrets with the per-agent key),
1508
+ * with no control-plane round-trip. The approval RESUME callback stays
1509
+ * Studio-signed and is reconciled locally by `approval.verify()`.
1510
+ * - `surfaces/external/` — managed partner SDKs the SDK wraps directly
1511
+ * (`ai` / OpenRouter, `storage` / S3 + R2 / MinIO). Observability is not a
1512
+ * creator surface: agent code observes runs through the handler-scoped
1513
+ * `ctx.logger` (and plain `console.*`, which the wrapper captures and
1514
+ * correlates), while the platform primitives live in the
1515
+ * `@stackbone/sdk/observability` subpath the runtime wires directly.
1516
+ * - `surfaces/control-plane/` — thin HTTP calls to Stackbone's control
1517
+ * plane (none today after secrets/config/approval moved agent-local).
1518
+ * - `surfaces/pending/` — public surface is stable but the runtime is
1519
+ * not built yet. They are wired as live `client.X` accessors so MVP
1520
+ * agent code can be authored against the eventual contract today; each
1521
+ * method returns `Result<{ error: { code: "not_implemented" } }>` until
1522
+ * the backing runtime ships. Today: queues, memory.
1285
1523
  *
1286
- * Memory note: `download()` materialises the whole object as a `Blob` in
1287
- * memory. For agents that need to stream large objects, prefer
1288
- * `getSignedDownloadUrl()` and `fetch()` the URL directly to consume the
1289
- * stream incrementally.
1290
- */
1291
- declare class StorageModule {
1292
- private readonly _resolved;
1293
- private _s3;
1294
- private readonly _gate;
1295
- constructor(_resolved: ResolvedConfig,
1296
- /** Test seam — see `ModuleGate`. Defaults to the lazy contract gate. */
1297
- gate?: ModuleGate);
1298
- from(bucket: string): StorageBucket;
1299
- private settings;
1300
- }
1301
- declare class StorageBucket {
1302
- private readonly bucketName;
1303
- private readonly resolveSettings;
1304
- private readonly gate;
1305
- constructor(bucketName: string, resolveSettings: () => Result<S3Settings>, gate: ModuleGate);
1306
- upload(key: string, body: StorageBody, options?: UploadOptions): Promise<Result<{
1307
- key: string;
1308
- etag?: string;
1309
- }>>;
1310
- download(key: string): Promise<Result<Blob>>;
1311
- list(options?: ListOptions): Promise<Result<{
1312
- objects: readonly StorageObject[];
1313
- nextCursor?: string;
1314
- }>>;
1315
- remove(key: string): Promise<Result<{
1316
- key: string;
1317
- }>>;
1318
- /**
1319
- * Returns the canonical S3-style URL for an object. Pure URL-builder — no
1320
- * S3 round-trip — so this method intentionally bypasses the contract gate.
1321
- * Whether the URL is publicly fetchable depends on the bucket policy; for
1322
- * private buckets, use `getSignedDownloadUrl` instead.
1323
- */
1324
- getPublicUrl(key: string): Result<string>;
1325
- getSignedUploadUrl(key: string, options?: SignedUrlOptions): Promise<Result<SignedUrl>>;
1326
- getSignedDownloadUrl(key: string, options?: SignedUrlOptions): Promise<Result<SignedUrl>>;
1327
- private signUrl;
1328
- /**
1329
- * Resolves S3 settings, validates the user key, and prefixes it with
1330
- * `${agentId}/${bucketName}/`. Rejects path-traversal segments (`..`) so a
1331
- * caller-controlled key cannot escape the agent's namespace.
1332
- */
1333
- private resolve;
1334
- }
1335
-
1336
- /**
1337
- * Each accessor builds its module on first access and caches it — env vars
1338
- * and partner SDK init costs are paid only when the agent actually touches
1339
- * that surface.
1524
+ * Each accessor builds its surface on first access and caches it — env
1525
+ * vars and partner SDK init costs are paid only when the agent actually
1526
+ * touches that surface.
1527
+ *
1528
+ * Capability gating: every surface listed in `MODULE_CAPABILITIES` builds
1529
+ * its `ModuleGate` against the per-client `ContractStore` owned by this
1530
+ * instance — two clients constructed in the same process do NOT share
1531
+ * handshake cache or suppression-warning state. See
1532
+ * `contract/capability-registry.ts` for the membership rule.
1340
1533
  */
1341
1534
  declare class StackboneClient {
1342
1535
  private readonly resolved;
1343
1536
  private _database?;
1344
1537
  private _storage?;
1345
1538
  private _ai?;
1346
- private _queues?;
1347
1539
  private _rag?;
1348
- private _memory?;
1349
- private _observability?;
1350
1540
  private _approval?;
1351
1541
  private _secrets?;
1352
- private _connections?;
1353
1542
  private _config?;
1354
- private _events?;
1543
+ private _queues?;
1544
+ private _memory?;
1355
1545
  private _prompts?;
1356
1546
  private _httpClient?;
1547
+ private readonly _contractStore;
1357
1548
  constructor(resolved: ResolvedConfig);
1358
1549
  private http;
1359
1550
  get database(): DatabaseModule;
1360
1551
  get storage(): StorageModule;
1361
1552
  get ai(): AiModule;
1362
- get queues(): QueuesModule;
1363
1553
  get rag(): RagModule;
1364
- get memory(): MemoryModule;
1365
- get observability(): ObservabilityModule;
1366
1554
  get approval(): ApprovalFacade;
1367
1555
  get secrets(): SecretsFacade;
1368
- get connections(): ConnectionsFacade;
1369
1556
  get config(): ConfigFacade;
1370
- get events(): EventsFacade;
1557
+ /**
1558
+ * Live surface — the agent's handle on the BullMQ-backed job system.
1559
+ * `publish` / `schedule` / `unschedule` / `listSchedules` make authenticated
1560
+ * calls to the control-plane dispatcher (`/api/v1/agent/queues/*`); the
1561
+ * agent never touches Redis. Gated against `queues.jobs` so the
1562
+ * handshake-blocked / capability-missing paths are exercised the same way
1563
+ * as the other gated surfaces.
1564
+ */
1565
+ get queues(): QueuesModule;
1566
+ /**
1567
+ * Pending surface — runtime not built. Every method returns
1568
+ * `not_implemented`. Mem0 is a third-party integration, not a Stackbone
1569
+ * Agent Protocol capability, so this surface is intentionally not gated.
1570
+ */
1571
+ get memory(): MemoryModule;
1572
+ /**
1573
+ * Live agent-local surface — reads prompts from `stackbone_platform.prompts`
1574
+ * in the agent DB over the shared `client.database` pool and compiles
1575
+ * templates with the local `@stackbone/prompt-compiler`. Constructed with
1576
+ * the same `(resolved, () => this.database)` shape as `secrets`/`config`;
1577
+ * like them it is NOT wired to the control-plane handshake gate (it talks to
1578
+ * the agent's own tables, not a control-plane datapath).
1579
+ */
1371
1580
  get prompts(): PromptsFacade;
1372
1581
  /**
1373
1582
  * Read-only view of the most recently resolved Stackbone Agent Protocol
@@ -1375,9 +1584,466 @@ declare class StackboneClient {
1375
1584
  * handshake has resolved successfully (the handshake fires lazily on the
1376
1585
  * first gated module call — slices #4/#5). Synchronous, never throws,
1377
1586
  * performs no fetches.
1587
+ *
1588
+ * Backed by this client's own `ContractStore`, so two `StackboneClient`
1589
+ * instances see independent `client.contract` views even when they point
1590
+ * at the same baseUrl.
1378
1591
  */
1379
1592
  get contract(): ContractResponse | null;
1380
1593
  }
1381
1594
  declare function createClient(config?: ClientConfig): StackboneClient;
1382
1595
 
1383
- export { type AddMemoryRequest, type AggregateRunCostOptions, type AggregateRunCostResult, type ApprovalListOptions, type ApprovalListResult, type ApprovalRecord, type ApprovalRequest, type ApprovalRequestOptions, type ApprovalStatus, type ApprovalTool, type ApprovalToolSpec, type ApprovalTopic, type ApproverInfo, type ChunkOptions, type ChunkStrategy, type ClientConfig, type CreatePromptRequest, type Decision, type DecisionStatus, type DeleteAllMemoryRequest, type DeleteOptions, type DeletePromptOptions, type DeletePromptResult, type DeleteResponse, type EndSessionOptions, type EndSessionResult, type GeneratedImage, type GetPromptOptions, type ImageGenerateParams, type ImagesResponse, type IngestAsyncHandle, type IngestChunk, type IngestRequest, type IngestRequestAutoEmbed, type IngestRequestPrecomputed, type IngestResponse, LOG_LEVELS, type ListMemoryRequest, type ListMemoryResult, type ListOptions, type ListPromptsOptions, type ListPromptsResult, type LogLevelName, type LogLevelNumber, type LogRecord, type MemoryContent, type MemoryHistoryEntry, type MemoryHistoryEvent, type MemoryHit, type MemoryItem, type MemoryScope, type ModelsListResponse, type OpenRouterModel, type ParseInput, type ParseOptions, type PinoLike, type PlatformLogger, type PlatformLoggerMode, type PlatformLoggerOptions, type PostgresLike, type Prompt, type PublishRequest, RUN_ID_ATTRIBUTE, RUN_STEP_TYPES, type RagIngestProgress, type ReadableSpanLike, type Result, type RetrieveHit, type RetrieveRequest, type RetrieveRequestAutoEmbed, type RetrieveRequestPrecomputed, type RunStepType, RunStepsSpanProcessor, type RunStepsSpanProcessorOptions, STEP_TYPE_ATTRIBUTE, type SdkError, type SearchMemoryOptions, type SignedUrl, type SignedUrlOptions, type SpanContextLike, StackboneClient, type StorageBody, type StorageObject, type UpdateMemoryOptions, type UpdatePromptOptions, type UploadOptions, type VerifyOptions, aggregateRunCost, createClient, createPlatformLogger, defaultRunsDir };
1596
+ /**
1597
+ * Structured fields the wrapper, an upstream consumer, or a handler can
1598
+ * attach to a single log line. Values are `unknown` because the wrapper
1599
+ * does not enforce a schema — the destination decides how to serialise
1600
+ * each value.
1601
+ */
1602
+ type LoggerBindings = Record<string, unknown>;
1603
+ /**
1604
+ * The handler-facing logger. Every method takes a human-readable message
1605
+ * plus an optional bag of structured fields the destination merges with
1606
+ * the bindings already set by the wrapper (typically `invocationId` and
1607
+ * `runId`).
1608
+ *
1609
+ * F5 wired the binding contract only; F6 replaces the stub implementation
1610
+ * with the JSON/STACKBONE_MODE-aware variant. Keeping the surface here
1611
+ * (instead of in the CLI) lets the SDK ship a typed seam creators can
1612
+ * reference from their own tests.
1613
+ */
1614
+ interface Logger {
1615
+ /** Fine-grained diagnostic information. */
1616
+ debug(msg: string, meta?: LoggerBindings): void;
1617
+ /** Routine progress information. */
1618
+ info(msg: string, meta?: LoggerBindings): void;
1619
+ /** Recoverable problem the handler still resolved. */
1620
+ warn(msg: string, meta?: LoggerBindings): void;
1621
+ /** Unrecoverable failure the handler is about to report. */
1622
+ error(msg: string, meta?: LoggerBindings): void;
1623
+ /**
1624
+ * Returns a new logger that merges `bindings` into every subsequent
1625
+ * log line. The original logger is left untouched.
1626
+ */
1627
+ child(bindings: LoggerBindings): Logger;
1628
+ }
1629
+ /**
1630
+ * Minimal writable surface the structured logger drains into. Both
1631
+ * `process.stdout` and arbitrary test doubles satisfy it — every consumer
1632
+ * only needs `write(string): unknown`.
1633
+ */
1634
+ interface LogSink {
1635
+ write(chunk: string): unknown;
1636
+ }
1637
+ interface StructuredLoggerOptions {
1638
+ /** Output stream. Defaults to `process.stdout` for production use. */
1639
+ stream?: LogSink;
1640
+ /** Initial bindings every line inherits (e.g. process-wide metadata). */
1641
+ bindings?: LoggerBindings;
1642
+ /** Clock seam for the `ts` field. Defaults to `() => new Date()`. */
1643
+ now?: () => Date;
1644
+ }
1645
+ /**
1646
+ * Builds a structured `Logger` that emits one NDJSON line per call. Output
1647
+ * shape is fixed by the wrapper PRD: `{ ts, level, msg, ...bindings,
1648
+ * ...meta }`. The wrapper layers extra fields on top via `child()`
1649
+ * (`invocationId`, `runId`, `latencyMs`, `status`).
1650
+ */
1651
+ declare const createStructuredLogger: (opts?: StructuredLoggerOptions) => Logger;
1652
+
1653
+ /**
1654
+ * The data the wrapper hands to the creator's `run` function. F5 widens the
1655
+ * shape from F1's bare `{ input }` to the full per-invocation context:
1656
+ *
1657
+ * - `signal` is a fresh `AbortSignal` per invocation. The wrapper aborts
1658
+ * it when the deadline expires; the handler is expected to honour it
1659
+ * on cancellable work (fetches, DB queries, model calls).
1660
+ * - `invocationId` / `runId` come straight from the parsed envelope.
1661
+ * - `client` is the **same** `StackboneClient` instance for every
1662
+ * invocation in the process, so sub-modules can keep their internal
1663
+ * pools warm.
1664
+ * - `logger` is a child of the wrapper's structured logger already
1665
+ * bound to `invocationId` and `runId`, so the handler does not have to
1666
+ * pass them on every line. The actual JSON format lands in F6.
1667
+ */
1668
+ interface InvokeContext<I extends z.ZodType> {
1669
+ input: z.infer<I>;
1670
+ signal: AbortSignal;
1671
+ invocationId: string;
1672
+ runId: string;
1673
+ client: StackboneClient;
1674
+ logger: Logger;
1675
+ }
1676
+ /**
1677
+ * The synchronous `invoke` capability. Mirrors the wrapper's three HTTP verbs
1678
+ * (`/invoke`, `/health`, `/schema`): `invoke` is the only request path, its
1679
+ * `output` is the `/invoke` response body, and its schemas drive `/schema`.
1680
+ *
1681
+ * The fields are deliberately enumerated (no `[k: string]: unknown` escape
1682
+ * hatch) so the precise type the creator gets from `defineAgent` rejects
1683
+ * unknown keys on the block at compile time.
1684
+ */
1685
+ interface InvokeCapability<I extends z.ZodType, O extends z.ZodType> {
1686
+ /** Zod schema for the parsed `/invoke` request body. */
1687
+ input: I;
1688
+ /** Zod schema for the `/invoke` response body. */
1689
+ output: O;
1690
+ /** Creator-supplied handler. Returns either the parsed output or a promise of it. */
1691
+ run: (ctx: InvokeContext<I>) => z.infer<O> | Promise<z.infer<O>>;
1692
+ /** Optional per-invocation timeout in milliseconds. */
1693
+ timeoutMs?: number;
1694
+ }
1695
+ /**
1696
+ * A named **job handler** — a sibling of `invoke` at the top level of the spec,
1697
+ * reached asynchronously via `client.queues.publish({ name })` (the BullMQ
1698
+ * dispatcher pushes the job back to `/invoke` with that name; the wrapper routes
1699
+ * it here instead of to `invoke.run`).
1700
+ *
1701
+ * Same shape as `invoke` with one difference: `output` is **optional**. A job is
1702
+ * fire-and-forget — the dispatcher only checks the 2xx, the return value is
1703
+ * journalled, not delivered to a caller. Declaring `output` self-validates the
1704
+ * job's result; omitting it lets `run` return anything (or nothing).
1705
+ */
1706
+ interface JobCapability<I extends z.ZodType, O extends z.ZodType = z.ZodType> {
1707
+ /** Zod schema for the job payload (`publish`'s `payload`, revalidated here). */
1708
+ input: I;
1709
+ /** Optional Zod schema for the job's return value. Validated + journalled, never returned. */
1710
+ output?: O;
1711
+ /** Creator-supplied job handler. May return nothing — the result is not delivered. */
1712
+ run: (ctx: InvokeContext<I>) => z.infer<O> | void | Promise<z.infer<O> | void>;
1713
+ /** Optional per-job timeout in milliseconds. */
1714
+ timeoutMs?: number;
1715
+ }
1716
+ /**
1717
+ * Loose, non-generic view of a single capability (invoke or a job handler).
1718
+ * The wrapper / `loadSpec` read the spec through this — the precise per-handler
1719
+ * typing lives on the value `defineAgent` returns to the creator, not here.
1720
+ *
1721
+ * `run` is a method signature (not an arrow property) on purpose: it makes the
1722
+ * parameter bivariant so every concrete `InvokeCapability` / `JobCapability`
1723
+ * (whose `run` is typed against its own narrow input) is assignable to it, which
1724
+ * is what lets `AgentSpec` carry a string index signature of capabilities.
1725
+ */
1726
+ interface AnyCapability {
1727
+ input: z.ZodType;
1728
+ output?: z.ZodType | undefined;
1729
+ run(ctx: InvokeContext<z.ZodType>): unknown;
1730
+ timeoutMs?: number | undefined;
1731
+ }
1732
+ /**
1733
+ * The declarative shape every Stackbone agent returns from `src/index.ts`, as
1734
+ * the wrapper / `loadSpec` consume it: the **loose runtime view**. Every
1735
+ * capability — `invoke` and each named job handler — is an `AnyCapability`
1736
+ * (`output` optional, `run` loose). `invoke` is required; the string index
1737
+ * carries the job handlers, reached by name through the BullMQ dispatcher.
1738
+ *
1739
+ * The precise per-handler typing (each `ctx.input` inferred, `invoke.output`
1740
+ * required) lives on the value `defineAgent` returns, which is assignable to
1741
+ * this. At runtime `invoke.output` is always present — the spec seed forces it —
1742
+ * so the wrapper reads it behind a small guard.
1743
+ */
1744
+ interface AgentSpec {
1745
+ /** The synchronous entry: the `/invoke` response + the `GET /schema` source. */
1746
+ invoke: AnyCapability;
1747
+ /** Named job handlers, reached via `client.queues.publish({ name })`. */
1748
+ [name: string]: AnyCapability;
1749
+ }
1750
+ /**
1751
+ * Top-level keys the platform owns. A job handler may not use these names.
1752
+ * Only `invoke` for now; the contract adds `health` / `schema` / `events` /
1753
+ * `schedules` here if/when they become declarable capabilities (see the named
1754
+ * job handlers ADR).
1755
+ */
1756
+ declare const RESERVED_HANDLER_NAMES: readonly ["invoke"];
1757
+ /**
1758
+ * Hard ceiling on any capability's `timeoutMs`. The platform saga kills the
1759
+ * Machine before the wrapper would time out at higher values, so a creator
1760
+ * declaring a 10-minute timeout would publish a contract the runtime cannot
1761
+ * keep. Surface that conflict at boot instead of as a mysterious mid-invocation
1762
+ * SIGKILL in production.
1763
+ */
1764
+ declare const INVOKE_TIMEOUT_HARD_CAP_MS = 300000;
1765
+ /**
1766
+ * One handler block, generic **directly** on its input Zod schema `I`. `input: I`
1767
+ * is a single-level type parameter (not a nested indexed access like
1768
+ * `S['input']`), which is the shape TypeScript can invert when reading the spec
1769
+ * literal back — so every handler's `ctx.input` is inferred from its own schema.
1770
+ * `output` stays loose (`z.ZodType`) and `run`'s return is `unknown`: a handler's
1771
+ * result is validated against `output` at runtime (the wrapper), not at the type
1772
+ * layer, so output inference is not needed for the contract to hold.
1773
+ */
1774
+ type HandlerBlock<I extends z.ZodType> = {
1775
+ input: I;
1776
+ output?: z.ZodType;
1777
+ run: (ctx: InvokeContext<I>) => unknown | Promise<unknown>;
1778
+ timeoutMs?: number;
1779
+ };
1780
+ /**
1781
+ * Per-key precise shape of the spec the creator writes. `T` maps each handler
1782
+ * name to its **input** schema; `[K in keyof T]: HandlerBlock<T[K]>` is a
1783
+ * homomorphic mapped type whose value (`input: T[K]`) is a single-level access,
1784
+ * so TypeScript infers `T` back from the literal and keeps the concrete key
1785
+ * names. The `& { invoke: ... }` arm makes `invoke` required and forces its
1786
+ * `output` to be present (a job's `output` is optional).
1787
+ */
1788
+ type AgentSpecParam<T extends Record<string, z.ZodType>> = {
1789
+ [K in keyof T]: HandlerBlock<T[K]>;
1790
+ } & {
1791
+ invoke: {
1792
+ input: z.ZodType;
1793
+ output: z.ZodType;
1794
+ };
1795
+ };
1796
+ /**
1797
+ * Anchor + guard for `defineAgent`:
1798
+ *
1799
+ * 1. **Inference anchor** — each handler (`invoke` and every named job) infers
1800
+ * its own `ctx.input` from its `input` schema. The creator never repeats a
1801
+ * type.
1802
+ * 2. **Boot-time invariant** — `invoke` is required; every other top-level key
1803
+ * must be a valid handler object reached via `client.queues.publish({ name })`.
1804
+ * The TypeScript signature already shapes this; the runtime guard is the
1805
+ * safety net for creators forcing a shape through `as any`.
1806
+ *
1807
+ * Returns the spec verbatim so callers can `export default defineAgent({...})`
1808
+ * and the wrapper can read the same object back through dynamic import.
1809
+ */
1810
+ declare function defineAgent<T extends Record<string, z.ZodType>>(spec: AgentSpecParam<T>): {
1811
+ [K in keyof T]: HandlerBlock<T[K]>;
1812
+ };
1813
+
1814
+ interface InvocationContext {
1815
+ /** The envelope's `invocationId` — surfaced on log lines as `trace_id`. */
1816
+ readonly invocationId: string;
1817
+ /** The run this invoke belongs to — surfaced on log lines as `run_id`. */
1818
+ readonly runId: string;
1819
+ /**
1820
+ * The handler this invoke routed to — `invoke` for a direct entry or the
1821
+ * named job handler (e.g. `demo-log-job`) the dispatcher delivered to.
1822
+ * Surfaced on log lines as `handler` so the Studio logs trail can tell which
1823
+ * entrypoint emitted each line. Optional so a caller that binds a context
1824
+ * without routing info (older tests, lifecycle hooks) still type-checks.
1825
+ */
1826
+ readonly handler?: string;
1827
+ }
1828
+ /**
1829
+ * Runs `fn` with `context` bound as the active invocation. The context stays
1830
+ * available to every synchronous and asynchronous continuation rooted in this
1831
+ * call (i.e. across `await`s inside `fn`). Returns whatever `fn` returns.
1832
+ */
1833
+ declare function runWithInvocationContext<T>(context: InvocationContext, fn: () => T): T;
1834
+ /**
1835
+ * Returns the active invocation context, or `undefined` when called outside any
1836
+ * invoke (process boot, lifecycle events, tests that don't set one up).
1837
+ */
1838
+ declare function getInvocationContext(): InvocationContext | undefined;
1839
+
1840
+ /** The subset of the global `console` this bridge replaces. */
1841
+ interface ConsoleLike {
1842
+ log: (...args: unknown[]) => void;
1843
+ info: (...args: unknown[]) => void;
1844
+ warn: (...args: unknown[]) => void;
1845
+ error: (...args: unknown[]) => void;
1846
+ debug: (...args: unknown[]) => void;
1847
+ }
1848
+ interface InstallConsoleCaptureOptions {
1849
+ /** Console object to patch. Defaults to the global `console`. Test seam. */
1850
+ console?: ConsoleLike;
1851
+ /** Sink for `log` / `info` / `debug`. Defaults to `process.stdout`. Test seam. */
1852
+ stdout?: LogSink;
1853
+ /** Sink for `warn` / `error`. Defaults to `process.stderr`. Test seam. */
1854
+ stderr?: LogSink;
1855
+ /** Clock for the `ts` field. Defaults to `() => new Date()`. Test seam. */
1856
+ now?: () => Date;
1857
+ }
1858
+ /**
1859
+ * Replaces `console.log/info/debug/warn/error` so that, while an invocation
1860
+ * context is active, each call is re-emitted as a structured log line carrying
1861
+ * `run_id` / `trace_id`. Returns a function that restores the originals.
1862
+ * Calling it twice on the same console is a no-op (returns a no-op restore).
1863
+ */
1864
+ declare function installInvocationConsoleCapture(options?: InstallConsoleCaptureOptions): () => void;
1865
+
1866
+ declare const STORED_TO_SDK_ENV: Readonly<Record<string, string>>;
1867
+ /** Encrypted envelope row as stored in `stackbone_platform.secrets`. */
1868
+ interface SystemSecretRow {
1869
+ name: string;
1870
+ version: string;
1871
+ nonce: Buffer | Uint8Array;
1872
+ ciphertext: Buffer | Uint8Array;
1873
+ }
1874
+ /**
1875
+ * Maps the `is_system` rows to the SDK env names, decrypting ONLY the rows that
1876
+ * have a known mapping, then derives two env-only channels. Existing values are
1877
+ * never overwritten (an explicit container/Machine override always wins). The
1878
+ * SDK env names this call populated are returned (sorted) so the caller can log
1879
+ * an observable, value-free "what got rehydrated" line — values are never
1880
+ * logged. Shared by both the cloud neon reader and the postgres-js reader so
1881
+ * they cannot drift on the mapping.
1882
+ *
1883
+ * Decryption is lazy on purpose: an unmapped stored row is skipped WITHOUT
1884
+ * decrypting it, so a non-envelope row (e.g. a probe row a stubbed query
1885
+ * returns) never throws. The two derived channels:
1886
+ * - `STACKBONE_POSTGRES_URL` from `databaseUrl` (the SDK's `client.database`
1887
+ * reads this name; the cloud harness's own Neon driver keeps `DATABASE_URL`).
1888
+ * - `STACKBONE_APPROVAL_SIGNING_KEY` from `HMAC_SECRET` so the SDK's approval
1889
+ * callback verifier reconciles with the signer (which signs with the same
1890
+ * boot-bundle HMAC).
1891
+ */
1892
+ declare function rehydrateSystemSecretsRows(env: NodeJS.ProcessEnv, rows: readonly SystemSecretRow[], cipher: SecretCipher, databaseUrl: string): string[];
1893
+ /**
1894
+ * Reads the `is_system` rows from `stackbone_platform.secrets`. Injectable so
1895
+ * tests run against a stub without a real Postgres; production opens a
1896
+ * short-lived `postgres-js` connection (see `loadSystemSecretsIntoEnv`).
1897
+ */
1898
+ type SystemSecretsReader = () => Promise<SystemSecretRow[]>;
1899
+ interface LoadSystemSecretsArgs {
1900
+ /** Connection string for the agent's Postgres (`STACKBONE_POSTGRES_URL`). */
1901
+ readonly databaseUrl: string;
1902
+ /** Per-agent base64 key the rows are decrypted with (`STACKBONE_SECRET_KEY`). */
1903
+ readonly secretKey: string;
1904
+ /** Target env to populate. Defaults to `process.env`. */
1905
+ readonly env?: NodeJS.ProcessEnv;
1906
+ /** Override the row reader (tests inject a stub). */
1907
+ readonly reader?: SystemSecretsReader;
1908
+ }
1909
+ /**
1910
+ * Reads every `is_system` row from the agent's Postgres via `postgres-js`,
1911
+ * decrypts each with the per-agent key, and rehydrates them into `env` under
1912
+ * the SDK's expected names. Used by the `stackbone dev` emulator and the
1913
+ * self-host runtime entry to give the agent the same credentials the cloud
1914
+ * harness rehydrates — without depending on a Neon HTTP endpoint, which local
1915
+ * dev does not have.
1916
+ */
1917
+ declare function loadSystemSecretsIntoEnv(args: LoadSystemSecretsArgs): Promise<string[]>;
1918
+
1919
+ /**
1920
+ * Reserved error codes. Order is intentional: keep them flat (string) so
1921
+ * the envelope JSON stays trivial to inspect.
1922
+ *
1923
+ * F2 owns `VALIDATION_ERROR` and `HANDLER_ERROR`. `TIMEOUT_ERROR` is
1924
+ * declared here so the collision check is stable across slices — the F5
1925
+ * timeout slice will start emitting it without needing to amend the
1926
+ * envelope contract.
1927
+ */
1928
+ declare const RESERVED_ERROR_CODES: readonly ["VALIDATION_ERROR", "HANDLER_ERROR", "TIMEOUT_ERROR"];
1929
+ type ReservedErrorCode = (typeof RESERVED_ERROR_CODES)[number];
1930
+ /**
1931
+ * HTTP header carrying `invocationId` so the dispatcher can correlate
1932
+ * requests without parsing the JSON body.
1933
+ */
1934
+ declare const INVOCATION_ID_HEADER = "X-Stackbone-Invocation-Id";
1935
+ /**
1936
+ * HTTP header carrying `runId` so the dispatcher can correlate requests
1937
+ * across approval callbacks, log lookups, and trace spans without parsing
1938
+ * the JSON body.
1939
+ */
1940
+ declare const RUN_ID_HEADER = "X-Stackbone-Run-Id";
1941
+ /**
1942
+ * Envelope a client (the platform dispatcher, the local emulator, or a
1943
+ * curl smoke test) sends to `POST /invoke`. `payload` is unknown at this
1944
+ * layer — the wrapper revalidates it against `spec.invoke.input` before
1945
+ * handing it to the creator's `run`.
1946
+ */
1947
+ declare const invokeRequestSchema: z.ZodObject<{
1948
+ invocationId: z.ZodString;
1949
+ runId: z.ZodString;
1950
+ payload: z.ZodUnknown;
1951
+ }, z.core.$strip>;
1952
+ type InvokeRequest = z.infer<typeof invokeRequestSchema>;
1953
+ /**
1954
+ * Error portion of a failure envelope. `issues` mirrors Zod's `ZodIssue[]`
1955
+ * shape verbatim so the dispatcher can render the same field-level paths
1956
+ * an SDK consumer would see locally.
1957
+ */
1958
+ interface InvokeEnvelopeError {
1959
+ code: string;
1960
+ message: string;
1961
+ issues?: z.ZodIssue[];
1962
+ }
1963
+ /**
1964
+ * Success envelope returned by `/invoke`. `TResult` is left generic so the
1965
+ * wrapper can wrap the parsed output without paying for an extra type
1966
+ * assertion at the call site.
1967
+ */
1968
+ interface InvokeSuccessEnvelope<TResult = unknown> {
1969
+ invocationId: string;
1970
+ runId: string;
1971
+ result: TResult;
1972
+ }
1973
+ /**
1974
+ * Failure envelope returned by `/invoke`. Keeps `invocationId` and `runId`
1975
+ * at the top level so a client can correlate a 4xx/5xx with the request
1976
+ * without inspecting `error.issues`.
1977
+ */
1978
+ interface InvokeErrorEnvelope {
1979
+ invocationId: string;
1980
+ runId: string;
1981
+ error: InvokeEnvelopeError;
1982
+ }
1983
+ type InvokeResponse<TResult = unknown> = InvokeSuccessEnvelope<TResult> | InvokeErrorEnvelope;
1984
+ /**
1985
+ * True when `code` is one of the reserved tokens the wrapper owns. Used by
1986
+ * the duck-typed error path: a creator that throws an error tagged with a
1987
+ * reserved code falls back to `HANDLER_ERROR` so reserved codes always
1988
+ * mean what the platform expects.
1989
+ */
1990
+ declare const isReservedErrorCode: (code: string) => code is ReservedErrorCode;
1991
+
1992
+ /**
1993
+ * Which half of the contract the diagnostic belongs to. The wrapper's error
1994
+ * message names the half so the creator can find the offending field
1995
+ * immediately ("input.user.email", not just "user.email").
1996
+ */
1997
+ type SchemaHalf = 'input' | 'output';
1998
+ /**
1999
+ * Constructs the wrapper either rejects outright or warns about. Listed
2000
+ * verbatim so a future ADR can extend the union without changing the type
2001
+ * shape downstream consumers rely on.
2002
+ */
2003
+ type FatalConstruct = 'transform' | 'preprocess' | 'coerce';
2004
+ type WarnConstruct = 'refine' | 'lazy';
2005
+ interface SchemaDiagnostic<Construct extends string> {
2006
+ half: SchemaHalf;
2007
+ /** Dotted JSON pointer-ish path: `email`, `user.address.street`, `items[]`. */
2008
+ path: string;
2009
+ construct: Construct;
2010
+ }
2011
+ interface AgentSchemaPair {
2012
+ input: z.ZodType;
2013
+ output: z.ZodType;
2014
+ }
2015
+ interface SchemaIntrospectionResult {
2016
+ fatal: SchemaDiagnostic<FatalConstruct>[];
2017
+ warnings: SchemaDiagnostic<WarnConstruct>[];
2018
+ schemas: {
2019
+ input: JsonSchemaDocument;
2020
+ output: JsonSchemaDocument;
2021
+ };
2022
+ }
2023
+ /**
2024
+ * JSON Schema 2020-12 document. We intentionally keep this loose — Zod's
2025
+ * `toJSONSchema` returns an arbitrary structure and the wrapper only needs
2026
+ * to serialise it verbatim to the catalog. A future ADR can tighten this
2027
+ * to a generated type once the catalog has a real consumer.
2028
+ */
2029
+ interface JsonSchemaDocument {
2030
+ $schema?: string;
2031
+ type?: string | string[];
2032
+ properties?: Record<string, JsonSchemaDocument>;
2033
+ required?: string[];
2034
+ [key: string]: unknown;
2035
+ }
2036
+ /**
2037
+ * Walks the agent's input/output schemas, classifies offending constructs,
2038
+ * and emits the JSON Schema 2020-12 documents the wrapper serves from
2039
+ * `GET /schema`. The function never throws — the caller decides what to do
2040
+ * with the fatal list.
2041
+ *
2042
+ * Derivation runs through Zod's native `z.toJSONSchema`. We pass
2043
+ * `unrepresentable: 'any'` so a `.refine()`-bearing string still surfaces as
2044
+ * a string (the catalog drops the constraint, the wrapper still runs it at
2045
+ * invoke time).
2046
+ */
2047
+ declare const analyzeAgentSchemas: (pair: AgentSchemaPair) => SchemaIntrospectionResult;
2048
+
2049
+ export { type AddMemoryRequest, type AgentSchemaPair, type AgentSpec, type AnyCapability, type ApprovalListOptions, type ApprovalListResult, type ApprovalRecord, type ApprovalRequest, type ApprovalRequestOptions, type ApprovalStatus, type ApprovalTool, type ApprovalToolSpec, type ApprovalTopic, type ApproverInfo, type ClientConfig, type CompilePromptResult, type ConsoleLike, type CreatePromptRequest, type Decision, type DecisionStatus, type DeleteAllMemoryRequest, type DeletePromptOptions, type DeletePromptResult, type EndSessionOptions, type EndSessionResult, type FatalConstruct, type GeneratedImage, type GetPromptOptions, INVOCATION_ID_HEADER, INVOKE_TIMEOUT_HARD_CAP_MS, type ImageGenerateParams, type ImagesResponse, type IngestAsyncHandle, type InstallConsoleCaptureOptions, type InvocationContext, type InvokeCapability, type InvokeContext, type InvokeEnvelopeError, type InvokeErrorEnvelope, type InvokeRequest, type InvokeResponse, type InvokeSuccessEnvelope, type JobCapability, type JsonSchemaDocument, type ListMemoryRequest, type ListMemoryResult, type ListOptions, type ListPromptsOptions, type ListPromptsResult, type LoadSystemSecretsArgs, type LogSink, type Logger, type LoggerBindings, type MemoryContent, type MemoryHistoryEntry, type MemoryHistoryEvent, type MemoryHit, type MemoryItem, type MemoryScope, type ModelsListResponse, type OpenRouterModel, type Prompt, type PublishRequest, RESERVED_ERROR_CODES, RESERVED_HANDLER_NAMES, RUN_ID_HEADER, type ReservedErrorCode, type Result, STORED_TO_SDK_ENV, type ScheduleRequest, type SchemaDiagnostic, type SchemaHalf, type SchemaIntrospectionResult, type SdkError, type SdkErrorCode, type SearchMemoryOptions, type SignedUrl, type SignedUrlOptions, StackboneClient, type StorageBody, type StorageObject, type StructuredLoggerOptions, type SystemSecretRow, type SystemSecretsReader, type UnscheduleRequest, type UpdateMemoryOptions, type UpdatePromptOptions, type UploadOptions, type VerifyOptions, type WarnConstruct, analyzeAgentSchemas, createClient, createStructuredLogger, defineAgent, getInvocationContext, installInvocationConsoleCapture, invokeRequestSchema, isReservedErrorCode, isSdkErrorCode, loadSystemSecretsIntoEnv, rehydrateSystemSecretsRows, runWithInvocationContext };