REF · STACKBONE / WIKI · v0.9.4 stackbone

@stackbone/sdk overview

Official TypeScript SDK for Stackbone — every published agent depends on it.

@stackbone/sdk overview

The TypeScript SDK every published Stackbone agent depends on. One client, lazy modules, a uniform Result<T> envelope, and a protocol-level handshake that fails fast when the agent and the datapath drift apart.

Install

pnpm add @stackbone/sdk

The package is a thin convenience layer over the partner SDKs the platform provisions for you (postgres-js + Drizzle, @aws-sdk/*, the openai SDK pointed at OpenRouter, …). You should never have to add those partner libraries to your package.json directly — the SDK's barrel re-exports the symbols you need.

Create a client

createClient(config?) returns a StackboneClient. Every module is built lazily on first access, so createClient() itself is cheap and side-effect-free.

import { createClient } from '@stackbone/sdk';

const client = createClient();

// Drizzle handle, S3 client, OpenRouter pool, etc. are built only when
// you actually touch the corresponding module.
const rows = await client.database.select().from(leads);

config is fully optional. Each field falls back to a documented env var when omitted. The full shape lives in ClientConfig; the most common overrides are:

Config key Falls back to env Used by
agentJwt STACKBONE_AGENT_JWT Every facade HTTP call.
stackboneApiUrl STACKBONE_API_URL Contract handshake + every facade HTTP call.
agentId STACKBONE_AGENT_ID client.storage key prefix.
installationId STACKBONE_INSTALLATION_ID Sent as X-Stackbone-Installation-Id.
databaseUrl DATABASE_URL client.rag, observability exporter.
openrouterKey OPENROUTER_API_KEY client.ai.
openrouterBaseUrl OPENROUTER_BASE_URL client.ai. Defaults to OpenRouter cloud.
s3.{accessKeyId,…} AWS_ACCESS_KEY_ID, … client.storage.
approvalSigningKey STACKBONE_APPROVAL_SIGNING_KEY client.approval.verify.
protocolRequired agent.yaml.protocol.required Contract handshake floor (see below).

client.database reads STACKBONE_POSTGRES_URL and is intentionally decoupled from DATABASE_URL — see client.database → Connection lifecycle.

The modules at a glance

Module Purpose Required capability
client.database Drizzle handle bound to the agent's Postgres. database.postgres_direct
client.storage S3-compatible object storage with per-agent key prefixing. storage.s3
client.ai OpenAI-compatible chat, embeddings, image generation, model catalogue. ai.openrouter
client.rag Parse → chunk → embed → store → retrieve on top of client.database. rag.basic
client.approval Human-in-the-loop inbox with HMAC-signed callbacks and an LLM-tool wrapper. approval.fire_and_forget
client.secrets Read workspace-encrypted secrets registered in the dashboard. secrets.read_write
client.config Typed reads of dynamic per-agent config set in the dashboard. config.read_write
client.queues Publish to the platform's pgmq backed by STACKBONE_API_URL. queues.pgmq
client.events Emit named events into the agent's run timeline. events.bus
client.memory Long-term memory (mem0) — not gated on the contract handshake.
client.observability OTel exporter helpers and the RunStepsSpanProcessor.
client.connections, client.prompts Reserved surfaces. Currently return not_implemented.

Every method on every module returns a Result<T>:

import type { Result, SdkError } from '@stackbone/sdk';

type Result<T> = { data: T; error: null } | { data: null; error: SdkError };

Narrowing on result.error refines result.data to the success payload. The SDK never throws for expected failure modes — auth, validation, missing config, contract drift, partner errors all surface through error.code. The one deliberate exception is client.database: its query-builder verbs return Drizzle's native chainable types (typed rows, not envelopes), and a contract-gate failure throws a tagged Error instead of swallowing the signature.

The contract handshake

Every Stackbone datapath (the local emulator started by stackbone dev, and the cloud apps/api) exposes GET /api/contract returning the negotiated Stackbone Agent Protocol version and the set of capabilities it advertises:

{
  "version": 10,
  "minSupported": 1,
  "capabilities": [
    "database.postgres_direct",
    "rag.basic",
    "rag.async_ingest",
    "queues.pgmq",
    "events.bus",
    "secrets.read_write",
    "config.read_write",
    "approval.fire_and_forget",
    "storage.s3",
    "ai.openrouter"
  ],
  "build": { "name": "stackbone-cli", "version": "0.x.y" }
}

The SDK fires this handshake lazily on the first gated module call, caches the result for the process lifetime (per baseUrl), and reuses it for every subsequent call. Two concurrent first-callers share a single in-flight fetch (single-flight). You can inspect the last resolved contract synchronously through client.contract:

const c = client.contract;
if (c) {
  console.log('Speaking protocol v', c.version, 'against', c.build.name);
}

client.contract is null until at least one gated module call has resolved successfully. It never fetches and never throws.

Capability gating

Each module declares the single capability its surface depends on (see the table above). Before forwarding any call to its underlying implementation, the module awaits the handshake and asserts the capability is advertised. If it is not, the call short-circuits with one of two stable error codes:

  • contract_version_unsupported — the negotiated contract.version is below either MIN_SUPPORTED_CONTRACT_VERSION (the SDK's hard floor) or your agent's declared agent.yaml.protocol.required floor, whichever is higher.
  • capability_unavailable — the version check passes but the datapath does not advertise the capability the module needs.

Both errors carry actionable meta (detected, required, available, …). When the handshake itself cannot complete (network error, 404, malformed body) the call surfaces contract_unreachable or contract_malformed instead — these are always hard errors because the SDK genuinely cannot tell what it is talking to.

Pinning a minimum protocol version

If your agent depends on capabilities introduced after a specific contract version, pin the floor in agent.yaml:

protocol:
  required: 10

The CLI forwards this to the SDK as protocolRequired. The gate enforces the agent floor before the per-module capability check, so a stale datapath that happens to advertise the capability under an older protocol still fails closed.

Escape hatch — STACKBONE_REQUIRE_CONTRACT=0

For migrations and local debugging, set STACKBONE_REQUIRE_CONTRACT=0 to suppress the gate. The handshake still runs (client.contract is still populated) and the SDK still computes the gate, but capability/version errors are downgraded to a one-shot stderr warning per (baseUrl, capability) and the underlying call is allowed through. Reachability errors are never suppressed — the SDK can still not let the call through if it has no idea what the datapath is.

This flag is intended as a safety valve while bumping STACKBONE_CONTRACT_VERSION; production agents should leave it unset.

Tuning

Env var Default Meaning
STACKBONE_API_URL Base URL the handshake (and every facade) targets. Required.
STACKBONE_REQUIRE_CONTRACT 1 (gating on) Set to 0 to suppress capability/version errors (warning instead).
STACKBONE_CONTRACT_TTL_MS unset (process) Re-fetch the handshake after this many milliseconds. Default is no TTL.
STACKBONE_DEBUG unset Set to 1 to log a one-line handshake-resolved message per baseUrl.

stackbone dev --print-contract

The CLI also exposes the negotiated contract on demand without booting the agent:

stackbone dev --print-contract

…prints the GET /api/contract payload of the local emulator and exits. Handy for sanity-checking the version + capability set before debugging a capability_unavailable error.

Where to go next