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

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