# Theo — full documentation corpus
> Every page of https://docs.usetheo.dev inlined as plain text (llmstxt.org
> `llms-full.txt` convention). The curated index lives at /llms.txt. Generated
> from the MDX sources — code samples are verbatim; site-only React components
> were stripped. Pages: 982.
---
# Overview
Source: https://docs.usetheo.dev/paas
Build and ship apps and automations from a prompt.
**TheoCloud** is the managed runtime where your agent runs in production. From
your project to a live URL in **minutes** — with the code in your repo, the
stack you chose, and **no lock-in**.
A real app — code in your repo, running on real infrastructure — not a
generated preview.
## Install
```bash
curl -fsSL https://get.usetheo.dev | sh
```
The CLI auto-detects your shell and configures completions for `bash`, `zsh`,
and `fish`.
## Deploy
```bash
theo deploy
```
That is the entire workflow. The CLI builds your project, ships it to a
production environment, and hands you a live URL. The first deploy takes a few
minutes; subsequent deploys are seconds.
## What you get
## Quick start
```bash
# 1. Install the CLI
curl -fsSL https://get.usetheo.dev | sh
# 2. Authenticate
theo login
# 3. Initialize the project (one-time)
theo init
# 4. Deploy
theo deploy
```
That is the whole loop. The first run prints the live URL when the deploy
finishes; subsequent runs print the URL of the new version.
## How it fits the ecosystem
| You have | You run | You get |
|---|---|---|
| A blank repo | `npx create-theokit my-app` (TheoKit) | A working agent project |
| A project | `theo deploy` (TheoCloud) | A live URL |
| A live agent | Switch models, watch every call, keep state | Production at scale |
TheoCloud is the commercial managed runtime. The framework (TheoKit) and the
open-source primitives — including TheoKit-SDK and TheoUI — are Apache-2.0 and
run on any infrastructure. TheoCloud is opt-in: pay to skip operations, or
self-host with a commercial license.
## What `theo deploy` actually does
You give it a project. It gives you back a live URL. Between those two events
it builds your code, ships an immutable image, runs health checks, and routes
traffic with automatic rollback if anything fails — all of it scriptable, all
of it auditable, none of it your problem.
## Where to go next
---
# Overview
Source: https://docs.usetheo.dev/theo-guardrails
Composable inline guardrails for AI agents.
**Theo Guardrails** is the inline, synchronous enforcement layer of Theo's trust
plane — a stateless, dependency-light library that inspects agent input and
output before it flows through and can **allow**, **block**, or **modify** it.
Guardrails compose into a sequential pipeline that short-circuits on the first
`block` and chains rewritten content through `modify` steps. It is **fail-closed**
by design: a guardrail that throws becomes a `block` unless explicitly marked
`failOpen`.
Pre-release (`0.1.0`). The package `@usetheo/trust-guardrails` is Apache-2.0 and
**not yet published to npm**. It ships the pipeline engine plus credential
masking, PII redaction, and shell-pattern blocking; the shared policy taxonomy is
a later milestone.
## Quick start
```ts title="guardrails.ts"
import {
createGuardrailPipeline,
createCredentialMaskingGuardrail,
createPiiGuardrail,
createShellPatternGuardrail,
type GuardrailEntry,
} from '@usetheo/trust-guardrails';
const entries: GuardrailEntry[] = [
{ guardrail: createShellPatternGuardrail(), options: { name: 'shell' } },
{ guardrail: createCredentialMaskingGuardrail(), options: { name: 'credentials' } },
{ guardrail: createPiiGuardrail({ strategy: 'redact' }), options: { name: 'pii' } },
];
const pipeline = createGuardrailPipeline(entries);
const result = await pipeline.run({
direction: 'input', // 'input' | 'output' | 'tool_input' | 'tool_output'
content: 'my key is sk-ant-XXXXXXXX and email a@b.com',
});
// result: { action: 'allow' | 'block' | 'modify', reason?, modified? }
if (result.action === 'block') reject(result.reason);
else if (result.action === 'modify') use(result.modified);
```
An empty pipeline allows; the first `block` short-circuits; a `modify` feeds its
rewritten content into the next guardrail; the caller's context is never mutated.
## Guardrails included
## Where to go next
---
# Overview
Source: https://docs.usetheo.dev/theo-knowledge
RAG with citations, on your own Postgres.
**Theo Knowledge** is the open-source RAG engine of the Theo ecosystem — drop
documents in, get **cited** answers out. It runs the whole pipeline
(`ingest → parse → chunk → embed → store → retrieve → answer`) on **your**
Postgres (with `pgvector`) and **your** LLM key: no proprietary vector database,
no vendor lock-in, Apache-2.0. Use it as a composable TypeScript library
(`@usetheo/rag`), a typed SDK, or a REST/MCP service — all validated by one Zod
contract.
Pre-release (`rc.60`). The core `ingest → retrieve → answer` flow is
feature-complete, but the package is **not yet on npm** — run it from source for
now. We don't claim it production-ready until backed by sustained usage evidence.
## Install
```bash
git clone https://github.com/usetheodev/theo-rag.git
cd theo-rag
pnpm install
```
Requires Node ≥ 20 and any Postgres with the `pgvector` extension. The core
library publishes as `@usetheo/rag` (with `@usetheo/rag-sdk` for the typed HTTP
client and `@usetheo/rag-mcp` for the MCP server).
## Quick start
Compose a pipeline from swappable stages and ask a question — the answer comes
back with the exact chunks it used:
```ts title="answer.ts"
import { Pool } from 'pg';
import { createUnpdfLoader } from '@usetheo/rag/loaders';
import { createTiktokenTokenizer, createRecursiveCharacterChunker } from '@usetheo/rag/chunkers';
import { createOpenAIEmbedder } from '@usetheo/rag/embedders';
import { createVectorRetriever } from '@usetheo/rag/retrievers';
import { createOpenAILlmProvider } from '@usetheo/rag/llm-providers';
import { createAnswerPipeline } from '@usetheo/rag/pipeline';
const pool = new Pool({ connectionString: process.env.THEORAG_PG_URI });
const embedder = createOpenAIEmbedder({ model: 'text-embedding-3-small' });
const pipeline = createAnswerPipeline({
pool,
loader: createUnpdfLoader(),
chunker: createRecursiveCharacterChunker({
tokenizer: createTiktokenTokenizer(),
chunkSize: 512,
chunkOverlap: 64,
}),
embedder,
retriever: createVectorRetriever({ pool, embedder }),
llmProvider: createOpenAILlmProvider({ apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o-mini' }),
});
const answer = await pipeline.answer('What is our refund window?');
// answer.citations → [{ document_id, chunk_id }]
```
Prefer HTTP? The `@usetheo/rag-sdk` `RagClient` mirrors the same surface
(`theo.answers.create(...)`, `theo.answers.stream(...)`), and the REST API
exposes `POST /v1/answers` returning `{ answer, citations, retrieved_chunks }`.
## What you get
## Where to go next
---
# Overview
Source: https://docs.usetheo.dev/theo-memory
Persistent memory for AI agents.
**Theo Memory** gives your agents long-term, scoped, searchable memory backed by
Postgres + `pgvector`. It exposes a four-verb API — **remember · recall · forget
· reflect** — across three tiers (`user` / `session` / `agent`), with hybrid
retrieval (semantic + BM25 + entity boost) and bi-temporal queries. It runs on
**your** Postgres and **your** LLM key (Apache-2.0), and is consumable as an SDK,
a REST API, an MCP server, or a CLI.
Published on npm as `@usetheo/memory` (`0.2.0`, pre-release). Feature-complete
for the core use case; the production-ready bar stays gated on sustained usage
evidence.
## Install
```bash
npm install @usetheo/memory
```
Requires Node ≥ 20 and a Postgres with `pgvector`. Ships two binaries —
`themory` (REST server + CLI) and `themory-mcp` (MCP server).
## Quick start
```ts title="memory.ts"
import { createLocalMemory } from '@usetheo/memory';
const memory = await createLocalMemory({ vectorStore, embedder, llm });
// Your agent remembers
await memory.user('alice').remember('Prefers dark mode. Speaks Portuguese.');
// Your agent recalls
const facts = await memory.user('alice').recall('UI preferences');
// → [{ text: 'Prefers dark mode', score: 0.94 }]
// Consolidate episodic → semantic
await memory.user('alice').reflect();
```
Run it as a service instead:
```bash
export THEOMEM_PG_URI=postgresql://themem:themem@localhost:5432/themem
npm run db:push # apply schema
npx themory server # REST API on :8080 (POST /v1/remember, /v1/recall, …)
```
## Core concepts
## Where to go next
---
# Overview
Source: https://docs.usetheo.dev/theo-prompts
Versioned prompts, resolved at runtime.
**Theo Prompts** manages your agent's prompts as **versioned, immutable
revisions** and resolves them at runtime — over REST, a TypeScript SDK, or MCP.
A prompt is identified by name and has N revisions; you point **environment
labels** (`prod`, `staging`) at a revision and **promote by moving the label**,
so the next resolution reflects the new prompt with **no redeploy**. All three
interfaces delegate to the same core (Postgres source of truth, optional Redis
cache), and `{{var}}` templating renders client-side so variable values never
leave your process.
Pre-1.0, in active development. The packages (`@usetheo/promptly-sdk`,
`@usetheo/promptly-api`, `@usetheo/promptly-mcp`) are **not yet published to
npm** — they run inside the workspace for now. API-key auth is required.
## Quick start
```ts title="prompts.ts"
import { PromptlyClient } from '@usetheo/promptly-sdk';
const client = new PromptlyClient({
baseUrl: 'http://localhost:3000',
apiKey: process.env.THEOPROMPTLY_API_KEY!, // required — blank throws at construction
cacheTtlSeconds: 60,
});
await client.createPrompt('greet', 'hi {{name}}'); // registers revision v1
await client.assignLabel('greet', 'prod', 1); // point prod → v1
const p = await client.getPrompt('greet', { label: 'prod', vars: { name: 'Theo' } });
p.content; // 'hi Theo' — rendered SDK-side; the value never reaches the server
// Later: promote to v2 with no app rebuild
await client.assignLabel('greet', 'prod', 2);
```
The REST API exposes the same surface (`POST /prompts`,
`GET /prompts/:name?label=prod`, `PUT /prompts/:name/labels/:label`,
`POST /prompts/:name/rollback`, `GET /openapi.json`), and the MCP server exposes
`get_prompt`, `list_revisions`, and `list_labels` as agent tools.
## Core concepts
## Where to go next
---
# Overview
Source: https://docs.usetheo.dev/theo-traces
OpenTelemetry-native trace explorer for AI agents.
**Theo Traces** is an OpenTelemetry-native trace explorer for AI agents — "Jaeger
for agents." Instrument a TypeScript/Node agent in a few lines and it emits OTLP
`gen_ai` spans to an API that persists them to Postgres and serves a React trace
explorer with session replay, cost, and tool analytics. Spans are the source of
truth; traces and sessions are aggregates recomputed from them.
Pre-1.0 (`0.0.0`). Apache-2.0 and Docker-first. The SDK `@usetheo/lens-sdk` is
**not yet published to npm** — run the stack with Docker and install the SDK from
a local tarball for now. Not claimed production-ready.
## Run the stack
```bash
git clone https://github.com/usetheodev/theo-lens.git
cd theo-lens
docker compose up -d # Postgres + API + UI on http://localhost:4318
```
## Instrument your agent
```ts title="agent.ts"
import { initTheoLens, startAgent, recordToolCall } from '@usetheo/lens-sdk';
const theo = initTheoLens({ endpoint: 'http://localhost:4318', serviceName: 'checkout-agent' });
await startAgent(theo.tracer, 'checkout', async (span) => {
span.setModel('gpt-4o').setProvider('openai').setUsage({ inputTokens: 12, outputTokens: 8 });
recordToolCall(span, 'search-products', { query: 'running shoes' });
});
await theo.shutdown();
```
Instrumentation is **fail-open**: a telemetry-backend problem never crashes the
instrumented app, and `shutdown()` resolves even if the flush fails. Refresh
`http://localhost:4318` to see the trace.
## What you get
## Where to go next
---
# Overview
Source: https://docs.usetheo.dev/theodb
AI + vector search as SQL, inside Postgres.
**TheoDB** is a PostgreSQL 17 distribution that puts AI — embeddings, generation,
ranking, natural-language queries — and vector search into the database as plain
SQL functions. It is **not** a fork or a new engine: it composes upstream
PostgreSQL with a curated set of extensions (a customized `pgvector`,
`pgvectorscale`) plus its own Rust extension, so one `CREATE EXTENSION` exposes an
`ai.*` / `theodb.*` surface. You generate embeddings, run vector + hybrid search,
and call an LLM directly in SQL — against the same transactional rows as your
operational data, with **no ETL to a separate vector store**.
TheoDB is a **commercial** product, pre-1.0 and in active development. It is
available via **early access** — request access at
[usetheo.dev/theodb](https://usetheo.dev/theodb). No production-ready claim is
made yet, and TheoDB makes no speed/throughput superiority claim over `pgvector`
or ScaNN (parity at best on the recall × QPS frontier).
## The AI surface, in SQL
Once the extension is enabled, AI is just SQL. The database ships no model and
stores no keys — you point it at any OpenAI-compatible endpoint via session
settings.
```sql
CREATE EXTENSION IF NOT EXISTS theodb CASCADE; -- pulls vector + vectorscale
-- Embeddings
SELECT theodb.embed('running shoes for trail'); -- → vector
-- Generation / classification, per row
SELECT ai.summarize(description) AS gist,
ai.analyze_sentiment(review) AS mood
FROM products;
```
The unified query is the point — vector search, a relational JOIN, and an AI call
in **one** transaction:
```sql
SELECT p.id, p.description,
ai.summarize(p.description) AS gist -- AI leg
FROM products p
JOIN inventory i ON i.product_id = p.id -- relational JOIN
WHERE i.in_stock AND p.category_id = 3 -- relational filter
ORDER BY p.embedding <=> '[0.1, 0.2, ...]'::vector -- vector leg
LIMIT 5;
```
## Capabilities
Columnar / HTAP is on the roadmap (decided and benchmarked), not yet in the
shipped extension surface. AI calls are synchronous per row (one HTTP round-trip)
— plan for cost and latency accordingly.
## Where to go next
---
# Advanced
Source: https://docs.usetheo.dev/theokit/a2a/advanced
The MessageBus routing API, request timeouts, the A2AMessage shape and provenance, and how a2a relates to subagents and handoffs.
# Advanced A2A
Verified against `@theokit/sdk/a2a`.
## `MessageBus` — the router
`AgentMailbox` is a convenience wrapper; the bus is the primitive:
```ts
import { MessageBus } from "@theokit/sdk/a2a";
const bus = new MessageBus();
bus.register("worker", async (msg) => (msg.type === "ping" ? "pong" : undefined));
bus.unregister("worker");
await bus.send("supervisor", "worker", { type: "note", payload: "hi" }); // fire-and-forget
const reply = await bus.request("supervisor", "worker", { type: "ping", payload: null }); // → "pong"
```
Sending or requesting an **unregistered** agent throws (`Agent "" not registered on MessageBus`).
## Request timeouts — `RequestOptions`
`request(..., { timeoutMs })` rejects if the target doesn't reply in time:
```ts
await mailbox.request("worker", { type: "translate", payload: "…" }, { timeoutMs: 2000 });
// throws: "A2A request timeout: worker did not respond within 2000ms"
```
Fire-and-forget `send` never waits; only `request` has a timeout.
## The message — `A2AMessage`
```ts
interface A2AMessage {
type: string;
payload: T;
from: string;
to: string;
origin: MessageOrigin; // { kind: "peer", from } — provenance
}
```
`origin` is a thin projection of `from` (not a parallel system). A handler that turns a message into
an agent turn forwards it via `SendOptions.origin`, so `RunResult.origin` attributes the turn to the
peer that triggered it.
## Where a2a sits
`a2a` is the **substrate**; the higher-level patterns build on it:
- [Subagents](/theokit/subagents) — a supervisor delegates to a specialist and gets the answer back.
- [Handoffs](/theokit/handoffs) — one agent transfers control to another.
- [Squad](/theokit/squad) — a fixed sequential pipeline.
Reach for `MessageBus` / `AgentMailbox` directly when you need free-form peer messaging that those
patterns don't model — e.g. long-lived agents exchanging events.
## Reference
- [`MessageBus`](/theokit/reference/MessageBus) · [`AgentMailbox`](/theokit/reference/AgentMailbox) · [`A2AMessage`](/theokit/reference/A2AMessage) · [`MessageHandler`](/theokit/reference/MessageHandler) · [`RequestOptions`](/theokit/reference/RequestOptions)
---
# Overview
Source: https://docs.usetheo.dev/theokit/a2a
Agent-to-agent messaging — a mailbox and message bus so agents send each other work and carry provenance.
# A2A (agent-to-agent)
`@theokit/sdk/a2a` lets agents address and message each other directly — the substrate under
multi-agent patterns (a supervisor routing to workers, peers collaborating).
- **`AgentMailbox`** — each agent's inbox; other agents send `A2AMessage`s to it.
- **`MessageBus`** — routes messages between mailboxes (`RequestOptions` for request/response).
- **`MessageHandler`** — how an agent reacts to an incoming message.
- **Provenance** — turns triggered by a peer carry an `origin` (`{ kind: "peer", from }`), so you can
attribute and route by who triggered them.
For the higher-level patterns built on this, see [Subagents](/theokit/subagents) and
[Handoffs](/theokit/handoffs).
## Next
- [Message another agent](/theokit/a2a/message-another-agent) — a runnable mailbox example.
- [Advanced](/theokit/a2a/advanced) — the bus API, request timeouts, and the message + provenance shape.
## Reference
- [`AgentMailbox`](/theokit/reference/AgentMailbox) · [`MessageBus`](/theokit/reference/MessageBus) · [`A2AMessage`](/theokit/reference/A2AMessage)
---
# Message another agent
Source: https://docs.usetheo.dev/theokit/a2a/message-another-agent
Register two mailboxes on a MessageBus and send fire-and-forget + request/response messages between them — deterministic, no LLM.
# Message another agent
`MessageBus` routes messages between agents by id; `AgentMailbox` wraps it with a per-agent
`send` / `request` / `onMessage` API. The whole exchange is in-process — deterministic, no LLM.
```ts title="run.ts"
import { AgentMailbox, MessageBus } from "@theokit/sdk/a2a";
const bus = new MessageBus();
// Worker: replies to "translate" requests.
const worker = new AgentMailbox("worker", bus);
worker.onMessage((msg) => {
if (msg.type === "translate") return `[fr] ${String(msg.payload)}`;
});
// Supervisor: fire-and-forget, then request a reply.
const supervisor = new AgentMailbox("supervisor", bus);
await supervisor.send("worker", { type: "note", payload: "starting" });
const reply = await supervisor.request("worker", { type: "translate", payload: "good morning" });
console.log("reply:", reply);
worker.dispose();
supervisor.dispose();
```
## Output
Deterministic — the worker's handler return value is the request's reply:
```text
reply: [fr] good morning
```
## What it shows
- **`new AgentMailbox(id, bus)`** registers the agent on the bus; **`onMessage(handler)`** sets how it
reacts to incoming `A2AMessage`s.
- **`send(to, { type, payload })`** is fire-and-forget; **`request(to, …)`** resolves to the handler's
return value (with an optional timeout via `RequestOptions`).
- Each message carries **provenance** — `origin: { kind: "peer", from }` — so a receiver that turns a
message into a turn can forward it via `SendOptions.origin`.
## Example
Full runnable source:
[`examples/a2a-mailbox`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/a2a-mailbox).
---
# Advanced
Source: https://docs.usetheo.dev/theokit/acp/advanced
The full AcpServerOptions — permission mode and timeout, trusted tools, prompt-size cap, the initialize handshake, logging, and test seams.
# Advanced ACP
Verified against `@theokit/acp` (`serveAcp`, `AcpServerOptions`).
## `AcpServerOptions`
```ts
await serveAcp({
agent, // required — SDKAgent | (sessionId) => SDKAgent
info: { name, version }, // advertised in the ACP `initialize` handshake
capabilities: { /* … */ }, // capability overrides for the handshake
permissionDefault: "ask", // permission mode — default "ask"
permissionTimeoutMs: 60_000, // veto a tool call if the editor doesn't answer in time
trustedTools: ["read_file"], // tool names that bypass "ask"
maxPromptBytes: 2 * 1024 * 1024, // cap on total prompt bytes (default 2 MiB)
log: (msg) => process.stderr.write(`${msg}\n`), // logger (default stderr, per D359)
stdin, stdout, // override the streams (test seam)
});
```
## Permission round-trips
With `permissionDefault: "ask"`, every tool call sends a `requestPermission` to the editor and waits
for the user's answer. If the editor doesn't reply within `permissionTimeoutMs` (default 60s), the
call is **vetoed** (fail-safe). List tool names in `trustedTools` to run them without asking.
## Per-session isolation
Passing a **factory** (`(sessionId) => SDKAgent`) rather than a single agent gives each ACP session
its own agent instance (D351) — separate conversation history, memory, and tool state. This is the
recommended shape for a multi-tab editor.
## Prompt-size cap
`maxPromptBytes` (default 2 MiB) bounds the total prompt an editor may send. Exceeding it throws
`PromptTooLargeError` rather than forwarding an oversized request to the model.
## The handshake
`info` (name + version) and `capabilities` are advertised in the ACP `initialize` exchange — how the
editor discovers what your agent is and what it can do. Both default to the package metadata / sane
capabilities when omitted.
## Reference
- [`serveAcp`](/theokit/reference/serveAcp) · [`AcpServerOptions`](/theokit/reference/AcpServerOptions) · [`AgentFactory`](/theokit/reference/AgentFactory) · [`AcpAgentInfo`](/theokit/reference/AcpAgentInfo) · [`PromptTooLargeError`](/theokit/reference/PromptTooLargeError)
---
# Overview
Source: https://docs.usetheo.dev/theokit/acp
Serve an agent over the Agent Client Protocol (ACP) so an editor like Zed can drive it — a stdio JSON-RPC server with per-session agent isolation.
# ACP server
**ACP** (Agent Client Protocol) is the protocol editors like Zed speak to drive an agent. `serveAcp`
(from **`@theokit/acp`**) hosts your agent as an ACP server over stdio JSON-RPC — the editor becomes
the front end for your `@theokit/sdk` agent.
```ts
import { serveAcp } from "@theokit/acp";
import { Agent } from "@theokit/sdk";
await serveAcp({
// a single agent, or a factory that returns a fresh agent per session (isolation)
agent: (sessionId) => Agent.create({ apiKey, model, systemPrompt: "You are a coding agent." }),
});
```
- **`serveAcp({ agent, … })`** — runs the server; blocks on stdio, translating ACP requests into
`agent.send()` and streaming results back.
- **`agent`** — a single `SDKAgent` (shared) or a `(sessionId) => SDKAgent` factory (a fresh,
isolated agent per editor session — the recommended pattern).
- **Permissions** — every tool call can round-trip a `requestPermission` to the editor; the default
mode is `"ask"`.
## Next
- [Serve an ACP agent](/theokit/acp/serve-an-acp-agent) — the server entry point, and how an editor connects.
- [Advanced](/theokit/acp/advanced) — permission mode, trusted tools, prompt caps, and the handshake.
## Reference
- [`serveAcp`](/theokit/reference/serveAcp) · [`AcpServerOptions`](/theokit/reference/AcpServerOptions) · [`AgentFactory`](/theokit/reference/AgentFactory)
---
# Serve an ACP agent
Source: https://docs.usetheo.dev/theokit/acp/serve-an-acp-agent
Write a serveAcp entry point with a per-session agent factory, then point an editor at it over stdio.
# Serve an ACP agent
An ACP server is a stdio process an editor spawns. Give `serveAcp` a **factory** so each editor
session gets its own isolated agent, and it handles the ACP handshake, prompts, and tool-permission
round-trips.
```ts title="acp-server.ts"
import { serveAcp } from "@theokit/acp";
import { Agent, type SDKAgent } from "@theokit/sdk";
// A fresh agent per session — no shared state across editor tabs (D351).
async function createAgentForSession(sessionId: string): Promise {
return Agent.create({
apiKey: process.env.OPENROUTER_API_KEY,
model: { id: "openai/gpt-4o-mini" },
systemPrompt: "You are a coding agent.",
local: { cwd: process.cwd(), settingSources: ["project"] }, // pick up project skills/hooks/MCP
});
}
await serveAcp({
agent: createAgentForSession,
info: { name: "my-coding-agent", version: "1.0.0" },
permissionDefault: "ask", // tool calls prompt the editor for approval
trustedTools: ["read_file"], // …except these, which run without asking
});
```
Run it (`node acp-server.js` / `tsx acp-server.ts`) and register the command in your editor's ACP
agent settings — the editor spawns it and speaks ACP over its stdin/stdout.
## What it shows
- **A per-session factory** — `(sessionId) => Agent.create(...)` gives each editor session an isolated
agent, so concurrent sessions don't share conversation state.
- **`serveAcp` blocks** — it owns the process's stdio and runs until the editor disconnects.
- **Permissions flow to the editor** — with `permissionDefault: "ask"`, each tool call round-trips a
`requestPermission` the user approves in the editor; `trustedTools` skip the prompt.
ACP is a long-running stdio server driven by an editor, not a print-and-exit script — so it runs from
your own checkout (`@theokit/acp`), not the docs sandbox.
---
# ACP
Source: https://docs.usetheo.dev/theokit/acp
Drive an agent from an editor.
# ACP
The Agent Client Protocol adapter exposes an agent over stdio JSON-RPC, so editors like Zed, Cursor, and Claude Desktop can drive it.
| Capability | What it adds |
| --- | --- |
| `@theokit/acp` | stdio JSON-RPC server (`theokit-acp`) |
---
# Advanced
Source: https://docs.usetheo.dev/theokit/agents/advanced
The full agent surface — creation, running, continuation drivers, goal loops, forking, personalities, inspectors, lifecycle, and the cloud runtime.
# Advanced agents
The complete `Agent` (static namespace) and agent-handle (instance) surface. Most methods that touch
a whole capability (memory, subagents, guardrails…) have their own section; this page is the
exhaustive index and the agent-level controls that live nowhere else.
## Create, retrieve, manage — `Agent.*` (static)
| Call | What it does |
| --- | --- |
| `Agent.create(options)` | Create a fresh agent handle. |
| `Agent.getOrCreate(agentId, options)` | Idempotent — return the existing agent for that id, or create it. |
| `Agent.prompt(message, options)` | One-shot: create → send → dispose, returning the result. No persistent handle. |
| `Agent.builder()` | A fluent [`AgentBuilder`](/theokit/reference/AgentBuilder) for step-by-step configuration. |
| `Agent.resume(agentId, options?)` | Re-attach to a persisted agent/run and continue it. |
| `Agent.get(agentId, options?)` | Metadata for one agent (`SDKAgentInfo`). |
| `Agent.list(options?)` | Paginated list of agents. |
| `Agent.listRuns(agentId, options?)` / `Agent.getRun(runId, options?)` | Enumerate / fetch runs. |
| `Agent.archive(id)` · `Agent.unarchive(id)` · `Agent.delete(id)` | Lifecycle management. |
| `Agent.registry` | The live in-process agent registry. |
`Agent.get` / `Agent.resume` auto-detect runtime from the id prefix — `agent-…` local, `bc-…` cloud.
## Run
| Call | Returns |
| --- | --- |
| `agent.send(message, options?)` | A [`Run`](/theokit/reference/Run) — the core turn. |
| `agent.generate(message, { output })` | A validated, typed object (`{ object, result, raw, usage }`) — see [Structured output](/theokit/structured-output). |
| `Agent.generateObject(...)` / `Agent.streamObject(...)` | Static structured-output forms (buffered / streaming). |
| `Agent.batch(prompts, options)` | Run many prompts with bounded concurrency — see [Batch below](#batch). |
## Continuation drivers
A single `send()` can stop at the loop's iteration ceiling. These drive it to a genuine terminal
(`done` / `step_limit` / `no_progress`), re-prompting over the stateful session. **Local only** —
cloud agents throw `UnsupportedRunOperationError` (the cloud runtime manages continuation server-side).
| Call | Shape |
| --- | --- |
| `agent.runToCompletion(message, options?)` | Buffered — resolves to the final `RunToCompletionResult`. |
| `agent.streamToCompletion(message, options?)` | `AsyncGenerator` of `SDKMessage`s live; the result is the generator's return value. |
## Goal loops
- **`agent.runUntil(goal, options?)`** — an **ephemeral, per-call** goal-driven loop: `send → judge →
continuation` until an auxiliary judge model returns `done`, the judge fails too often, max turns
are exhausted, or you abort. Yields a `GoalEvent` per transition, returns a `GoalResult`. Local
only. Pass the goal on every call — a no-goal `runUntil()` pauses. See [Goals](/theokit/goals).
> The durable, thread-scoped objective surface (`setObjective` / `getObjective` /
> `updateObjectiveOptions` / `clearObjective`, `AgentOptions.goal`) was **removed in v4.0**.
## Fork a sub-agent
**`agent.fork(options)`** spins up a short-lived sub-agent with the parent's credentials + a
byte-identical system prompt (cache hit) and a restricted tool whitelist (AsyncLocalStorage
isolation). Local only. For richer delegation see [Subagents](/theokit/subagents) and
[Handoffs](/theokit/handoffs).
## Personalities
**`agent.usePersonality(name, opts?)`** activates a personality preset for the next `send`. Reserved
names `"none"` / `"default"` / `"neutral"` clear it. `{ save: true }` persists across restarts
(`$THEOKIT_HOME/personality.json`); `{ reset: true }` also clears the session. Returns the resolved
`PersonalityPreset` (name, systemPrompt, tools, model, tags, source). Local only.
## Inspectors (read-only, conditionally populated)
| Property | Populated when | Surfaces |
| --- | --- | --- |
| `agent.context` | `AgentOptions.context` enabled | The `SDKContextManager`. |
| `agent.providers` | ≥ 1 provider route configured | Provider routing inspector. |
| `agent.skills` | project skills / `skills.enabled` | `list()` / `get(name)` of skills. |
| `agent.plugins` | project plugins / `plugins.enabled` | `list()` of plugin metadata. |
| `agent.memory` | a memory adapter registered via `plugins` | Direct `write` / `recall` / `delete` (fans out + dedupes across adapters). |
## Lifecycle & cache
- **`agent.dispose()`** / **`await using`** (`[Symbol.asyncDispose]`) — async, idempotent resource release.
- **`agent.close()`** — fire-and-forget disposal.
- **`agent.reload()`** — re-read filesystem config (context, hooks, project MCP, subagents) without disposing.
- **`agent.invalidateCache(reason, { applyNow? })`** — signal a prompt-cache rebuild. Deferred to the
next `send` by default; a cache rebuild is a **cost regression** (the provider recharges full price),
so use it deliberately.
## Batch
`Agent.batch(prompts, options)` runs many prompts with **bounded concurrency** (a positive integer)
— the primitive behind fan-out jobs. Invalid concurrency throws a `ConfigurationError`.
## Cloud runtime (pre-release)
Cloud agents (id prefix `bc-`) add cloud-only surfaces a local agent does not:
`agent.listArtifacts()` (local returns `[]`), `agent.downloadArtifact(path)` (local throws
`UnsupportedRunOperationError`), plus `cloudPayload` / `autoCreatePR` / `envVars`. The cloud runtime
depends on **Theo PaaS**, currently pre-release — the local runtime is the primary tested path.
## Reference
- [`Agent`](/theokit/reference/Agent) · [`SDKAgent`](/theokit/reference/SDKAgent) · [`AgentOptions`](/theokit/reference/AgentOptions) · [`AgentFactory`](/theokit/reference/AgentFactory) · [`PersonalityPreset`](/theokit/reference/PersonalityPreset)
---
# Overview
Source: https://docs.usetheo.dev/theokit/agents
What an agent is, the parts it's made of, and the capabilities you attach to it.
# Agents
An **agent** is a model with instructions and tools that runs a task to completion.
You create one with `Agent.create()`, send it messages with `agent.send()`, and read or
stream the result. Everything else — memory, structured output, subagents, guardrails — is
a **capability you attach**, not a separate framework to learn.
## Anatomy of an agent
| Part | What it is | Configured via |
| --- | --- | --- |
| **Model** | the LLM and provider that reasons | `model` + `apiKey` |
| **Instructions** | the system prompt — static or resolved per send | `systemPrompt` |
| **Tools** | typed functions the model may call | `tools: [Tool.create(...)]` |
| **Memory** | durable facts recalled across sessions | `memory` |
| **Runtime** | where it executes — local (in-process) or cloud | `local` / a cloud key |
## What you can do
Each of these is a runnable how-to — open one and press **Run**:
| Task | Guide |
| --- | --- |
| Create an agent and read its reply | [Run an agent](/theokit/agents/run) |
| Consume the output as it arrives | [Stream a response](/theokit/streaming) |
| Get a validated, typed object back | [Return structured output](/theokit/structured-output) |
## Capabilities you attach
| Group | Capabilities |
| --- | --- |
| **Model & tools** | [Tools](/theokit/concepts/tools) · [Providers](/theokit/concepts/providers-bedrock-vertex) · Skills · Structured output |
| **Memory & context** | [Memory](/theokit/concepts/memory) · [Sessions](/theokit/concepts/sessions) |
| **Multi-agent** | Subagents · [Handoffs](/theokit/concepts/handoffs) · [Workflows](/theokit/concepts/workflows) |
| **Control & safety** | [Hooks](/theokit/concepts/hooks) · [Security](/theokit/concepts/security) · Guardrails |
| **Operations** | [Schedules](/theokit/concepts/cron) · [Evals](/theokit/concepts/eval) · [Observability](/theokit/concepts/telemetry) · [Budget](/theokit/concepts/budget) · [Cache](/theokit/concepts/cache) |
| **Connections** | [MCP](/theokit/concepts/mcp) · [Channels](/theokit/concepts/gateways) |
## Reference
- [`Agent`](/theokit/reference/Agent) — the factory and its options
- [`Run` / `RunResult`](/theokit/reference/Run) — the handle a send returns
- [`SDKMessage`](/theokit/reference/SDKMessage) — the streamed event union
---
# Run an agent
Source: https://docs.usetheo.dev/theokit/agents/run
Create a local agent, send it a message, and read the reply.
# Run an agent
Create an agent with `Agent.create()`, send it a message with `agent.send()`, and read
the reply from `run.wait()`. That is the whole loop.
```ts title="run.ts"
import { Agent } from "@theokit/sdk";
const apiKey = process.env.OPENROUTER_API_KEY;
if (apiKey === undefined || apiKey.length === 0) {
throw new Error("Set OPENROUTER_API_KEY (env or .env) — see https://openrouter.ai/keys");
}
const agent = await Agent.create({
apiKey,
model: { id: "openai/gpt-4o-mini" },
name: "explainer-bot",
systemPrompt: "You are a concise assistant. Answer in at most two sentences.",
// Local runtime, no sandbox — runs inline in this Node process.
local: { cwd: process.cwd(), sandboxOptions: { enabled: false } },
});
const run = await agent.send("What is an AI agent? Answer for a developer.");
const result = await run.wait();
console.log("Status:", result.status);
console.log("Model: ", result.model);
console.log("Reply: ", result.result);
if (result.status === "error") {
console.error("Error: ", JSON.stringify(result.error, null, 2));
}
await agent.dispose();
```
## Run
```bash
export OPENROUTER_API_KEY=sk-or-... # https://openrouter.ai/keys
pnpm install
pnpm run run
```
## Output
Verified against `openai/gpt-4o-mini` via OpenRouter:
```text
Status: finished
Model: { id: 'openai/gpt-4o-mini' }
Reply: An AI agent is a software component that perceives its environment (e.g., via data,
sensors, or APIs), makes decisions using models or algorithms (often machine-learning or
reasoning), and takes actions to achieve defined goals autonomously or semi-autonomously.
Developers integrate agents by wiring up input adapters, a decision engine, and output
handlers that execute the chosen actions.
```
_Sample of a real run — the model is non-deterministic, so your output will read differently. Click **Run** above for a fresh one._
## What it shows
- `Agent.create({ apiKey, model, name, systemPrompt })` — the canonical factory. The
runtime is chosen by the key you pass; `apiKey` + `local` selects the **local** runtime.
- `agent.send(message)` returns a `Run`; `await run.wait()` resolves to a `RunResult`.
- `RunResult` carries `result` (the text), `status` (`finished` | `error` | `cancelled`),
`model`, `usage`, `cost`, and a typed `error` (populated only on `status: "error"`).
- `agent.dispose()` releases the agent's resources. Prefer
`await using agent = await Agent.create(...)` when your TypeScript target supports it.
## Error handling
`run.wait()` does **not** throw on a provider error by default — it resolves with
`status: "error"` and a typed `error` object (e.g. an expired key surfaces as
`code: "auth_failed"`, HTTP 401). Inspect `result.status` before trusting `result.result`.
To make `send()` reject instead, pass `throwOnError: true` to `Agent.create`.
## Example
Full runnable source:
[`examples/agent-basics`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/agent-basics).
## Next
- [Stream a response](/theokit/streaming) — consume the run as it happens.
- [Return structured output](/theokit/structured-output) — get a typed object back.
---
# Advanced
Source: https://docs.usetheo.dev/theokit/cache/advanced
Wire the cache as an agent plugin, tune threshold + TTL with an exclude regex, persist to JSON, read stats, and distinguish it from provider prompt-cache invalidation.
# Advanced cache
Verified against `@theokit/sdk-cache`.
## Transparent caching — `asPlugin()`
The explicit `consult` / `remember` API is one way in; the other is to let the cache work
transparently on every `send()`:
```ts
import { Cache, createLexicalEmbedder } from "@theokit/sdk-cache";
const cache = Cache.semantic({ embedder: createLexicalEmbedder() });
const agent = await Agent.create({
apiKey, model,
plugins: [cache.asPlugin()], // caches responses on send(), serves hits automatically
});
```
`asPlugin()` is **memoized** — repeated calls return the same `Plugin`, so wiring it into multiple
agents shares one store.
## `Cache.semantic` options
```ts
Cache.semantic({
embedder, // REQUIRED — no autoselect (avoids surprise API calls)
threshold: 0.85, // cosine distance 0..2; LOWER = stricter. Default 0.85
ttl: { default: "1h" }, // TTL config (see below). Default { default: "1h" }
namespace: "global", // multi-tenant isolation. Default "global"
modelId: "gpt-4o-mini", // default modelId stamped on entries
maxEntries: 1000, // LRU eviction cap. Default 1000
persistence: { backend: "memory" }, // or { backend: "json", dir }
});
```
## TTL + never-cache exclude
```ts
ttl: {
default: "30m", // "1h" | "30m" | 86400 (seconds)
exclude: /weather|today|now/i, // queries matching this are NEVER cached
}
```
The `exclude` regex is the escape hatch for time-sensitive prompts — a "what's the weather now?" answer
should never be served stale.
## Persistence
```ts
persistence: { backend: "json", dir: "./.cache" } // survives restarts
```
`"memory"` (default) is process-local; `"json"` hydrates from `dir` on first lookup and persists
entries across runs.
## Stats + maintenance
```ts
cache.stats(); // { entries, kvHits, semanticHits, misses, excluded, evicted, embedderFailures }
cache.evictExpired(); // drop expired entries now; returns the count removed
await cache.clear(); // empty the store
```
## Not to be confused with provider prompt-cache — `agent.invalidateCache`
`@theokit/sdk-cache` caches **whole responses** by prompt similarity. Separately,
`agent.invalidateCache(reason, { applyNow? })` signals the *provider's* prompt-cache to rebuild — a
cost regression you trigger deliberately (e.g., after changing the system prompt). `applyNow: true`
disposes the agent immediately (recreate to continue); the default defers to the next `send()`. See
[Agents › Advanced](/theokit/agents/advanced).
## Reference
- [`Cache`](/theokit/reference/Cache) · [`CacheSemanticOptions`](/theokit/reference/CacheSemanticOptions) · [`CacheTTLConfig`](/theokit/reference/CacheTTLConfig) · [`CachePersistenceOptions`](/theokit/reference/CachePersistenceOptions) · [`CacheStats`](/theokit/reference/CacheStats)
---
# Cache similar prompts
Source: https://docs.usetheo.dev/theokit/cache/cache-similar-prompts
Store one answer and serve it for a similar prompt with Cache.semantic + the lexical embedder — no second LLM call. Deterministic.
# Cache similar prompts
`Cache.semantic` embeds prompts and serves a stored answer when a new prompt is *close enough*. Using
the dependency-free `createLexicalEmbedder`, the whole thing runs with **no network and no LLM** — an
exact prompt is a `kv` hit, a reworded one is a `semantic` hit, an unrelated one misses.
Cache lives in the companion package **`@theokit/sdk-cache`** — `npm i @theokit/sdk-cache`. (A release
compatible with `@theokit/sdk@3.0.0`'s SE36 API is pending, so this one runs from your own checkout
rather than the live sandbox.)
```ts title="run.ts"
import { Cache, createLexicalEmbedder } from "@theokit/sdk-cache";
const cache = Cache.semantic({
embedder: createLexicalEmbedder(), // dependency-free — no API calls
threshold: 0.5, // cosine distance; higher = more lenient match
});
await cache.remember("How do I deploy the app?", "Run the `ship` command.");
const exact = await cache.consult("How do I deploy the app?");
const similar = await cache.consult("How do I deploy my application?");
const miss = await cache.consult("What is the capital of France?");
console.log("exact: ", exact.hit ? `${exact.source} hit -> ${exact.response}` : "miss");
console.log("similar:", similar.hit ? `${similar.source} hit -> ${similar.response}` : "miss");
console.log("miss: ", miss.hit ? "hit" : "miss");
const s = cache.stats();
console.log(`stats: kvHits=${s.kvHits} semanticHits=${s.semanticHits} misses=${s.misses}`);
```
## Output
Deterministic — the lexical embedder is fully offline:
```text
exact: kv hit -> Run the `ship` command.
similar: semantic hit -> Run the `ship` command.
miss: miss
stats: kvHits=1 semanticHits=1 misses=1
```
## What it shows
- **`remember(prompt, response)`** feeds the cache; **`consult(prompt)`** looks it up and returns
`{ hit, source: "kv" | "semantic", distance? }` or `{ hit: false }`.
- **`kv` vs `semantic`** — an identical prompt is a keyed hit; a reworded prompt within `threshold`
cosine distance is a semantic hit and returns the *same* stored answer.
- **`stats()`** breaks hits down by kind (`kvHits`, `semanticHits`, `misses`) so you can measure the
cache's real payoff.
## Example
Full runnable source:
[`examples/cache-semantic`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/cache-semantic).
---
# Overview
Source: https://docs.usetheo.dev/theokit/cache
A semantic LLM response cache — serve a cached answer when a new prompt is close enough, cutting cost and latency.
# Cache
`@theokit/sdk-cache` is a **semantic** response cache: it embeds prompts and serves a stored answer
when a new prompt is similar enough, instead of paying for another provider call.
```ts
import { Cache } from "@theokit/sdk-cache";
const cache = Cache.semantic({ /* CacheSemanticOptions */ });
await Agent.create({ /* … */, plugins: [cache.asPlugin()] });
```
- **`Cache.semantic(options)`** — build the cache (`embedder` is required; tune `threshold`, `ttl`,
`namespace`, `maxEntries`, `persistence`).
- **`cache.asPlugin()`** — a `Plugin` you add to `Agent.create({ plugins })`; caching then happens
transparently on `send()`.
- **`cache.consult(prompt)` / `cache.remember(prompt, response)`** — the explicit API: look up or feed
the cache by hand (no agent required).
- **`createLexicalEmbedder()`** — a dependency-free lexical embedder for the similarity match (no API
calls). Swap in a real embedding provider for production-grade semantics.
- **`cache.stats()`** — hit/miss counters (`CacheStats`: `kvHits`, `semanticHits`, `misses`, …).
- **`agent.invalidateCache(reason, { applyNow? })`** — signal a *provider* prompt-cache rebuild (a cost
regression — use deliberately; see [Agents › Advanced](/theokit/agents/advanced)).
Cache ships as a separate package, **`@theokit/sdk-cache`**, and its factory is `Cache.semantic(...)`
(the embedding strategy is the constructor). Install it alongside `@theokit/sdk`.
## Next
- [Cache similar prompts](/theokit/cache/cache-similar-prompts) — a runnable `consult`/`remember` example.
- [Advanced](/theokit/cache/advanced) — the plugin path, threshold + TTL tuning, persistence, and stats.
## Reference
- [`Cache`](/theokit/reference/Cache) · [`CacheSemanticOptions`](/theokit/reference/CacheSemanticOptions) · [`CacheStats`](/theokit/reference/CacheStats) · [`createLexicalEmbedder`](/theokit/reference/createLexicalEmbedder)
---
# Advanced
Source: https://docs.usetheo.dev/theokit/cloud/advanced
The full CloudOptions surface, cloud-only agent operations, the secret-handling boundary, and the typed cloud client.
# Advanced cloud
Verified against `@theokit/sdk` (`types/agent.ts`, cloud config serializer). The cloud runtime is
**pre-release** — this documents the contract.
## `CloudOptions`
```ts
cloud: {
env: { type: "cloud" | "pool" | "machine", name?: string }, // which cloud environment runs the agent
repos: [{ url, startingRef?, prUrl? }], // repositories the agent works on
workOnCurrentBranch: false, // commit on the current branch vs a fresh one
autoCreatePR: true, // open a PR when the agent finishes
skipReviewerRequest: false, // don't auto-request a reviewer on the PR
envVars: { API_TOKEN: "…" }, // short-lived, agent-scoped secrets (names must not start with THEOKIT_)
}
```
## Cloud-only operations
A **local** agent does not have these — they throw `UnsupportedRunOperationError`:
- **`agent.listArtifacts()` / `agent.downloadArtifact(path)`** — files a run produced in the cloud
workspace.
- **`autoCreatePR`** + `git` metadata on the result (branch, commit, PR url).
- **`cloud.envVars`** — encrypted at rest, scoped to the agent, deleted with it.
Conversely, some surfaces are **local-only** and throw on cloud (e.g. `usePersonality`, `fork`,
`runUntil`, `runToCompletion`) — the cloud runtime manages those server-side.
## Secret handling
The `cloudPayload` you can inspect is **redacted by design** — it never carries `envVars` or the raw
`env` selector. Those are transmitted **separately over TLS at create time**, encrypted at rest, and
deleted when the agent is removed. Never put a secret anywhere it would land in the per-run payload.
## The typed client — `@theokit/sdk/client`
`TheoKitClient` talks to the cloud runtime over the network with the same `send` contract:
```ts
import { TheoKitClient } from "@theokit/sdk/client";
const client = new TheoKitClient({ /* ClientOptions: baseUrl, apiKey, … */ });
const res = await client.send(agentId, message); // SendResponse
for await (const ev of client.stream(agentId, message)) { /* StreamEvent */ }
```
Same discriminated `SDKMessage` / `StreamEvent` surface as the local runtime — the network is the only
difference. Enabled when Theo PaaS ships.
## Runtime detection
`Agent.get(id)` / `Agent.resume(id)` read the id prefix — `bc-` → cloud, `agent-` → local — and route
automatically. `Agent.list({ runtime: "cloud" })` scopes a listing to the cloud registry.
## Reference
- [`CloudOptions`](/theokit/reference/CloudOptions) · [`CloudEnv`](/theokit/reference/CloudEnv) · [`CloudRepo`](/theokit/reference/CloudRepo) · [`TheoKitClient`](/theokit/reference/TheoKitClient) · [`ClientOptions`](/theokit/reference/ClientOptions)
---
# Overview
Source: https://docs.usetheo.dev/theokit/cloud
The cloud runtime (pre-release) — run agents on Theo PaaS and reach them from a typed client.
# Cloud runtime
Pre-release. The cloud runtime depends on **Theo PaaS**, which is not yet a live endpoint. The
local runtime is the primary, tested path; this documents the contract for when PaaS ships.
Cloud agents carry the id prefix `bc-` (local agents are `agent-`), and `Agent.get` / `Agent.resume`
auto-detect the runtime from that prefix. Cloud-only surfaces (a local agent does not have these):
- `agent.listArtifacts()` / `agent.downloadArtifact(path)` — run artifacts.
- `cloud.envVars` — short-lived credentials sent to PaaS at create time over TLS (never in the
redacted per-run `cloudPayload`).
- `autoCreatePR`, `git` metadata on results.
## The client — `@theokit/sdk/client`
`TheoKitClient` is the typed client for talking to the cloud runtime (`ClientOptions`,
`SendResponse`, `StreamEvent`) — the same `send` contract, over the network.
## Next
- [Inspect the cloud payload](/theokit/cloud/inspect-the-cloud-payload) — a runnable `cloudPayload` example.
- [Advanced](/theokit/cloud/advanced) — the full `CloudOptions`, cloud-only ops, secrets, and the client.
## Reference
- [`Agent`](/theokit/reference/Agent) · [`CloudOptions`](/theokit/reference/CloudOptions) · [`TheoKitClient`](/theokit/reference/TheoKitClient) · [`Theokit`](/theokit/reference/Theokit)
---
# Inspect the cloud payload
Source: https://docs.usetheo.dev/theokit/cloud/inspect-the-cloud-payload
Passing cloud:{} builds a cloud agent whose redacted cloudPayload is serialized locally — inspect the exact contract sent to Theo PaaS. Deterministic, no network.
# Inspect the cloud payload
Passing `cloud: {...}` to `Agent.create` selects the **cloud runtime** — the agent id is prefixed
`bc-` and its config is serialized into a redacted `cloudPayload`. That serialization happens
**locally** (no network), so you can inspect the exact contract the SDK would send to Theo PaaS today,
even though the runtime itself is pre-release.
```ts title="run.ts"
import { Agent } from "@theokit/sdk";
const agent = await Agent.create({
apiKey: process.env.THEOKIT_API_KEY ?? "theo_test_cloud",
model: { id: "openai/gpt-4o-mini" },
systemPrompt: "You are a release bot.",
cloud: {
env: { type: "cloud" },
autoCreatePR: true,
repos: [{ url: "https://github.com/acme/widget", startingRef: "main" }],
},
});
const payload = (agent as { cloudPayload: any }).cloudPayload;
console.log("agentId prefix:", agent.agentId.slice(0, 3)); // bc- for cloud, agent- for local
console.log("schemaVersion: ", payload.schemaVersion);
console.log("autoCreatePR: ", payload.cloud.autoCreatePR);
console.log("repo: ", payload.cloud.repos[0].url);
console.log("model: ", payload.model.id);
await agent.dispose?.();
```
## Output
Deterministic — the payload is built locally from your config:
```text
agentId prefix: bc-
schemaVersion: 1.0
autoCreatePR: true
repo: https://github.com/acme/widget
model: openai/gpt-4o-mini
```
## What it shows
- **`cloud: {...}` picks the cloud runtime** — the id is `bc-…` (vs `agent-…` for local), and
`Agent.get` / `Agent.resume` auto-detect the runtime from that prefix.
- **`cloudPayload`** is the redacted config the SDK sends to PaaS — repos, `autoCreatePR`, model,
system prompt. **Secrets are never in it**: `cloud.envVars` and the `env` selector are sent
separately over TLS at create time, never in this per-run payload.
- **No network** — building the agent and reading the payload is local; only `send()` would reach
PaaS (pre-release).
The cloud runtime depends on **Theo PaaS**, which is not yet a live endpoint. This inspects the
contract; sending against cloud is enabled when PaaS ships.
## Example
Full runnable source:
[`examples/cloud-payload`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/cloud-payload).
---
# Use the typed client
Source: https://docs.usetheo.dev/theokit/cloud/use-the-client
Reach a hosted agent from a browser or another service with TheoKitClient — send a message or consume a live stream over HTTP.
# Use the typed client — `TheoKitClient`
`TheoKitClient` (from `@theokit/sdk/client`) is the HTTP client for reaching an agent served by the cloud runtime (or any endpoint that speaks the same contract) from outside the agent's process — a browser, an edge function, or another service.
```ts
import { TheoKitClient } from "@theokit/sdk/client";
const client = new TheoKitClient({
baseUrl: "https://runtime.example.com",
headers: { Authorization: `Bearer ${token}` }, // your auth — the client is transport-only
});
```
## Send a message
```ts
const res = await client.send("summarize the latest run");
// res: { status, output?, error? }
if (res.status === "finished") console.log(res.output);
```
## Consume a live stream
```ts
for await (const event of client.stream("write the release notes")) {
// event: { type, text?, … } — discriminate on event.type
if (event.type === "text") process.stdout.write(event.text ?? "");
}
```
## Options
| Option | Purpose |
| --- | --- |
| `baseUrl` | The runtime endpoint (required). |
| `basePath` | Path prefix if the API is mounted under a sub-path. |
| `headers` | Extra headers sent on every request — this is where your auth token goes. |
The client is deliberately thin and auth-agnostic: it carries whatever `headers` you give it, so it works with the [OAuth orchestrator](../server/auth), a bearer token, or a signed cookie — whatever your server expects.
> The cloud runtime is **pre-release** (Theo PaaS). The client contract is stable and documented here; the hosted endpoint ships with PaaS.
## Reference
- [`TheoKitClient`](/theokit/reference/TheoKitClient) · [`ClientOptions`](/theokit/reference/ClientOptions) · [`SendResponse`](/theokit/reference/SendResponse) · [`StreamEvent`](/theokit/reference/StreamEvent)
---
# Advanced
Source: https://docs.usetheo.dev/theokit/compaction/advanced
Compact a transcript with a summary checkpoint, keep-recent vs keep-tokens windows, fail-safe summarization, and checkpoint filtering.
# Advanced compaction
Verified against `@theokit/sdk/compaction`. Everything here operates on a transcript
(`CompressibleMessage[]`) and never mutates the input.
## Compact a transcript — `compactTranscript`
```ts
import { compactTranscript, SUMMARY_TEMPLATE } from "@theokit/sdk/compaction";
const compacted = await compactTranscript(messages, {
keepRecent: 6, // trailing turns kept verbatim by COUNT (default 6)
summarize: async (older, template) => {
// older = the window being compressed; template = SUMMARY_TEMPLATE by default
const summary = await myLlm(template, older);
return { role: "assistant", content: summary };
},
});
```
- **Older window** is summarized via `summarize` (which receives the 7-section `SUMMARY_TEMPLATE`) and
collapsed into one checkpoint turn. **Omit `summarize`** and the older window is simply *dropped*.
- **Recent window** is preserved verbatim, plus leading system prompts (in `keepRecent` mode).
- The result is `[...systemPrompts, summaryTurn, ...recent]` — a shorter transcript that keeps the thread.
## Two windowing modes
| Option | Mode | Behavior |
| --- | --- | --- |
| `keepRecent` (default `6`) | by COUNT | Keep the last N turns verbatim; preserve leading system prompts. |
| `keepTokens` | by TOKEN BUDGET | Keep trailing turns within a token budget; **takes precedence** over `keepRecent` and disables system-prompt special-casing. |
## Fail-safe summarization — `failSafe`
By default a thrown `summarize` **propagates** — the caller decides the fallback. Set `failSafe: true`
and a summarizer that throws returns the **original transcript unchanged** (with a redacted
`[compaction] summarizer failed` warning). Compaction becomes a pure optimization that can never lose
data:
```ts
await compactTranscript(messages, { summarize, failSafe: true });
// summarize threw ⇒ you get `messages` back, not an exception
```
## Checkpoints — `buildCheckpoint` / `filterFromLatestCheckpoint`
```ts
import {
buildCheckpoint, filterFromLatestCheckpoint, CHECKPOINT_MARKER,
} from "@theokit/sdk/compaction";
const turn = buildCheckpoint(summaryText); // prefixes CHECKPOINT_MARKER (custom marker optional)
const trimmed = filterFromLatestCheckpoint(messages); // drop everything before the last checkpoint
```
- **`CHECKPOINT_MARKER`** (`"[[theokit:checkpoint]] "`) tags a summary turn so it can be found later.
Only `buildCheckpoint` should produce content starting with it.
- **`filterFromLatestCheckpoint`** scans for the marker and drops everything before the most recent
one — useful to resume a session from its last checkpoint. A custom `marker` is honored on both.
## Detect provider overflow — `isContextOverflowError`
```ts
import { isContextOverflowError } from "@theokit/sdk/compaction";
try {
await agent.send(hugePrompt);
} catch (err) {
if (isContextOverflowError(err)) await compactAndRetry();
else throw err;
}
```
Turns a provider's context-length error into a boolean so you can trigger compaction reactively
instead of pre-estimating every turn.
## The summary template — `SUMMARY_TEMPLATE`
`SUMMARY_TEMPLATE` is a 7-section shape (Goal / Constraints / Progress / Decisions / Next / Critical /
Files) with every header always present, so the summarizer cannot silently drop a section. Pass your
own via `summaryTemplate` to change the shape.
## Reference
- [`compaction`](/theokit/reference/index) — `compactTranscript`, `estimateTokens`, `shouldCompact`,
`buildCheckpoint`, `filterFromLatestCheckpoint`, `isContextOverflowError`, `SUMMARY_TEMPLATE`, `CHECKPOINT_MARKER`
---
# Decide when to compact
Source: https://docs.usetheo.dev/theokit/compaction/decide-when-to-compact
Estimate a transcript's token count and check it against the model's window before the next request — deterministic, no network.
# Decide when to compact
Before you send the next turn, ask: *will it fit?* `estimateTokens` gives a cheap char-based token
estimate; `shouldCompact` compares an estimate to the model's window minus a reserved buffer. Both are
pure functions — no network, no LLM — so you can gate every turn for free.
```ts title="run.ts"
import { estimateTokens, shouldCompact } from "@theokit/sdk/compaction";
const transcript = "hello world ".repeat(50);
console.log("Estimated tokens:", estimateTokens(transcript));
console.log("Near limit? ", shouldCompact({ estimated: 9700, contextWindow: 10000, buffer: 500 }));
console.log("Room left? ", shouldCompact({ estimated: 5000, contextWindow: 10000, buffer: 500 }));
```
## Output
Deterministic — no LLM, no network:
```text
Estimated tokens: 150
Near limit? true
Room left? false
```
## What it shows
- **`estimateTokens(text)`** → a char-based token estimate (roughly 4 chars per token). Cheap enough
to run on every turn; not a replacement for a provider's exact tokenizer.
- **`shouldCompact({ estimated, contextWindow, buffer, maxOutput? })`** → `true` when
`estimated >= contextWindow - buffer - maxOutput`. Reserve `buffer` as headroom and `maxOutput` for
the response you still need room to generate.
- When it returns `true`, run [`compactTranscript`](/theokit/compaction/advanced) to summarize the
older turns before the next request.
## Example
Full runnable source:
[`examples/compaction-basics`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/compaction-basics).
---
# Overview
Source: https://docs.usetheo.dev/theokit/compaction
Compress a long conversation into a structured summary before it overflows the context window — losslessly by default.
# Compaction
Long sessions outgrow the context window. Compaction (`@theokit/sdk/compaction`) summarizes older
turns into a compact, structured checkpoint so the agent keeps its thread without blowing the budget.
- **`shouldCompact(input)`** / **`estimateTokens(text)`** — decide when to compact.
- **`buildCheckpoint(...)`** — produce a summary turn using `SUMMARY_TEMPLATE` (Goal / Constraints /
Progress / Decisions / Next / Critical / Files); a custom `marker` (`CHECKPOINT_MARKER`) is honored.
- **`filterFromLatestCheckpoint(...)`** — drop everything before the last checkpoint.
- **`isContextOverflowError(err)`** — detect a provider context-overflow to trigger compaction.
By default a failed summarize **propagates** (the caller decides the fallback); `failSafe: true`
returns the original transcript unchanged — compaction as an optimization that never loses data.
## Next
- [Decide when to compact](/theokit/compaction/decide-when-to-compact) — a runnable `estimateTokens` +
`shouldCompact` example.
- [Advanced](/theokit/compaction/advanced) — `compactTranscript`, windowing modes, fail-safe
summarization, checkpoints, and overflow detection.
## Reference
- [`compaction`](/theokit/reference/index) · [`Agent`](/theokit/reference/Agent)
---
# Coming soon
Source: https://docs.usetheo.dev/theokit/concepts/_placeholder
Placeholder while Phase 3 lands the real concept pages.
This page is a placeholder. Real concept pages land in Phase 3 of the docs-site plan.
---
# ACP server
Source: https://docs.usetheo.dev/theokit/concepts/acp-server
Expose your @theokit/sdk agent as an Agent Client Protocol (ACP) server so Zed, Cursor, and Claude Desktop can drive it.
# ACP server
`@theokit/acp` exposes any `@theokit/sdk` `SDKAgent` over stdio JSON-RPC using the official [`@agentclientprotocol/sdk`](https://www.npmjs.com/package/@agentclientprotocol/sdk). Hosts like [Zed](https://zed.dev), Cursor, and Claude Desktop can then drive your agent as if it were a built-in coding agent — no glue code required.
ADRs: D349-D360. Status: v0.1 (server-only; ACP client deferred to v0.2).
## Quick start
```bash
npm i @theokit/acp @theokit/sdk @agentclientprotocol/sdk
```
Create an entry file that default-exports either an `SDKAgent` or a factory:
```ts title="src/index.ts"
import { Agent } from "@theokit/sdk";
export default async (sessionId: string) => {
return Agent.create({
apiKey: process.env.OPENROUTER_API_KEY,
model: { id: "openai/gpt-4o-mini" },
local: { cwd: process.cwd() },
name: `acp-${sessionId}`,
});
};
```
Launch the server:
```bash
npx theokit-acp --entry ./src/index.ts
```
Or programmatically:
```ts
import { Agent } from "@theokit/sdk";
import { serveAcp } from "@theokit/acp";
await serveAcp({
agent: async (sessionId) => Agent.create({ /* ... */ }),
permissionDefault: "ask",
});
```
## Session lifecycle (D352)
| ACP method | SDK mapping |
| ------------------------ | --------------------------------------------------- |
| `initialize` | Capability advertisement |
| `new_session` | `Agent.create({ local: { cwd } })` |
| `load_session` | `Agent.resume(sessionId)` |
| `cancel` | `AbortController.abort()` on the session |
| `prompt` | `agent.send(text, { signal }).stream()` |
| `tool_call_permission_*` | `pre_tool_call` veto hook |
| `unstable_forkSession` | Deferred to v0.2 — SDK fork is one-shot, not a split |
ACP `sessionId` is 1:1 with our SDK `agentId`. Override the mapping by writing a factory:
```ts
serveAcp({
agent: (sessionId) =>
Agent.resume(customMapSessionToAgentId(sessionId), { local: { cwd } }),
});
```
## Streaming translation (D353)
The translator maps every `SDKMessage` variant to the right ACP `SessionUpdate`. The switch is exhaustive — adding a new SDK message variant fails compile until the translator handles it.
| `SDKMessage.type` | ACP `SessionUpdate.sessionUpdate` |
| ----------------- | -------------------------------------------- |
| `assistant` | `agent_message_chunk` or `tool_call` |
| `tool_call` | `tool_call_update` |
| `thinking` | `agent_thought_chunk` (or suppressed) |
| `system`/`user` | skipped (no ACP equivalent) |
| `status`/`task` | skipped (cloud / milestones) |
| `request` | routed via permission plugin |
| `object_delta` | skipped in v0.1 |
## Tool permissions (D355)
Three modes:
- `permissionDefault: "ask"` (default) — every tool call prompts the host UI.
- `permissionDefault: "auto"` — pass-through; no UI.
- `permissionDefault: "deny"` — reject every tool call (CI / headless).
`trustedTools` bypasses `ask` for known-safe names:
```ts
serveAcp({
agent: factory,
permissionDefault: "ask",
trustedTools: ["read_file", "list_dir", "git_diff", "search_text"],
});
```
`permissionTimeoutMs` (default 60_000) prevents prompt-hang when the host doesn't respond. After timeout the tool call is auto-denied with a clear message.
## CLI flags
```text
theokit-acp [--entry ]
[--permission ask|auto|deny]
[--trusted-tools name1,name2]
[--permission-timeout-ms 60000]
```
## Registry manifest
`@theokit/acp` ships a marketplace manifest at `packages/acp/registry/agent.json`. Install in Zed:
```bash
mkdir -p ~/.config/zed/external_agents/usetheo-sdk
cp node_modules/@theokit/acp/registry/agent.json ~/.config/zed/external_agents/usetheo-sdk/
cp node_modules/@theokit/acp/registry/icon.svg ~/.config/zed/external_agents/usetheo-sdk/
```
Edit the copied `agent.json` to point `distribution.args.[--entry]` at your entry file. Restart Zed → "Theokit SDK" appears in the External Agents palette.
## Caveats
- **Cancel cascade:** cancelling a session cancels every fork derived from it (D324 + D352). Intentional.
- **In-memory session store:** sessions live for the lifetime of the process. JSON-file persistence is on the v0.2 roadmap (D356).
- **CloudAgent + fork:** CloudAgent does not implement `agent.fork()` (D122/D169). Even when v0.2 ships `forkSession`, CloudAgent will surface `invalid_request` (EC-3).
- **Stdout reserved for JSON-RPC** (D359). Internal logs go to stderr. The `log` option accepts a custom sink.
- **2 MiB prompt cap** (D360, EC-DoS). Configurable via `maxPromptBytes`.
- **Serverless `load_session`:** session state lives in the native transcript on disk under `local.baseDir`. On a serverless restart with an ephemeral filesystem, that transcript is gone unless `baseDir` points at durable, shared storage — see [Session persistence](/theokit/guides/conversation-storage) (EC-6).
## See also
- [Session persistence](/theokit/guides/conversation-storage) — how the native transcript persists; relevant for serverless ACP deployments.
- [Tool hooks](/theokit/concepts/hooks#pre_tool_call) — what backs the permission flow.
- [Cookbook: serve as ACP agent](/theokit/guides/serve-as-acp-agent)
---
# Agent
Source: https://docs.usetheo.dev/theokit/concepts/agent
The agent loop — Agent.create, agent.send, lifecycle, persistence, LocalAgent vs CloudAgent.
> **Canonical contract:** [`packages/sdk/docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md) — the source-of-truth file for every Agent option.
## What is an Agent
An `Agent` is a typed loop around a model, a set of tools, an optional memory store, and a session. You hand it a prompt; it decides whether to answer directly, call tools, hand off to another agent, or stream a response.
The SDK ships two flavours:
- **LocalAgent** — runs in your process. Backed by a local agent loop, MCP servers spawned via stdio, in-process tool registry, and a SQLite session store on disk. This is what you get from `Agent.create({ apiKey, model })`.
- **CloudAgent** — pointer to a remote agent running on TheoCloud (pre-release). Auto-detected when the agent id starts with `bc-` (vs `agent-` for local).
## Lifecycle
```
Agent.create(opts) → agent.send(prompt) → run.wait() → agent.dispose()
│
└──▶ (concurrent) run.stream() → AsyncIterator
```
1. **Create.** `Agent.create({ apiKey, model, tools?, mcpServers?, memory?, ... })` constructs the loop, resolves the provider, opens any MCP servers, and assigns an `agentId`. Returns a `Promise`.
2. **Send.** `agent.send(prompt, options?)` enqueues a turn. Returns a `Run` handle.
3. **Wait or stream.** `run.wait()` blocks until the agent finishes. `run.stream()` yields `SDKMessage` events as they happen (text deltas, tool calls, partials).
4. **Dispose.** `agent.dispose()` closes MCP servers, releases file handles, and stops background tasks. Use `await using` for automatic disposal:
```ts
await using agent = await Agent.create({...});
// ... auto-disposes at block exit
```
## Persistence
Every `LocalAgent` gets an `agentId` (e.g. `agent-7a3...`) and persists its session as a native Claude Code `.jsonl` transcript at `/projects//.jsonl` (`baseDir` default `~/.theokit`; set `local.baseDir: "~/.claude"` for CLI `--continue`). Resume from a different process:
```ts
const agent = await Agent.resume(id);
```
The full message history, tool registry, and (if enabled) memory store rehydrate transparently. See [Sessions](./sessions) for the persistence contract.
## Multi-provider fallback
```ts
const agent = await Agent.create({
apiKey: process.env.OPENROUTER_API_KEY,
model: { id: "openai/gpt-4o-mini" },
providers: {
routes: [{ provider: "openrouter" }],
fallback: ["openai", "anthropic"],
},
});
```
If `openrouter` returns a transient error, the SDK retries on `openai`, then `anthropic`. See [Providers](./providers-bedrock-vertex) for credential pools (multi-key rotation per provider).
## Common patterns
| Pattern | Where to read more |
|---|---|
| Define typed tools | [Tools](./tools) |
| Stream tokens as they arrive | [Streaming](./streaming) |
| Add MCP servers | [MCP](./mcp) |
| Run multiple prompts in parallel | `Agent.batch(prompts, options)` — see cookbook |
| Hand off to another agent | [Handoffs](./handoffs) |
| Compose multi-step pipelines | [Workflows](./workflows) |
| Cache repeated prompts | [Cache](./cache) |
| Add long-term memory | [Memory](./memory) |
## API reference
---
# Budget
Source: https://docs.usetheo.dev/theokit/concepts/budget
Track token usage + USD cost + enforce per-window spend limits.
> **Canonical contract:** [`packages/sdk/docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md) — `Budget` namespace.
The `Budget` namespace provides observable token usage, estimated USD cost, and per-window spend enforcement. Closes the last gap vs Hermes Agent's `agent/usage_pricing.py`.
ADRs: **D375-D388**. Edge cases absorbed: **EC-1..EC-22**.
## The pieces
```
TokenUsage ← 5 buckets (input/output/cacheRead/cacheWrite/reasoning)
CostBreakdown ← amountUsd + status (actual/estimated/included/unknown) + source
Budget ← lifecycle + 3 modes (audit/warn/block) + stacked windows
```
## Quick start
```ts
import { Budget } from "@theokit/sdk";
// Set up a daily + monthly budget
const handle = Budget.create({
name: "production-api",
scope: "process",
mode: "warn",
limits: [
{ window: "1d", limitUsd: 5 },
{ window: "30d", limitUsd: 100 },
],
onThreshold: (e) => console.warn(`${e.budgetName} at ${e.threshold * 100}%`),
onExceed: (e) => console.error(`${e.budgetName} exceeded ${e.window}`),
});
// Inspect at any time
console.log(handle.spentIn("1d")); // current USD spent
console.log(handle.remainingIn("1d")); // USD remaining
// Snapshot all budgets
const all = Budget.snapshot();
// → [{ name, window, spentUsd, limitUsd, ratio }, ...]
```
## The 3 modes (D383)
| Mode | Behavior |
|---|---|
| `audit` | Log only. Never throws. Never blocks. Use for rollout phase. |
| `warn` | Charges + invokes `onThreshold`/`onExceed`. Logs to stderr. Never throws. |
| `block` | `preflightCheck` throws `BudgetExceededError` BEFORE the LLM call when the upcoming charge would exceed any limit. |
Default mode is `warn` (D383).
## 4-status cost (D377)
`CostBreakdown.status` is a closed 4-value enum — show, don't lie:
- `actual` — provider billing API returned the figure (OpenRouter `/generation`).
- `estimated` — computed from bundled pricing snapshot.
- `included` — subscription route (Codex CLI, Claude Pro).
- `unknown` — no pricing data; `amountUsd` is `undefined`.
UI should render `~$1.23`, `$1.23`, `included`, or `n/a` accordingly. **Do not** default `unknown` to `$0` — that's a lie that hides untracked spend.
## 5 token buckets (D376)
```ts
interface TokenUsage {
inputTokens: number;
outputTokens: number;
cacheReadTokens?: number;
cacheWriteTokens?: number;
reasoningTokens?: number;
totalTokens: number;
requests?: ReadonlyArray>;
}
```
Cache buckets matter for Anthropic prompt-caching (5min cache write × 1.25 base; 1h × 2 base; cache read × 0.10 base — see [Anthropic docs](https://platform.claude.com/docs/en/build-with-claude/prompt-caching)). Reasoning bucket matters for OpenAI o-series models.
## Edge cases documented
### EC-18 — Emergency stop
Use `mode: 'block', limits: [{ window: '1d', limitUsd: 0 }]` to block ALL sends from a specific Budget. Functions as a kill switch.
### EC-19 — Informational-only tracking
Pass `limits: []` for pure registry-side tracking. Thresholds/onExceed never fire, but `Budget.snapshot()` accumulates spend.
### EC-20 — Delete-during-flight
`Budget.delete()` is safe during in-flight `agent.send` calls; the subsequent charge becomes a silent no-op + stderr warn.
### EC-21 — gpt-tokenizer optional peer
For strict `mode: 'block'` pre-call enforcement, install `gpt-tokenizer@^3.4.0` as a peer dep:
```bash
pnpm add gpt-tokenizer
```
Without it, block mode degrades to post-call enforce (charge happens after LLM call; budgets exceeded by the call still surface as `onExceed`).
### EC-22 — Pricing snapshot staleness
The bundled `pricing-data.json` is a hand-curated snapshot of [LiteLLM `model_prices_and_context_window.json`](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json) as of 2026-05. The `pricingVersion` field on every `CostBreakdown` exposes the snapshot date so UI can warn on staleness. For live OpenRouter pricing, see the [OpenRouter API](https://openrouter.ai/api/v1/models) (snapshot is fallback only).
## v1.2 scope cut — caller-side composition
Auto-populate of `RunResult.usage` and `RunResult.cost` is deferred to v0.2. Today, callers compose explicitly:
```ts
import {
Budget,
computeCost,
normalizeUsage,
preflightCheck,
chargeAndCheckThresholds,
} from "@theokit/sdk";
Budget.create({ name: "test", scope: "process", mode: "block", limits: [{ window: "1d", limitUsd: 1 }] });
// Pre-call enforcement (block mode)
preflightCheck("test", 0.10);
const run = await agent.send(prompt);
const result = await run.wait();
// Parse provider response → canonical 5-bucket usage
const usage = normalizeUsage(result.usage, { provider: "anthropic" });
// Apply pricing
const cost = computeCost({ provider: "anthropic", model: "claude-opus-4-7", usage });
// Charge + dispatch thresholds
if (cost.amountUsd !== undefined) {
await chargeAndCheckThresholds("test", cost.amountUsd);
}
```
When v0.2 lands, `RunResult.usage` and `RunResult.cost` will be auto-populated and Budget enforcement integrated into `agent.send`.
## CLI inspector (v0.2)
A `theokit budget` subcommand is deferred to v0.2. Programmatic introspection via `Budget.snapshot()` covers in-process use today.
## Cloud rejection
`CloudAgent.send({ budget })` throws `UnsupportedBudgetOperationError` (D388). Cloud budget enforcement waits for TheoCloud GA.
## See also
- [Tasks](./tasks) — async work observability (paired primitive).
- [Cache](./cache) — semantic prompt caching (saves both tokens AND cost).
- [Agent](./agent) — the core `Agent.send` surface budgets attach to.
---
# Semantic cache
Source: https://docs.usetheo.dev/theokit/concepts/cache
Cache.semantic — embedding-based prompt cache with KV exact pre-filter.
> **Canonical contract:** [`packages/sdk/docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md) — "Semantic cache (v1.18+)".
A cache that recognizes *semantically equivalent* prompts, not just byte-equal ones. Cuts cost 30-70% in workloads with repetitive queries.
## Plugin mode (recall + inject)
```ts
import { Cache, Agent } from "@theokit/sdk";
const cache = Cache.semantic({
embedder: "openai/text-embedding-3-small",
threshold: 0.85,
ttl: 60 * 60 * 1000, // 1 hour
namespace: "weather-bot",
});
const agent = await Agent.create({
apiKey, model,
plugins: [cache.asPlugin()],
});
```
When a prompt arrives, the cache:
1. Looks up exact KV match (composite key: `namespace:embedderId:modelId:hash(prompt)`).
2. If miss, embeds the prompt and runs vector similarity against stored entries.
3. If a match scores ≥ threshold, injects the cached response as a hint (the LLM still runs, but with a strong nudge).
## Direct mode (true short-circuit)
For zero-LLM-call short-circuit:
```ts
const cached = await cache.consult(prompt);
if (cached) return cached;
const result = await /* expensive LLM call */;
await cache.remember(prompt, result);
```
`consult` returns `null` on miss. `remember` stores after the fact.
## Composition
The composite key (ADR D253) ensures changing the embedder or model invalidates the cache automatically (ADR D258).
## Skipped automatically
The cache skips storing runs that invoked tools (ADR D266). Tool-use turns aren't semantically equivalent enough — they have side effects.
## Persistence
Default: in-memory LRU (1000 entries, ADR D261). JSON-on-disk opt-in:
```ts
Cache.semantic({
// ...
persistence: { backend: "json", dir: ".theokit/cache" },
});
```
## Telemetry
OTel events `cache.lookup` / `cache.store` with hit/miss labels (ADR D262).
## Composes with Anthropic prompt caching
Anthropic's prompt_caching (cache_control breakpoints) is orthogonal. The SDK semantic cache runs above that layer (ADR D263).
## API reference
---
# Configuration files
Source: https://docs.usetheo.dev/theokit/concepts/configuration
Markdown + YAML frontmatter format, auto-discovery, project-vs-user precedence.
> **Canonical contract:** [`packages/sdk/docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md) — "Configuration files (v1.5+)" + ADR D74-D78, D150-D158.
The SDK reads several config files from `.theokit/` (project) and `~/.theokit/` (user). The format is **Markdown + YAML frontmatter** — same shape as `SKILL.md` / Claude Code (ADR D74).
## File formats
### Agents (`.theokit/agents/.md`)
```markdown
---
name: weather-bot
model:
id: openai/gpt-4o-mini
tools:
- get_weather
---
You are a concise weather assistant. Use the get_weather tool when relevant.
```
The body is the system prompt; the frontmatter is options.
### Skills (`.theokit/skills/.md`)
```markdown
---
name: morning-routine
description: Generate the morning briefing.
---
1. Check calendar for today.
2. Summarize unread emails.
3. Post to Slack #team.
```
### Personalities (`.theokit/personalities/.md`)
```markdown
---
name: coder
description: Terse, code-first persona.
tools:
- shell
- read_file
---
You answer in code, with minimal prose. Always show working snippets.
```
ADR D161 enforces lowercase-only slug + Zod-validated frontmatter.
## One file = one entity (ADR D75)
The SDK does NOT support multi-entity files (no `agents.json` with N agents inside). Each agent / skill / personality is its own `.md`. To disable, rename to `.md.disabled`.
## Validation
Every frontmatter is Zod-validated on load (ADR D76, D10). Invalid files emit a typed `ConfigurationError` — they don't break other files.
## Backward compat
The legacy JSON format (`.theokit/agents.json` etc.) still works but emits a deprecation warning. Sunset planned for v2.0 / Q2 2027 (ADR D77). Use the migration CLI:
```bash
pnpm exec theokit-migrate-config
```
Performs atomic write + timestamped backup (ADR D78).
## Auto-discovery (context files)
Beyond explicit config, the SDK auto-discovers context files via walk-up-to-git-root (ADR D151) — no `import` needed:
| File | Source |
|---|---|
| `.theokit/THEO.md` | Project-specific |
| `CLAUDE.md` | Claude Code standard |
| `AGENTS.md` | Multi-tool standard |
| `GEMINI.md` | Gemini Code Assist |
| `.cursor/rules/*.mdc` | Cursor IDE |
Merge is **concat-by-priority** (ADR D152) — not first-match-wins. All files contribute, ordered by priority.
### `@import` syntax (ADR D156)
`CLAUDE.md` and `GEMINI.md` support `@path/to/include.md` — the SDK resolves with 5-hop limit + per-import size cap.
### Size caps (ADR D155)
Per-file 40k, aggregate 120k. Over-limit → tie-break truncation, telemetry counter incremented (ADR D159).
## API reference
The configuration system is mostly transparent — you don't import a "config loader". The SDK reads files on `Agent.create` / `Skill.list` / `Personality.list`.
---
# Cron
Source: https://docs.usetheo.dev/theokit/concepts/cron
Schedule agent runs — Cron.create with croner-flavored expressions.
> **Canonical contract:** [`packages/sdk/docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md) — `Cron` namespace.
The `Cron` namespace lets you schedule recurring agent runs. Backed by [`croner`](https://github.com/Hexagon/croner) (ADR D7), persisted as JSON to `.theokit/cron/jobs.json` with atomic writes (ADR D8).
## Schedule a job
```ts
import { Cron } from "@theokit/sdk";
const job = await Cron.create({
apiKey: process.env.OPENROUTER_API_KEY,
schedule: "0 9 * * *", // every day at 09:00 local
prompt: "Summarize yesterday's logs and post to Slack.",
agent: {
model: { id: "openai/gpt-4o-mini" },
name: "log-summarizer",
},
});
console.log(job.id, job.nextFire); // "cron-abc...", "2026-05-24T09:00:00..."
```
## Persistence + restart
Jobs survive process restart. On next `Cron.list()` or `Cron.create()`, the persisted jobs reload and continue firing.
## List / cancel
```ts
const all = await Cron.list();
await Cron.cancel(job.id);
```
## Manual fire (testing)
```ts
await Cron.fire(job.id);
```
## Schedule expression
`croner` accepts:
- 5-field cron: `* * * * *` (min hour dom month dow)
- 6-field cron: `* * * * * *` (sec min hour dom month dow)
- Aliases: `@hourly`, `@daily`, `@weekly`, `@monthly`
## Caveats
- Process MUST be running when the schedule fires. Cron isn't a daemon — it's an in-process scheduler. Use a process supervisor (pm2, systemd, Cloudflare Cron Triggers, etc.) to keep your Node app alive.
- File lock via `proper-lockfile` (ADR D61) prevents two processes from corrupting `jobs.json`.
## API reference
---
# Errors
Source: https://docs.usetheo.dev/theokit/concepts/errors
Typed error hierarchy — TheokitAgentError + ErrorCode discriminator.
> **Canonical contract:** [`packages/sdk/docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md) — error hierarchy + `ErrorMetadata`.
Every error thrown by the SDK extends `TheokitAgentError`. The hierarchy is closed (no dynamic subclassing) so `switch` on `error.code` is exhaustive.
## Base class
```ts
class TheokitAgentError extends Error {
code: ErrorCode; // exhaustive enum
metadata?: ErrorMetadata; // optional structured context
}
```
`ErrorCode` is a finite literal union (ADR D66). `ErrorMetadata` is an optional field with `{ provider, endpoint, status, headers, body, rawTruncated }` shape (ADR D65, D73).
## Hierarchy
```
TheokitAgentError
├── AuthenticationError (code: "auth_failed", "missing_api_key")
├── RateLimitError (code: "rate_limit")
├── ConfigurationError (code: "invalid_request", "transport_unavailable", ...)
│ └── IntegrationNotConnectedError (MCP / OAuth not set up)
├── NetworkError (code: "timeout", "server_error")
├── UnknownAgentError (code: "unknown")
├── AgentRunError (code: "agent_run_failed")
├── UnsupportedRunOperationError (feature not available on this agent variant)
├── CredentialPoolExhaustedError (all keys cooling down)
└── MemoryAdapterError (code: MemoryAdapterErrorCode)
```
Plus feature-scoped errors that also extend `TheokitAgentError`:
`EvalAlreadyRunningError`, `GenerateObjectError`, `StreamObjectError`, `HandoffLoopError`, `HandoffNameCollisionError`, `HandoffPairLoopError`, `HandoffReceiverDisposedError`, `HandoffSelfReferenceError`, `CacheEmbedderError`, `CacheInvalidTtlError`, `WorkflowAlreadyRunningError`, `WorkflowCompensateNotImplementedError`, `WorkflowDuplicateStepIdError`, `WorkflowMaxIterationsExceededError`, `WorkflowNotSerializableError`, `WorkflowParallelError`, `WorkflowResumeStepNotFoundError`, `WorkflowSnapshotNotFoundError`.
## Exhaustive switch
```ts
import { TheokitAgentError } from "@theokit/sdk";
try {
await agent.send("hi");
} catch (err) {
if (!(err instanceof TheokitAgentError)) throw err;
switch (err.code) {
case "auth_failed": return notify("Re-authenticate");
case "rate_limit": return retryWithBackoff(err);
case "context_too_long": return startNewSession();
// ... TS forces all cases handled
}
}
```
## Provider-specific error mappers
Provider HTTP errors are mapped to canonical codes by per-dialect mappers (ADR D67, D300). Bedrock and Vertex have their own mappers; OpenAI and Anthropic share `shared.ts`. You don't call mappers directly — they run inside the LLM clients.
## Redaction
Errors carry `metadata.body` and `metadata.headers` for diagnosis. The SDK applies `redactSecrets` at output boundaries (ADR D73) — logs, telemetry attrs, transcript files. The error object itself contains raw fields; the OUTPUT is sanitized.
## API reference
---
# Eval suite
Source: https://docs.usetheo.dev/theokit/concepts/eval
Eval.create and Eval.run — eval-as-code with built-in scorers.
> **Canonical contract:** [`packages/sdk/docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md) — "Eval suite (v1.15+)".
The `Eval` API lets you write evals as code (not as dashboards). Inspired by Braintrust + LangSmith but local-first.
## Define an eval
```ts
import { Eval, Scorers, Agent } from "@theokit/sdk";
const evaluation = Eval.create({
name: "math-basics",
dataset: [
{ input: "What is 2+2?", expected: "4" },
{ input: "What is 10*5?", expected: "50" },
{ input: "What is sqrt(16)?", expected: "4" },
],
scorers: [
Scorers.contains("expected"),
Scorers.llmJudge({
apiKey: process.env.OPENROUTER_API_KEY,
prompt: "Is the output a correct numeric answer? Score 0-1.",
}),
],
agent: async (input) => {
const agent = await Agent.create({
apiKey: process.env.OPENROUTER_API_KEY,
model: { id: "openai/gpt-4o-mini" },
});
try {
const r = await (await agent.send(input)).wait();
return r.result ?? "";
} finally {
await agent.dispose();
}
},
});
const run = await evaluation.run();
console.log(run.aggregate); // { meanScore, p50, p95, errorRows, tokensIn, tokensOut }
```
## Built-in scorers
| Scorer | Returns | Use |
|---|---|---|
| `Scorers.exactMatch("expected")` | 0 or 1 | Strict equality |
| `Scorers.contains("expected")` | 0 or 1 | Substring match |
| `Scorers.jsonValid()` | 0 or 1 | Is the output parseable JSON |
| `Scorers.lengthBetween(min, max)` | 0 or 1 | Length in range |
| `Scorers.llmJudge({ apiKey, prompt })` | 0..1 | LLM-as-judge with separate apiKey |
`Scorers.llmJudge` requires its own `apiKey` (ADR D205), separate from the agent under test — prevents cross-contamination of metrics.
## Parallelism
`Eval.run` internally uses `Agent.batch` (ADR D204) — default concurrency 4, per-prompt isolation (ADR D208), pool sharing via AsyncLocalStorage.
## Error isolation
If one row throws, the run continues — the row gets `error: { message }` in its result and `errorRows` increments in the aggregate. Plan accordingly.
## CLI
The `theokit` CLI wraps `Eval.run` (ADR D212):
```bash
theokit eval ./my-eval.ts
```
## Telemetry
Eval traces piggyback on the `Telemetry` namespace (D34) — `eval.run` and per-row spans. No separate tracer.
## API reference
---
# Gateways
Source: https://docs.usetheo.dev/theokit/concepts/gateways
Build agents that chat on Telegram, Discord, Slack, WhatsApp, Teams, and Email.
> **Canonical contract:** [`packages/sdk/docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md) — "Slack gateway (v1.19+)".
A gateway is a channel adapter — it lets your agent receive messages from a platform (Telegram, Slack, ...) and reply, hiding platform-specific shape behind a portable `MessageEvent` union.
## Shipped adapters
| Package | Platform | Transport |
|---|---|---|
| `@theokit/gateway-telegram` | Telegram | long-polling + webhook |
| `@theokit/gateway-discord` | Discord | WebSocket Gateway |
| `@theokit/gateway-slack` | Slack | Socket Mode |
| `@theokit/gateway-whatsapp` | WhatsApp | Meta Cloud API webhook + whatsapp-web.js bridge |
| `@theokit/gateway-teams` | Microsoft Teams | `@microsoft/teams.apps` v2 SDK + Express webhook |
| `@theokit/gateway-email` | Email | IMAP IDLE inbound + SMTP outbound (nodemailer + imapflow + mailparser) |
| `@theokit/gateway-sms` | SMS | Twilio + Plivo + Vonage backends (HTTP webhook + REST outbound) |
| `@theokit/gateway-mattermost` | Mattermost | WebSocket gateway (`@mattermost/client` v9+) + REST v4 |
| `@theokit/gateway-line` | LINE Messaging API | Webhook-only (HMAC-SHA256 signed) + Reply/Push fallback |
| `@theokit/gateway-matrix` | Matrix protocol | Sync long-poll via `matrix-js-sdk` v32+; federated by default |
Each adapter is its own workspace package with peer-dep policy (ADR D171) — install only the ones you use.
The Matrix adapter (ADRs D413-D421) wraps `matrix-js-sdk@^32.0.0` (~2MB lazy-loaded peer-dep). Federation is transparent (D420) — your bot at `@bot:matrix.org` can be added to rooms by users on any homeserver and the protocol routes events automatically. DM detection (D416) uses the canonical heuristic `memberCount === 2 → dm`; else `group`. Aliases (`#general:matrix.org`) are resolved to room ids on first send + cached (D419). EC-3 absorbed: `matrix-js-sdk` initial sync delivers ~10 historical events per joined room (a 50-room bot would fire 500 LLM calls on boot); the adapter filters `event.getTs() < Date.now() - 60_000` so only live messages dispatch. E2EE rooms refused with a one-shot stderr warn (D418, deferred to v0.2). MSC4140 threads deferred to v0.2 (D417). See [`examples/matrix-bot/`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/matrix-bot).
The LINE adapter (ADRs D405-D412) is webhook-only (D406 — LINE doesn't offer a WebSocket gateway). HMAC-SHA256 signature validation on every POST via `crypto.timingSafeEqual` (D408 — refuses unsigned mode at construction). `sendMessage` uses **Reply token first** (free, 60s TTL, one-shot) and falls back to **Push API** when token expired (D407) — `ReplyTokenCache` LRU(1000 entries) handles the lifecycle automatically. Source-type mapping (D410): `user` → `dm`, `group`/`room` → `group`. Mention guard (D409) uses LINE's out-of-band `event.message.mentionees: [{ userId }]` array (no inline-text confusion). EC-4 absorbed: webhook delivers 9 event types (`follow`, `unfollow`, `postback`, `beacon`, image/sticker/audio/video messages, etc.) — adapter filters at the top so handlers never see TypeError on `event.message.text` for non-text events. 5000-char surrogate-safe multipart split (D411). See [`examples/line-bot/`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/line-bot).
The Mattermost adapter (ADRs D397-D404) uses the modern `@mattermost/client@^9.0.0` SDK with WebSocket gateway for real-time `posted` events and Client4 REST for outbound. Works with self-hosted Mattermost (any baseUrl) and Mattermost Cloud. PAT auth (D401); OAuth deferred to v0.2. Channel-type mapping: `D` → `dm`, `G`/`O`/`P` → `group` (raw type preserved in `event.mattermost.channelType`). Thread replies bidirectional via `root_id` ↔ `topicId` (D399). `requireMention: true` default for non-DM channels (D403). EC-2 absorbed: mention check prioritizes `metadata.mentions` array (unambiguous user-id list); text fallback uses word-boundary regex `\b@${botUsername}\b` so `@theory_dept` does NOT match a bot called `theo`. See [`examples/mattermost-bot/`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/mattermost-bot).
The SMS adapter ships **three backends** (ADR D389): Twilio, Plivo, and Vonage. Pick via `backend: "twilio" | "plivo" | "vonage"`. Each backend validates inbound HMAC signatures BEFORE handler dispatch (ADR D392); webhook public endpoint without signing secret is refused at construction time (EC-1 absorbed). Outbound text is segmented into `(i/N)` parts at 1600 chars per part, grapheme-cluster-safe via `Intl.Segmenter` (ADR D393, EC-7). All phone numbers are normalized to E.164 via `libphonenumber-js` (D391). v0.1 covers texto only — MMS/group SMS/budget-per-message are deferred to v0.2. See [`examples/sms-bot/`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/sms-bot).
The WhatsApp adapter ships **two backends** (ADR D303): the official Meta WhatsApp Business Cloud API (Bearer token + signed webhook) and the unofficial `whatsapp-web.js` subprocess bridge (personal accounts). Pick via `backend: "cloud" | "web"`.
The Microsoft Teams adapter is built on the **modern `@microsoft/teams.apps` v2 SDK** (ADR D315). The SDK handles JWT validation, mention stripping, and proactive send routing — we expose `BasePlatformAdapter` over it. Setup requires Azure AD App Registration + Azure Bot Service. See [`examples/teams-bot/`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/teams-bot) for the 8-step walkthrough.
The Email adapter speaks the **2026 community-standard Node stack**: `nodemailer` for SMTP outbound, `imapflow` for IMAP IDLE inbound (RFC 2177 push, ~28-min auto-refresh), and `mailparser` for RFC 5322 parsing. Threading is reconstructed automatically from `Message-ID` / `In-Reply-To` / `References` (ADR D337, RFC 5322 §3.6.4) so replies land in the same thread in the user's inbox. The adapter ships hardened filters for own-address loopback (EC-1 CRITICAL), automated senders (`Auto-Submitted`, `Precedence: bulk|list`, `noreply@`, `postmaster@`), and an optional `allowedSenders` allowlist with bracket-form normalization (EC-3). Works with Gmail App Passwords, Outlook/Office 365, Yahoo, and Fastmail. See [`examples/email-bot/`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/email-bot).
## Architecture
```
Platform (Telegram/Slack/...)
│
▼
PlatformAdapter (per-platform package)
│ MessageEvent (portable, discriminated by `platform`)
▼
SessionRouter ←→ Agent.resume / Agent.getOrCreate
│
▼
DeliveryRouter ←→ Cron (for scheduled delivery)
```
The `BasePlatformAdapter` (ADR D172) handles lifecycle, retry, and hook plumbing. New adapters subclass it.
## Minimal Slack example
```ts
import { SlackAdapter } from "@theokit/gateway-slack";
import { Agent } from "@theokit/sdk";
const adapter = new SlackAdapter({
appToken: process.env.SLACK_APP_TOKEN!,
botToken: process.env.SLACK_BOT_TOKEN!,
});
adapter.onInbound(async (event) => {
const agent = await Agent.getOrCreate({
agentId: `slack-${event.slack!.channel.id}`,
apiKey: process.env.OPENROUTER_API_KEY,
model: { id: "openai/gpt-4o-mini" },
});
const reply = await (await agent.send(event.text)).wait();
await adapter.send({ chatId: event.slack!.channel.id, text: reply.result! });
await agent.dispose();
});
await adapter.connect();
```
## Portable vs platform-specific (ADR D180)
`MessageEvent` always has portable fields (`text`, `userId`, `topicId`). Platform-specific structures sit behind `event.{telegram,discord,slack}?.raw` — escape hatch for richer rendering (Adaptive Cards, Block Kit, etc.).
## Hooks (gateway-level)
Gateways expose their own hook contract (ADR D176) — separate from agent plugin hooks. Hook signature mirrors `pre_tool_call` veto pattern (ADR D177).
## Roadmap
v1.4 ships: ✅ `@theokit/gateway-whatsapp`, ✅ `@theokit/gateway-teams`, ✅ `@theokit/gateway-email` (this release). Next: `@theokit/skills-google-workspace`. See `CLAUDE.md` Adoption Roadmap.
## API reference
The gateway packages are separate from `@theokit/sdk` — see each package's README:
- [`@theokit/gateway-telegram`](https://github.com/usetheodev/theokit-sdk/tree/main/packages/gateway-telegram)
- [`@theokit/gateway-discord`](https://github.com/usetheodev/theokit-sdk/tree/main/packages/gateway-discord)
- [`@theokit/gateway-slack`](https://github.com/usetheodev/theokit-sdk/tree/main/packages/gateway-slack)
- [`@theokit/gateway-whatsapp`](https://github.com/usetheodev/theokit-sdk/tree/main/packages/gateway-whatsapp)
- [`@theokit/gateway-teams`](https://github.com/usetheodev/theokit-sdk/tree/main/packages/gateway-teams)
- [`@theokit/gateway-email`](https://github.com/usetheodev/theokit-sdk/tree/main/packages/gateway-email)
---
# Handoffs
Source: https://docs.usetheo.dev/theokit/concepts/handoffs
Peer-to-peer agent handoffs — declarative handoffs[] + Handoff.create.
> **Canonical contract:** [`packages/sdk/docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md) — "Agent handoffs (v1.16+)".
Handoffs let an agent transfer control to another peer agent — useful for routing, specialization, and multi-agent flows.
## Declarative
```ts
const billing = await Agent.create({
apiKey, model,
name: "billing-agent",
systemPrompt: "You handle billing questions.",
});
const router = await Agent.create({
apiKey, model,
name: "router",
systemPrompt: "Triage. Transfer to billing-agent if the user mentions invoice or charge.",
handoffs: [billing], // synthesized tool: transfer_to_billing-agent
});
await (await router.send("I was charged twice this month")).wait();
// Router calls transfer_to_billing-agent → billing handles the rest.
```
## Handoff-as-tool
Handoffs are implemented as synthetic tools (ADR D214). The agent decides to "call" `transfer_to_` like any other tool. Inspired by `openai-agents-python` — proven pattern.
## `Handoff.create` for customization
```ts
import { Handoff } from "@theokit/sdk";
import { z } from "zod";
const handoff = Handoff.create(billing, {
toolName: "escalate_to_billing",
inputType: z.object({ reason: z.string() }),
onHandoff: async (input) => console.log("[handoff]", input.reason),
inputFilter: (history) => history.slice(-5), // only last 5 messages
});
```
## Guards
- **Peer-to-peer (D217)** — handoffs are not parent-child. The receiver runs independently.
- **Max depth 5 (D218)** — default chain limit. Configurable.
- **Single-flight per pair (D221)** — same (sender, receiver) pair can't recurse within one `send()`.
- **First-wins on parallel handoff tools (D226)** — if the model emits 2 handoffs in one turn, only the first executes.
- **CloudAgent throws `UnsupportedRunOperationError`** (D122) — handoffs are local-only in v1.
## API reference
---
# Hooks
Source: https://docs.usetheo.dev/theokit/concepts/hooks
Lifecycle hooks for plugins — pre_tool_call veto, post_assistant_reply, and 6 others.
> **Canonical contract:** [`packages/sdk/docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md) — `HookName` enum, `Plugin` discriminated union.
Hooks are typed extension points in the agent loop. Plugins register handlers; the loop invokes them at fixed lifecycle moments. The list of hook names is **closed** (8 fixed hooks, ADR D100) — adding a new one is a SDK release, not a runtime concern.
## The 8 hooks
| Hook | When | Can veto? |
|---|---|---|
| `pre_user_send` | Before user prompt added to history | No |
| `post_user_send` | After user prompt added | No |
| `pre_tool_call` | Before agent invokes a tool | **Yes** (return `{ block: true, message? }`) |
| `post_tool_call` | After tool returns | No |
| `pre_assistant_reply` | Before model generates reply | No |
| `post_assistant_reply` | After model emits final assistant message | No |
| `pre_dispose` | Before agent disposes | No |
| `post_dispose` | After agent disposed | No |
## Veto pattern
`pre_tool_call` is the only veto hook. Returning `{ block: true, message: "reason" }` aborts the tool call — the agent receives a synthetic `tool_result isError: true` and continues. Never throw from a veto hook (ADR D101).
```ts
import { Plugin } from "@theokit/sdk";
const noShellInProd = Plugin.create({
kind: "hook",
name: "no-shell-in-prod",
hooks: {
pre_tool_call: (ctx) => {
if (ctx.toolName === "shell" && process.env.NODE_ENV === "production") {
return { block: true, message: "Shell tool disabled in production." };
}
},
},
});
const agent = await Agent.create({ apiKey, model, plugins: [noShellInProd] });
```
## Plugin context
Every hook receives a typed context object. The shape depends on the hook. In dev mode the context is sealed via Proxy (ADR D99) — assigning to it throws, helping you catch accidental mutation.
Example `PreToolCallContext`:
```ts
type PreToolCallContext = {
toolName: string;
toolArgs: unknown;
agentId: string;
runId: string;
turnIndex: number;
// ...
};
```
See the [Plugins concept](./plugins) for the full Plugin contract.
## API reference
---
# MCP (Model Context Protocol)
Source: https://docs.usetheo.dev/theokit/concepts/mcp
Connect to stdio + HTTP MCP servers, including OAuth 2.1 PKCE flows.
> **Canonical contract:** [`packages/sdk/docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md) — `mcpServers` option, OAuth flow.
[Model Context Protocol](https://modelcontextprotocol.io) is an open standard for connecting AI agents to data sources and tools. The SDK is a first-class MCP client.
## Stdio servers (local processes)
Most MCP servers ship as npx-runnable Node processes:
```ts
const agent = await Agent.create({
apiKey, model,
mcpServers: {
filesystem: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"],
},
puppeteer: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-puppeteer"],
},
},
});
```
The SDK spawns each server, discovers its tools/resources/prompts, and exposes them to the agent loop. On `dispose()`, all child processes are terminated.
## HTTP servers (remote)
For remote MCP servers (e.g. SaaS APIs):
```ts
mcpServers: {
notion: {
type: "http",
url: "https://mcp.notion.com",
headers: { Authorization: `Bearer ${process.env.NOTION_TOKEN}` },
},
}
```
## OAuth 2.1 PKCE (remote SaaS APIs)
For MCP servers that require OAuth (e.g. Notion, Linear), the SDK ships a PKCE flow:
```bash
pnpm exec theokit-mcp-auth-notion --setup
```
This walks the user through browser auth, stores the token in OS keychain (fallback: `~/.theokit/mcp-tokens.json` with 0600 perms — ADR D41), and the agent picks it up automatically:
```ts
mcpServers: {
notion: {
type: "http",
url: "https://mcp.notion.com",
oauth: { provider: "notion" },
},
}
```
See the [mcp-oauth-notion example](https://github.com/usetheodev/theokit-sdk/tree/main/examples/mcp-oauth-notion).
## MCP config files
Beyond inline config, the SDK reads `.theokit/mcp.json` (project) and `~/.theokit/mcp.json` (user). Project entries override user entries by key. See [Configuration](./configuration).
## Configuration reference
```ts
type McpServerConfig =
| { command: string; args?: string[]; env?: Record } // stdio
| { type: "http"; url: string; headers?: Record; oauth?: McpOauthConfig };
```
## API reference
---
# Memory
Source: https://docs.usetheo.dev/theokit/concepts/memory
Long-term memory — SQLite + sqlite-vec + FTS5 by default, LanceDB optional, pluggable adapters.
> **Canonical contract:** [`packages/sdk/docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md) — `Memory.*`, embedding adapters, MemoryAdapter interface.
Memory lets an agent recall facts across sessions. The SDK ships:
- **Default backend:** SQLite + sqlite-vec + FTS5 — zero setup, written to `.theokit/memory/`.
- **LanceDB backend** (opt-in via `@lancedb/lancedb`) — scales beyond ~100k facts.
- **Pluggable adapters:** `@theokit/memory-supermemory`, `@theokit/memory-honcho`, `@theokit/memory-mem0`.
## Enable Active Memory
```ts
const agent = await Agent.create({
apiKey, model,
memory: { enabled: true },
});
```
That's it. The SDK adds two hooks (ADR D145):
1. `pre_user_send` — query memory for relevant facts, inject as `` block in the prompt.
2. `post_assistant_reply` — extract new facts from the conversation, store them.
## Direct API
For programmatic access:
```ts
await agent.memory.write({ content: "User's home city is Brasília." });
const hits = await agent.memory.recall("home city");
await agent.memory.delete(hits[0].id);
```
## Embedding providers
Memory uses embeddings to do semantic recall. Built-in adapters (ADR D11):
| Provider | Env var |
|---|---|
| OpenAI (`text-embedding-3-small`) | `OPENAI_API_KEY` |
| Mistral | `MISTRAL_API_KEY` |
| OpenRouter | `OPENROUTER_API_KEY` |
| Voyage | `VOYAGE_API_KEY` |
| DeepInfra | `DEEPINFRA_API_KEY` |
| Ollama (local) | none |
Configure via `memory: { embedding: { provider: "voyage" } }`.
## LanceDB (scale)
```bash
pnpm add @lancedb/lancedb
```
```ts
memory: { enabled: true, backend: "lancedb" }
```
Migration tool: `pnpm exec theokit-migrate-memory` (CLI, ADR D44).
## Custom adapters
Implement the `MemoryAdapter` interface (ADR D141) and pass via `memory: { adapter: myAdapter }`. The three shipped adapter packages are reference implementations:
- `@theokit/memory-supermemory` — MIT, zero-dep cloud client (default suggestion)
- `@theokit/memory-honcho` — dialectic reasoning (AGPL — read the README disclosure)
- `@theokit/memory-mem0` — cloud-only, unique `history(id)` API (CVSS disclosure in README)
## API reference
---
# Plugins
Source: https://docs.usetheo.dev/theokit/concepts/plugins
Plugin.create — discriminated union by kind, sealed PluginContext, closed HookName enum.
> **Canonical contract:** [`packages/sdk/docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md) — ADRs D97-D104.
Plugins are the extension point of the SDK. The contract lives at `internal/plugins/` (ADR D97) and reaches consumers via `Plugin.create` + the `Plugin` discriminated union.
## Plugin kinds
```ts
type Plugin =
| { kind: "hook"; ... } // lifecycle hooks
| { kind: "tool"; ... } // toolset provider
| { kind: "cache"; ... } // semantic cache (Cache.semantic().asPlugin())
| { kind: "telemetry"; ... } // custom telemetry sink
| ... // see docs.md for full list
```
The union is **closed by `kind`** (ADR D98) — TypeScript forces exhaustive handling.
## `Plugin.create`
```ts
import { Plugin } from "@theokit/sdk";
const myPlugin = Plugin.create({
kind: "hook",
name: "rate-limit-tool-calls",
hooks: {
pre_tool_call: async (ctx) => {
if (await isRateLimited(ctx.agentId)) {
return { block: true, message: "Slow down." };
}
},
},
});
const agent = await Agent.create({ apiKey, model, plugins: [myPlugin] });
```
## `PluginContext` (sealed in dev mode)
The context object passed to plugin hooks is sealed via Proxy in dev mode (ADR D99). Trying to assign to it throws — catches accidental mutation early.
In production, the Proxy is bypassed (no overhead).
## Hooks
See the [Hooks concept](./hooks) — 8 closed hook names (ADR D100), `pre_tool_call` is the only veto hook (ADR D101).
## Toolset plugins
A plugin with `kind: "tool"` provides a `Toolset`. The `ToolRegistry` is 3-layer (ADR D102):
1. **Registration** — plugin declares its tools.
2. **Exposure** — agent decides which subset to surface to the model per turn (via `check_fn` predicate, TTL-cached 30s — ADR D103).
3. **Availability** — per-turn check.
A `Toolset` is a flat list — no `extends` (ADR D104). Composition via multiple plugins.
## Discovery
Beyond programmatic `plugins: [...]`, the SDK loads `.theokit/plugins/*.ts` (project) and `~/.theokit/plugins/*.ts` (user). Each file must `export default` a `Plugin`.
## API reference
---
# AWS Bedrock + GCP Vertex
Source: https://docs.usetheo.dev/theokit/concepts/providers-bedrock-vertex
Enterprise cloud providers — Bearer/SigV4 auth, region routing, dialect dispatch.
> **Canonical contract:** [`packages/sdk/docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md) — "Bedrock provider" + "Vertex AI provider".
The SDK ships two built-in profiles for enterprise cloud LLM providers — Bedrock (AWS) and Vertex (GCP). Both reuse the existing transport architecture (ADRs D105/D106) — `ProviderProfile` is data-only, transport is orthogonal.
## Bedrock
Claude models on AWS. v1 uses Bearer-only auth (no SigV4) — see ADR D286.
```ts
const agent = await Agent.create({
apiKey: process.env.AWS_BEARER_TOKEN_BEDROCK ?? "__bedrock_lazy_token__",
model: { id: "bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0" },
});
```
The model id has 3 parts:
- `bedrock/` — provider prefix (stripped before sending)
- `us.` (or `eu.`, `apac.`, `jp.`, `global.`) — region prefix → routes to the right Bedrock regional endpoint (ADR D290)
- `anthropic.claude-...` — the model id Bedrock expects
### Auto-token via AWS credential chain
For long-running services, install the optional peers and the SDK auto-generates a Bearer from your standard AWS credentials:
```bash
pnpm add @aws/bedrock-token-generator @aws-sdk/credential-providers
```
The SDK reads from env vars → `AWS_PROFILE` → `~/.aws/credentials` → IMDS, in that order, mints a Bearer, and caches it for 1.5h (ADR D295).
### Common gotcha
The AWS account must complete the **"Anthropic use case details"** form in the Bedrock Console — separate from "Model access". Without it, every call returns `ResourceNotFoundException: Model use case details have not been submitted`. The error mapper surfaces an actionable message.
## Vertex AI
Both Claude and Gemini available. ADC auth via `google-auth-library` (required peer, ADR D288).
```bash
pnpm add google-auth-library
gcloud auth application-default login
export GOOGLE_CLOUD_PROJECT="your-project-id"
```
Two dialects:
```ts
// Claude on Vertex (Anthropic Messages API via :rawPredict)
model: { id: "vertex/anthropic/claude-sonnet-4-5-20250929" }
// Gemini on Vertex (OpenAI-compat endpoint)
model: { id: "vertex/google/gemini-2.0-flash-001" }
```
The SDK dispatches between dialects at stream time (ADR D291/D292). Both reuse existing clients (`OpenAIClient` rewriter for Gemini, dedicated `VertexAnthropicClient` for Claude).
### Global location
`vertex/anthropic/...` with `VERTEX_LOCATION=global` forces baseUrl `aiplatform.googleapis.com` (no region prefix) — fixes the cline#10287 routing issue (ADR D293).
## Deferred to v1.x
| Feature | ADR |
|---|---|
| Bedrock streaming (Event Stream binary parser) | D302 |
| Bedrock Converse + Computer Use | D296 |
| Vertex WIF (Workload Identity Federation) walkthrough | D297 |
| SigV4 transport for Bedrock | D298 |
## API reference
---
# Security
Source: https://docs.usetheo.dev/theokit/concepts/security
Secret redaction at output boundaries, path traversal defense, TOCTOU primitives.
> **Canonical contract:** [`packages/sdk/docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md) — "Security — secret redaction" + "Security — path traversal + TOCTOU".
The SDK takes two security concerns seriously: **secrets in observable output** and **filesystem operations on untrusted input**.
## Secret redaction (ADR D68-D73)
Every output boundary (logs, telemetry attrs, error metadata, transcripts) is wrapped by `redactSecrets`. The canonical implementation lives at `internal/security/redact.ts` — there's one source of truth.
**What gets redacted:**
- API key patterns (`sk-...`, `Bearer ey...`, `xoxb-...`)
- AWS access keys (`AKIA[0-9A-Z]{16}`)
- GitHub tokens (`ghp_...`, `gho_...`)
- JWT-like tokens (3-part base64url)
- URL `?token=` / `?api_key=` query params
- Environment-variable values when stringified
**Two-bucket masking (ADR D71):**
- Short tokens (less than 18 chars) → `***`
- Long tokens → `prefix...suffix` (first 4 + last 4)
**Opt-out (NOT recommended):**
```bash
export THEOKIT_REDACT_SECRETS=0
```
Emits a one-time stderr warning (ADR D70). The env var is snapshotted at module init (ADR D69) — prompt-injection defense.
**`codeFile: true` flag (ADR D72):**
When generating `.env.example` placeholders or similar, set `codeFile: true` to skip PARAM_PATTERN redaction — preserves intentional examples like `OPENAI_API_KEY=sk-...`.
## Path traversal + TOCTOU (ADRs D79-D85)
When the SDK writes to user-supplied paths (skills, agents, personalities, OAuth tokens), it routes through path-safety primitives:
| Primitive | Use |
|---|---|
| `safePathJoin(base, ...parts)` | Resolves THEN prefix-checks (defeats normalized `..` escape) |
| `sanitizeIdentifier(name)` | Strict grammar `^[a-z0-9][a-z0-9_-]*$` |
| `createExclusive(path, mode)` | `O_EXCL` open with default mode 0o600 |
| `casUpdate(db, stmt, params)` | SQLite optimistic compare-and-swap |
Available as `Security.*` namespace and at `@theokit/sdk/path-safety` sub-export (separate from main barrel to keep DTS bundle clean).
```ts
import { Security } from "@theokit/sdk";
const safe = Security.safePathJoin(".theokit/skills", userProvidedName);
```
## CI lint gate (ADR D85)
`packages/sdk/tests/lint/no-unredacted-sink.test.ts` runs in CI and fails if a new `console.log`/`writeFile`/`span.setAttribute` lands in `src/` without going through `redactSecrets`. Each whitelisted file carries a rationale comment.
`packages/sdk/tests/lint/no-todo-fixme.test.ts` prevents production stubs.
## API reference
---
# Sessions
Source: https://docs.usetheo.dev/theokit/concepts/sessions
How conversations persist across processes via Agent.resume and the native Claude Code .jsonl transcript.
> **Canonical contract:** [`docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/docs.md) — `Agent.resume`, native session transcript contract.
A *session* is the message history, tool registry, and optional memory of a single agent. Every `LocalAgent` gets a stable `agentId` and persists its session as a **native Claude Code `.jsonl` transcript** at `/projects//.jsonl` (`baseDir` defaults to `~/.theokit`; set `local.baseDir: "~/.claude"` for Claude Code CLI `--continue` interop).
## The agent id
```ts
const agent = await Agent.create({ apiKey, model });
console.log(agent.agentId); // "agent-7a3c2f..."
```
Two namespaces:
- `agent-...` — LocalAgent (default).
- `bc-...` — CloudAgent (TheoCloud, pre-release).
`Agent.resume(id)` auto-detects by prefix and rehydrates from the appropriate backend.
## Resume across processes
```ts
// Process 1
const agent = await Agent.create({...});
const id = agent.agentId;
await (await agent.send("Hi, my name is Paulo.")).wait();
await agent.dispose();
// Process 2 (later, fresh Node process)
const resumed = await Agent.resume(id);
const r = await (await resumed.send("What's my name?")).wait();
console.log(r.result); // "Your name is Paulo."
```
The store is filesystem-based — survives crashes, replicable via `cp -r`, observable via `cat`.
## `Agent.getOrCreate`
Idempotent helper that tries `resume(id)` first; if the id doesn't exist, falls back to `create(options)` with that id:
```ts
const agent = await Agent.getOrCreate({
agentId: "stable-id-from-my-app",
apiKey, model,
});
```
Use this when your application has a stable per-user id (e.g. Telegram chat id) and wants "one agent per user, ever".
## What's persisted
| Persisted | Not persisted |
|---|---|
| Message history (user + assistant + tool) | Background MCP servers (re-spawned on resume) |
| Tool registry definitions | LLM client state (re-resolved from env) |
| Active memory facts (if memory enabled) | Live streams |
| Personality preset (if set) | Hooks (re-loaded from .theokit/hooks.json) |
| Cron job snapshots | Telemetry traces (already exported) |
## Disposal & cleanup
Always dispose:
```ts
await using agent = await Agent.create({...}); // Symbol.asyncDispose
// ...
```
Or explicit:
```ts
try { /* ... */ } finally { await agent.dispose(); }
```
`dispose()` is idempotent. The transcript on disk remains until you explicitly delete it (`/projects//.jsonl`).
## API reference
---
# Skills — Google Workspace
Source: https://docs.usetheo.dev/theokit/concepts/skills-google-workspace
Wire Calendar, Drive, Sheets, Docs, Gmail, Slides, and Forms into your agent via the combined google-workspace-mcp server.
> **Concept:** `@theokit/skills-google-workspace` is a **skills bundle**, not a gateway. Gateways carry user conversation **into** the agent; skills are **tools the agent uses**. Use this package when you want your agent to read your calendar, summarize a Doc, append rows to a Sheet, etc.
A single workspace package that emits one `McpServerConfig` running the [`google-workspace-mcp`](https://github.com/pm990320/google-workspace-mcp) server (pm990320, MIT, GitHub Actions OIDC trusted publisher). One subprocess covers seven Google products. Read-only is the default per ADR D343.
## Quick start
```bash
pnpm add @theokit/skills-google-workspace
npx theokit setup gworkspace # validates OAuth client + delegates to upstream installer
```
```ts
import { Agent } from "@theokit/sdk";
import { googleWorkspace } from "@theokit/skills-google-workspace";
const agent = await Agent.create({
apiKey: process.env.OPENROUTER_API_KEY!,
model: { id: "openai/gpt-4o-mini" },
mcpServers: googleWorkspace(), // read-only by default
});
const reply = await (await agent.send("What's on my calendar tomorrow?")).wait();
console.log(reply.result);
await agent.dispose();
```
## Tools exposed
The upstream server provides **95+ tools** across:
| Product | Examples |
|---|---|
| Calendar | `listCalendars`, `listCalendarEvents`, `createCalendarEvent` |
| Drive | `listGoogleDocs`, `searchGoogleDocs`, `getRecentGoogleDocs` |
| Docs | `readGoogleDoc`, `appendToGoogleDoc`, `insertText`, comments |
| Sheets | `readSpreadsheet`, `writeSpreadsheet`, `appendSpreadsheetRows` |
| Gmail | `listGmailMessages`, `searchGmail`, `createGmailDraft`, `sendGmailDraft` (draft workflow for safety) |
| Slides | `listPresentations`, `createPresentation`, `addSlide`, `addTextToSlide` |
| Forms | `listForms`, `readForm`, `getFormResponses`, `createForm` |
## Read-only by default (ADR D343)
```ts
googleWorkspace() // safe — write tools return errors
googleWorkspace({ writable: true }) // unlocks Drive write, Gmail send, Calendar create, ...
```
The OAuth consent grants broad scopes (the upstream server uses one wide consent screen). `--read-only` is a runtime safety on top of that, NOT scope narrowing. To genuinely have narrow scopes, manage scope selection inside Google Cloud Console at consent time.
## CLI setup walkthrough
```bash
npx theokit setup gworkspace
```
What it does:
1. **EC-1 guard (MUST FIX):** validates that `~/.google-mcp/credentials.json` is a *Desktop application* OAuth client (not the *Web application* type that silently fails downstream). Bails with an actionable error if the wrong type is detected.
2. **EC-2 guard:** validates JSON shape before shelling out.
3. **Delegates** to `npx google-workspace-mcp setup` and `accounts add default` for the actual OAuth dance.
Flags:
- `--probe` — runs `npx google-workspace-mcp status` after staging (10s timeout per upstream call).
- `--credentials-path ` — override the default `~/.google-mcp/credentials.json`.
- `--non-interactive` — refuses prompts; suitable for CI.
- `--writable calendar,drive` — informational only; the actual write toggle lives in your `googleWorkspace({ writable: true })` call.
## Multi-account
```bash
npx google-workspace-mcp accounts add work
npx google-workspace-mcp accounts add personal
```
```ts
const workAgent = await Agent.create({
apiKey: ...,
mcpServers: googleWorkspace({ account: "work" }),
});
```
The factory keys the MCP server under `gworkspace-work` (or `gworkspace` for the default account), so it does not collide with other agents.
## ADRs
- **D340** — separate workspace package (matches gateway-* pattern, D170/D171)
- **D341** — single factory returns `Record`
- **D342** — stdio launch via `npx google-workspace-mcp serve`
- **D343** — read-only-by-default
- **D344** — credentials at upstream default `~/.google-mcp/credentials.json`
- **D345** — OAuth delegated to upstream `accounts add` CLI
- **D346** — `theokit setup` is a new CLI verb (future-proofs `setup notion`, `setup linear`)
- **D347** — six cookbook recipes, last one combines Calendar + Drive
- **D348** — package version 0.1.0 per D181 pre-1.0 policy
## See also
- [`examples/skills-google-workspace/`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/skills-google-workspace) — six recipes
- [`@theokit/skills-google-workspace`](https://github.com/usetheodev/theokit-sdk/tree/main/packages/skills-google-workspace) — package source
- Upstream MCP server: [github.com/pm990320/google-workspace-mcp](https://github.com/pm990320/google-workspace-mcp)
---
# Streaming
Source: https://docs.usetheo.dev/theokit/concepts/streaming
Read tokens, tool calls, and artifacts as they arrive — AsyncIterator of SDKMessage.
> **Canonical contract:** [`packages/sdk/docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md) — `SDKMessage` discriminated union, `run.stream()`, Vercel AI Data Stream v1 wire format.
`agent.send(prompt)` returns a `Run`. You can `await run.wait()` for the full result, OR iterate `run.stream()` to consume events as they happen.
## Iterate the stream
```ts
const run = await agent.send("Tell me a story about robots.");
for await (const msg of run.stream()) {
switch (msg.type) {
case "text_delta":
process.stdout.write(msg.text);
break;
case "tool_call":
console.log("[calling tool]", msg.name);
break;
case "tool_result":
console.log("[tool returned]", msg.content);
break;
case "finished":
console.log("\n[done]", msg.stopReason);
break;
}
}
```
`SDKMessage` is a discriminated union. The full variant list:
| `type` | Meaning |
|---|---|
| `text_delta` | Incremental text token from the model |
| `tool_call` | Model emitted a tool call (name + args) |
| `tool_result` | Tool execution finished (success or error) |
| `partial_object` | When using `streamObject`, partial typed payload |
| `complete_object` | When using `streamObject`, final typed payload |
| `finished` | Run is over (with `stopReason: end_turn | max_tokens | tool_use | ...`) |
| `error` | A non-recoverable error was emitted |
## Structured streaming (`streamObject`)
For typed payloads, use `Agent.streamObject`:
```ts
import { z } from "zod";
const schema = z.object({ title: z.string(), bullets: z.array(z.string()) });
const stream = await Agent.streamObject({
apiKey, model, schema,
prompt: "Outline a blog post about Rust ownership.",
});
for await (const msg of stream) {
if (msg.type === "partial_object") console.log("partial:", msg.value);
if (msg.type === "complete_object") console.log("final:", msg.value);
}
```
The SDK uses a synthetic forced tool (ADR D33) — the model calls a single tool whose schema is your Zod schema, the SDK validates each partial, and you get progressively-typed updates.
## React hooks
For browser apps, `@theokit/react` exposes hooks that consume the same wire format (Vercel AI Data Stream v1, ADR D38):
- `useTheoChat` — multi-turn chat
- `useTheoCompletion` — single-shot text
- `useTheoAssistant` — object streaming
See the [react-nextjs example](https://github.com/usetheodev/theokit-sdk/tree/main/examples/react-nextjs).
## API reference
---
# Tasks
Source: https://docs.usetheo.dev/theokit/concepts/tasks
Observable async work — Task.submit / list / get / cancel / subscribe.
> **Canonical contract:** [`packages/sdk/docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md) — `Task` namespace.
The `Task` namespace gives every piece of asynchronous work in the SDK a stable id, a 5-state lifecycle, an event stream, and a way to cancel it — without you writing your own job queue. It is the second gap closed against Hermes Agent's kanban registry, the first being [`ACP`](./acp-server).
ADRs: **D361-D374**. Edge cases absorbed: **EC-1..EC-16**.
## The 5 states
```
queued ─▶ running ─▶ finished
├─▶ error
└─▶ cancelled
queued ──────────────▶ cancelled (direct, no run)
```
Closed enum (D362). Transitions are acyclic — once a task is `finished` it stays `finished` until evicted.
## Submitting work
```ts
import { Task } from "@theokit/sdk";
const handle = await Task.submit("custom", async (ctx) => {
ctx.emit({ step: "starting" });
await doSomeWork({ signal: ctx.signal });
return "result";
});
console.log(handle.id); // queued task id
```
`Task.submit(kind, work, options?)` takes:
- **`kind`** — one of `run | batch | workflow | cron | custom`. Discriminator only (D374); the registry does not branch on it.
- **`work`** — `(ctx) => Promise | T`. `ctx.signal` is the AbortSignal honored on cancel; `ctx.emit(payload)` produces a `progress` event observable via `subscribe`.
- **`options`** — `{ id?: string; meta?: Record; signal?: AbortSignal }`.
Returns a `TaskHandle` in `state: "queued"` immediately. Transitions happen in the background.
## Querying
```ts
// Single handle
const handle = await Task.get("abc-123");
// All handles, optionally filtered
const running = await Task.list({ state: "running" });
const cronFires = await Task.list({ kind: "cron", limit: 50 });
```
`Task.list(filter?)` returns up to `filter.limit ?? 100` handles. `JsonFileTaskStore` hard-caps loaded entries at 256 — page deeper by walking `submittedBefore`.
## Subscribing to progress
```ts
for await (const event of Task.subscribe(handle.id)) {
switch (event.type) {
case "submitted": console.log("queued", event.taskId); break;
case "started": console.log("running"); break;
case "progress": console.log("progress", event.payload); break;
case "finished": console.log("done", event.result); return;
case "errored": console.error("err", event.error); return;
case "cancelled": console.log("cancelled"); return;
}
}
```
- **Late attach** is safe: the ring buffer (cap 64, D372) replays buffered events before tailing live. If the buffer was full at attach, the first replayed event carries a `truncated: true` flag.
- **Cleanup is automatic**: terminal events close the iterator; `break` inside the `for await` cleans up the subscriber (EC-10).
## Cancelling
```ts
const { cancelled, alreadyTerminal } = await Task.cancel("abc-123");
```
`Task.cancel` is **idempotent** (D365):
- Unknown id → `{ cancelled: false, alreadyTerminal: false }`. No throw.
- Terminal task → `{ cancelled: false, alreadyTerminal: true }`.
- Queued task → state transitions to `cancelled` immediately. AbortController not invoked.
- Running task → `ctx.signal` is aborted; the work function unwinds naturally.
## Persistence
```ts
import { Task } from "@theokit/sdk";
Task.configure({
store: { backend: "json", dir: "/var/lib/myapp/tasks" },
maxConcurrent: 16,
retentionMs: 24 * 60 * 60 * 1000, // 7 days
});
```
- **`memory`** (default) — transient, lost on process exit. Retention 1 hour.
- **`json`** (opt-in) — one file per task. Survives restart. Retention 7 days. **Single-process** (EC-15) — concurrent writers from multiple Node processes may corrupt the store. v0.2 will add a SQLite backend behind the same interface.
> Configure once, before the first `submit`. Subsequent `configure()` calls are no-ops with a one-line stderr warning (EC-13).
## ID grammar
User-supplied ids must match `^[a-z0-9][a-z0-9_-]*$` and must NOT start with the reserved prefixes `wf-`, `b-`, or `cron-` (D368). Auto-generated ids use `crypto.randomUUID()`.
## CLI
```bash
theokit tasks list
theokit tasks list --state running --json
theokit tasks inspect
theokit tasks cancel
```
The CLI reads the `JsonFileTaskStore` at `$THEOKIT_HOME/tasks/` (fallback: `/.theokit/tasks/`). Cross-process cancel sets `cancelRequested: true` on the handle — the owning process honors it at the next checkpoint (EC-7, best-effort).
## Wrapping existing work
The simplest pattern: caller envelops async work themselves.
```ts
const handle = await Task.submit("run", async (ctx) => {
const run = await agent.send(prompt, { signal: ctx.signal });
for await (const msg of run.stream()) {
ctx.emit({ chunk: msg });
}
return (await run.wait()).result;
});
```
Fan-out batch with parent / children:
```ts
const parent = await Task.submit("batch", async (ctx) => {
return Promise.all(
inputs.map((input) =>
Task.submit("run", async (childCtx) => {
childCtx.emit({ input });
return await processOne(input, childCtx.signal);
}, { meta: { parentId: ctx.signal ? "auto" : undefined } }),
),
);
});
```
> Auto-wrapping of `Agent.send` / `Agent.batch` / `Workflow.run` / `Cron` via an `{ task: true }` option on each is deferred to **v0.2**. v1 ships the primitive only; the user-side pattern above covers every observability use case today.
## Telemetry
Three OTel spans emitted via the existing seam (D34, D371):
- `task.submit` — attrs: `task.id`, `task.kind`.
- `task.transition` — attrs: `task.id`, `task.state.from`, `task.state.to`.
- `task.cancel` — attrs: `task.id`, `task.cancel.reason`, `task.cancel.via`.
When OTel is absent, spans are no-ops (D34 safe-noop). No extra peer deps required.
## Cloud rejection
`Task` is local-only in v1. CloudAgent surfaces wrapping with `{ task: true }` throw `UnsupportedTaskOperationError` (D370). Cloud support waits for TheoCloud GA.
## See also
- [ACP server](./acp-server) — the first Hermes Agent gap closed (2026-05-27).
- [Cron](./cron) — scheduled jobs; pair with `Task.submit("cron", ...)` for observability.
- [Workflows](./workflows) — declarative composition; pair with `Task.submit("workflow", ...)`.
---
# Telemetry
Source: https://docs.usetheo.dev/theokit/concepts/telemetry
OpenTelemetry spans, privacy-by-default, lazy load.
> **Canonical contract:** [`packages/sdk/docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md).
The SDK emits OpenTelemetry spans for agent runs, tool calls, workflow steps, cache lookups, eval rows, and handoff transfers. Telemetry is **lazy-loaded** — if `@opentelemetry/api` isn't installed, you pay zero cost (ADR D34).
## Enable
```bash
pnpm add @opentelemetry/api
```
Plus your exporter of choice (Langfuse, Sentry, PostHog, console):
```bash
pnpm add @langfuse/otel-sdk
```
The SDK auto-detects exporters via `createRequire` feature-detect (ADR D42). No code change needed.
## Spans emitted
| Span | Where |
|---|---|
| `agent.send` | Per `agent.send()` call |
| `agent.tool_call.` | Per tool invocation |
| `workflow.run` | Per `Workflow.run()` |
| `workflow.step.` | Per workflow step (retries share kind) |
| `cache.lookup` / `cache.store` | Semantic cache hits/misses |
| `eval.run` / `eval.row.` | Eval suite |
| `handoff.transfer` | Per peer handoff |
| `memory.recall` / `memory.write` | Per memory op |
## Privacy by default
Attribute values are routed through `redactSecrets` via the `wrapSpan` decorator (ADR D34 + D68). No raw API key, OAuth token, or env-var value lands in spans.
## Toggle
```bash
export THEOKIT_TELEMETRY=0 # disable entirely
export THEOKIT_REDACT_SECRETS=0 # disable redaction (NOT recommended)
```
Disabling redaction emits a one-time stderr warning (ADR D70).
## Custom tracer
For a custom tracer (e.g. your team's wrapper):
```ts
import { trace } from "@opentelemetry/api";
trace.setGlobalTracerProvider(myProvider);
// SDK picks it up via the standard OTel global tracer
```
## Telemetry namespace (SDK-internal)
The `internal/telemetry/tracer.ts` module is the canonical wrapper. All other modules (cache, eval, handoff, workflow telemetry) go through it — never call `setAttribute` directly on raw spans.
## API reference
The `Telemetry` namespace itself isn't exposed publicly — telemetry happens transparently inside agent operations. Configure via env vars + OTel ecosystem.
---
# Tools
Source: https://docs.usetheo.dev/theokit/concepts/tools
Tool.create, Zod schemas, error handling, dynamic tool dispatch.
> **Canonical contract:** [`packages/sdk/docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md) — `Tool.create` + `Agent.create({ tools })`.
A tool is a typed function the agent can call. The SDK validates inputs against a Zod schema, surfaces errors as `tool_result isError: true` (never throws into the loop), and converts the schema to JSON Schema for the provider.
## Define a tool
```ts
import { Tool } from "@theokit/sdk";
import { z } from "zod";
const getWeather = Tool.create({
name: "get_weather",
description: "Look up the current weather in a given city.",
inputSchema: z.object({
city: z.string().describe("City name, e.g. 'Brasília'"),
}),
async execute({ city }) {
return await fetch(`https://api.weather.com/${city}`).then((r) => r.text());
},
});
```
Pass `tools: [getWeather]` to `Agent.create` and the agent decides when to call it based on the user prompt + tool description.
## Error handling
`execute` can throw — the SDK catches and converts to a `tool_result isError: true` block that the agent receives as part of the conversation. The loop continues; the agent typically retries or apologizes.
```ts
async execute({ city }) {
if (city.length === 0) throw new Error("City is required");
// The agent sees: { type: "tool_result", isError: true, content: "City is required" }
}
```
This is intentional — see [`docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md) and ADR D89.
## Repair: malformed model output
If the model emits a malformed tool call (extra whitespace in JSON, missing trailing brace, etc.), the SDK applies 3 idempotent repairs before giving up. See ADR D87. You don't configure this — it's automatic.
## Dynamic tool dispatch (subagents / toolsets)
Agents can expose tools that aren't all available at once. The `Toolset` primitive lets you group tools and check availability per turn:
```ts
const agent = await Agent.create({
// ...
tools: [
{ name: "web", tools: [search, fetch] },
{ name: "shell", tools: [run, ls] },
],
});
```
See `docs.md` for the full Toolset / Subagent surface.
## MCP tools
Tools can also come from MCP servers (Model Context Protocol). MCP servers expose tools via stdio or HTTP and the SDK consumes them transparently. See [MCP](./mcp).
## API reference
---
# Workflows
Source: https://docs.usetheo.dev/theokit/concepts/workflows
Declarative multi-step pipelines — Workflow.create with 7 control-flow primitives.
> **Canonical contract:** [`packages/sdk/docs.md`](https://github.com/usetheodev/theokit-sdk/blob/main/packages/sdk/docs.md) — "Workflows (v1.17+)".
`Workflow` is a declarative DSL for multi-step pipelines. Inspired by Mastra + Temporal. Use when imperative `agent.send` loops get awkward.
## Build + run
```ts
import { Workflow, Agent } from "@theokit/sdk";
const triage = Workflow
.create({ name: "support-triage" })
.then("fetch", async (input: { ticketId: string }) => {
return { text: await db.getTicket(input.ticketId) };
})
.branch("route", [
[(s) => s.text.includes("refund"), {
step: { kind: "fn", id: "refund-flow", fn: async (s) => ({ result: "refund handled" }) },
}],
[(s) => s.text.includes("bug"), {
step: { kind: "fn", id: "bug-flow", fn: async (s) => ({ result: "bug logged" }) },
}],
])
.commit();
const result = await Workflow.run(triage, { ticketId: "T-1234" });
console.log(result.output);
```
## 7 primitives
| Primitive | Purpose |
|---|---|
| `.then(id, fn)` | Sequential step |
| `.parallel([...], { errorPolicy })` | Run N steps concurrently |
| `.branch(id, [[pred, branch], ...])` | First-match-wins routing |
| `.foreach(id, items, fn, { concurrency })` | Map over a collection |
| `.dowhile(id, predicate, body)` | Loop until predicate false |
| `.sleep(id, ms)` | Wait |
| `.suspend(payload?)` | Pause; resume via `Workflow.resume(...)` |
## Suspend / resume
For long-running flows (human in the loop, external webhook):
```ts
const wf = Workflow.create({ name: "pr-review" })
.then("draft", draftReview)
.suspend() // ← pause here
.then("publish", publishReview)
.commit();
const run = await Workflow.run(wf, input);
// run.status === "suspended", run.suspendPayload visible
// Later, after the human reviews:
const finished = await Workflow.resume({
workflowId: run.workflowId,
runId: run.runId,
resumeInput: { approved: true },
});
```
## Retry policies
Per-step Temporal-style retry (ADR D237):
```ts
.then("flaky", flakyApiCall, {
retry: { maxAttempts: 3, backoffMs: 500, coef: 2 },
})
```
## Cancellation
`AbortSignal` is honored at step boundaries (ADR D245). Pass via options:
```ts
const ctrl = new AbortController();
const run = Workflow.run(wf, input, { signal: ctrl.signal });
// elsewhere: ctrl.abort();
```
## Persistence
Default: in-memory. Opt-in JSON snapshots:
```ts
Workflow.run(wf, input, {
persistence: { backend: "json", dir: ".theokit/workflows" },
});
```
## Telemetry
OTel spans `workflow.run` + `workflow.step.` via the existing telemetry seam (ADR D241).
## CloudAgent
Workflow steps throw `UnsupportedRunOperationError` on CloudAgent — local-only in v1 (ADR D244).
## API reference
---
# Advanced
Source: https://docs.usetheo.dev/theokit/context/advanced
The context discovery registry and priority, the token/byte budget with truncation, refresh-time snapshots, and reload.
# Advanced context
Verified against `@theokit/sdk` (`src/types/context.ts` + the discovery runner).
## Discovery registry
With `settingSources: ["project"]`, the file context manager scans the working directory for the
2026 industry-standard set, concatenating by **priority** (lower number wins on conflict):
| Source | Pattern | Priority | Notes |
| --- | --- | --- | --- |
| `AGENTS.md` | `AGENTS.md` | 10 | The cross-tool standard. |
| `GEMINI.md` | `GEMINI.md` | 20 | Follows `@path` import directives. |
| `CLAUDE.md` | `CLAUDE.md` | 30 | Follows `@path` import directives. |
| Cursor rules | `.cursor/rules/*.mdc` | 40 | Globbed; path-scoped via `globs:`. |
| Theokit rules | `.theokit/rules/*.md` | 45 | Globbed; path-scoped via `paths:`/`globs:`. |
| Legacy Theokit | `.theokit/context/*.md` | 50 | Backward-compatible. |
| `THEO.md` | `.theokit/THEO.md` | 60 | Theokit-native. |
`CLAUDE.md` / `GEMINI.md` may pull in additional files via `@path` import directives — those are
resolved and folded in too.
## Path-scoped rules
`.theokit/rules/*.md` (and `.cursor/rules/*.mdc`) carry frontmatter that gates activation:
`alwaysApply: true` loads every send, while `paths:` / `globs:` load only when a file in the current
send's scope matches. You declare the scope per send with `SendOptions.contextPaths`. See
[Scope rules by path](/theokit/context/scope-rules-by-path) for a runnable walkthrough.
## The budget — `ContextSettings`
```ts
context: {
manager: "file", // the only backend today (default)
maxTokens: 8_000, // hard cap on tokens emitted into the system prompt
maxBytesPerFile: 40_000, // per-file truncation cap in chars (default 40_000, ~10k tokens)
maxBytesTotal: 120_000, // aggregate cap across all files (default 120_000)
}
```
- **Per-file truncation** — a file larger than `maxBytesPerFile` is truncated to a **70% head + 20%
tail** with a marker in between, so both the intro and the conclusion survive. Its snapshot `status`
becomes `"summarized"`.
- **Aggregate cap** — when the total exceeds `maxBytesTotal`, **lower-priority sources are dropped**
first (their `status` becomes `"excluded"` with a `reason`).
- **`maxTokens`** caps what reaches the system prompt regardless of byte budgets.
## Snapshots are refresh-time — `snapshot()` and `reload()`
The snapshot reflects the state at the **last refresh** — editing a context file mid-flight does not
auto-update the agent. Call **`agent.reload()`** to re-read the working directory and pick up changes,
then `snapshot()` again to confirm.
```ts
await agent.reload(); // re-discover + re-read context files
const fresh = await agent.context?.snapshot();
```
`snapshot()` is **secret-free by design** (ADR D-context): it never contains raw secrets, local
absolute paths, or exact token counts — safe to log, persist, and diff in golden tests.
## The snapshot shape
```ts
interface ContextSnapshot {
runtime: "local" | "cloud";
sources: ContextSource[]; // { name, path?, status, reason? }
budget?: { maxTokens?; usedTokens?: number | string[] };
}
```
## Reference
- [`SDKContextManager`](/theokit/reference/SDKContextManager) · [`ContextSettings`](/theokit/reference/ContextSettings) · [`ContextSnapshot`](/theokit/reference/ContextSnapshot) · [`ContextSource`](/theokit/reference/ContextSource)
---
# Overview
Source: https://docs.usetheo.dev/theokit/context
Give an agent structured file context from its working directory, and inspect exactly what it loaded with a secret-free snapshot.
# Context
Beyond the system prompt, an agent can carry **structured context** — files on disk that every run
sees without baking them into each message. Point an agent at a working directory and it discovers
context files (`AGENTS.md`, `CLAUDE.md`, `THEO.md`, `.cursor/rules/*.mdc`, `.theokit/context/*.md`),
loads them under a token budget, and folds them into the system prompt.
```ts
const agent = await Agent.create({
apiKey, model,
local: { cwd, settingSources: ["project"] }, // opt in to reading project files
context: {}, // enable the file context manager
});
const snap = await agent.context?.snapshot(); // inspect what actually loaded
```
- **`AgentOptions.context`** (`ContextSettings`) — enable context; tune `maxTokens`, `maxBytesPerFile`,
`maxBytesTotal`.
- **`local.settingSources: ["project"]`** — the opt-in that lets the manager read project files.
Without it, discovery finds nothing (safe default).
- **`agent.context`** (`SDKContextManager`) — the inspector; `snapshot()` returns a secret-free
`ContextSnapshot` of the sources it resolved and their budget status.
- **Per-tool context** — a *different* thing: a shared value (e.g. `projectRoot`) passed once on
`send()` reaches every tool via the 2nd `ToolContext` argument (see
[Tools › Advanced](/theokit/tools/advanced)).
## Next
- [Inspect loaded context](/theokit/context/inspect-loaded-context) — a runnable `snapshot()` example.
- [Advanced](/theokit/context/advanced) — discovery formats, the token budget, truncation, and refresh.
## Reference
- [`SDKContextManager`](/theokit/reference/SDKContextManager) · [`ContextSettings`](/theokit/reference/ContextSettings) · [`ContextSnapshot`](/theokit/reference/ContextSnapshot) · [`AgentOptions`](/theokit/reference/AgentOptions)
---
# Inspect loaded context
Source: https://docs.usetheo.dev/theokit/context/inspect-loaded-context
Enable the file context manager and read agent.context.snapshot() to see exactly which context files were loaded — deterministic, no LLM.
# Inspect loaded context
Turn on file context with `context: {}` and `settingSources: ["project"]`, then read
`agent.context.snapshot()` to see exactly what the manager resolved. Creating the agent and reading
the snapshot is a **local file read** — no network, no LLM.
```ts title="run.ts"
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
import { Agent } from "@theokit/sdk";
const here = dirname(fileURLToPath(import.meta.url)); // this dir holds AGENTS.md
const agent = await Agent.create({
apiKey: "theo_test_context", // fixture key — no network, no LLM
model: { id: "openai/gpt-4o-mini" },
local: { cwd: here, settingSources: ["project"] },
context: {},
});
const snap = await agent.context?.snapshot();
console.log("Runtime:", snap?.runtime);
const agents = snap?.sources?.find((s) => s.name.startsWith("AGENTS.md"));
console.log("AGENTS.md status:", agents?.status ?? "not found");
await agent.dispose?.();
```
## Output
Deterministic — the example ships an `AGENTS.md` beside `run.ts`:
```text
Runtime: local
AGENTS.md status: included
```
## What it shows
- **`context: {}`** enables the file context manager; **`settingSources: ["project"]`** is the opt-in
that lets it read project files. Omit `settingSources` and discovery finds nothing.
- **`snapshot()`** returns a `ContextSnapshot` — `runtime`, a `sources[]` list (each with `name`,
`path?`, `status`, `reason?`), and an optional `budget`. It is **secret-free by design** — safe to
log and persist; raw secrets, absolute paths, and exact token counts are never present.
- **`status`** is `"included"`, `"excluded"`, or `"summarized"` (trimmed to fit the token budget).
## Example
Full runnable source:
[`examples/context-inspect`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/context-inspect).
---
# Scope rules by path
Source: https://docs.usetheo.dev/theokit/context/scope-rules-by-path
Author .theokit/rules/*.md rule files that load into the agent's context only when you're working on matching files — declared per send via contextPaths. Deterministic, no LLM.
# Scope rules by path
`.theokit/rules/*.md` are theokit-native rule files, mirroring Claude Code's `.claude/rules/`. Each
file's frontmatter decides **when** the rule loads into the agent's context:
- `alwaysApply: true` — load on every send.
- `paths:` / `globs:` — load only when a file in the current send's scope matches one of the glob
patterns. `paths:` is the Claude Code spelling; `globs:` is a Cursor-compatible alias (they are
unioned).
You declare the in-scope files per send with `SendOptions.contextPaths` — "which files am I working
on right now". Building the agent and reading `agent.context.snapshot()` is a **local file read** —
no network, no LLM.
```ts title="run.ts"
import { Agent } from "@theokit/sdk";
// A project with two rule files under .theokit/rules/:
// always.md → --- alwaysApply: true --- (loads every send)
// api.md → --- paths: [ src/api/**/*.ts ] --- (loads only in API scope)
const agent = await Agent.create({
apiKey: "theo_test_rules", // fixture key — deterministic, no LLM
model: { id: "openai/gpt-4o-mini" },
local: { cwd, settingSources: ["project"] },
context: { manager: "file" },
});
// Working on an API file → the src/api/** rule activates for this send.
await (await agent.send("Add an endpoint.", { contextPaths: ["src/api/users.ts"] })).wait();
// agent.context.snapshot() → sources include rules/api.md AND rules/always.md
// Working on a UI file → the API rule stays dormant (no leak); alwaysApply remains.
await (await agent.send("Tweak the button.", { contextPaths: ["src/ui/button.tsx"] })).wait();
// agent.context.snapshot() → sources include only rules/always.md
```
## Output
Deterministic — the example writes the two rule files, then prints which rule files are active
under each scope:
```text
in scope [src/api/users.ts] -> [ 'rules/always.md', 'rules/api.md' ]
in scope [src/ui/button.tsx] -> [ 'rules/always.md' ]
OK — path-scoped rules activate by contextPaths; alwaysApply always on.
```
## What it shows
- **`paths:` / `globs:`** are glob-pattern arrays (not exact paths). Globs support `**` (any depth —
`src/**/*.ts` matches `src/x.ts` and `src/a/b/x.ts`), `*` (a single path segment), and `?` (a
single non-separator char).
- **`contextPaths`** is the per-send scope signal. Omit it and only `alwaysApply` rules load — the
create-time snapshot is untouched, so agents that never scope pay nothing.
- **No leak between sends** — changing `contextPaths` on the next send re-scopes; a rule that
matched the previous send but not this one drops out.
- The same signal also activates conditional **`.cursor/rules/*.mdc`** globs, if you keep Cursor
rules alongside.
## Example
Full runnable source:
[`examples/rules-path-scoped`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/rules-path-scoped).
---
# Advanced
Source: https://docs.usetheo.dev/theokit/cost/advanced
Budgets with scopes, windows, and warn/block thresholds; preflight enforcement; usage accumulation; and raw pricing lookup.
# Advanced cost
Verified against the SDK.
## Budgets — `Budget.create`
Register a budget with stacked limits over time windows, attributed to a scope:
```ts
import { Budget } from "@theokit/sdk";
const b = Budget.create({
name: "monthly", // grammar ^[a-z0-9][a-z0-9_-]*$ (else ConfigurationError)
scope: /* where the charge is attributed */,
limits: [ /* stacked warn/block limits over windows; [] ⇒ registry-only tracking */ ],
});
Budget.get("monthly"); // the handle, or undefined
Budget.list(); // all registered budgets
```
A `BudgetHandle` exposes per-window accounting — `spentIn(window)`, `remainingIn(window)`, and a
`snapshot()` — plus charging that fires the `warn` / `block` threshold callbacks.
## Preflight enforcement — `preflightCheck`
Before an expensive call, check the estimate against a budget:
```ts
import { preflightCheck } from "@theokit/sdk";
preflightCheck("monthly", estimatedUsd); // throws BudgetExceededError if the block limit would be crossed
```
Distinguish it from a post-hoc charge: `preflightCheck` blocks *before* spending; charging records
what was spent and updates thresholds. An unsupported budget operation throws
`UnsupportedBudgetOperationError`.
## Usage accounting
- **`normalizeUsage(raw)`** — canonicalize a provider's usage shape into `TokenUsage`
(`inputTokens` / `outputTokens` / `cacheReadTokens` / `cacheWriteTokens` / `reasoningTokens` /
`totalTokens`).
- **`UsageAccumulator`** — sum usage across many calls (a batch, a multi-turn run) into one total to
cost once.
- **`getPricingEntry(provider, model)`** — the raw pricing row behind `computeCost`, for your own
math or display.
- **`inferApiMode`** — resolve the API mode used for pricing.
## Subscription-included providers
Some providers are billed by subscription, not per-token — `computeCost` returns a
`subscription_included` source for those, so a flat-rate provider doesn't report a misleading
per-call USD figure.
## Reference
- [`Budget`](/theokit/reference/Budget) · [`BudgetExceededError`](/theokit/reference/BudgetExceededError) · [`CostBreakdown`](/theokit/reference/CostBreakdown) · [`UsageAccumulator`](/theokit/reference/UsageAccumulator)
---
# Estimate cost
Source: https://docs.usetheo.dev/theokit/cost/estimate-cost
Map a run's token usage to a USD CostBreakdown with computeCost — deterministic, no network.
# Estimate cost
`computeCost` turns `{ usage, provider, model }` into a `CostBreakdown` from the bundled pricing
snapshot. Pass a `RunResult.usage` straight in.
```ts title="run.ts"
import { computeCost } from "@theokit/sdk";
const cost = computeCost({
usage: { inputTokens: 1500, outputTokens: 500 },
provider: "openai",
model: "gpt-4o-mini",
});
console.log("Amount USD:", cost.amountUsd);
console.log("Status: ", cost.status);
console.log("Breakdown: ", JSON.stringify(cost.detail));
```
## Output
Deterministic — from the bundled pricing snapshot:
```text
Amount USD: 0.000525
Status: estimated
Breakdown: {"input":0.000225,"output":0.0003}
```
## What it shows
- **`computeCost({ usage, provider, model })`** → a `CostBreakdown` with `amountUsd`, a `status`
(`"estimated"` from the snapshot), and per-bucket `detail` (input / output / cache / reasoning).
- **No network, no LLM** — pricing is a bundled snapshot (`source: "litellm_snapshot"`,
`pricingVersion` stamped), so costing is instant and reproducible.
- **Unknown model** → `amountUsd: undefined` with a note, rather than a wrong number.
## Example
Full runnable source:
[`examples/cost-basics`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/cost-basics).
---
# Overview
Source: https://docs.usetheo.dev/theokit/cost
Estimate the USD cost of token usage from a bundled pricing snapshot, and enforce per-scope budgets with warn/block thresholds.
# Cost
Two things: **estimate** what a run costs, and **enforce** a spending limit.
```ts
import { computeCost } from "@theokit/sdk";
const cost = computeCost({
usage: { inputTokens: 1500, outputTokens: 500 },
provider: "openai",
model: "gpt-4o-mini",
});
cost.amountUsd; // 0.000525
```
`computeCost` maps a run's token usage to a `CostBreakdown` using a **bundled pricing snapshot** — no
network, no LLM. Every `RunResult` already carries its `usage`, so costing a run is one call.
## Estimate — `computeCost`
```ts
interface CostBreakdown {
amountUsd: number | undefined; // undefined when the model isn't in the snapshot
status: CostStatus; // e.g. "estimated"
currency: "USD";
source: CostSource; // e.g. "litellm_snapshot"
pricingVersion: string | undefined;
detail?: { input?; output?; cacheRead?; cacheWrite?; reasoning? }; // per-bucket USD
}
```
Helpers: `normalizeUsage` (canonicalize a provider's usage shape), `getPricingEntry` (raw pricing for
a model), `inferApiMode`, and `UsageAccumulator` (sum usage across many calls).
## Enforce — `Budget`
`Budget.create({ name, scope, limits })` registers a budget with stacked `warn` / `block` limits over
time windows. `preflightCheck(name, estimatedUsd)` throws a `BudgetExceededError` before an expensive
call; charging a run updates the budget and fires threshold callbacks. See
[Advanced](/theokit/cost/advanced).
## Next
- [Estimate cost](/theokit/cost/estimate-cost) — a runnable computeCost example.
- [Advanced](/theokit/cost/advanced) — budgets, scopes, windows, and preflight enforcement.
## Reference
- [`Budget`](/theokit/reference/Budget) · [`CostBreakdown`](/theokit/reference/CostBreakdown) · [`UsageAccumulator`](/theokit/reference/UsageAccumulator)
---
# The Container
Source: https://docs.usetheo.dev/theokit/di/concepts/container
Registering providers, resolving tokens, request scopes, freezing, and dependency analysis.
The `Container` is the DI runtime. It holds a registry of providers, resolves tokens to
values (honouring scope), detects cycles, and disposes instances when it shuts down.
## Creating a container
Seed providers declaratively, or register them imperatively — both go through the same
validation path:
```ts
import { Container, Injectable } from "@theokit/di";
@Injectable()
class UserService {}
// Declarative
const container = new Container({ providers: [UserService] });
// Imperative
const c2 = new Container();
c2.register(UserService);
c2.register({ provide: "API_URL", useValue: "https://api.example.com" });
```
`register()` accepts a full `Provider` or a bare class (shorthand for
`{ provide: X, useClass: X }`). Registering the same token twice replaces the previous
registration (NestJS "last write wins") and emits a single stderr warning.
## Resolving
Use `resolve()` for synchronous graphs and `resolveAsync()` when any factory in the chain
returns a Promise:
```ts
const user = container.resolve(UserService);
const db = await container.resolveAsync("DB");
```
Calling `resolve()` on a chain that contains an async provider throws
`AsyncProviderInSyncResolveError` — switch that call to `resolveAsync()`.
## Freeze after first resolve
To prevent the subtle class of bug where a singleton is built against one set of
registrations and a different set is added later, the container **freezes on first
resolve**. Late `register()` / `registerModule()` calls throw `ContainerFrozenError`.
```ts
const container = new Container({ providers: [UserService] });
container.resolve(UserService);
container.register(OtherService); // throws ContainerFrozenError
```
Need late registration (e.g. in tests)? Opt in explicitly:
```ts
const container = new Container({ allowDynamicRegistration: true });
```
## Request scopes
`runInRequest(callback)` opens a fresh `REQUEST` scope. Every `REQUEST`-scoped provider
resolved inside the callback (or any async continuation) shares one per-request cache, and
those instances are disposed when the callback settles — even if it throws.
```ts
await container.runInRequest(async () => {
const ctx = container.resolve(RequestContext); // one per request
await handle(ctx);
});
```
See [Scopes](/theokit/di/concepts/scopes) for the full model.
## Analyzing the graph
`analyze()` returns a snapshot of nodes, edges, and cycles — including cycles in providers
you never resolve. Resolve-time detection only fires for paths you actually traverse, so
`analyze()` is the way to surface latent cycles proactively in a test or dev boot check.
```ts
const { nodes, edges, cycles } = container.analyze();
if (cycles.length > 0) throw new Error("DI cycle detected at boot");
```
## Disposal
`dispose()` runs `dispose()` (or `[Symbol.asyncDispose]`) on every singleton instance in
reverse construction order, then clears the cache. It is idempotent; resolving after
disposal throws `ContainerDisposedError`. The container is itself an async-disposable, so
`await using` works:
```ts
await using container = new Container({ providers: [Db] });
// container.dispose() runs automatically on scope exit
```
Disposal keys off the `Disposable` shape (`dispose()`) or `Symbol.asyncDispose`. See
[Lifecycle](/theokit/di/concepts/lifecycle) for how request-scoped and singleton
instances are torn down.
---
# Errors
Source: https://docs.usetheo.dev/theokit/di/concepts/errors
The typed domain errors the container throws, and what each one means.
`@theokit/di` fails fast with typed, specific errors — never a generic string. Each one is
a distinct class you can catch and branch on.
## Resolution errors
| Error | Thrown when |
| --- | --- |
| `TokenNotFoundError` | A token has no registration (and the parameter isn't `@Optional()`). Includes the resolution path. |
| `CyclicDependencyError` | The dependency chain forms a cycle. The message renders the full `A → B → A` path. |
| `AsyncProviderInSyncResolveError` | `resolve()` hit an async provider in the chain — switch that call to `resolveAsync()`. |
| `ScopeViolationError` | A `REQUEST`-scoped provider was resolved outside an active `runInRequest(...)`. |
## Registration & configuration errors
| Error | Thrown when |
| --- | --- |
| `MissingInjectableError` | A class provider's class is not decorated with `@Injectable()`. |
| `ContainerFrozenError` | `register()` / `registerModule()` was called after the first resolve (without `allowDynamicRegistration`). |
| `ContainerDisposedError` | `resolve()` was called after `dispose()`. |
| `ReflectMetadataMissingError` | `reflect-metadata` wasn't imported before resolving a class provider. |
## Module errors
| Error | Thrown when |
| --- | --- |
| `InvalidModuleError` | A class passed to `registerModule()` isn't decorated with `@Module()`. |
| `InvalidExportError` | A module exports a token none of its providers or imports supply. |
| `CyclicModuleImportError` | The module `imports` graph contains a cycle. |
## Catching
All errors extend the built-in `Error`, so you can catch broadly or narrow by class:
```ts
import { TokenNotFoundError, CyclicDependencyError } from "@theokit/di";
try {
container.resolve(UserService);
} catch (err) {
if (err instanceof CyclicDependencyError) {
// structural bug — fix the graph, don't retry
} else if (err instanceof TokenNotFoundError) {
// a provider is missing from the registry
}
throw err;
}
```
For proactive detection of cycles before they're hit at runtime, use
[`container.analyze()`](/theokit/di/concepts/container#analyzing-the-graph).
---
# Injectable & Inject
Source: https://docs.usetheo.dev/theokit/di/concepts/injectable-inject
Marking classes DI-managed, overriding tokens, and optional dependencies.
## @Injectable
Marks a class as DI-managed and, optionally, sets its scope. A class provider **must** be
`@Injectable()` — the container rejects an undecorated class at registration with
`MissingInjectableError`.
```ts
@Injectable()
class UserService {
constructor(private readonly db: DbConnection) {}
}
@Injectable({ scope: Scope.REQUEST })
class RequestLogger {}
```
Constructor parameters typed as classes auto-resolve by their class token — the container
reads them from the `emitDecoratorMetadata` output via `reflect-metadata`.
## @Inject
Class tokens cover class-typed parameters. For anything **without** a class token — a
string token, or an interface (TypeScript emits `Object` for interfaces) — annotate the
parameter with `@Inject` to name the token explicitly.
```ts
@Injectable()
class GreeterService {
constructor(
@Inject("DATABASE_URL") private readonly dbUrl: string,
@Inject("Logger") private readonly logger: Logger, // interface → string token
) {}
}
```
A primitive or interface parameter **without** `@Inject` cannot be auto-resolved — the
container throws a `TypeError` explaining that primitives and interfaces need an explicit
token. Mark it `@Optional()` if it may legitimately be absent.
## @Optional
Marks a constructor parameter optional. If its dependency is not registered, the parameter
receives `undefined` instead of throwing `TokenNotFoundError`.
```ts
@Injectable()
class GreeterService {
constructor(@Optional() private readonly logger?: Logger) {}
}
```
`@Optional()` only swallows `TokenNotFoundError`. Other failures — a factory that throws, a
cyclic dependency — still propagate, so a genuinely broken optional dependency never fails
silently.
---
# Lifecycle & disposal
Source: https://docs.usetheo.dev/theokit/di/concepts/lifecycle
Tearing down instances with dispose() and Symbol.asyncDispose, and the status of @PostConstruct / @PreDestroy.
## Disposal (works today)
The container tracks any instance that implements the `Disposable` shape — a `dispose()`
method — or Node's `[Symbol.asyncDispose]`. When the container (or a request scope) ends,
those instances are torn down in **reverse construction order**, so dependents are disposed
before the dependencies they rely on.
```ts
@Injectable()
class DbConnection {
async dispose() {
await this.pool.end();
}
}
await using container = new Container({ providers: [DbConnection] });
container.resolve(DbConnection);
// on scope exit: container.dispose() → DbConnection.dispose() awaited
```
- **Singletons** are disposed on `container.dispose()`.
- **Request-scoped** instances are disposed when their `runInRequest(...)` callback settles
— even if it throws.
- Disposal is idempotent; if several instances throw while disposing, the errors are
collected and re-thrown as an `AggregateError` rather than silently dropped.
```ts
await container.runInRequest(async () => {
container.resolve(RequestScopedResource); // disposed when this callback returns
});
```
## @PostConstruct & @PreDestroy
These decorators mark a method to run after construction (`@PostConstruct`) or before
teardown (`@PreDestroy`):
```ts
@Injectable()
class CacheService {
@PostConstruct
async init() {
this.cache = await loadCacheFromRedis();
}
@PreDestroy
async close() {
await this.flush();
}
}
```
**v1 status — declared, not yet invoked by the resolver.** In the current release these
decorators record their metadata but the container does **not** call the marked methods
automatically. For teardown today, implement `dispose()` (or `[Symbol.asyncDispose]`) —
that path is fully wired. For async initialization, use an async `useFactory` that
performs setup before returning the instance:
```ts
{
provide: CacheService,
useFactory: async () => {
const svc = new CacheService();
await svc.init();
return svc;
},
}
```
Adopt `@PostConstruct` / `@PreDestroy` to express intent; move off the workaround once
resolver support lands.
---
# Modules
Source: https://docs.usetheo.dev/theokit/di/concepts/modules
Group providers, import other modules, and control visibility with exports.
A `@Module` groups related providers and declares which of them are visible to modules that
import it. The module class is never instantiated — the decorator only attaches metadata
that `Container.registerModule()` reads.
```ts
import { Module, Injectable } from "@theokit/di";
@Injectable()
class LoggerService {}
@Module({
providers: [LoggerService],
exports: [LoggerService],
})
class LoggingModule {}
@Module({
providers: [UserService, { provide: "DB_URL", useValue: process.env.DB_URL }],
imports: [LoggingModule],
exports: [UserService],
})
class AppModule {}
```
## The three fields
- **`providers`** — providers registered into the container when the module loads. Bare
classes expand to `ClassProvider` shorthand.
- **`imports`** — other `@Module()` classes whose **exported** providers become visible to
this module.
- **`exports`** — the subset of this module's providers made visible to modules that import
it. A token you don't export stays private to the module.
## Loading a module
`registerModule()` walks the import graph breadth-first, registers every provider, and
validates exports:
```ts
const container = new Container();
container.registerModule(AppModule);
container.resolve(UserService);
```
Passing a class that isn't decorated with `@Module()` throws `InvalidModuleError`.
Exporting a token that no imported module provides throws `InvalidExportError`. A cycle in
the module import graph throws `CyclicModuleImportError`.
## Modules vs. flat providers
For small apps, a flat `new Container({ providers: [...] })` is enough. Reach for modules
when you want **encapsulation** — keeping internal providers private and exposing a small,
deliberate surface through `exports`. This is the same mental model as NestJS modules.
---
# Providers
Source: https://docs.usetheo.dev/theokit/di/concepts/providers
The four provider types — useClass, useFactory, useValue, useExisting.
A **provider** tells the container how to materialize a value for a token. Exactly one of
`useClass`, `useFactory`, `useValue`, or `useExisting` must be set. Every provider has a
`provide` token — a class or a non-empty string.
## useClass
Instantiate a class, resolving its constructor dependencies. The class must be
`@Injectable()`. A bare class is shorthand for `{ provide: X, useClass: X }`.
```ts
{ provide: UserService, useClass: UserService }
// or just: UserService
// Swap an implementation behind an interface token:
{ provide: "PaymentGateway", useClass: StripeGateway, scope: Scope.SINGLETON }
```
## useFactory
Compute the value with a function. List its dependencies in `inject` — they are resolved
and passed as positional arguments. The factory may be async (return a Promise), in which
case consumers must use `resolveAsync`.
```ts
{
provide: "DB",
useFactory: (config: AppConfig) => createPool(config.databaseUrl),
inject: [AppConfig],
scope: Scope.SINGLETON,
}
```
If a factory returns a Promise, the container caches the in-flight Promise so concurrent
async resolves share one instance rather than running the factory twice.
## useValue
Provide an already-constructed value — config objects, constants, pre-built clients. Always
`SINGLETON`; no dependencies.
```ts
{ provide: "APP_NAME", useValue: "theo" }
{ provide: AppConfig, useValue: { databaseUrl: process.env.DATABASE_URL! } }
```
## useExisting
Alias one token to another. Resolving the alias resolves the target — useful for exposing
one implementation under multiple tokens.
```ts
{ provide: StripeGateway, useClass: StripeGateway }
{ provide: "PaymentGateway", useExisting: StripeGateway } // alias
```
## Summary
| Provider | Field | Async? | Scope | Deps |
| --- | --- | --- | --- | --- |
| Class | `useClass` | via async deps | configurable | constructor (metadata) |
| Factory | `useFactory` | yes | configurable | explicit `inject` list |
| Value | `useValue` | no | always SINGLETON | none |
| Existing | `useExisting` | follows target | follows target | the aliased token |
---
# Qualifiers & Primary
Source: https://docs.usetheo.dev/theokit/di/concepts/qualifiers
Declaring a default implementation and disambiguating multiple bindings.
`@Primary` and `@Qualifier` express **intent** about which implementation to pick when more
than one is available for the same abstraction.
- **`@Primary`** — a class decorator marking a provider as the default choice.
- **`@Qualifier(name)`** — a parameter decorator that narrows a dependency to a
specifically-named binding.
```ts
@Injectable()
@Primary
class StripePayments implements PaymentGateway {}
@Injectable()
class PayPalPayments implements PaymentGateway {}
@Injectable()
class OrderService {
constructor(@Qualifier("stripe") private payments: PaymentGateway) {}
}
```
The intended resolution priority is **`@Qualifier` > `@Primary` > error**.
**v1 status — declared, not yet wired into resolution.** In the current release these
decorators record their metadata (a `@Primary` flag on the class, a qualifier name per
parameter) but the resolver does **not** yet perform multi-binding selection: the registry
is one provider per token, last-write-wins. To choose between implementations today, use
distinct string tokens and `@Inject`, or `useExisting` to alias a chosen default:
```ts
{ provide: "payments.stripe", useClass: StripePayments }
{ provide: "payments.paypal", useClass: PayPalPayments }
{ provide: "PaymentGateway", useExisting: "payments.stripe" } // the default
@Injectable()
class OrderService {
constructor(@Inject("PaymentGateway") private payments: PaymentGateway) {}
}
```
Adopt `@Primary` / `@Qualifier` now to communicate intent; switch off the string-token
workaround once resolver support lands.
---
# Scopes
Source: https://docs.usetheo.dev/theokit/di/concepts/scopes
SINGLETON, TRANSIENT, and REQUEST — the three lifecycle scopes and when to use each.
Every provider resolves under one of three scopes. Scope is set on the `@Injectable`
decorator or on the provider entry, and defaults to `SINGLETON`.
```ts
import { Scope } from "@theokit/di";
Scope.SINGLETON; // "singleton" — one instance per container (default)
Scope.TRANSIENT; // "transient" — a fresh instance every resolve
Scope.REQUEST; // "request" — one instance per runInRequest(...) boundary
```
## SINGLETON
One shared instance for the container's lifetime. Constructed lazily on first resolve,
cached thereafter, and disposed when the container disposes. This is the default and the
right choice for stateless services, clients, and configuration.
```ts
@Injectable() // scope defaults to SINGLETON
class MetricsClient {}
```
## TRANSIENT
A new instance on every `resolve()`. Never cached. Use for lightweight, stateful helpers
that must not be shared.
```ts
@Injectable({ scope: Scope.TRANSIENT })
class RequestId {
readonly value = crypto.randomUUID();
}
```
## REQUEST
One instance per `runInRequest(...)` boundary, backed by Node's `AsyncLocalStorage`. Every
`REQUEST`-scoped provider resolved within the same request — across any `await` — shares a
single cache, and is disposed when the request settles.
```ts
@Injectable({ scope: Scope.REQUEST })
class RequestContext {
user?: User;
}
await container.runInRequest(async () => {
const a = container.resolve(RequestContext);
const b = container.resolve(RequestContext);
console.log(a === b); // true — same instance within the request
});
```
Resolving a `REQUEST`-scoped provider **outside** an active `runInRequest` throws
`ScopeViolationError` — this is the fail-fast guard that keeps request state from leaking
into singletons.
`REQUEST` scope is the wedge behind per-request isolation. `@theokit/di-agent` uses it to
give every HTTP request its own isolated `@theokit/sdk` Agent — see
[InjectAgent](/theokit/di-agent/concepts/inject-agent).
## Choosing a scope
| Scope | Instances | Cached | Disposed | Use for |
| --- | --- | --- | --- | --- |
| `SINGLETON` | one per container | yes | on `container.dispose()` | stateless services, clients, config |
| `TRANSIENT` | one per resolve | no | not tracked | short-lived stateful helpers |
| `REQUEST` | one per request boundary | per request | on request settle | per-request context, tenant, agent |
---
# Getting started
Source: https://docs.usetheo.dev/theokit/di/getting-started
Install @theokit/di, enable decorator metadata, and resolve your first service.
## Install
```bash
pnpm add @theokit/di reflect-metadata
```
`reflect-metadata` is a peer dependency — the container reads constructor parameter types
from the metadata TypeScript emits. Import it **once**, at your app entrypoint, before any
decorated class is loaded:
```ts
import "reflect-metadata";
```
## TypeScript configuration
Both flags are required. Without them, decorator metadata is never emitted and the
container cannot auto-resolve constructor parameters.
```json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
```
If a class has `@Injectable()` but the container reports "no constructor metadata", it
almost always means `emitDecoratorMetadata` is off, or `reflect-metadata` was imported
too late. Import it at the very top of your entrypoint.
## Your first service
Declare a service, group it in a module, and resolve it:
```ts
import "reflect-metadata";
import { Container, Injectable, Inject, Module } from "@theokit/di";
@Injectable()
class Clock {
now() {
return new Date().toISOString();
}
}
@Injectable()
class GreeterService {
constructor(
private readonly clock: Clock,
@Inject("APP_NAME") private readonly appName: string,
) {}
greet(name: string) {
return `[${this.appName} @ ${this.clock.now()}] Hello, ${name}!`;
}
}
@Module({
providers: [Clock, GreeterService, { provide: "APP_NAME", useValue: "theo" }],
})
class AppModule {}
const container = new Container();
container.registerModule(AppModule);
const greeter = container.resolve(GreeterService);
console.log(greeter.greet("world"));
```
`Clock` auto-resolves by its class type; the string token `"APP_NAME"` is supplied
explicitly with `@Inject` because primitives and interfaces have no class token.
## Async resolution and disposal
When a provider factory returns a Promise, resolve it with `resolveAsync`. Clean up
resources by implementing `dispose()` — the container tears instances down in reverse
construction order:
```ts
await using container = new Container({
providers: [
{
provide: "DB",
useFactory: async () => connectToDatabase(process.env.DATABASE_URL!),
},
],
});
const db = await container.resolveAsync("DB");
// on scope exit, `await using` calls container.dispose()
```
## Next
---
# Overview
Source: https://docs.usetheo.dev/theokit/di
A lightweight, NestJS-flavoured IoC container for TypeScript — @theokit/di.
**`@theokit/di`** is the dependency-injection core of the Theo backend ecosystem — a
lightweight, framework-agnostic IoC container with a **NestJS-compatible API**:
`@Injectable`, `@Inject`, `@Module`, and `providers: []`. It is the foundation the
DI-driven ORM (`@theokit/orm`) and the agent-aware layer (`@theokit/di-agent`) build on,
but it stands on its own in any TypeScript backend.
`@theokit/di` is a standalone container — no NestJS, no framework lock-in. If you know
the NestJS decorators, you already know this API. Requires `reflect-metadata` and TypeScript
`experimentalDecorators` + `emitDecoratorMetadata`.
## Why `@theokit/di`
- **Three lifecycle scopes.** `SINGLETON` (one per container), `TRANSIENT` (one per
resolve), and `REQUEST` (one per `runInRequest(...)` boundary, backed by Node's
`AsyncLocalStorage`) — the wedge that makes per-request isolation trivial.
- **Four provider types.** `useClass`, `useFactory`, `useValue`, `useExisting` — the same
surface you reach for in NestJS.
- **Sync and async resolution.** `resolve()` for synchronous graphs, `resolveAsync()`
when any factory returns a Promise — with a promise-lock cache so concurrent async
resolves share one instance.
- **Fail-fast by design.** Cycle detection at resolve-time, a container that freezes after
the first resolve, typed domain errors, and an `analyze()` helper that surfaces latent
cycles in providers you haven't even resolved yet.
- **Disposal lifecycle.** Instances with `dispose()` (or `[Symbol.asyncDispose]`) are torn
down in reverse construction order when the container or a request scope ends.
## Quick code
```ts
import "reflect-metadata";
import { Container, Injectable, Module } from "@theokit/di";
@Injectable()
class GreeterService {
greet(name: string) {
return `Hello, ${name}!`;
}
}
@Module({ providers: [GreeterService] })
class AppModule {}
const container = new Container();
container.registerModule(AppModule);
const greeter = container.resolve(GreeterService);
console.log(greeter.greet("Theo")); // "Hello, Theo!"
```
## Navigate
---
# For AI agents (llms.txt)
Source: https://docs.usetheo.dev/theokit/di/llms-txt
Machine-readable ground truth of @theokit/di for LLMs — package metadata, exported symbols, container API, conventions, and the honest v1 status of metadata-only decorators. Download it or curl it directly.
`@theokit/di` ships an `llms.txt` file following the
[llmstxt.org convention](https://llmstxt.org/) — a single Markdown document that
gives any LLM (Claude, ChatGPT, Cursor, Copilot, a local model) the **factual
ground truth** of this package without crawling the whole site:
- Exact package name, version, license, module format, Node floor, peer deps
- The single-barrel import path and every exported symbol
- The `Container` API surface and the four provider types
- Conventions to follow (reflect-metadata once, `@Inject` for primitives, freeze-on-resolve)
- The **honest v1 status**: `@Primary` / `@Qualifier` / `@PostConstruct` / `@PreDestroy`
are metadata-only and NOT yet wired into resolution — with the workarounds that do work
- Anti-patterns to avoid
## Download
## Curl it directly
```bash
# Save to your project root
curl -o theokit-di-llms.txt https://docs.usetheo.dev/theokit/di/llms.txt
# Or pipe straight into a prompt
curl -s https://docs.usetheo.dev/theokit/di/llms.txt | head -120
```
The source files always win. If a bullet in `llms.txt` disagrees with
`packages/di/src/index.ts` or the per-symbol reference on this site, the code is
correct and the file is stale — regenerate it from the barrel.
---
# AsyncProviderInSyncResolveError
Source: https://docs.usetheo.dev/theokit/di/reference/AsyncProviderInSyncResolveError
Thrown when sync `resolve()` is called on a chain that contains an
# `AsyncProviderInSyncResolveError`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Thrown when sync `resolve()` is called on a chain that contains an
async provider. The user must switch to `resolveAsync()`.
## Signature
```ts
class AsyncProviderInSyncResolveError {
captureStackTrace(...): ...
prepareStackTrace(...): ...
}
```
## Kind
`class`
## Source
[`packages/di/src/errors.ts:61`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/errors.ts#L61)
---
# ClassConstructor
Source: https://docs.usetheo.dev/theokit/di/reference/ClassConstructor
Constructor of a class — what TypeScript emits for `class X { ... }`.
# `ClassConstructor`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Constructor of a class — what TypeScript emits for `class X { ... }`.
Used as a Class-token (per ADR D2).
## Signature
```ts
type ClassConstructor
```
## Kind
`type`
## Source
[`packages/di/src/types.ts:11`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/types.ts#L11)
---
# ClassProvider
Source: https://docs.usetheo.dev/theokit/di/reference/ClassProvider
_No description available._
# `ClassProvider`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
_No description available._
## Signature
```ts
interface ClassProvider { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di/src/types.ts:48`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/types.ts#L48)
---
# Container
Source: https://docs.usetheo.dev/theokit/di/reference/Container
Lightweight DI container. See `README.md` for usage examples.
# `Container`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Lightweight DI container. See `README.md` for usage examples.
**Auditor-acknowledged size (info-level):** the 2026-06-06 architecture
audit (`/loop-architecture-review` Phase 3 principles-auditor) flagged
this class as 812 LOC (above the 500 LOC heuristic file budget) under
principle violation PV#10 (SRP / clean_function category, INFO severity).
The auditor's own description annotates it as "large but justified DI
orchestrator". The class is the Single-Point-of-Truth for DI resolution:
registry lookup, lifecycle (SINGLETON/TRANSIENT/REQUEST), `@Injectable`
metadata read, alias resolution, request-scope ALS propagation, and
dispose chain. Splitting these concerns into separate classes would
fragment cohesion and force consumers to coordinate across an internal
micro-interface that adds no testability or extensibility per
`rules/architecture.md § 3` (module cohesion) + KISS / YAGNI. ADR D422
documents an ongoing Extract-Method refactor targeting individual long
methods (not class-level split). Plan `arch-review-fixes-2026-06-06`
T11.2 documents this trade-off; audit DB row `principle_violations.id=10`
@ `packages/di/src/container.ts:87`; report at
`architecture-output/final_report.md § Findings by dimension` PV#10.
## Signature
```ts
class Container {
[asyncDispose](...): ...
analyze(...): ...
dispose(...): ...
has(...): ...
register(...): ...
registerModule(...): ...
resolve(...): ...
resolveAsync(...): ...
runInRequest(...): ...
}
```
## Kind
`class`
## Source
[`packages/di/src/container.ts:105`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/container.ts#L105)
---
# ContainerDisposedError
Source: https://docs.usetheo.dev/theokit/di/reference/ContainerDisposedError
Thrown when `container.dispose()` is called and then a subsequent
# `ContainerDisposedError`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Thrown when `container.dispose()` is called and then a subsequent
`resolve()` / `resolveAsync()` is attempted.
## Signature
```ts
class ContainerDisposedError {
captureStackTrace(...): ...
prepareStackTrace(...): ...
}
```
## Kind
`class`
## Source
[`packages/di/src/errors.ts:108`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/errors.ts#L108)
---
# ContainerFrozenError
Source: https://docs.usetheo.dev/theokit/di/reference/ContainerFrozenError
Thrown when `register()` or `registerModule()` is called AFTER the first
# `ContainerFrozenError`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Thrown when `register()` or `registerModule()` is called AFTER the first
`resolve()` has materialized a singleton. Per ADR D7 + EC-R2-5, the
container freezes after first resolve unless
`allowDynamicRegistration: true` was passed to the constructor.
## Signature
```ts
class ContainerFrozenError {
captureStackTrace(...): ...
prepareStackTrace(...): ...
}
```
## Kind
`class`
## Source
[`packages/di/src/errors.ts:121`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/errors.ts#L121)
---
# ContainerOptions
Source: https://docs.usetheo.dev/theokit/di/reference/ContainerOptions
Options passed to `new Container({...})`.
# `ContainerOptions`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Options passed to `new Container({...})`.
## Signature
```ts
interface ContainerOptions { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di/src/types.ts:88`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/types.ts#L88)
---
# CyclicDependencyError
Source: https://docs.usetheo.dev/theokit/di/reference/CyclicDependencyError
Thrown when a resolution chain contains a cycle (A → B → A).
# `CyclicDependencyError`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Thrown when a resolution chain contains a cycle (A → B → A).
Per ADR D7: detected at resolve-time (not register-time).
v1.2 EC-R2-1: cycle check happens BEFORE cache lookup in resolveAsync
to prevent infinite Promise await deadlocks on async REQUEST-scoped
cycles.
## Signature
```ts
class CyclicDependencyError {
captureStackTrace(...): ...
prepareStackTrace(...): ...
}
```
## Kind
`class`
## Source
[`packages/di/src/errors.ts:50`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/errors.ts#L50)
---
# CyclicModuleImportError
Source: https://docs.usetheo.dev/theokit/di/reference/CyclicModuleImportError
Thrown when module imports form a cycle (Module A imports B, B imports A).
# `CyclicModuleImportError`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Thrown when module imports form a cycle (Module A imports B, B imports A).
## Signature
```ts
class CyclicModuleImportError {
captureStackTrace(...): ...
prepareStackTrace(...): ...
}
```
## Kind
`class`
## Source
[`packages/di/src/internal/module-loader.ts:48`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/internal/module-loader.ts#L48)
---
# DependencyGraph
Source: https://docs.usetheo.dev/theokit/di/reference/DependencyGraph
`analyze()` debug return shape (per ADR D7).
# `DependencyGraph`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
`analyze()` debug return shape (per ADR D7).
## Signature
```ts
interface DependencyGraph { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di/src/types.ts:116`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/types.ts#L116)
---
# describeToken
Source: https://docs.usetheo.dev/theokit/di/reference/describeToken
Renders a token to a human-readable string for error messages.
# `describeToken`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Renders a token to a human-readable string for error messages.
- Class tokens: `MyService`
- String tokens: `"DATABASE_URL"`
- Anything else: `` (defensive — should never happen)
## Signature
```ts
function describeToken(token: Token): string
```
## Kind
`function`
## Source
[`packages/di/src/errors.ts:14`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/errors.ts#L14)
---
# Disposable
Source: https://docs.usetheo.dev/theokit/di/reference/Disposable
Anything that implements a `dispose()` method participates in lifecycle
# `Disposable`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Anything that implements a `dispose()` method participates in lifecycle
cleanup when the container or REQUEST scope ends.
## Signature
```ts
interface Disposable { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di/src/types.ts:109`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/types.ts#L109)
---
# ExistingProvider
Source: https://docs.usetheo.dev/theokit/di/reference/ExistingProvider
_No description available._
# `ExistingProvider`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
_No description available._
## Signature
```ts
interface ExistingProvider { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di/src/types.ts:67`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/types.ts#L67)
---
# FactoryProvider
Source: https://docs.usetheo.dev/theokit/di/reference/FactoryProvider
_No description available._
# `FactoryProvider`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
_No description available._
## Signature
```ts
interface FactoryProvider { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di/src/types.ts:54`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/types.ts#L54)
---
# API reference
Source: https://docs.usetheo.dev/theokit/di/reference
Every public symbol in @theokit/di, auto-generated from TypeDoc.
# API reference
This section is **auto-generated** from TypeDoc on every build. 36 symbols exported by @theokit/di.
## Classes
- [`AsyncProviderInSyncResolveError`](/theokit/di/reference/AsyncProviderInSyncResolveError) — Thrown when sync `resolve()` is called on a chain that contains an
- [`Container`](/theokit/di/reference/Container) — Lightweight DI container. See `README.md` for usage examples.
- [`ContainerDisposedError`](/theokit/di/reference/ContainerDisposedError) — Thrown when `container.dispose()` is called and then a subsequent
- [`ContainerFrozenError`](/theokit/di/reference/ContainerFrozenError) — Thrown when `register()` or `registerModule()` is called AFTER the first
- [`CyclicDependencyError`](/theokit/di/reference/CyclicDependencyError) — Thrown when a resolution chain contains a cycle (A → B → A).
- [`CyclicModuleImportError`](/theokit/di/reference/CyclicModuleImportError) — Thrown when module imports form a cycle (Module A imports B, B imports A).
- [`InvalidExportError`](/theokit/di/reference/InvalidExportError) — Thrown when a module declares an export for a token that isn't in its
- [`InvalidModuleError`](/theokit/di/reference/InvalidModuleError) — Thrown when a class is passed to `Container.registerModule()` without
- [`MissingInjectableError`](/theokit/di/reference/MissingInjectableError) — Thrown when a class is registered as a provider (via `useClass` or shorthand)
- [`ReflectMetadataMissingError`](/theokit/di/reference/ReflectMetadataMissingError) — Thrown when `reflect-metadata` is not loaded. The polyfill mutates the
- [`ScopeViolationError`](/theokit/di/reference/ScopeViolationError) — Thrown when a REQUEST-scoped provider is resolved outside of
- [`TokenNotFoundError`](/theokit/di/reference/TokenNotFoundError) — Thrown when `resolve()` / `resolveAsync()` is asked for a token that
## Constantes
- [`METADATA_KEYS`](/theokit/di/reference/METADATA_KEYS) — _no description_
## Functiones
- [`describeToken`](/theokit/di/reference/describeToken) — Renders a token to a human-readable string for error messages.
- [`Inject`](/theokit/di/reference/Inject) — Overrides the default class-token resolution for a constructor parameter.
- [`Injectable`](/theokit/di/reference/Injectable) — Marks a class as DI-managed. Without this decorator, the container
- [`Module`](/theokit/di/reference/Module) — Mark a class as a DI module. The class itself never gets instantiated —
- [`Optional`](/theokit/di/reference/Optional) — Marks a constructor parameter as optional. If the corresponding
- [`PostConstruct`](/theokit/di/reference/PostConstruct) — _no description_
- [`PreDestroy`](/theokit/di/reference/PreDestroy) — _no description_
- [`Primary`](/theokit/di/reference/Primary) — _no description_
- [`Qualifier`](/theokit/di/reference/Qualifier) — @Qualifier(name) — parameter decorator for disambiguation.
## Interfacees
- [`ClassProvider`](/theokit/di/reference/ClassProvider) — _no description_
- [`ContainerOptions`](/theokit/di/reference/ContainerOptions) — Options passed to `new Container({...})`.
- [`DependencyGraph`](/theokit/di/reference/DependencyGraph) — `analyze()` debug return shape (per ADR D7).
- [`Disposable`](/theokit/di/reference/Disposable) — Anything that implements a `dispose()` method participates in lifecycle
- [`ExistingProvider`](/theokit/di/reference/ExistingProvider) — _no description_
- [`FactoryProvider`](/theokit/di/reference/FactoryProvider) — _no description_
- [`InjectableOptions`](/theokit/di/reference/InjectableOptions) — Options accepted by `@Injectable({...})`.
- [`ModuleMetadata`](/theokit/di/reference/ModuleMetadata) — Module declaration shape accepted by `@Module({...})`.
- [`ResolutionContext`](/theokit/di/reference/ResolutionContext) — Resolution context passed to factories. Mostly internal but exposed so
- [`ValueProvider`](/theokit/di/reference/ValueProvider) — _no description_
## Typees
- [`ClassConstructor`](/theokit/di/reference/ClassConstructor) — Constructor of a class — what TypeScript emits for `class X { ... }`.
- [`Provider`](/theokit/di/reference/Provider) — Tells the container HOW to materialize a value for a token.
- [`Scope`](/theokit/di/reference/Scope) — Lifecycle scope (per ADR D5). The container honors three modes:
- [`Token`](/theokit/di/reference/Token) — Token: anything that identifies a dependency in the container.
---
# Inject
Source: https://docs.usetheo.dev/theokit/di/reference/Inject
Overrides the default class-token resolution for a constructor parameter.
# `Inject`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Overrides the default class-token resolution for a constructor parameter.
Required when the parameter type is an interface (TS emits `Object`) or
a string token (e.g., `'DATABASE_URL'`).
## Signature
```ts
function Inject(token: Token): ParameterDecorator
```
## Kind
`function`
## Example
```ts
class GreeterService {
constructor(
@Inject('DATABASE_URL') readonly dbUrl: string,
@Inject(LoggerInterface) readonly logger: LoggerInterface,
) {}
}
```
## Source
[`packages/di/src/decorators/inject.ts:17`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/decorators/inject.ts#L17)
---
# Injectable
Source: https://docs.usetheo.dev/theokit/di/reference/Injectable
Marks a class as DI-managed. Without this decorator, the container
# `Injectable`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Marks a class as DI-managed. Without this decorator, the container
rejects the class at registration time (per v1.1 EC-1).
## Signature
```ts
function Injectable(options: InjectableOptions): ClassDecorator
```
## Kind
`function`
## Example
```ts
@Injectable()
class UserService { constructor(private db: DbConnection) {} }
@Injectable({ scope: Scope.REQUEST })
class RequestLogger { ... }
```
## Source
[`packages/di/src/decorators/injectable.ts:22`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/decorators/injectable.ts#L22)
---
# InjectableOptions
Source: https://docs.usetheo.dev/theokit/di/reference/InjectableOptions
Options accepted by `@Injectable({...})`.
# `InjectableOptions`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Options accepted by `@Injectable({...})`.
## Signature
```ts
interface InjectableOptions { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di/src/decorators/injectable.ts:7`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/decorators/injectable.ts#L7)
---
# InvalidExportError
Source: https://docs.usetheo.dev/theokit/di/reference/InvalidExportError
Thrown when a module declares an export for a token that isn't in its
# `InvalidExportError`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Thrown when a module declares an export for a token that isn't in its
own `providers` (v1.1 EC-8 SHOULD TEST — pin register-time validation).
## Signature
```ts
class InvalidExportError {
captureStackTrace(...): ...
prepareStackTrace(...): ...
}
```
## Kind
`class`
## Source
[`packages/di/src/internal/module-loader.ts:32`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/internal/module-loader.ts#L32)
---
# InvalidModuleError
Source: https://docs.usetheo.dev/theokit/di/reference/InvalidModuleError
Thrown when a class is passed to `Container.registerModule()` without
# `InvalidModuleError`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Thrown when a class is passed to `Container.registerModule()` without
the `@Module()` decorator (v1.1 EC-4 MUST FIX).
## Signature
```ts
class InvalidModuleError {
captureStackTrace(...): ...
prepareStackTrace(...): ...
}
```
## Kind
`class`
## Source
[`packages/di/src/internal/module-loader.ts:18`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/internal/module-loader.ts#L18)
---
# METADATA_KEYS
Source: https://docs.usetheo.dev/theokit/di/reference/METADATA_KEYS
_No description available._
# `METADATA_KEYS`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
_No description available._
## Signature
```ts
const METADATA_KEYS
```
## Kind
`constant`
## Source
[`packages/di/src/internal/metadata.ts:9`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/internal/metadata.ts#L9)
---
# MissingInjectableError
Source: https://docs.usetheo.dev/theokit/di/reference/MissingInjectableError
Thrown when a class is registered as a provider (via `useClass` or shorthand)
# `MissingInjectableError`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Thrown when a class is registered as a provider (via `useClass` or shorthand)
but lacks the `@Injectable()` decorator. The decorator emits
`design:paramtypes` metadata that the container needs to auto-resolve
constructor parameters.
v1.1 EC-1: validateClassProvider() is called by BOTH the declarative
`providers: []` path AND the imperative `container.register()` path.
## Signature
```ts
class MissingInjectableError {
captureStackTrace(...): ...
prepareStackTrace(...): ...
}
```
## Kind
`class`
## Source
[`packages/di/src/errors.ts:94`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/errors.ts#L94)
---
# Module
Source: https://docs.usetheo.dev/theokit/di/reference/Module
Mark a class as a DI module. The class itself never gets instantiated —
# `Module`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Mark a class as a DI module. The class itself never gets instantiated —
the decorator just attaches metadata that `Container.registerModule()`
reads.
## Signature
```ts
function Module(metadata: ModuleMetadata): ClassDecorator
```
## Kind
`function`
## Example
```ts
@Module({
providers: [UserService, { provide: 'DB_URL', useValue: process.env.DB }],
imports: [LoggingModule],
exports: [UserService],
})
class UserModule {}
```
## Source
[`packages/di/src/decorators/module.ts:39`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/decorators/module.ts#L39)
---
# ModuleMetadata
Source: https://docs.usetheo.dev/theokit/di/reference/ModuleMetadata
Module declaration shape accepted by `@Module({...})`.
# `ModuleMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Module declaration shape accepted by `@Module({...})`.
- `providers` — providers registered into the container when this module
is loaded. Bare classes are expanded to ClassProvider shorthand.
- `imports` — other `@Module()` classes whose exported providers are
visible to this module.
- `exports` — tokens from this module's `providers` that should be
visible to outer modules that `import` this one.
## Signature
```ts
interface ModuleMetadata { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di/src/decorators/module.ts:13`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/decorators/module.ts#L13)
---
# Optional
Source: https://docs.usetheo.dev/theokit/di/reference/Optional
Marks a constructor parameter as optional. If the corresponding
# `Optional`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Marks a constructor parameter as optional. If the corresponding
dependency is not registered, the parameter receives `undefined`
instead of throwing `TokenNotFoundError`.
`@Optional()` ONLY swallows `TokenNotFoundError` — other errors
(e.g., factory throws, cyclic dependency) propagate.
## Signature
```ts
function Optional(): ParameterDecorator
```
## Kind
`function`
## Example
```ts
class GreeterService {
constructor(@Optional() private logger?: Logger) {}
}
```
## Source
[`packages/di/src/decorators/optional.ts:16`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/decorators/optional.ts#L16)
---
# PostConstruct
Source: https://docs.usetheo.dev/theokit/di/reference/PostConstruct
_No description available._
# `PostConstruct`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
_No description available._
## Signature
```ts
function PostConstruct(target: object, propertyKey: unknown): void
```
## Kind
`function`
## Example
```ts
@Injectable()
class CacheService {
private cache!: Map
@PostConstruct
async init() {
this.cache = await loadCacheFromRedis()
}
}
```
## Source
[`packages/di/src/decorators/lifecycle.ts:23`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/decorators/lifecycle.ts#L23)
---
# PreDestroy
Source: https://docs.usetheo.dev/theokit/di/reference/PreDestroy
_No description available._
# `PreDestroy`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
_No description available._
## Signature
```ts
function PreDestroy(target: object, propertyKey: unknown): void
```
## Kind
`function`
## Example
```ts
@Injectable()
class DbConnection {
@PreDestroy
async close() {
await this.pool.end()
}
}
```
## Source
[`packages/di/src/decorators/lifecycle.ts:42`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/decorators/lifecycle.ts#L42)
---
# Primary
Source: https://docs.usetheo.dev/theokit/di/reference/Primary
_No description available._
# `Primary`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
_No description available._
## Signature
```ts
function Primary(target: Function): void
```
## Kind
`function`
## Example
```ts
@Injectable()
@Primary
class StripePayments implements PaymentGateway { ... }
@Injectable()
class PayPalPayments implements PaymentGateway { ... }
// Without @Qualifier, StripePayments is resolved (it's @Primary)
```
## Source
[`packages/di/src/decorators/primary.ts:21`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/decorators/primary.ts#L21)
---
# Provider
Source: https://docs.usetheo.dev/theokit/di/reference/Provider
Tells the container HOW to materialize a value for a token.
# `Provider`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Tells the container HOW to materialize a value for a token.
Exactly one of `useClass | useFactory | useValue | useExisting` MUST be set.
## Signature
```ts
type Provider
```
## Kind
`type`
## Source
[`packages/di/src/types.ts:42`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/types.ts#L42)
---
# Qualifier
Source: https://docs.usetheo.dev/theokit/di/reference/Qualifier
@Qualifier(name) — parameter decorator for disambiguation.
# `Qualifier`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
@Qualifier(name) — parameter decorator for disambiguation.
When multiple providers match the same token,
## Signature
```ts
function Qualifier(name: string): ParameterDecorator
```
## Kind
`function`
## Example
```ts
@Injectable()
class OrderService {
constructor(@Qualifier('stripe') private payments: PaymentGateway) {}
}
```
## Source
[`packages/di/src/decorators/qualifier.ts:17`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/decorators/qualifier.ts#L17)
---
# ReflectMetadataMissingError
Source: https://docs.usetheo.dev/theokit/di/reference/ReflectMetadataMissingError
Thrown when `reflect-metadata` is not loaded. The polyfill mutates the
# `ReflectMetadataMissingError`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Thrown when `reflect-metadata` is not loaded. The polyfill mutates the
global `Reflect` object; without it, the container cannot read decorator
metadata. Surfaces at first `resolve()` of a class provider — but the
Container constructor also probes proactively.
## Signature
```ts
class ReflectMetadataMissingError {
captureStackTrace(...): ...
prepareStackTrace(...): ...
}
```
## Kind
`class`
## Source
[`packages/di/src/errors.ts:137`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/errors.ts#L137)
---
# ResolutionContext
Source: https://docs.usetheo.dev/theokit/di/reference/ResolutionContext
Resolution context passed to factories. Mostly internal but exposed so
# `ResolutionContext`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Resolution context passed to factories. Mostly internal but exposed so
factories can `inject` siblings without hard-coding container references.
## Signature
```ts
interface ResolutionContext { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di/src/types.ts:76`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/types.ts#L76)
---
# Scope
Source: https://docs.usetheo.dev/theokit/di/reference/Scope
Lifecycle scope (per ADR D5). The container honors three modes:
# `Scope`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Lifecycle scope (per ADR D5). The container honors three modes:
- `SINGLETON` — single instance shared across the entire container
- `TRANSIENT` — fresh instance per resolve
- `REQUEST` — single instance per `container.runInRequest(...)` boundary
(uses Node's `AsyncLocalStorage`)
## Signature
```ts
type Scope
```
## Kind
`type`
## Source
[`packages/di/src/types.ts:30`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/types.ts#L30)
---
# ScopeViolationError
Source: https://docs.usetheo.dev/theokit/di/reference/ScopeViolationError
Thrown when a REQUEST-scoped provider is resolved outside of
# `ScopeViolationError`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Thrown when a REQUEST-scoped provider is resolved outside of
`container.runInRequest()`.
## Signature
```ts
class ScopeViolationError {
captureStackTrace(...): ...
prepareStackTrace(...): ...
}
```
## Kind
`class`
## Source
[`packages/di/src/errors.ts:75`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/errors.ts#L75)
---
# Token
Source: https://docs.usetheo.dev/theokit/di/reference/Token
Token: anything that identifies a dependency in the container.
# `Token`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Token: anything that identifies a dependency in the container.
Per ADR D2: Class primary (auto-resolution via `reflect-metadata`),
String fallback (for primitives / interfaces). Symbol explicitly NOT
supported in v1 (deferred to v2 if real demand surfaces).
## Signature
```ts
type Token
```
## Kind
`type`
## Source
[`packages/di/src/types.ts:20`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/types.ts#L20)
---
# TokenNotFoundError
Source: https://docs.usetheo.dev/theokit/di/reference/TokenNotFoundError
Thrown when `resolve()` / `resolveAsync()` is asked for a token that
# `TokenNotFoundError`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
Thrown when `resolve()` / `resolveAsync()` is asked for a token that
was never registered.
## Signature
```ts
class TokenNotFoundError {
captureStackTrace(...): ...
prepareStackTrace(...): ...
}
```
## Kind
`class`
## Source
[`packages/di/src/errors.ts:28`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/errors.ts#L28)
---
# ValueProvider
Source: https://docs.usetheo.dev/theokit/di/reference/ValueProvider
_No description available._
# `ValueProvider`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di` source to change this page.
_No description available._
## Signature
```ts
interface ValueProvider { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di/src/types.ts:62`](https://github.com/usetheodev/theokit-di/blob/main/packages/di/src/types.ts#L62)
---
# Agent decorators
Source: https://docs.usetheo.dev/theokit/di-agent/concepts/decorators
The declarative capability decorators — how the annotate + read-back pattern works, and the full catalogue.
`@theokit/di-agent` ships a family of decorators that let you **declare** agent
capabilities — tools, cron schedules, retrievers, squads — directly on your classes and
methods.
## The annotate + read-back pattern
Every decorator here follows the same shape: it records its options as `reflect-metadata`
on the class, and the package exports a matching `read*Metadata()` reader.
```ts
import { Tool, readToolMetadata } from "@theokit/di-agent";
class SearchAgent {
@Tool({ name: "web_search", description: "Search the web", inputSchema: schema })
search(query: string) { /* ... */ }
}
// later, at wiring time:
const tools = readToolMetadata(SearchAgent); // Map
```
**These decorators are declarative — they store metadata, they do not execute.** Applying
`@Tool` does not by itself register a tool with an Agent; it records the intent. You (or a
higher-level wiring layer) call `readToolMetadata(...)` and connect it to `@theokit/sdk`.
The two surfaces that *do* materialize on their own are
[`createAgentProvider`](/theokit/di-agent/concepts/inject-agent) and
[`buildWorkflow`](/theokit/di-agent/concepts/workflow-builder).
## Catalogue
Each decorator pairs with a `read*Metadata()` reader of the same name (e.g. `@Tool` →
`readToolMetadata`).
| Decorator | Target | Key options |
| --- | --- | --- |
| `@Tool` | property | `name`, `description`, `inputSchema?` |
| `@SubAgent` | property | `name`, `description`, `instructions`, `model?`, `maxDelegationDepth?` |
| `@Squad` | property | `agents[]`, `process?` (`sequential` \| `hierarchical`), `name?` |
| `@Cron` | method | `schedule`, `timezone?` |
| `@Hitl` | method | `tools[]`, `timeoutMs?` |
| `@Retriever` | property | `topK?`, `threshold?` |
| `@Reranker` | property | `provider?`, `model?`, `topN?` |
| `@TextSplitter` | property | `strategy?`, `chunkSize?`, `overlap?` |
| `@MemoryScopeDecorator` | property | `path` |
| `@AutoSummarize` | class | `triggerFraction?`, `keepNewest?`, `model?` |
| `@Auth` | class | `providers?[]`, `sessionConfig?` |
| `@UseSandbox` | property | `backend?` (`local` \| `docker` \| …), `workDir?`, `timeoutMs?` |
| `@Subscription` | property | `name`, `transport?` |
| `@EvalDecorator` | class | `name?`, `scorers?[]`, `dataset?` |
`@Step` and `@Workflow` are covered separately in
[Workflow builder](/theokit/di-agent/concepts/workflow-builder), because they have a real
materializer (`buildWorkflow`).
## Example — a tool-carrying agent class
```ts
import { Tool, Retriever, Cron, readToolMetadata, readCronMetadata } from "@theokit/di-agent";
class SupportAgent {
@Retriever({ topK: 5, threshold: 0.7 })
knowledgeBase!: unknown;
@Tool({ name: "create_ticket", description: "Open a support ticket", inputSchema: ticketSchema })
createTicket(input: TicketInput) { /* ... */ }
@Cron({ schedule: "0 9 * * *", timezone: "America/Sao_Paulo" })
dailyDigest() { /* ... */ }
}
// wiring layer reads the intent back:
const tools = readToolMetadata(SupportAgent); // Map with "createTicket"
const cron = readCronMetadata(SupportAgent); // { schedule, timezone, methodKey }
```
---
# InjectAgent
Source: https://docs.usetheo.dev/theokit/di-agent/concepts/inject-agent
createAgentProvider, @InjectAgent, AGENT_TOKEN, and per-request Agent isolation.
The core of `@theokit/di-agent` is a small, sharp integration: register an Agent factory
under a well-known token, scoped so each request gets its own instance.
## createAgentProvider
Returns a `FactoryProvider` bound to `AGENT_TOKEN`. You supply the factory that builds an
Agent; the package supplies the scope + token wiring. The default scope is `REQUEST`.
```ts
import { createAgentProvider } from "@theokit/di-agent";
import { Agent } from "@theokit/sdk";
createAgentProvider({
factory: () => Agent.create({ apiKey: process.env.OPENROUTER_API_KEY!, model: { id: "openai/gpt-4o-mini" } }),
});
```
```ts
interface CreateAgentProviderOptions {
factory: () => TAgent | Promise;
scope?: Scope; // default: Scope.REQUEST
}
```
The options are deliberately structural — the package doesn't hard-depend on the SDK's
`AgentOptions` type. Your `factory` typically wraps `Agent.create({...})`, but any producer
works.
## @InjectAgent
A parameter decorator that injects the Agent bound to `AGENT_TOKEN`. It is exactly
`@Inject(AGENT_TOKEN)` — the named decorator just documents intent at the call site.
```ts
import { Injectable } from "@theokit/di";
import { InjectAgent } from "@theokit/di-agent";
@Injectable()
class ChatController {
constructor(@InjectAgent() private readonly agent: Agent) {}
}
```
## AGENT_TOKEN
The string token the provider and decorator share:
```ts
export const AGENT_TOKEN = "@theokit/di-agent:Agent";
```
Import it directly when you want to register a custom Agent factory (or a mock in tests)
under the same token without using `createAgentProvider`:
```ts
import { AGENT_TOKEN } from "@theokit/di-agent";
{ provide: AGENT_TOKEN, useValue: fakeAgent } // e.g. in a test module
```
## Request isolation
REQUEST scope is what makes this safe under concurrency. Within a single
`container.runInRequest(...)` boundary, every `@InjectAgent()` resolves the **same** Agent;
across two concurrent requests, they resolve **different** Agents. The Agent is disposed
when the request settles.
```ts
await container.runInRequest(async () => {
const a = container.resolve(ChatController);
const b = container.resolve(ChatController);
// a.agent === b.agent within this request; a different Agent in the next request
});
```
Resolving an `@InjectAgent()` dependency **outside** `runInRequest(...)` throws
`ScopeViolationError` (from `@theokit/di`). For CLI/cron/single-tenant use where a shared
Agent is fine, register it as `SINGLETON`:
`createAgentProvider({ factory, scope: Scope.SINGLETON })`.
See [Scopes](/theokit/di/concepts/scopes) for the underlying scope model.
---
# Workflow builder
Source: https://docs.usetheo.dev/theokit/di-agent/concepts/workflow-builder
Author a workflow with @Step / @Workflow and compile it into a @theokit/sdk Workflow via buildWorkflow.
Unlike the [declarative decorators](/theokit/di-agent/concepts/decorators), `@Step` and
`@Workflow` have a real materializer: `buildWorkflow()` compiles a decorated class instance
into a runnable `@theokit/sdk` `Workflow`.
`buildWorkflow` is the one bridge module that imports the `@theokit/sdk` peer directly
(via `@theokit/sdk/workflow`). It composes the existing `Workflow` engine — it adds no
orchestration of its own.
## @Step
Marks a method as a workflow step. A step receives its upstream step's return value (or the
workflow input, if it's the entry step) and returns its own output.
```ts
interface StepMetadata {
after?: string; // method name of the upstream step; omitted = entry step
name?: string; // step id, defaults to the method name
}
```
## @Workflow
Marks the class as a workflow and names it. `name` is used as the compiled workflow's name;
the other fields are declarative options.
```ts
interface WorkflowOptions {
name?: string;
retryPolicy?: unknown;
inputSchema?: unknown;
outputSchema?: unknown;
}
```
## buildWorkflow
Pass an **instance** of a `@Step`-decorated class. `buildWorkflow` reads the step metadata,
topologically orders steps by their single `after` dependency into a linear chain, threads
each step's output into the next, and returns a committed `Workflow`.
```ts
import { Workflow as WorkflowDecorator, Step, buildWorkflow } from "@theokit/di-agent";
@WorkflowDecorator({ name: "onboarding" })
class OnboardingFlow {
@Step() // entry step — receives the workflow input
createUser(input: { email: string }) {
return { userId: "u_123", email: input.email };
}
@Step({ after: "createUser" })
sendWelcome(user: { userId: string; email: string }) {
return { sent: true, userId: user.userId };
}
}
const workflow = buildWorkflow(new OnboardingFlow());
// workflow is a @theokit/sdk Workflow — run it with the SDK's workflow runtime
```
## Validation (fail-fast)
`buildWorkflow` validates the step graph before building, and throws when:
- the class has **no** `@Step` methods;
- a step's `after` references an **unknown** step;
- the `after` graph contains a **cycle**.
The MVP supports a **single upstream dependency per step** — a linear `then(...)` chain.
Branching and fan-out remain on the imperative `@theokit/sdk` `Workflow` surface; reach
for that directly when you need more than a linear pipeline.
---
# Getting started
Source: https://docs.usetheo.dev/theokit/di-agent/getting-started
Install @theokit/di-agent, register a REQUEST-scoped Agent, and inject it per request.
## Install
```bash
pnpm add @theokit/di-agent @theokit/di @theokit/sdk reflect-metadata
```
`@theokit/di-agent` builds on [`@theokit/di`](/theokit/di/getting-started) — the same
`reflect-metadata` import and TypeScript decorator flags apply. `@theokit/sdk` is a peer
dependency (`^1.9.0`); you provide the Agent factory, this package supplies the scope +
token wiring.
```json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
```
## Register the agent provider
`createAgentProvider()` returns a `FactoryProvider` bound to `AGENT_TOKEN`, REQUEST-scoped
by default. You pass the factory that builds a fresh Agent:
```ts
import { Agent } from "@theokit/sdk";
import { Module } from "@theokit/di";
import { createAgentProvider } from "@theokit/di-agent";
@Module({
providers: [
createAgentProvider({
factory: () =>
Agent.create({
apiKey: process.env.OPENROUTER_API_KEY!,
model: { id: "openai/gpt-4o-mini" },
}),
}),
],
})
class AppModule {}
```
## Inject the Agent
`@InjectAgent()` is a documented alias for `@Inject(AGENT_TOKEN)`:
```ts
import { Injectable } from "@theokit/di";
import { InjectAgent } from "@theokit/di-agent";
import type { Agent } from "@theokit/sdk";
@Injectable()
class ChatController {
constructor(@InjectAgent() private readonly agent: Agent) {}
async chat(message: string) {
const run = await this.agent.send(message);
return (await run.wait()).result;
}
}
```
## Resolve inside a request scope
Because the Agent is REQUEST-scoped, resolve it inside `runInRequest(...)` — typically once
per HTTP request. Each request gets an isolated Agent, disposed when the request settles:
```ts
app.post("/chat", async (req, res) => {
const answer = await container.runInRequest(async () => {
const controller = container.resolve(ChatController);
return controller.chat(req.body.message);
});
res.json({ answer });
});
```
Need a single shared Agent (CLI, cron, single-tenant)? Override the scope:
`createAgentProvider({ factory, scope: Scope.SINGLETON })`. A runnable Express example
ships in the repo under `examples/di-agent-express`.
## Next
---
# Overview
Source: https://docs.usetheo.dev/theokit/di-agent
Agent-first DI — a REQUEST-scoped @theokit/sdk Agent per request, plus declarative agent decorators.
**`@theokit/di-agent`** is the agent-first integration layer for [`@theokit/di`](/theokit/di).
Its headline feature is `createAgentProvider()` + `@InjectAgent()` — a **REQUEST-scoped
`@theokit/sdk` Agent** so every HTTP request gets its own isolated agent, wired through the
container. Around that it ships a family of **declarative decorators** (`@Tool`,
`@Workflow`, `@Cron`, `@Retriever`, and more) for annotating agent capabilities on your
classes.
`@theokit/di-agent` consumes `@theokit/sdk` as a published peer dependency (`^1.9.0`), not
a workspace link. Decorators are an **optional DX layer** — the SDK itself no longer
requires them (ADR D431 revoked the decorators-mandatory rule).
## The wedge: one Agent per request
```ts
import { Agent } from "@theokit/sdk";
import { Container, Module, Injectable } from "@theokit/di";
import { createAgentProvider, InjectAgent } from "@theokit/di-agent";
@Injectable()
class ChatController {
constructor(@InjectAgent() private readonly agent: Agent) {}
chat(message: string) {
return this.agent.send(message);
}
}
@Module({
providers: [
ChatController,
createAgentProvider({
factory: () =>
Agent.create({ apiKey: process.env.OPENROUTER_API_KEY!, model: { id: "openai/gpt-4o-mini" } }),
}),
],
})
class AppModule {}
// per HTTP request:
await container.runInRequest(async () => {
const controller = container.resolve(ChatController); // fresh Agent, isolated per request
await controller.chat("hello");
});
```
## Two kinds of surface
- **Materialized wiring (runs today).** `createAgentProvider()` produces a REQUEST-scoped
Agent factory; `buildWorkflow()` compiles a `@Step`-decorated class into a `@theokit/sdk`
`Workflow`. These two do real work.
- **Declarative decorators (annotation + read-back).** `@Tool`, `@Cron`, `@Retriever`,
`@Reranker`, `@Squad`, `@Hitl`, `@Auth`, and the rest record their options as metadata and
expose a matching `read*Metadata()` reader. They describe intent; your wiring (or a future
materializer) reads the metadata and connects it to the SDK.
## Navigate
---
# For AI agents (llms.txt)
Source: https://docs.usetheo.dev/theokit/di-agent/llms-txt
Machine-readable ground truth of @theokit/di-agent for LLMs — package metadata, createAgentProvider / @InjectAgent, the full declarative decorator catalogue with read*Metadata pattern, and buildWorkflow. Download it or curl it directly.
`@theokit/di-agent` ships an `llms.txt` file following the
[llmstxt.org convention](https://llmstxt.org/) — a single Markdown document that
gives any LLM the **factual ground truth** of this package without crawling the site:
- Exact package name, version, license, and the three required peers (`@theokit/di`,
`@theokit/sdk`, `reflect-metadata`)
- The REQUEST-scoped Agent wedge: `createAgentProvider`, `@InjectAgent`, `AGENT_TOKEN`
- The **full declarative decorator catalogue** (`@Tool`, `@Cron`, `@Retriever`, `@Squad`, …)
with target, options, and the paired `read*Metadata()` reader
- The clear split between **materialized wiring** (`createAgentProvider`, `buildWorkflow`)
and **metadata-only decorators** that you read back and wire yourself
- Anti-patterns to avoid (don't assume decorators self-wire; don't resolve outside a request)
## Download
## Curl it directly
```bash
# Save to your project root
curl -o theokit-di-agent-llms.txt https://docs.usetheo.dev/theokit/di-agent/llms.txt
# Or pipe straight into a prompt
curl -s https://docs.usetheo.dev/theokit/di-agent/llms.txt | head -120
```
The source files always win. If a bullet in `llms.txt` disagrees with
`packages/di-agent/src/index.ts` or the per-symbol reference on this site, the code is
correct and the file is stale — regenerate it from the barrel.
---
# AGENT_TOKEN
Source: https://docs.usetheo.dev/theokit/di-agent/reference/AGENT_TOKEN
Token used by `@InjectAgent()` to look up the Agent provider. Importing
# `AGENT_TOKEN`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
Token used by `@InjectAgent()` to look up the Agent provider. Importing
this directly (instead of using the decorator) lets you register a
custom factory under the same token.
## Signature
```ts
const AGENT_TOKEN
```
## Kind
`constant`
## Source
[`packages/di-agent/src/tokens.ts:6`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/tokens.ts#L6)
---
# Auth
Source: https://docs.usetheo.dev/theokit/di-agent/reference/Auth
_No description available._
# `Auth`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function Auth(options: AuthOptions): ClassDecorator
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/auth.ts:9`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/auth.ts#L9)
---
# AuthOptions
Source: https://docs.usetheo.dev/theokit/di-agent/reference/AuthOptions
_No description available._
# `AuthOptions`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
interface AuthOptions { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di-agent/src/decorators/auth.ts:4`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/auth.ts#L4)
---
# AutoSummarize
Source: https://docs.usetheo.dev/theokit/di-agent/reference/AutoSummarize
_No description available._
# `AutoSummarize`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function AutoSummarize(options: AutoSummarizeOptions): ClassDecorator
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/auto-summarize.ts:16`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/auto-summarize.ts#L16)
---
# AutoSummarizeOptions
Source: https://docs.usetheo.dev/theokit/di-agent/reference/AutoSummarizeOptions
_No description available._
# `AutoSummarizeOptions`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
interface AutoSummarizeOptions { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di-agent/src/decorators/auto-summarize.ts:5`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/auto-summarize.ts#L5)
---
# buildWorkflow
Source: https://docs.usetheo.dev/theokit/di-agent/reference/buildWorkflow
Build a `Workflow` from a `@Step`-decorated instance. Validates the step
# `buildWorkflow`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
Build a `Workflow` from a `@Step`-decorated instance. Validates the step
graph (fail-fast) before constructing the workflow.
## Signature
```ts
function buildWorkflow(instance: object): Workflow
```
## Kind
`function`
## Source
[`packages/di-agent/src/workflow-builder.ts:34`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/workflow-builder.ts#L34)
---
# createAgentProvider
Source: https://docs.usetheo.dev/theokit/di-agent/reference/createAgentProvider
Build a `FactoryProvider` that materializes an Agent on demand.
# `createAgentProvider`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
Build a `FactoryProvider` that materializes an Agent on demand.
## Signature
```ts
function createAgentProvider(options: CreateAgentProviderOptions): FactoryProvider
```
## Kind
`function`
## Example
```ts
import { Agent } from "@theokit/sdk";
import { Module } from "@theokit/di";
import { createAgentProvider } from "@theokit/di-agent";
@Module({
providers: [
createAgentProvider({
factory: () => Agent.create({ apiKey: process.env.OPENROUTER_API_KEY!, model: { id: "openai/gpt-4o-mini" } }),
}),
],
})
class AppModule {}
```
## Source
[`packages/di-agent/src/agent-provider.ts:45`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/agent-provider.ts#L45)
---
# CreateAgentProviderOptions
Source: https://docs.usetheo.dev/theokit/di-agent/reference/CreateAgentProviderOptions
Options accepted by `createAgentProvider()`. Mirrors the public
# `CreateAgentProviderOptions`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
Options accepted by `createAgentProvider()`. Mirrors the public
`AgentOptions` from `@theokit/sdk` but kept structural so this package
does not have a hard import dependency on the SDK's type tree.
The factory `async () => Agent.create(options)` is provided by the
consumer (see `createAgentProvider` below). This package supplies the
scope + token wiring.
## Signature
```ts
interface CreateAgentProviderOptions { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di-agent/src/agent-provider.ts:14`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/agent-provider.ts#L14)
---
# Cron
Source: https://docs.usetheo.dev/theokit/di-agent/reference/Cron
_No description available._
# `Cron`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function Cron(options: CronOptions): MethodDecorator
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/cron.ts:13`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/cron.ts#L13)
---
# CronMetadata
Source: https://docs.usetheo.dev/theokit/di-agent/reference/CronMetadata
_No description available._
# `CronMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
interface CronMetadata { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di-agent/src/decorators/cron.ts:9`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/cron.ts#L9)
---
# CronOptions
Source: https://docs.usetheo.dev/theokit/di-agent/reference/CronOptions
_No description available._
# `CronOptions`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
interface CronOptions { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di-agent/src/decorators/cron.ts:4`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/cron.ts#L4)
---
# EvalDecorator
Source: https://docs.usetheo.dev/theokit/di-agent/reference/EvalDecorator
_No description available._
# `EvalDecorator`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function EvalDecorator(options: EvalOptions): ClassDecorator
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/eval-decorator.ts:10`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/eval-decorator.ts#L10)
---
# EvalOptions
Source: https://docs.usetheo.dev/theokit/di-agent/reference/EvalOptions
_No description available._
# `EvalOptions`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
interface EvalOptions { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di-agent/src/decorators/eval-decorator.ts:4`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/eval-decorator.ts#L4)
---
# Hitl
Source: https://docs.usetheo.dev/theokit/di-agent/reference/Hitl
_No description available._
# `Hitl`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function Hitl(options: HitlOptions): MethodDecorator
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/hitl.ts:14`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/hitl.ts#L14)
---
# HitlMetadata
Source: https://docs.usetheo.dev/theokit/di-agent/reference/HitlMetadata
_No description available._
# `HitlMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
interface HitlMetadata { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di-agent/src/decorators/hitl.ts:10`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/hitl.ts#L10)
---
# HitlOptions
Source: https://docs.usetheo.dev/theokit/di-agent/reference/HitlOptions
_No description available._
# `HitlOptions`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
interface HitlOptions { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di-agent/src/decorators/hitl.ts:5`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/hitl.ts#L5)
---
# API reference
Source: https://docs.usetheo.dev/theokit/di-agent/reference
Every public symbol in @theokit/di-agent, auto-generated from TypeDoc.
# API reference
This section is **auto-generated** from TypeDoc on every build. 55 symbols exported by @theokit/di-agent.
## Constantes
- [`AGENT_TOKEN`](/theokit/di-agent/reference/AGENT_TOKEN) — Token used by `@InjectAgent()` to look up the Agent provider. Importing
## Functiones
- [`Auth`](/theokit/di-agent/reference/Auth) — _no description_
- [`AutoSummarize`](/theokit/di-agent/reference/AutoSummarize) — _no description_
- [`buildWorkflow`](/theokit/di-agent/reference/buildWorkflow) — Build a `Workflow` from a `@Step`-decorated instance. Validates the step
- [`createAgentProvider`](/theokit/di-agent/reference/createAgentProvider) — Build a `FactoryProvider` that materializes an Agent on demand.
- [`Cron`](/theokit/di-agent/reference/Cron) — _no description_
- [`EvalDecorator`](/theokit/di-agent/reference/EvalDecorator) — _no description_
- [`Hitl`](/theokit/di-agent/reference/Hitl) — _no description_
- [`InjectAgent`](/theokit/di-agent/reference/InjectAgent) — Parameter decorator — injects the REQUEST-scoped Agent into a constructor.
- [`MemoryScopeDecorator`](/theokit/di-agent/reference/MemoryScopeDecorator) — _no description_
- [`readAuthMetadata`](/theokit/di-agent/reference/readAuthMetadata) — _no description_
- [`readAutoSummarizeMetadata`](/theokit/di-agent/reference/readAutoSummarizeMetadata) — _no description_
- [`readCronMetadata`](/theokit/di-agent/reference/readCronMetadata) — _no description_
- [`readEvalDecoratorMetadata`](/theokit/di-agent/reference/readEvalDecoratorMetadata) — _no description_
- [`readHitlMetadata`](/theokit/di-agent/reference/readHitlMetadata) — _no description_
- [`readMemoryScopeMetadata`](/theokit/di-agent/reference/readMemoryScopeMetadata) — _no description_
- [`readRerankerMetadata`](/theokit/di-agent/reference/readRerankerMetadata) — _no description_
- [`readRetrieverMetadata`](/theokit/di-agent/reference/readRetrieverMetadata) — _no description_
- [`readSandboxMetadata`](/theokit/di-agent/reference/readSandboxMetadata) — _no description_
- [`readSquadMetadata`](/theokit/di-agent/reference/readSquadMetadata) — Read `@Squad()` metadata off a class constructor.
- [`readStepMetadata`](/theokit/di-agent/reference/readStepMetadata) — Read `@Step()` metadata off a class constructor (insertion-ordered).
- [`readSubAgentMetadata`](/theokit/di-agent/reference/readSubAgentMetadata) — _no description_
- [`readSubscriptionMetadata`](/theokit/di-agent/reference/readSubscriptionMetadata) — _no description_
- [`readTextSplitterMetadata`](/theokit/di-agent/reference/readTextSplitterMetadata) — _no description_
- [`readToolMetadata`](/theokit/di-agent/reference/readToolMetadata) — _no description_
- [`readWorkflowMetadata`](/theokit/di-agent/reference/readWorkflowMetadata) — _no description_
- [`Reranker`](/theokit/di-agent/reference/Reranker) — _no description_
- [`Retriever`](/theokit/di-agent/reference/Retriever) — _no description_
- [`Squad`](/theokit/di-agent/reference/Squad) — `@Squad(metadata)` — declare a sequential agent team on a property. The DI
- [`Step`](/theokit/di-agent/reference/Step) — `@Step(metadata?)` — mark a class method as a workflow step. The method
- [`SubAgent`](/theokit/di-agent/reference/SubAgent) — _no description_
- [`Subscription`](/theokit/di-agent/reference/Subscription) — _no description_
- [`TextSplitter`](/theokit/di-agent/reference/TextSplitter) — _no description_
- [`Tool`](/theokit/di-agent/reference/Tool) — _no description_
- [`UseSandbox`](/theokit/di-agent/reference/UseSandbox) — _no description_
- [`Workflow`](/theokit/di-agent/reference/Workflow) — _no description_
## Interfacees
- [`AuthOptions`](/theokit/di-agent/reference/AuthOptions) — _no description_
- [`AutoSummarizeOptions`](/theokit/di-agent/reference/AutoSummarizeOptions) — _no description_
- [`CreateAgentProviderOptions`](/theokit/di-agent/reference/CreateAgentProviderOptions) — Options accepted by `createAgentProvider()`. Mirrors the public
- [`CronMetadata`](/theokit/di-agent/reference/CronMetadata) — _no description_
- [`CronOptions`](/theokit/di-agent/reference/CronOptions) — _no description_
- [`EvalOptions`](/theokit/di-agent/reference/EvalOptions) — _no description_
- [`HitlMetadata`](/theokit/di-agent/reference/HitlMetadata) — _no description_
- [`HitlOptions`](/theokit/di-agent/reference/HitlOptions) — _no description_
- [`MemoryScopeOptions`](/theokit/di-agent/reference/MemoryScopeOptions) — _no description_
- [`RerankerOptions`](/theokit/di-agent/reference/RerankerOptions) — _no description_
- [`RetrieverOptions`](/theokit/di-agent/reference/RetrieverOptions) — _no description_
- [`SquadMetadata`](/theokit/di-agent/reference/SquadMetadata) — Metadata captured by the `@Squad()` property decorator. Declares a sequential
- [`StepMetadata`](/theokit/di-agent/reference/StepMetadata) — Metadata captured by the `@Step()` method decorator. Declares one step of a
- [`SubAgentOptions`](/theokit/di-agent/reference/SubAgentOptions) — _no description_
- [`SubscriptionOptions`](/theokit/di-agent/reference/SubscriptionOptions) — _no description_
- [`TextSplitterOptions`](/theokit/di-agent/reference/TextSplitterOptions) — _no description_
- [`ToolOptions`](/theokit/di-agent/reference/ToolOptions) — _no description_
- [`UseSandboxOptions`](/theokit/di-agent/reference/UseSandboxOptions) — _no description_
- [`WorkflowOptions`](/theokit/di-agent/reference/WorkflowOptions) — _no description_
---
# InjectAgent
Source: https://docs.usetheo.dev/theokit/di-agent/reference/InjectAgent
Parameter decorator — injects the REQUEST-scoped Agent into a constructor.
# `InjectAgent`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
Parameter decorator — injects the REQUEST-scoped Agent into a constructor.
Equivalent to `@Inject(AGENT_TOKEN)` but documents intent.
## Signature
```ts
function InjectAgent(): ParameterDecorator
```
## Kind
`function`
## Example
```ts
@Injectable()
class ChatController {
constructor(@InjectAgent() private readonly agent: Agent) {}
async chat(message: string) {
return this.agent.send(message);
}
}
```
## Source
[`packages/di-agent/src/inject-agent.ts:19`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/inject-agent.ts#L19)
---
# MemoryScopeDecorator
Source: https://docs.usetheo.dev/theokit/di-agent/reference/MemoryScopeDecorator
_No description available._
# `MemoryScopeDecorator`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function MemoryScopeDecorator(options: MemoryScopeOptions): PropertyDecorator
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/memory-scope.ts:9`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/memory-scope.ts#L9)
---
# MemoryScopeOptions
Source: https://docs.usetheo.dev/theokit/di-agent/reference/MemoryScopeOptions
_No description available._
# `MemoryScopeOptions`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
interface MemoryScopeOptions { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di-agent/src/decorators/memory-scope.ts:5`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/memory-scope.ts#L5)
---
# readAuthMetadata
Source: https://docs.usetheo.dev/theokit/di-agent/reference/readAuthMetadata
_No description available._
# `readAuthMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function readAuthMetadata(target: Function): unknown
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/auth.ts:15`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/auth.ts#L15)
---
# readAutoSummarizeMetadata
Source: https://docs.usetheo.dev/theokit/di-agent/reference/readAutoSummarizeMetadata
_No description available._
# `readAutoSummarizeMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function readAutoSummarizeMetadata(target: unknown): unknown
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/auto-summarize.ts:23`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/auto-summarize.ts#L23)
---
# readCronMetadata
Source: https://docs.usetheo.dev/theokit/di-agent/reference/readCronMetadata
_No description available._
# `readCronMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function readCronMetadata(target: unknown): unknown
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/cron.ts:20`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/cron.ts#L20)
---
# readEvalDecoratorMetadata
Source: https://docs.usetheo.dev/theokit/di-agent/reference/readEvalDecoratorMetadata
_No description available._
# `readEvalDecoratorMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function readEvalDecoratorMetadata(target: Function): unknown
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/eval-decorator.ts:16`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/eval-decorator.ts#L16)
---
# readHitlMetadata
Source: https://docs.usetheo.dev/theokit/di-agent/reference/readHitlMetadata
_No description available._
# `readHitlMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function readHitlMetadata(target: unknown): unknown
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/hitl.ts:21`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/hitl.ts#L21)
---
# readMemoryScopeMetadata
Source: https://docs.usetheo.dev/theokit/di-agent/reference/readMemoryScopeMetadata
_No description available._
# `readMemoryScopeMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function readMemoryScopeMetadata(target: unknown): ReadonlyMap
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/memory-scope.ts:18`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/memory-scope.ts#L18)
---
# readRerankerMetadata
Source: https://docs.usetheo.dev/theokit/di-agent/reference/readRerankerMetadata
_No description available._
# `readRerankerMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function readRerankerMetadata(target: unknown): ReadonlyMap
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/reranker.ts:19`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/reranker.ts#L19)
---
# readRetrieverMetadata
Source: https://docs.usetheo.dev/theokit/di-agent/reference/readRetrieverMetadata
_No description available._
# `readRetrieverMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function readRetrieverMetadata(target: unknown): ReadonlyMap
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/retriever.ts:18`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/retriever.ts#L18)
---
# readSandboxMetadata
Source: https://docs.usetheo.dev/theokit/di-agent/reference/readSandboxMetadata
_No description available._
# `readSandboxMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function readSandboxMetadata(target: unknown): ReadonlyMap
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/use-sandbox.ts:20`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/use-sandbox.ts#L20)
---
# readSquadMetadata
Source: https://docs.usetheo.dev/theokit/di-agent/reference/readSquadMetadata
Read `@Squad()` metadata off a class constructor.
# `readSquadMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
Read `@Squad()` metadata off a class constructor.
## Signature
```ts
function readSquadMetadata(target: unknown): ReadonlyMap
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/squad.ts:35`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/squad.ts#L35)
---
# readStepMetadata
Source: https://docs.usetheo.dev/theokit/di-agent/reference/readStepMetadata
Read `@Step()` metadata off a class constructor (insertion-ordered).
# `readStepMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
Read `@Step()` metadata off a class constructor (insertion-ordered).
## Signature
```ts
function readStepMetadata(target: unknown): ReadonlyMap
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/step.ts:37`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/step.ts#L37)
---
# readSubAgentMetadata
Source: https://docs.usetheo.dev/theokit/di-agent/reference/readSubAgentMetadata
_No description available._
# `readSubAgentMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function readSubAgentMetadata(target: unknown): ReadonlyMap
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/sub-agent.ts:22`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/sub-agent.ts#L22)
---
# readSubscriptionMetadata
Source: https://docs.usetheo.dev/theokit/di-agent/reference/readSubscriptionMetadata
_No description available._
# `readSubscriptionMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function readSubscriptionMetadata(target: unknown): ReadonlyMap
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/subscription.ts:18`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/subscription.ts#L18)
---
# readTextSplitterMetadata
Source: https://docs.usetheo.dev/theokit/di-agent/reference/readTextSplitterMetadata
_No description available._
# `readTextSplitterMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function readTextSplitterMetadata(target: unknown): ReadonlyMap
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/text-splitter.ts:19`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/text-splitter.ts#L19)
---
# readToolMetadata
Source: https://docs.usetheo.dev/theokit/di-agent/reference/readToolMetadata
_No description available._
# `readToolMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function readToolMetadata(target: unknown): ReadonlyMap
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/tool.ts:19`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/tool.ts#L19)
---
# readWorkflowMetadata
Source: https://docs.usetheo.dev/theokit/di-agent/reference/readWorkflowMetadata
_No description available._
# `readWorkflowMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function readWorkflowMetadata(target: Function): unknown
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/workflow.ts:17`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/workflow.ts#L17)
---
# Reranker
Source: https://docs.usetheo.dev/theokit/di-agent/reference/Reranker
_No description available._
# `Reranker`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function Reranker(options: RerankerOptions): PropertyDecorator
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/reranker.ts:10`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/reranker.ts#L10)
---
# RerankerOptions
Source: https://docs.usetheo.dev/theokit/di-agent/reference/RerankerOptions
_No description available._
# `RerankerOptions`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
interface RerankerOptions { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di-agent/src/decorators/reranker.ts:4`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/reranker.ts#L4)
---
# Retriever
Source: https://docs.usetheo.dev/theokit/di-agent/reference/Retriever
_No description available._
# `Retriever`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function Retriever(options: RetrieverOptions): PropertyDecorator
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/retriever.ts:9`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/retriever.ts#L9)
---
# RetrieverOptions
Source: https://docs.usetheo.dev/theokit/di-agent/reference/RetrieverOptions
_No description available._
# `RetrieverOptions`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
interface RetrieverOptions { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di-agent/src/decorators/retriever.ts:4`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/retriever.ts#L4)
---
# Squad
Source: https://docs.usetheo.dev/theokit/di-agent/reference/Squad
`@Squad(metadata)` — declare a sequential agent team on a property. The DI
# `Squad`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
`@Squad(metadata)` — declare a sequential agent team on a property. The DI
container / agent provider materializes it into a `createSquad(...)` at wiring
time.
## Signature
```ts
function Squad(metadata: SquadMetadata): PropertyDecorator
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/squad.ts:25`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/squad.ts#L25)
---
# SquadMetadata
Source: https://docs.usetheo.dev/theokit/di-agent/reference/SquadMetadata
Metadata captured by the `@Squad()` property decorator. Declares a sequential
# `SquadMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
Metadata captured by the `@Squad()` property decorator. Declares a sequential
agent team by referencing the agent property names that compose it. Mirrors
the `@theokit/sdk` `createSquad` factory (decorator mandate — every agentic
capability ships a decorator alongside the factory).
## Signature
```ts
interface SquadMetadata { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di-agent/src/decorators/squad.ts:11`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/squad.ts#L11)
---
# Step
Source: https://docs.usetheo.dev/theokit/di-agent/reference/Step
`@Step(metadata?)` — mark a class method as a workflow step. The method
# `Step`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
`@Step(metadata?)` — mark a class method as a workflow step. The method
receives the upstream step's return value (or the workflow input for an
entry step) and returns this step's output.
## Signature
```ts
function Step(metadata: StepMetadata): MethodDecorator
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/step.ts:27`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/step.ts#L27)
---
# StepMetadata
Source: https://docs.usetheo.dev/theokit/di-agent/reference/StepMetadata
Metadata captured by the `@Step()` method decorator. Declares one step of a
# `StepMetadata`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
Metadata captured by the `@Step()` method decorator. Declares one step of a
decorator-driven workflow and its single upstream dependency. Compiled into
a `@theokit/sdk` `Workflow` by `buildWorkflow` (decorator mandate — every
agentic capability ships a decorator alongside the factory/builder).
## Signature
```ts
interface StepMetadata { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di-agent/src/decorators/step.ts:11`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/step.ts#L11)
---
# SubAgent
Source: https://docs.usetheo.dev/theokit/di-agent/reference/SubAgent
_No description available._
# `SubAgent`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function SubAgent(options: SubAgentOptions): PropertyDecorator
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/sub-agent.ts:13`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/sub-agent.ts#L13)
---
# SubAgentOptions
Source: https://docs.usetheo.dev/theokit/di-agent/reference/SubAgentOptions
_No description available._
# `SubAgentOptions`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
interface SubAgentOptions { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di-agent/src/decorators/sub-agent.ts:5`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/sub-agent.ts#L5)
---
# Subscription
Source: https://docs.usetheo.dev/theokit/di-agent/reference/Subscription
_No description available._
# `Subscription`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function Subscription(options: SubscriptionOptions): PropertyDecorator
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/subscription.ts:9`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/subscription.ts#L9)
---
# SubscriptionOptions
Source: https://docs.usetheo.dev/theokit/di-agent/reference/SubscriptionOptions
_No description available._
# `SubscriptionOptions`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
interface SubscriptionOptions { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di-agent/src/decorators/subscription.ts:4`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/subscription.ts#L4)
---
# TextSplitter
Source: https://docs.usetheo.dev/theokit/di-agent/reference/TextSplitter
_No description available._
# `TextSplitter`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function TextSplitter(options: TextSplitterOptions): PropertyDecorator
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/text-splitter.ts:10`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/text-splitter.ts#L10)
---
# TextSplitterOptions
Source: https://docs.usetheo.dev/theokit/di-agent/reference/TextSplitterOptions
_No description available._
# `TextSplitterOptions`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
interface TextSplitterOptions { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di-agent/src/decorators/text-splitter.ts:4`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/text-splitter.ts#L4)
---
# Tool
Source: https://docs.usetheo.dev/theokit/di-agent/reference/Tool
_No description available._
# `Tool`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function Tool(options: ToolOptions): PropertyDecorator
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/tool.ts:10`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/tool.ts#L10)
---
# ToolOptions
Source: https://docs.usetheo.dev/theokit/di-agent/reference/ToolOptions
_No description available._
# `ToolOptions`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
interface ToolOptions { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di-agent/src/decorators/tool.ts:4`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/tool.ts#L4)
---
# UseSandbox
Source: https://docs.usetheo.dev/theokit/di-agent/reference/UseSandbox
_No description available._
# `UseSandbox`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function UseSandbox(options: UseSandboxOptions): PropertyDecorator
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/use-sandbox.ts:11`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/use-sandbox.ts#L11)
---
# UseSandboxOptions
Source: https://docs.usetheo.dev/theokit/di-agent/reference/UseSandboxOptions
_No description available._
# `UseSandboxOptions`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
interface UseSandboxOptions { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di-agent/src/decorators/use-sandbox.ts:5`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/use-sandbox.ts#L5)
---
# Workflow
Source: https://docs.usetheo.dev/theokit/di-agent/reference/Workflow
_No description available._
# `Workflow`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
function Workflow(options: WorkflowOptions): ClassDecorator
```
## Kind
`function`
## Source
[`packages/di-agent/src/decorators/workflow.ts:11`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/workflow.ts#L11)
---
# WorkflowOptions
Source: https://docs.usetheo.dev/theokit/di-agent/reference/WorkflowOptions
_No description available._
# `WorkflowOptions`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/di-agent` source to change this page.
_No description available._
## Signature
```ts
interface WorkflowOptions { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/di-agent/src/decorators/workflow.ts:4`](https://github.com/usetheodev/theokit-di/blob/main/packages/di-agent/src/decorators/workflow.ts#L4)
---
# Advanced
Source: https://docs.usetheo.dev/theokit/errors/advanced
AgentRunError's structured fields and codes, throwOnError vs the RunResult.error branch, Retry-After hints, and secret-safe raw bodies.
# Advanced errors
Verified against `@theokit/sdk` (`src/errors.ts`).
## `AgentRunError` — a run that ended in error
When a run terminates with `status: "error"`, the failure is an `AgentRunError` carrying everything you
need to route it or file a support ticket:
```ts
class AgentRunError extends TheokitAgentError {
readonly code: AgentRunErrorCode; // stable machine code (see below)
readonly provider?: string; // which provider failed
readonly requestId?: string; // provider x-request-id — quote in support tickets
readonly conversationId?: string; // the SDK conversation it was raised in
readonly raw?: string; // provider response body (secret-redacted at the getter)
get retriable(): boolean; // alias of isRetryable (handoff contract)
get retryAfterMs(): number | undefined; // provider Retry-After hint, in ms (0 is valid — use === undefined)
}
```
### `AgentRunErrorCode`
A closed union you can `switch` on:
```
rate_limit · auth_failed · invalid_request · timeout · server_error · context_too_long ·
content_filtered · model_unavailable · network · quota_exceeded · unknown ·
tool_runtime_error · aborted · invalid_model · safety_blocked · provider_unreachable
```
```ts
catch (err) {
if (err instanceof AgentRunError) {
switch (err.code) {
case "auth_failed": return rotateKey();
case "rate_limit": return backoff(err.retryAfterMs); // honor the provider hint
case "context_too_long": return compactAndRetry();
default: throw err;
}
}
}
```
## Two ways to surface a run error — `throwOnError`
By default (`throwOnError: false`), a failed run is **data**: `RunResult.status === "error"` with a
structured `RunResult.error`. Set `throwOnError: true` (on `send` / `prompt`) and the same failure is
**thrown** as an `AgentRunError` — catch once and branch on `code`:
```ts
try {
await Agent.prompt("hi", { apiKey, model, throwOnError: true });
} catch (err) {
if (err instanceof AgentRunError && err.code === "auth_failed") { /* bad key */ }
}
```
Pick the style that fits: inspect `RunResult.error` inline, or `try/catch` at a boundary.
## `Retry-After` hints
`retryAfterMs` surfaces the provider's `Retry-After` header (stored in `metadata.retryAfter` as
seconds) multiplied to **milliseconds**, so it composes directly with `setTimeout` / `Date.now()`.
`0` is a legitimate value — check `=== undefined`, not truthiness.
## Secret safety
`raw` (the provider response body) is wrapped in `redactSecrets` **at the getter boundary** —
secret-shaped substrings (`sk-…`, Bearer JWTs) are stripped before they reach you, and `raw` is
**never** serialized into `.message` (anti-leak invariant). Error objects are safe to log.
## Reference
- [`TheokitAgentError`](/theokit/reference/TheokitAgentError) · [`AgentRunError`](/theokit/reference/AgentRunError) · [`RateLimitError`](/theokit/reference/RateLimitError) · [`AuthenticationError`](/theokit/reference/AuthenticationError) · [`isTransientError`](/theokit/reference/isTransientError)
---
# Catch typed errors
Source: https://docs.usetheo.dev/theokit/errors/catch-typed-errors
Every SDK error extends TheokitAgentError — inspect instanceof, isRetryable, and isTransientError to route failures. Deterministic, no LLM.
# Catch typed errors
Because every SDK error extends `TheokitAgentError`, you can branch on `instanceof` and the structured
`isRetryable` flag instead of matching strings. `isTransientError` is the SDK's own classification of
"worth retrying".
```ts title="run.ts"
import {
TheokitAgentError,
AuthenticationError,
RateLimitError,
NetworkError,
ConfigurationError,
isTransientError,
} from "@theokit/sdk";
const errors: TheokitAgentError[] = [
new AuthenticationError("invalid API key"),
new RateLimitError("429 — slow down"),
new NetworkError("connection reset", { code: "network_error" }),
new ConfigurationError("missing model id"),
];
for (const err of errors) {
console.log(
`${err.name.padEnd(20)} base=${err instanceof TheokitAgentError} ` +
`retryable=${err.isRetryable} transient=${isTransientError(err)}`,
);
}
```
## Output
Deterministic — the flags come from each error's own construction:
```text
AuthenticationError base=true retryable=false transient=false
RateLimitError base=true retryable=true transient=true
NetworkError base=true retryable=true transient=true
ConfigurationError base=true retryable=false transient=false
```
## What it shows
- **`instanceof TheokitAgentError`** is `true` for every SDK error — one catch clause covers them all.
- **`isRetryable`** is baked into each error at construction — `RateLimitError` / `NetworkError` are
retryable; `AuthenticationError` / `ConfigurationError` are not (retrying a bad key never helps).
- **`isTransientError(err)`** is the SDK's transient-failure predicate — pair it with
[`Retry.create`](/theokit/resilience/retry-a-flaky-call), which uses it as its default `isRetryable`.
## Example
Full runnable source:
[`examples/errors-catalog`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/errors-catalog).
---
# Overview
Source: https://docs.usetheo.dev/theokit/errors
One typed error hierarchy — every SDK error extends TheokitAgentError, so you catch once and branch on code, isRetryable, and instanceof.
# Errors
Every error the SDK throws extends a single base class, **`TheokitAgentError`**. That means you catch
once and branch on structured fields — never parse a message string.
```ts
import { TheokitAgentError, AuthenticationError } from "@theokit/sdk";
try {
await agent.send("hi");
} catch (err) {
if (err instanceof AuthenticationError) rotateKey();
else if (err instanceof TheokitAgentError && err.isRetryable) backoffAndRetry();
else throw err;
}
```
## The base — `TheokitAgentError`
```ts
class TheokitAgentError extends Error {
readonly name: string;
readonly isRetryable: boolean; // is this worth retrying?
readonly code?: string; // stable machine code, e.g. "auth_failed"
readonly protoErrorCode?: string;
readonly metadata?: ErrorMetadata;
// `cause` carries the underlying error when wrapping
}
```
## The hierarchy
| Error | When |
| --- | --- |
| `AuthenticationError` | Invalid API key, not logged in, insufficient permissions. |
| `RateLimitError` | Provider 429 — retryable. |
| `NetworkError` | Timeout, connection reset, transport failure — retryable. |
| `ConfigurationError` | Invalid options (bad model id, malformed config). |
| `IntegrationNotConnectedError` | A required integration isn't connected (extends `ConfigurationError`). |
| `AgentRunError` | A run terminated with `status: "error"` (carries `code`, `provider`, `requestId`). |
| `ToolError` | A tool threw during execution. |
| `BudgetExceededError` | A spending budget's block limit was crossed. |
| `AgentDisposedError` | Used an agent after `dispose()`. |
| `UnsupportedRunOperationError` | An operation the current runtime can't serve (e.g. artifacts on local). |
| `TaskNotFoundError` · `InvalidTaskIdError` · `UnsupportedTaskOperationError` | Background-task errors. |
| `MemoryAdapterError` · `CredentialPoolExhaustedError` · `UnknownAgentError` | Subsystem-specific. |
Plus the helper **`isTransientError(err)`** — the SDK's own "is this a transient failure?" predicate,
the same one [`Retry.create`](/theokit/resilience) uses by default.
## Next
- [Catch typed errors](/theokit/errors/catch-typed-errors) — a runnable example that inspects each type.
- [Advanced](/theokit/errors/advanced) — `AgentRunError` fields, `throwOnError`, and retry integration.
## Reference
- [`TheokitAgentError`](/theokit/reference/TheokitAgentError) · [`AgentRunError`](/theokit/reference/AgentRunError) · [`isTransientError`](/theokit/reference/isTransientError)
---
# Errors
Source: https://docs.usetheo.dev/theokit/errors
The typed error hierarchy.
# Errors
Every error extends `TheokitAgentError`, so you can catch broadly or narrowly. Also available at the `@theokit/sdk/errors` subpath.
| Capability | What it adds |
| --- | --- |
| `TheokitAgentError` | Base class for all SDK errors |
| Typed subclasses | `AuthenticationError` · `ConfigurationError` · `RateLimitError` · `BudgetExceededError` · … |
---
# Advanced
Source: https://docs.usetheo.dev/theokit/evals/advanced
The eval run and its aggregate, datasets from JSONL, row concurrency and cancellation, resumable persistence, custom scorers, LLM-judge, and the code-verify gate.
# Advanced evals
Verified against `@theokit/sdk/eval` (`eval.ts`, `scorers.ts`, `types/eval.ts`).
## Running — `eval.run`
```ts
const run = await evaluation.run({
signal, // cancels pending rows; in-flight rows finish
persist: { /* durable, resumable per-row persistence */ },
classify: (row) => row.output.length > 100 ? "verbose" : "concise", // taxonomy tag per row
});
```
`run()` returns an `EvalRun`:
```ts
interface EvalRun {
id: string; name: string;
startedAt: number; endedAt: number; durationMs: number;
aggregate: EvalAggregate; // rolled-up scores across all rows
rows: EvalRowResult[]; // per-row output + scores + outcome
metadata?: Record;
}
```
Per-row errors are **isolated** — one row throwing doesn't abort the run; it lands in `rows` with its
error.
## Datasets
A `Dataset` is a list of rows — `{ input, expected?, metadata? }` — or a loader. Load one from JSONL:
```ts
import { loadJsonl } from "@theokit/sdk/eval";
const dataset = await loadJsonl("./cases.jsonl"); // throws JsonlParseError on a bad line
```
## Concurrency
`Eval.create({ concurrency })` bounds how many rows run at once — default **4** (matches
`Agent.batch`). Tune it to your provider's rate limits.
## Custom scorers
A scorer is just a function — `(output, expected?) => Score | Promise`. Wrap it with a name to
show up in reports:
```ts
const startsWithHi: NamedScorer = {
name: "starts-with-hi",
score: (output) => ({ score: output.startsWith("Hi") ? 1 : 0 }),
};
Eval.create({ dataset, scorers: [startsWithHi], agent });
```
## LLM-as-judge — `Scorers.llmJudge`
```ts
Scorers.llmJudge({ /* rubric, model, … */ }); // an LLM grades the answer (uses the agent facade)
```
Use it when correctness is subjective (tone, helpfulness) and no string/regex rule captures it. Unlike
the string scorers, this one **calls an LLM** — budget accordingly.
## Code-verify gate — `Scorers.verifyGate`
Grades a code patch by actually running tests (SWE-bench style):
```ts
Scorers.verifyGate({
repoDir, // where to run the tests
failToPass: ["test_x"], // tests that must flip to passing after the patch
passToPass: ["test_y"], // tests that must stay passing
sandbox, // Local / Docker / E2B (defaults to LocalSandbox)
});
```
Pair it with `captureArtifact` to grade the working-tree diff an agent produced. See
[Sandbox](/theokit/sandbox) for the execution backends.
## Reference
- [`Eval`](/theokit/reference/Eval) · [`Scorers`](/theokit/reference/Scorers) · [`EvalRun`](/theokit/reference/EvalRun) · [`EvalOptions`](/theokit/reference/EvalOptions) · [`loadJsonl`](/theokit/reference/loadJsonl)
---
# Overview
Source: https://docs.usetheo.dev/theokit/evals
Grade agent output against a dataset with built-in and custom scorers — exact match, substring, regex, JSON shape, LLM-as-judge, and a code-verify gate.
# Evals
Run an agent over a dataset and **score** every answer. `Eval.create` pairs a dataset with an agent
and a list of scorers; `run()` grades each row with per-row error isolation.
```ts
import { Eval, Scorers } from "@theokit/sdk/eval";
const evaluation = Eval.create({
dataset: [{ input: "Capital of France?", expected: "Paris" }],
scorers: [Scorers.containsExpected()],
agent: { apiKey, model }, // a fresh agent per row (state isolation)
});
const run = await evaluation.run();
```
## Built-in scorers — `Scorers`
Each returns a `NamedScorer` you drop into `scorers[]`; each is also directly callable —
`scorer.score(output, expected)` → `{ score, reason? }`.
| Scorer | Grades |
| --- | --- |
| `Scorers.exactMatch(opts?)` | Exact equality (case-sensitive by default). |
| `Scorers.containsExpected(opts?)` | Substring match (case-insensitive by default). |
| `Scorers.regex(pattern)` | Output matches a `RegExp`. |
| `Scorers.jsonShape(schema, opts?)` | Output parses and matches a Zod schema. |
| `Scorers.llmJudge(opts)` | An LLM grades the answer (uses the agent facade). |
| `Scorers.verifyGate(opts)` | Grades a code patch by running a verification command. |
## The agent, three ways
`Eval.create({ agent })` accepts an `SDKAgent` instance (shared, no isolation), an
`EvalAgentOptions` object (a fresh agent per row — the **default**, for state isolation), or a
`(entry) => SDKAgent` function for dynamic selection.
## Next
- [Score a prediction](/theokit/evals/score-a-prediction) — a runnable scorers example (deterministic).
- [Advanced](/theokit/evals/advanced) — the run, concurrency, datasets from JSONL, and custom scorers.
## Reference
- [`Eval`](/theokit/reference/Eval) · [`Scorers`](/theokit/reference/Scorers) · [`EvalOptions`](/theokit/reference/EvalOptions) · [`Score`](/theokit/reference/Score)
---
# Score a prediction
Source: https://docs.usetheo.dev/theokit/evals/score-a-prediction
Apply built-in Scorers directly to a prediction and expected answer — deterministic, no LLM. The same scorers drop into Eval.create to grade a dataset.
# Score a prediction
Every `Scorers.*` returns a `NamedScorer` whose `.score(output, expected)` is a **pure function** —
`{ score, reason? }`. You can call it directly to see exactly how a scorer grades, then hand the same
scorer to `Eval.create` to grade a whole dataset.
```ts title="run.ts"
import { Scorers } from "@theokit/sdk/eval";
const contains = Scorers.containsExpected(); // case-insensitive substring
const exact = Scorers.exactMatch(); // case-sensitive equality
const rx = Scorers.regex(/\b\d{4}\b/); // pattern match
console.log("contains:", await contains.score("The capital is Paris.", "paris"));
console.log("exact: ", await exact.score("Paris", "paris"));
console.log("regex: ", await rx.score("Released in 2026", undefined));
```
## Output
Deterministic — the scorers are pure, no network:
```text
contains: { score: 1 }
exact: { score: 0, reason: 'mismatch' }
regex: { score: 1 }
```
## What it shows
- **`containsExpected`** is case-insensitive by default — `"The capital is Paris."` contains `"paris"`
→ `score: 1`.
- **`exactMatch`** is case-sensitive by default — `"Paris"` ≠ `"paris"` → `score: 0` with a
`reason: "mismatch"`.
- **`regex`** grades the output alone (no `expected`) — `\b\d{4}\b` matches `"2026"` → `score: 1`.
- A `Score` is `{ score: number; reason?: string }` — `0..1`, with an optional explanation.
## Example
Full runnable source:
[`examples/eval-scorers`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/eval-scorers).
---
# Evals and scorers
Source: https://docs.usetheo.dev/theokit/evals
Measure accuracy, cost, and reliability.
# Evals and scorers
Run a dataset through your agent and score each case with built-in scorers or an LLM judge.
| Capability | What it adds |
| --- | --- |
| `Eval` (`/eval`) | Dataset-driven eval runs |
| `Scorers` | `exactMatch` · `regex` · `jsonShape` · `llmJudge` · `verifyGate` |
## Reference
See the [Evals and scorers concept](/theokit/concepts/eval) for the deep dive.
---
# File-based config
Source: https://docs.usetheo.dev/theokit/file-based
Assemble an agent's skills, subagents, hooks, MCP servers, context, and cron jobs from files under .theokit/ — discovered by a code-created agent via settingSources.
# File-based configuration
A TheoKit agent is created in code with `Agent.create(...)`. But most of what an agent
*uses* — skills, subagents, hooks, MCP servers, context, scheduled jobs — can live as
**files in your repo** under `.theokit/`. A code-created agent discovers them
automatically, so the configuration travels with git and applies to every caller that
starts the agent.
This **augments a code-created agent** — it is not a bundler that discovers and
instantiates whole agents from directories. The agent's `model` and base `systemPrompt`
are always set in `Agent.create(...)`; the files below are discovered from the filesystem
and merged in. The native session transcript (`/projects//.jsonl`,
`baseDir` default `~/.theokit`) is runtime *state*, not an agent definition.
## Turn it on
File discovery is opt-in through `local.settingSources`:
```ts
const agent = await Agent.create({
apiKey,
model: { id: "openai/gpt-4o-mini" },
local: { cwd: process.cwd(), settingSources: ["project"] }, // read .theokit/ from cwd
});
```
- `settingSources: ["project"]` — read `.theokit/` from `local.cwd`.
- `settingSources: ["user"]` — also read `~/.theokit/` (user-level hooks and MCP servers).
- Omit `settingSources` — only inline (code) config is used; nothing is read from disk.
Call `agent.reload()` to re-read the files (context, skills, hooks, project MCP, subagents)
without disposing the agent or losing conversation state. A malformed skill or context
file raises `ConfigurationError` — it fails loudly, never silently ignored.
## What can live in files
```text
.theokit/
├── skills//SKILL.md # capability packs (name + description + body)
├── agents/.md # subagents (one markdown file each)
├── hooks.json # shell-command policy hooks (Claude Code shape)
├── mcp.json # MCP servers
├── context/.md # context sources (task working-set)
└── cron/jobs.json # scheduled agent runs (local scheduler)
```
Most files are **markdown + YAML frontmatter** (same shape as `SKILL.md`). The exception
is **hooks**, which use JSON in the exact Claude Code `settings.json` shape (a hook's
markdown body is inert, and Claude Code configures hooks in JSON).
### Skills — `.theokit/skills//SKILL.md`
A named capability pack. Required frontmatter `name` + `description`; optional `category`
and `dependencies`. Discovered skills are exposed to the model by name + description; read
the full body on demand.
```markdown title=".theokit/skills/release/SKILL.md"
---
name: release
description: How to cut a release — version bump, changelog, tag.
---
# Release skill
1. Consume the changeset, bump the version.
2. Move `[Unreleased]` into a dated section.
3. Open the develop→main PR; a human merges.
```
Inspect them at runtime with `agent.skills.list()` (metadata) and `agent.skills.get(name)`
(full body). See [Skills](/theokit/skills).
### Subagents — `.theokit/agents/.md`
One markdown file per subagent: frontmatter carries the `description` (and optional `model`,
a `tools` whitelist, and per-subagent `mcpServers`); the body becomes the subagent prompt.
Inline definitions override a file-based subagent of the same name.
```markdown title=".theokit/agents/code-reviewer.md"
---
description: Reviews code for bugs and security issues.
model: inherit
tools: read_file, list_dir
---
You are a meticulous code reviewer. Report bugs, security issues, and unclear
naming. Do not modify files.
```
A `tools: read_file, list_dir` subagent provably cannot call `write_file` or `shell` — the
whitelist is enforced at dispatch. See [Subagents](/theokit/subagents).
### Hooks — `.theokit/hooks.json`
Shell-command policy hooks, in the exact Claude Code shape. Events map to the SDK's firing
points: `PreToolUse`→preToolUse, `PostToolUse`→postToolUse, `UserPromptSubmit`→preRun,
`Stop`→stop (unsupported Claude Code events are skipped with a warning).
```json title=".theokit/hooks.json"
{
"hooks": {
"PreToolUse": [
{
"matcher": "shell",
"hooks": [{ "type": "command", "command": "node .theokit/policy.js", "timeout": 30 }]
}
]
}
}
```
The command receives the payload as JSON on stdin; a non-zero exit on `PreToolUse` /
`UserPromptSubmit` blocks. The old `.theokit/hooks/*.md` markdown form is no longer
supported (a stray dir warns to migrate). See [Hooks](/theokit/hooks).
### MCP servers — `.theokit/mcp.json`
Tool servers discovered from disk and merged with any inline `mcpServers`. Project servers
come from `.theokit/mcp.json` (with `settingSources: ["project"]`); user servers from
`~/.theokit/mcp.json` (with `"user"`).
```json title=".theokit/mcp.json"
{
"filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"] }
}
```
See [MCP](/theokit/mcp).
### Context — `.theokit/context/.md`
The task working-set offered to a run. With `context: { manager: "file" }` and
`settingSources: ["project"]`, the file context manager reads `.theokit/context/.md`
(alongside conventional `AGENTS.md`, `CLAUDE.md`, `THEO.md`, `.cursor/rules/*.mdc`). Inspect
the redacted result with `agent.context.snapshot()`. See [Context](/theokit/context).
### Scheduled jobs — `.theokit/cron/jobs.json`
Local cron jobs are persisted here and reloaded on `Cron.start({ cwd })`; they fire while
the host process is alive. See [Schedules](/theokit/schedules).
## Local vs cloud
- **Local** — files are read from `local.cwd` (and `~/.theokit/` for user sources) when
`settingSources` opts in.
- **Cloud** — commit the same `.theokit/` files to the repo passed in `cloud.repos`;
SDK-created cloud agents load project skills, hooks, and subagents automatically.
## Set in code (not file-based)
These stay in `Agent.create(...)` — there is no file convention for them today:
- **`model` and base `systemPrompt`** — the agent's identity.
- **Tools** (`Tool.create`) — though MCP servers *are* file-based (above).
- **Memory, guardrails, processors, workflows** — configured in code.
## Migrating legacy config
A standalone CLI converts legacy `.theokit/context.json` and
`.theokit/plugins//plugin.json` to the markdown form (hooks migrate the other way,
back to `.theokit/hooks.json`):
```bash
npx theokit-migrate-config --apply
```
---
# Advanced
Source: https://docs.usetheo.dev/theokit/filesystem/advanced
Read-only backends, the stale-write guard, FileStat, custom backends, and per-request multi-tenant filesystem providers.
# Advanced filesystem
Verified against `@theokit/sdk/filesystem` (ADR 0011).
## Read-only backends
```ts
const ro = new LocalFilesystem({ basePath: "./data", readOnly: true });
await ro.readFile("report.md"); // fine
await ro.writeFile("x", "y"); // throws FilesystemReadOnlyError
```
A read-only backend rejects **every** write. Note the ordering: a traversal path on a read-only backend
reports the **security** error first (fail-clear) — the boundary check runs before the read-only check.
## Stale-write guard — `expectedMtime`
Optimistic concurrency for writes: pass the `mtime` you last read, and the write fails if the file
changed underneath you.
```ts
const stat = await fs.stat("notes.txt");
await fs.writeFile("notes.txt", "new content", { expectedMtime: stat.mtimeMs });
// throws StaleFileError if the file was modified since you read `stat`
```
`StaleFileError` carries `expectedMtime` and `actualMtime` so you can surface a precise conflict.
## `FileStat`
```ts
interface FileStat {
size: number;
mtimeMs: number;
isFile: boolean;
isDirectory: boolean;
}
```
Returned by `writeFile` and `stat`. `mtimeMs` feeds the stale-write guard above.
## Custom backends — `FilesystemBackend`
Implement the four abstract methods and you have an S3 / GCS / in-memory backend that composes exactly
like `LocalFilesystem`:
```ts
import { FilesystemBackend } from "@theokit/sdk/filesystem";
class InMemoryFs extends FilesystemBackend {
async readFile(path) { /* … */ }
async writeFile(path, content, opts) { /* … */ }
async stat(path) { /* … */ }
async list(path) { /* … */ }
}
```
`exists()` and the `readOnly` / `basePath` accessors derive on the base class — you only write the four.
## Multi-tenant roots — `FilesystemProvider` + `resolveFilesystem`
A `FilesystemProvider` is either a backend instance or a **factory of `(ctx) => backend`** — so a
multi-tenant app can hand each request its own boundary root:
```ts
import { resolveFilesystem, LocalFilesystem } from "@theokit/sdk/filesystem";
const provider = (ctx: { tenantId: string }) =>
new LocalFilesystem({ basePath: `./tenants/${ctx.tenantId}` });
const fs = await resolveFilesystem(provider, { tenantId: "acme" }); // concrete backend for this request
```
`resolveFilesystem(provider, ctx)` returns the backend directly if `provider` is already an instance,
or calls the factory with `ctx` — the seam for per-request isolation.
## Reference
- [`LocalFilesystem`](/theokit/reference/LocalFilesystem) · [`FilesystemBackend`](/theokit/reference/FilesystemBackend) · [`FilesystemProvider`](/theokit/reference/FilesystemProvider) · [`resolveFilesystem`](/theokit/reference/resolveFilesystem) · [`StaleFileError`](/theokit/reference/StaleFileError)
---
# Enforce a storage boundary
Source: https://docs.usetheo.dev/theokit/filesystem/enforce-a-storage-boundary
Write, read, and list inside a LocalFilesystem root, and watch a path-traversal escape get rejected with a typed error — deterministic, no LLM.
# Enforce a storage boundary
`LocalFilesystem` resolves every path inside its `basePath`. Reads and writes within the boundary work
normally; anything that tries to escape it (`../`, absolute paths, symlink escape) is rejected with a
`FilesystemSecurityError`. It's all local file I/O — deterministic, no LLM.
```ts title="run.ts"
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { LocalFilesystem, FilesystemSecurityError } from "@theokit/sdk/filesystem";
const here = dirname(fileURLToPath(import.meta.url));
const fs = new LocalFilesystem({ basePath: join(here, "root") });
const stat = await fs.writeFile("notes.txt", "ship it");
console.log("wrote notes.txt, size:", stat.size);
console.log("readFile:", await fs.readFile("notes.txt"));
console.log("list: ", (await fs.list(".")).sort().join(", "));
try {
await fs.readFile("../run.ts"); // escape the boundary
} catch (err) {
console.log("traversal blocked:", err instanceof FilesystemSecurityError);
}
```
## Output
Deterministic — the boundary rejects the traversal every time:
```text
wrote notes.txt, size: 7
readFile: ship it
list: .gitkeep, notes.txt
traversal blocked: true
```
## What it shows
- **`writeFile` returns a `FileStat`** (`size`, `mtimeMs`, `isFile`, `isDirectory`) — the 7 bytes of
`"ship it"`.
- **`readFile` / `list`** operate relative to `basePath` — clean, root-relative paths.
- **`../run.ts` is rejected** — the resolver catches lexical traversal before touching the disk and
throws a typed `FilesystemSecurityError`, so a malicious tool call can't read outside the root.
## Example
Full runnable source:
[`examples/filesystem-boundary`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/filesystem-boundary).
---
# Overview
Source: https://docs.usetheo.dev/theokit/filesystem
A boundary-enforced, swappable storage backend for agent file tools — every path resolves within a root, escapes are rejected, and read-only is one flag.
# Filesystem
`@theokit/sdk/filesystem` gives agent file tools a **boundary-enforced** storage root. Every path is
resolved within `basePath`; lexical traversal (`..`, absolute paths, NUL) and symlink escape are
rejected with a typed `FilesystemSecurityError`.
```ts
import { LocalFilesystem } from "@theokit/sdk/filesystem";
const fs = new LocalFilesystem({ basePath: "./workspace", readOnly: false });
await fs.writeFile("notes.txt", "ship it");
await fs.readFile("notes.txt"); // "ship it"
await fs.list("."); // ["notes.txt"]
```
## The backend
`LocalFilesystem` implements four methods — `readFile`, `writeFile`, `stat`, `list` — plus a derived
`exists()`. It extends the abstract **`FilesystemBackend`**, so you can swap in your own (S3, GCS,
in-memory) by implementing the same four methods; the boundary and `readOnly` accessors derive on the
base class.
This is a **path boundary**, not an OS isolation boundary (same user, same machine). It stops traversal
and symlink escape; for untrusted code, still run it inside a container or VM sandbox — see
[Sandbox](/theokit/sandbox).
## Typed errors
| Error | When |
| --- | --- |
| `FilesystemSecurityError` | Path escapes the boundary (`..`, absolute, symlink escape). |
| `FilesystemReadOnlyError` | A write on a `readOnly: true` backend. |
| `FileNotFoundError` | Read/stat of a missing path. |
| `StaleFileError` | A write whose `expectedMtime` no longer matches (stale-write guard). |
## Next
- [Enforce a storage boundary](/theokit/filesystem/enforce-a-storage-boundary) — a runnable example.
- [Advanced](/theokit/filesystem/advanced) — read-only, the stale-write guard, custom backends, and
per-request multi-tenant providers.
## Reference
- [`LocalFilesystem`](/theokit/reference/LocalFilesystem) · [`FilesystemBackend`](/theokit/reference/FilesystemBackend) · [`FilesystemConfig`](/theokit/reference/FilesystemConfig) · [`FileStat`](/theokit/reference/FileStat)
---
# Require read-before-write
Source: https://docs.usetheo.dev/theokit/filesystem/read-before-write
Refuse blind or stale overwrites — make the write tool require a prior read, and reject a write whose file changed since, with a typed StaleFileError.
# Require read-before-write
A coding agent that overwrites a file it never read (or read a stale copy of) silently clobbers work. SE32 adds an opt-in safety: the write tool refuses to write unless the same file was read first, and refuses if the file changed on disk since that read.
## Wire it up (tool layer)
Give the read and write tools a shared `ReadTracker` and turn on `requireReadBeforeWrite`:
```ts
import { createReadFileTool, createWriteFileTool, ReadTracker } from "@theokit/sdk-tools";
const tracker = new ReadTracker();
const readTool = createReadFileTool({ projectRoot, readTracker: tracker });
const writeTool = createWriteFileTool({ projectRoot, readTracker: tracker, requireReadBeforeWrite: true });
```
Now the agent's writes behave like this:
| Situation | Result |
| --- | --- |
| File was read, unchanged since | write proceeds |
| File was **never read** | `{ ok: false, error: "read_required" }` |
| File **changed on disk** since it was read | `{ ok: false, error: "stale_file" }` |
| Brand-new file (does not exist) | write proceeds (nothing to clobber) |
Default is **off** — omit `requireReadBeforeWrite` and writes behave exactly as before (back-compat). Turning it on without a `readTracker` fails fast at construction, so you can't half-configure it.
## The backend guard (`expectedMtime` / `StaleFileError`)
Underneath, `@theokit/sdk/filesystem` enforces the same optimistic-concurrency check at the actual write. Pass `expectedMtime` and the write throws a typed `StaleFileError` — carrying `expectedMtime` and `actualMtime` — if the file moved under you, instead of clobbering it:
```ts
import { LocalFilesystem, StaleFileError } from "@theokit/sdk/filesystem";
const fs = new LocalFilesystem({ basePath: root });
const before = await fs.stat("doc.txt");
try {
await fs.writeFile("doc.txt", next, { expectedMtime: before.mtimeMs });
} catch (err) {
if (err instanceof StaleFileError) {
// someone edited doc.txt since we read it — re-read and reconcile
}
}
```
The tool-layer flag and the backend `expectedMtime` compose: the write tool forwards the tracked mtime as `expectedMtime` so the backend re-checks at write time (closing the read→write TOCTOU window a same-user stat cannot).
## Reference
- `ReadTracker` / `createWriteFileTool` (from `@theokit/sdk-tools`) · [`StaleFileError`](/theokit/reference/StaleFileError) · [`LocalFilesystem`](/theokit/reference/LocalFilesystem)
---
# Filesystem
Source: https://docs.usetheo.dev/theokit/filesystem
A pluggable, boundary-enforced filesystem.
# Filesystem
A filesystem provider seam — the storage-side twin of the sandbox — with enforced boundaries and read-only roots.
| Capability | What it adds |
| --- | --- |
| `LocalFilesystem` (`/filesystem`) | Boundary-enforced local FS |
| `resolveFilesystem` | Per-request provider resolution |
---
# @theokit/gateway
Source: https://docs.usetheo.dev/theokit/gateways/core
The transport-agnostic core — BasePlatformAdapter, GatewayRunner, SessionRouter, DeliveryRouter, the hook chain, the MessageEvent union, and the shared chunk/error primitives every adapter builds on.
`@theokit/gateway` is the transport-agnostic core of the gateway cluster. It defines the
contract every adapter implements and the orchestration that turns inbound platform
messages into agent replies — while **composing, not reimplementing**: session
persistence stays in `@theokit/sdk`, scheduling in `Cron`, prompt resolution in the
SDK's resolver. Adapters live in separate peer-dependency packages; the core imports
none of them.
## Install
```bash
pnpm add @theokit/gateway @theokit/sdk
# Plus one or more transport adapters:
pnpm add @theokit/gateway-telegram grammy
```
## Architecture (five pieces)
| Module | Responsibility |
|---|---|
| `BasePlatformAdapter` | The contract every adapter implements: `connect`, `disconnect`, `sendMessage`, `onInbound` (plus optional `startTyping`/`stopTyping`). Idempotent lifecycle; never throws on a platform error — returns a typed `SendResult`. |
| `GatewayRunner` | Orchestrator: holds the adapter map, runs the hook chain, dispatches each inbound `MessageEvent` to your handler, and auto-routes `ctx.reply` by `event.platform`. Drains in-flight replies on `stop()`. |
| `SessionRouter` | Pure `MessageEvent → agentId` strategy (composes `Agent.resume`, ADR D174). Swap the strategy for custom session keys. |
| `DeliveryRouter` | Dispatches outbound messages by platform (composes `Cron` for scheduled delivery, ADR D175). |
| `HookExecutor` | Runs the `pre_inbound` / `post_outbound` / `on_error` chain — the gateway's own extension contract (ADR D176), lives in `hooks/executor.ts`. |
## Minimal example
```ts title="server/bot.ts"
import { Agent } from '@theokit/sdk'
import { GatewayRunner, SessionRouter } from '@theokit/gateway'
import { TelegramAdapter } from '@theokit/gateway-telegram'
const router = new SessionRouter()
const adapter = new TelegramAdapter({ token: process.env.TELEGRAM_BOT_TOKEN! })
const runner = new GatewayRunner({
adapters: [adapter],
handler: async (event, ctx) => {
const agent = await Agent.resume(router.resolveAgentId(event), {
apiKey: process.env.OPENROUTER_API_KEY!,
model: { id: 'openai/gpt-4o-mini' },
})
const run = await agent.send(event.text)
const result = await run.wait()
await ctx.reply(result.result ?? 'no reply')
await agent.dispose()
},
})
await runner.start()
```
## The `MessageEvent` union
Every adapter emits the same portable shape — a discriminated union keyed by `platform`
with a `BaseMessageEvent` (id, sender, channel, text, `receivedAt`, optional `replyTo`)
plus a platform-specific sibling field (`telegram?`, `discord?`, …) carrying the raw
payload as an escape hatch. Narrowing is exhaustive:
```ts
switch (event.platform) {
case 'telegram': return event.telegram.threadId // narrowed
case 'discord': return event.discord.guildId // narrowed
}
```
The union is intentionally **closed** — all ten platform variants are declared in the
core so consumer `switch`es are compiler-checked for exhaustiveness. Adding an
eleventh platform is a bounded edit to the core union, by design (see
`docs/adr/0001-message-event-closed-union.md`).
## Hooks
Three fire points let you gate, observe, and recover around the handler without touching
it. `pre_inbound` runs sequentially — the first `{ block: true }` short-circuits (and
its `message`, if set, is replied before the handler is skipped); a throwing hook is
treated fail-safe as a block. `post_outbound` and `on_error` are fire-and-forget.
```ts
const hook = {
name: 'audit',
pre_inbound: async ({ event }) => { /* return { block: true } to stop */ },
post_outbound: async ({ event, outbound, result }) => { /* log delivery */ },
on_error: async ({ event, error }) => { /* report */ },
}
```
## Shared text + error primitives
The core single-sources the message-splitting and configuration-error knowledge the
adapters used to copy:
- **`chunkText(text, options)`** — boundary-preferring chunker (prefers `\n\n` → `\n` →
space, optional UTF-16 surrogate guard) used by the Slack/WhatsApp/Teams/Discord
adapters to respect per-platform length caps. Validates its inputs and throws
`RangeError` on misuse (positive-integer `limit`, `safeLimit <= limit`) — fail-fast,
never an infinite loop.
- **`chunkByGrapheme(text, options)`** — grapheme-cluster-safe (`Intl.Segmenter`)
chunker used by LINE and SMS; never severs an emoji, regional-indicator pair, or
combining sequence.
- **`GatewayConfigurationError`** (+ `GatewayConfigurationErrorOptions`) — the shared
base each adapter's `ConfigurationError` extends, so a construction-time misconfig
carries one consistent `code` / `detail` contract across the cluster.
## Design principles
- **Compose, don't reimplement.** The gateway never owns session persistence (the SDK
does), scheduling (`Cron`), or prompt resolution (`SystemPromptResolver`).
- **Adapters are peer-dep packages.** Install only the transports you need.
- **Hooks live in the gateway, not the SDK.** Transport-layer concerns don't pollute the
SDK's `Plugin` contract.
`@theokit/gateway` is versioned independently via Changesets and is a peer dependency
of every adapter. Keep them in step: an adapter that uses `chunkText` /
`GatewayConfigurationError` requires `@theokit/gateway@^0.5.0`.
---
# @theokit/gateway-discord
Source: https://docs.usetheo.dev/theokit/gateways/discord
Discord adapter for TheoKit gateways — wraps discord.js in the BasePlatformAdapter contract over the WebSocket Gateway, with MessageContent intents by default, bot-to-bot filtering, and 2000-char message splitting.
`@theokit/gateway-discord` is the Discord platform adapter for `@theokit/gateway`. It
wraps [discord.js](https://discord.js.org/) in the `BasePlatformAdapter` contract over the
**WebSocket Gateway**: `connect()` calls `client.login(token)` and awaits the `ready`
event, inbound Discord messages are normalized into a portable `MessageEvent`, and
outbound replies are auto-split at Discord's 2000-character cap. You construct a
`DiscordAdapter`, hand it to a `GatewayRunner`, and your handler stays platform-agnostic —
`ctx.reply` routes back to the same channel.
## Install
```bash
pnpm add @theokit/gateway-discord discord.js @theokit/gateway @theokit/sdk
```
`discord.js` is a peer dependency — install the version your bot needs. `@theokit/gateway`
and `@theokit/sdk` are peers too, so there is exactly one copy in your app.
## Usage
Construct the adapter, resolve an agent per session inside the handler, and reply:
```ts title="server/bot.ts"
import { Agent } from '@theokit/sdk'
import { GatewayRunner, SessionRouter } from '@theokit/gateway'
import { DiscordAdapter } from '@theokit/gateway-discord'
const router = new SessionRouter()
const adapter = new DiscordAdapter({
token: process.env.DISCORD_BOT_TOKEN!,
// intents defaults to [Guilds, GuildMessages, MessageContent,
// DirectMessages, DirectMessageReactions] — see the callout below.
})
const runner = new GatewayRunner({
adapters: [adapter],
handler: async (event, ctx) => {
const agent = await Agent.resume(router.resolveAgentId(event), {
apiKey: process.env.OPENROUTER_API_KEY!,
model: { id: 'openai/gpt-4o-mini' },
})
const run = await agent.send(event.text)
const result = await run.wait()
await ctx.reply(result.result ?? 'no reply')
await agent.dispose()
},
})
await runner.start()
```
`DiscordAdapterOptions` is `{ token: string; intents?: GatewayIntentBits[] }`. When
`intents` is omitted it falls back to `DEFAULT_DISCORD_INTENTS`; messages from other bots
(`msg.author.bot`) are always ignored so loops can't form.
### Platform escape hatch
For Discord-only features — slash commands via the Application Commands API,
`interactionCreate`, presence updates — reach the underlying discord.js `Client` with
`adapter.getBot()` and register handlers **before** calling `runner.start()`. Per-message
platform fields (guild id, channel id, message id, the raw discord.js `Message`) are on
`event.discord`.
## What you get
- **`DiscordAdapter`** — the discord.js wrapper implementing `BasePlatformAdapter`
(`connect` / `disconnect` / `sendMessage` / `onInbound`) over the WebSocket Gateway;
`connect()` never throws on a bad token (returns `false`).
- **`DEFAULT_DISCORD_INTENTS`** — `[Guilds, GuildMessages, MessageContent, DirectMessages,
DirectMessageReactions]`, the default intent set that ensures `msg.content` is populated.
- **Bot-to-bot filtering** — inbound messages from other bots (`msg.author.bot`) are
dropped at the adapter so bots can't reply to each other in a loop.
- **2000-char message splitting** — outbound text is auto-split at Discord's hard message
limit; each chunk is sent in order and the last message id is returned.
- **Error mapping** — discord.js `DiscordAPIError`s are mapped to stable send-result codes:
`dm_blocked` (50007), `no_permission` (50001/50013), and `rate_limited` (HTTP 429).
- **`event.discord` fields + `adapter.getBot()`** — `guildId`, `channelId`, `messageId`,
and `raw` (the discord.js `Message`) for platform-specific work, plus direct `Client`
access to register slash-command / interaction handlers.
This adapter is `0.1.0` (pre-release) and ships the **WebSocket Gateway only** — there is
no webhook bot mode in v0.1. The default intents include `MessageContent`, which is
**required** for the bot to see message text: without it discord.js delivers an empty
`msg.content` and your handler silently receives blank `event.text`. If you pass a custom
`intents` array, keep `GuildMessages` and `MessageContent` (passing `intents: []` logs a
warning). Slash commands, voice, and other Discord-native features are reached via
`adapter.getBot()` rather than the portable `MessageEvent`.
---
# @theokit/gateway-email
Source: https://docs.usetheo.dev/theokit/gateways/email
Email channel adapter for TheoKit gateways — inbound IMAP IDLE + outbound SMTP as a two-way bot channel, distinct from the transactional plugin-email. Reconstructs RFC 5322 threads and hardens against mail loops.
`@theokit/gateway-email` is the email platform adapter for `@theokit/gateway`. It turns a
plain mailbox into a two-way bot channel: inbound mail arrives over IMAP IDLE (with a
polling fallback), replies go out over SMTP, and threading headers are preserved so answers
land in the same conversation thread. You construct an `EmailAdapter`, hand it to a
`GatewayRunner`, and your handler stays platform-agnostic — `ctx.reply` routes back to the
sender.
This is **not** the transactional [`@theokit/plugin-email`](/theokit/plugins/email) (Resend
one-shot sends like magic links). This gateway is a conversational **inbound + outbound**
channel — the agent reads a mailbox and answers from it. It speaks the community-standard
2026 Node stack: [nodemailer](https://nodemailer.com/) (SMTP), [imapflow](https://imapflow.com/)
(IMAP IDLE), and [mailparser](https://nodemailer.com/extras/mailparser/) (RFC 5322 parsing).
## Install
```bash
pnpm add @theokit/gateway-email nodemailer imapflow mailparser @theokit/gateway @theokit/sdk
```
`nodemailer`, `imapflow`, and `mailparser` are peer dependencies — install the versions your
bot needs. `@theokit/gateway` and `@theokit/sdk` are peers too, so there is exactly one copy
in your app.
## Usage
Construct the adapter with SMTP + IMAP config, hand it to a `GatewayRunner`, and reply from
the handler:
```ts title="server/bot.ts"
import { Agent } from '@theokit/sdk'
import { GatewayRunner } from '@theokit/gateway'
import { EmailAdapter } from '@theokit/gateway-email'
const adapter = new EmailAdapter({
address: process.env.EMAIL_ADDRESS!, // mailbox the bot listens on + outbound From:
password: process.env.EMAIL_PASSWORD!, // Gmail: an App Password, NOT the account password
imapHost: 'imap.gmail.com', // inbound (default port 993 / SSL)
smtpHost: 'smtp.gmail.com', // outbound (default port 587 / STARTTLS)
fromName: 'Acme Assistant', // optional display name on outbound From:
allowedSenders: ['founder@acme.test'], // optional allowlist (case-insensitive)
})
const runner = new GatewayRunner({
adapters: [adapter],
handler: async (event, ctx) => {
const agent = await Agent.create({
apiKey: process.env.OPENROUTER_API_KEY!,
model: { id: 'openai/gpt-4o-mini' },
systemPrompt: 'You are a concise email assistant. Plain text only.',
})
const run = await agent.send(event.text)
const result = await run.wait()
await ctx.reply(result.result ?? 'no reply')
await agent.dispose()
},
})
await runner.start()
```
`ctx.reply` routes through the same `EmailAdapter`, which looks up the inbound thread and
prepends `Re:` plus the `In-Reply-To` / `References` headers — so the reply threads into the
original conversation instead of starting a new one.
### Options
`EmailAdapterOptions` requires `address`, `password`, `imapHost`, and `smtpHost` (validated
non-empty at construction — a missing one throws a `TypeError` immediately). The rest are
optional: `imapPort` (default `993`), `smtpPort` (default `587`), `fromName`, `allowedSenders`,
`allowAutomated` (default `false`), `pollIntervalMs` (default `15000`, used only when IMAP
IDLE is unavailable), and `maxBodyChars` (default `50000`, ~12k tokens — bodies above this are
truncated).
## What you get
- **`EmailAdapter`** — the nodemailer + imapflow + mailparser wrapper implementing
`BasePlatformAdapter` (`connect` / `disconnect` / `sendMessage` / `onInbound`). `connect()`
never throws on bad credentials — it logs and returns `false`.
- **IMAP IDLE inbound** — RFC 2177 push via imapflow when the server supports it, with a 15s
polling fallback otherwise. Each UNSEEN message is fetched, deduplicated by UID, and
dispatched once.
- **SMTP outbound with RFC 5322 threading** — replies reconstruct the thread from
`Message-ID` / `In-Reply-To` / `References`, so answers land in the same inbox thread as the
incoming mail.
- **Loopback guard (EC-1, critical)** — messages from the bot's own address are dropped
before anything else, so the agent can never answer itself into an infinite mail loop.
- **Automated-sender filter** — `noreply@` / `postmaster@` / `mailer-daemon@` / `bounce@`
addresses and `Auto-Submitted`, `Precedence: bulk|list`, and `X-Auto-Response-Suppress`
headers are dropped unless you set `allowAutomated: true`.
- **`allowedSenders` allowlist** — case-insensitive, display-name aware (`"Alice" `
matches `a@x`). Omit it for an open mailbox; an empty array closes the mailbox entirely.
This adapter is `0.1.x` (pre-release, ADR D338 — breaking changes allowed within `0.x`).
It authenticates with a plain user + password, so with Gmail you need an **App Password**
(2FA account, not the login password); Outlook/Office 365, Yahoo, and Fastmail work the
same way with their app-password + IMAP/SMTP host settings. The own-address **loopback
guard** is always on and non-negotiable — it is what keeps the bot from replying to itself
in a loop. There is a runnable `examples/email-bot` in the repo (full Gmail App Password
walkthrough) to copy from.
---
# Getting started
Source: https://docs.usetheo.dev/theokit/gateways/getting-started
The common adoption pattern for TheoKit gateways — install the core with an adapter and its peers, wire a GatewayRunner, resolve an agent per session, and reply.
Every gateway follows the same shape. Install `@theokit/gateway` and one adapter with
their peer dependencies, construct the adapter, hand it to a `GatewayRunner` with a
`handler`, and `start()`. The handler receives a portable `MessageEvent` and a `ctx`
whose `reply` is auto-routed back to the originating platform.
## Peer-dependency discipline
Adapters never bundle the framework. `@theokit/sdk` and `@theokit/gateway` are peer
dependencies you install alongside the adapter so there is exactly one copy in your app,
plus the platform SDK the adapter wraps. Each adapter's own page lists its full peer set.
```bash
# Core + SDK (always):
pnpm add @theokit/gateway @theokit/sdk
# Plus one or more transport adapters with their platform SDK:
pnpm add @theokit/gateway-telegram grammy
pnpm add @theokit/gateway-discord discord.js
```
## Minimal bot — one adapter
Construct the adapter, resolve an agent per session inside the handler, and reply:
```ts title="server/bot.ts"
import { Agent } from '@theokit/sdk'
import { GatewayRunner, SessionRouter } from '@theokit/gateway'
import { TelegramAdapter } from '@theokit/gateway-telegram'
const router = new SessionRouter()
const adapter = new TelegramAdapter({ token: process.env.TELEGRAM_BOT_TOKEN! })
const runner = new GatewayRunner({
adapters: [adapter],
handler: async (event, ctx) => {
const agent = await Agent.resume(router.resolveAgentId(event), {
apiKey: process.env.OPENROUTER_API_KEY!,
model: { id: 'openai/gpt-4o-mini' },
})
const run = await agent.send(event.text)
const result = await run.wait()
await ctx.reply(result.result ?? 'no reply')
await agent.dispose()
},
})
await runner.start()
```
`ctx.reply` sends back to the same chat on the same platform — no platform branching in
your handler. `runner.stop()` drains in-flight replies and disconnects every adapter.
## Multi-channel — many adapters, one handler
Pass several adapters to the same runner. The handler is platform-agnostic; narrow on
`event.platform` only when you need platform-specific fields:
```ts title="server/bot.ts"
const runner = new GatewayRunner({
adapters: [
new TelegramAdapter({ token: process.env.TELEGRAM_BOT_TOKEN! }),
new SlackAdapter({ botToken: process.env.SLACK_BOT_TOKEN!, appToken: process.env.SLACK_APP_TOKEN! }),
],
handler: async (event, ctx) => {
// event.platform narrows the union: "telegram" | "slack" | …
const agent = await Agent.resume(router.resolveAgentId(event), { /* … */ })
await ctx.reply((await (await agent.send(event.text)).wait()).result ?? '…')
await agent.dispose()
},
})
```
## Extend with hooks
Register `pre_inbound` / `post_outbound` / `on_error` hooks to gate, observe, or recover
without touching your handler. A `pre_inbound` hook returning `{ block: true }`
short-circuits before the handler runs:
```ts
const runner = new GatewayRunner({
adapters: [adapter],
hooks: [
{
name: 'rate-limit',
pre_inbound: async ({ event }) =>
overLimit(event.sender.id) ? { block: true, message: 'Slow down 🙏' } : undefined,
},
],
handler,
})
```
Each adapter has platform-specific setup (tokens, webhooks, intents, signing secrets).
Start from the adapter's page and the matching `examples/*-bot/` in the repo — every
shipped adapter has a runnable example. See [Core](/theokit/gateways/core) for the full
`GatewayRunner`, `SessionRouter`, `DeliveryRouter`, hook, and `MessageEvent` contracts.
---
# Overview
Source: https://docs.usetheo.dev/theokit/gateways
First-party TheoKit gateways — a transport-agnostic core plus ten platform adapters that let a TheoKit agent chat on Telegram, Discord, Slack, WhatsApp, Teams, Email, SMS, LINE, Matrix, and Mattermost.
**TheoKit gateways** are the channel layer of the framework — small, focused packages
published as **`@theokit/gateway*`** on npm that let an agent **receive messages from a
platform and reply**, hiding each platform's shape behind one portable `MessageEvent`
union. A single transport-agnostic core (`@theokit/gateway`) defines the contract and
routing; ten sibling adapters implement one platform each. You install only the
transports you use.
Eleven packages ship today, in **two layers**:
- **Core** — `@theokit/gateway` defines `BasePlatformAdapter`, the `GatewayRunner`
orchestrator, `SessionRouter`/`DeliveryRouter`, the `HookExecutor` extension chain,
the `MessageEvent` discriminated union, and shared primitives (`chunkText`,
`chunkByGrapheme`, `GatewayConfigurationError`). It never owns session persistence,
scheduling, or prompt resolution — it composes `@theokit/sdk` for those.
- **Adapters** — `telegram`, `discord`, `slack`, `whatsapp`, `teams`, `email`, `sms`,
`line`, `matrix`, and `mattermost` each wrap one platform SDK behind
`BasePlatformAdapter`. Each is an independent package with a peer-dependency policy
(ADR D171): its platform SDK, `@theokit/sdk`, and `@theokit/gateway` are peers you
install alongside it.
Adapters never bundle the framework or each other. `@theokit/sdk` and
`@theokit/gateway` are the common peers; the platform SDK (`grammy`, `discord.js`,
`@slack/bolt`, …) is the per-adapter peer. Import direction is one-way — adapters
depend on the core, the core depends on neither the SDK's internals nor any adapter,
so there are **zero circular dependencies** across the cluster.
## How a message flows
```
Platform (Telegram / Slack / …)
│
▼
PlatformAdapter per-platform package (grammy, discord.js, …)
│ MessageEvent portable, discriminated by `platform`
▼
GatewayRunner holds adapters, runs pre_inbound hooks, dispatches
│
▼
your handler(event, ctx) → ctx.reply(text) auto-routed back by event.platform
```
`BasePlatformAdapter` handles lifecycle, idempotent connect/disconnect, and the
never-throw-on-platform-error contract. `GatewayRunner` owns the adapter map, the hook
chain, and `ctx.reply` routing; your handler owns the agent logic.
## What you'd add and when
- **Ship a chat bot on one platform** — install `@theokit/gateway` + one adapter (e.g.
`@theokit/gateway-telegram`), wire a `GatewayRunner`, resolve an agent per session.
- **Be multi-channel** — pass several adapters to the same `GatewayRunner`; one handler
serves Telegram, Slack, and Email at once, replies routed by `event.platform`.
- **Reach consumer channels** — `whatsapp` (Cloud API or web bridge), `sms`
(Twilio/Plivo/Vonage), `line` for messaging-API markets.
- **Reach team channels** — `slack`, `teams`, `discord`, `mattermost`, `matrix` for
internal / community bots.
- **Reach the inbox** — `email` speaks IMAP IDLE inbound + SMTP outbound with automatic
RFC 5322 threading.
## Browse the gateways
---
# @theokit/gateway-line
Source: https://docs.usetheo.dev/theokit/gateways/line
LINE Messaging API adapter for TheoKit gateways — webhook-only with HMAC-SHA256 signature validation, reply-token-then-push delivery, source-type mapping, mentionee-based mention gating, and surrogate-safe 5000-char splitting.
`@theokit/gateway-line` is the LINE platform adapter for `@theokit/gateway`. LINE has no
WebSocket gateway, so this adapter is **webhook-only**: `createWebhookServer` validates the
`X-Line-Signature` HMAC on every POST, hands the batch envelope to the `LineAdapter`, and your
handler stays platform-agnostic — `ctx.reply` routes back to whoever spoke. Inbound LINE events
are normalized into a portable `MessageEvent`, and outbound replies use LINE's free one-shot
reply token first, falling back to the Push API when it expires.
## Install
```bash
pnpm add @theokit/sdk @theokit/gateway @theokit/gateway-line
pnpm add @line/bot-sdk express
```
`@line/bot-sdk` and `express` are optional peers — install them to run the built-in webhook
server. `@theokit/gateway` and `@theokit/sdk` are peers too, so there is exactly one copy in
your app.
## Usage
Construct the adapter with your channel secret and access token, hand it to a `GatewayRunner`,
then start the webhook server:
```ts title="server/bot.ts"
import { GatewayRunner } from '@theokit/gateway'
import { LineAdapter, createWebhookServer } from '@theokit/gateway-line'
const adapter = new LineAdapter({
channelSecret: process.env.LINE_CHANNEL_SECRET!,
channelAccessToken: process.env.LINE_CHANNEL_ACCESS_TOKEN!,
// botUserId: 'Uxxxxxx', // your bot's LINE user ID — enables the mention guard in groups
// requireMention: true, // default
})
const runner = new GatewayRunner({
adapters: [adapter],
handler: async (event, ctx) => {
if (event.platform !== 'line') return
await ctx.reply(`Echo: ${event.text}`)
},
})
await runner.start()
const server = await createWebhookServer({ adapter, port: 3000 })
await server.start()
```
Point your LINE channel's webhook URL at `https:///line` (for local dev,
`ngrok http 3000`). The channel secret is required at construction — the adapter refuses to
start without it, since there is no unsigned mode.
## What you get
- **`LineAdapter`** — implements `BasePlatformAdapter` (`connect` / `disconnect` /
`sendMessage` / `onInbound`) over the LINE Messaging API. Construction fails fast if
`channelSecret` or `channelAccessToken` is empty.
- **Webhook-only with HMAC-SHA256 validation** — `createWebhookServer({ adapter, port })`
verifies the `X-Line-Signature` on every POST before any event reaches your handler; there
is no WebSocket transport and no insecure mode.
- **Reply-token-then-push delivery** — the first outbound to a user consumes the free one-shot
reply token (cached per sender, ~60s TTL); once it expires the adapter auto-falls-back to the
Push API. A bounded LRU cache (1000 entries) manages the lifecycle.
- **Source-type mapping** — LINE `user` sources map to a `dm` channel; `group` and `room`
sources both map to `group`. The original is preserved on `event.line.sourceType`.
- **Mentionee-based mention guard** — LINE mentions are out-of-band (not inline `@text`).
With `requireMention` (default) and `botUserId` set, group messages only dispatch when
your bot's user ID appears in `event.line.mentionees`; DMs always dispatch.
- **Surrogate-safe splitting** — `splitForLine(text)` breaks text longer than LINE's 5000-char
cap on grapheme-cluster boundaries (via `Intl.Segmenter`), so emoji and combined characters
are never cut in half. Applied automatically by `sendMessage`.
- **Escape hatch** — `adapter.getClient()` returns the underlying `@line/bot-sdk` client for
features outside the portable contract (Flex Messages, carousels, quick replies).
This adapter is `0.1.x` (pre-release) and is **webhook-only** — LINE offers no WebSocket
gateway, so you must expose an HTTPS endpoint. Only `message` events of type `text` reach
your handler; the other 8 webhook event types (`follow`, `unfollow`, `join`, `leave`,
`postback`, `beacon`, `accountLink`, `things`) and non-text messages (image/audio/video/
sticker/location) are filtered out. The reply token is one-shot with a ~60s TTL, so replies
sent after that window (or to a user who never spoke) go out via the Push API, which is
metered beyond the free tier. There is a runnable `examples/line-bot` in the repo to copy
from.
---
# For AI agents (llms.txt)
Source: https://docs.usetheo.dev/theokit/gateways/llms-txt
Machine-readable ground truth of @theokit/gateway* for LLMs — the transport-agnostic core, the ten platform adapters, their real class names, transports, peers, and honest v0.x status.
The `@theokit/gateway*` cluster ships an `llms.txt` file following the
[llmstxt.org convention](https://llmstxt.org/) — a single Markdown document that gives any
LLM (Claude, ChatGPT, Cursor, Copilot, a local model) the **factual ground truth** of the
gateway packages without crawling the whole site:
- The two layers — the **core** (`@theokit/gateway`: `BasePlatformAdapter`,
`GatewayRunner`, `SessionRouter`, `DeliveryRouter`, `HookExecutor`, the `MessageEvent`
union, and the `chunkText` / `chunkByGrapheme` / `GatewayConfigurationError` primitives)
and the ten **adapters**
- All ten adapters, each with its real adapter class, the platform SDK it wraps, its
transport (long-poll / WebSocket / webhook / IMAP-IDLE), its common peers, current
version, and a link to its live page
- The install pattern (`pnpm add @theokit/gateway @theokit/sdk` plus the adapter and its
platform SDK peer)
- Honest per-adapter gotchas — WhatsApp's two backends (Cloud vs web bridge), SMS's
three backends + mandatory webhook signing, Matrix's initial-sync history filter and
refused E2EE rooms, LINE's reply-token-then-push fallback, Email's own-address loopback
guard
Every adapter is still **v0.x** and the core is `@theokit/gateway@0.5.0`. The file is
honest about what is shipped vs deferred (Matrix E2EE, Teams OAuth, MMS/group SMS), and
every class name and peer range is sourced from the package's `package.json` and README —
no fabricated APIs.
## Get it
```bash
curl https://docs.usetheo.dev/theokit/gateways/llms.txt
```
Or link your AI assistant directly at
[`https://docs.usetheo.dev/theokit/gateways/llms.txt`](https://docs.usetheo.dev/theokit/gateways/llms.txt).
## Why
Point your coding assistant at it when you want it to stop hallucinating adapter class
names, which platform SDK an adapter wraps, or how replies are routed. Adapters are
**peer-dependency packages behind one `BasePlatformAdapter` contract**, not standalone
servers; you construct an adapter, hand it to a `GatewayRunner`, and `ctx.reply` routes
back by `event.platform`. The file makes that model explicit so an agent wires each
gateway the right way the first time.
---
# @theokit/gateway-matrix
Source: https://docs.usetheo.dev/theokit/gateways/matrix
Matrix adapter for TheoKit gateways — wraps matrix-js-sdk in the BasePlatformAdapter contract, with transparent federation, member-count DM detection, cached alias resolution, and an initial-sync history filter.
`@theokit/gateway-matrix` is the Matrix platform adapter for `@theokit/gateway`. It wraps
[matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk) in the `BasePlatformAdapter`
contract: `connect()` starts the sync long-poll in the background, inbound timeline events
are normalized into a portable `MessageEvent`, and outbound replies route back to the same
room. You construct a `MatrixAdapter`, hand it to a `GatewayRunner`, and your handler stays
platform-agnostic — `ctx.reply` routes back to the originating room.
Matrix is decentralized: a bot at `@theo-bot:matrix.org` works against matrix.org,
self-hosted Synapse/Dendrite, and any other homeserver, and can be added to rooms by users
on any homeserver via federation — the adapter never touches that seam, the protocol
handles it.
## Install
```bash
pnpm add @theokit/gateway-matrix matrix-js-sdk @theokit/gateway @theokit/sdk
```
`matrix-js-sdk` is a peer dependency (~2MB) — install the version your bot needs. It is
lazy-loaded via a dynamic import inside `connect()`, so nothing is pulled into your bundle
until the adapter actually connects. `@theokit/gateway` and `@theokit/sdk` are peers too,
so there is exactly one copy in your app.
## Usage
Construct the adapter with the homeserver URL, an access token, and the bot's user id, then
hand it to a `GatewayRunner`:
```ts title="server/bot.ts"
import { Agent } from '@theokit/sdk'
import { GatewayRunner, SessionRouter } from '@theokit/gateway'
import { MatrixAdapter } from '@theokit/gateway-matrix'
const router = new SessionRouter()
const adapter = new MatrixAdapter({
homeserverUrl: 'https://matrix.org', // no trailing slash
accessToken: process.env.MATRIX_ACCESS_TOKEN!,
userId: '@theo-bot:matrix.org',
})
const runner = new GatewayRunner({
adapters: [adapter],
handler: async (event, ctx) => {
if (event.platform !== 'matrix') return
const agent = await Agent.resume(router.resolveAgentId(event), {
apiKey: process.env.OPENROUTER_API_KEY!,
model: { id: 'openai/gpt-4o-mini' },
})
const run = await agent.send(event.text)
const result = await run.wait()
await ctx.reply(result.result ?? 'no reply')
await agent.dispose()
},
})
await runner.start()
```
`MatrixAdapterOptions` is `{ homeserverUrl, accessToken, userId, freshnessWindowMs? }`. The
constructor validates all three required fields eagerly: an empty `homeserverUrl` or
`accessToken`, or a `userId` that doesn't start with `@`, throws a `ConfigurationError`
before any network call. Generate the access token via Element web:
**Settings → Help & About → Advanced → Access Token** — it grants full account access, so
keep it secret.
### Platform escape hatch
For Matrix-only features (reactions, redactions, media, replaying history), reach the
underlying matrix-js-sdk `MatrixClient` via `adapter.getClient()` and call `sendEvent(...)`
directly. Inbound events preserve the raw Matrix event under `event.matrix.raw`, so you can
read `getContent()` for a media file URL that arrived with empty `text`.
## What you get
- **`MatrixAdapter`** — the matrix-js-sdk wrapper implementing `BasePlatformAdapter`
(`connect` / `disconnect` / `sendMessage` / `onInbound`), with the SDK lazy-loaded on
first `connect()` and a `connect()` that returns `false` (never throws) on a bad token.
- **Transparent federation** — the adapter delegates federation entirely to the Matrix
protocol; your bot receives from and replies to users on any homeserver with no extra
wiring. Remote-homeserver failures surface as a `send_failed` `SendResult`.
- **DM detection** — Matrix has no native DM concept, so the adapter applies the canonical
member-count heuristic: `getJoinedMemberCount() === 2` → `channel.type: "dm"`, three or
more members → `"group"`.
- **Alias resolution + cache** — `sendMessage` accepts a room id (`!abc:matrix.org`) or a
human alias (`#general:matrix.org`); aliases are resolved to a room id on first send and
cached for the process lifetime.
- **Initial-sync history filter** — matrix-js-sdk delivers ~10 recent events per room on
boot, so a 50-room bot would fire ~500 stale LLM calls at startup. The adapter drops
events older than the freshness window (`event.getTs() < Date.now() - freshnessWindowMs`,
default 60s, overridable via `freshnessWindowMs`) so only genuinely live messages
dispatch.
- **Surrogate/raw handling** — inbound events carry the raw Matrix event under
`event.matrix.raw`; media arrives with empty `text` (read the raw content for the file
URL), and `adapter.getClient()` is the escape hatch for anything the portable contract
doesn't cover.
This adapter is `0.1.x` (pre-release). End-to-end encrypted rooms are **refused** — an
encrypted room is skipped on inbound and returns an `encrypted_room_unsupported` send
result, each logged once to stderr; E2EE is deferred to v0.2. MSC4140 threads are also
deferred to v0.2, so every reply lands at the room root. The `matrix-js-sdk` peer is
~2MB but lazy-loaded (dynamic import on `connect()`), so it costs nothing until the bot
connects. The sync token is process-local — a restart re-syncs, and the 60s freshness
filter is what keeps that re-sync from replaying old history. There is a runnable
`examples/matrix-bot` in the repo to copy from.
---
# @theokit/gateway-mattermost
Source: https://docs.usetheo.dev/theokit/gateways/mattermost
Mattermost adapter for TheoKit gateways — wraps @mattermost/client in the BasePlatformAdapter contract, with a WebSocket gateway for real-time inbound, REST for outbound, channel-type mapping, thread replies, and mention gating for non-DM channels.
`@theokit/gateway-mattermost` is the Mattermost platform adapter for `@theokit/gateway`. It
wraps [`@mattermost/client`](https://www.npmjs.com/package/@mattermost/client) in the
`BasePlatformAdapter` contract: `connect()` opens a WebSocket and streams real-time
`posted` events, inbound posts are normalized into a portable `MessageEvent`, and outbound
replies go back out over the Client4 REST API. You construct a `MattermostAdapter`, hand it
to a `GatewayRunner`, and your handler stays platform-agnostic — `ctx.reply` routes back to
the same channel. Works with self-hosted Mattermost (Docker / Kubernetes / bare metal) at
any `baseUrl` and with Mattermost Cloud.
## Install
```bash
pnpm add @theokit/gateway-mattermost @mattermost/client @theokit/gateway @theokit/sdk
```
`@mattermost/client` (`^9`) and `ws` (`^8`) are peer dependencies — install the versions
your bot needs. `@theokit/gateway` and `@theokit/sdk` are peers too, so there is exactly
one copy in your app.
## Usage
Construct the adapter with your server URL and a bot Personal Access Token, hand it to a
`GatewayRunner`, and reply:
```ts title="server/bot.ts"
import { Agent } from '@theokit/sdk'
import { GatewayRunner } from '@theokit/gateway'
import { MattermostAdapter } from '@theokit/gateway-mattermost'
const adapter = new MattermostAdapter({
baseUrl: 'https://mattermost.acme.com',
accessToken: process.env.MM_BOT_TOKEN!,
// Optional: ignore non-DM channels unless the bot is explicitly @mentioned (default: true).
// requireMention: true,
})
const runner = new GatewayRunner({
adapters: [adapter],
handler: async (event, ctx) => {
if (event.platform !== 'mattermost') return
await ctx.reply(`Echo: ${event.text}`)
},
})
await runner.start()
```
To create the token: sign in as a Mattermost admin, enable **System Console → Integrations
→ Bot Accounts**, add a bot account, copy the generated access token (shown once), and add
the bot to the channels where it should listen.
### Platform escape hatch
For features `v0.1` doesn't expose (file uploads, custom REST calls), reach the underlying
Client4 client and WebSocket handle via `adapter.getClient()` before or after
`runner.start()`.
## What you get
- **`MattermostAdapter`** — the `@mattermost/client` wrapper implementing
`BasePlatformAdapter` (`connect` / `disconnect` / `sendMessage` / `onInbound`): a
WebSocket gateway for real-time `posted` events and Client4 REST for outbound.
`connect()` never throws on a bad URL or token — it returns `false`.
- **Self-hosted and Cloud** — point `baseUrl` at any Mattermost server; the WebSocket URL
is derived from it automatically.
- **Channel-type mapping** — Mattermost `D` (Direct) → `dm`; `G` (Group DM), `O`
(Open/Public), and `P` (Private) → `group`. The raw Mattermost type is preserved on the
event so platform-specific logic can still see it.
- **Bidirectional thread replies** — a post with `root_id` set arrives as
`channel.type: "thread"` with `topicId` = the root post id; sending with
`type: "thread"` + `topicId: ` posts back as a thread reply via `root_id`.
- **Mention gating** — with `requireMention: true` (the default for non-DM channels), the
adapter checks Mattermost's `metadata.mentions` user-id list first (no ambiguity), then
falls back to a word-boundary regex `\b@${botUsername}\b` on the text — so a bot named
`theo` isn't triggered by `@theory_dept`. DMs always process inbound.
This adapter is `0.1.x` (pre-release). Auth is **Personal Access Token only** — OAuth is
deferred to `v0.2` (D401). `requireMention` defaults to `true`, so in group, public, and
private channels the bot stays quiet until it is `@mentioned`; set `requireMention: false`
to answer every message in every channel it belongs to (loud — use cautiously). File
uploads, slash-command webhooks, and ephemeral messages are not supported in `v0.1` — use
the `adapter.getClient()` escape hatch for raw REST. There is a runnable
`examples/mattermost-bot` in the repo to copy from.
---
# @theokit/gateway-slack
Source: https://docs.usetheo.dev/theokit/gateways/slack
Slack adapter for TheoKit gateways — wraps @slack/bolt in the BasePlatformAdapter contract over Socket Mode, with mention-required channel gating, a bot-loop guard, and surrogate-safe 4000-char message splitting.
`@theokit/gateway-slack` is the Slack platform adapter for `@theokit/gateway`. It wraps
[@slack/bolt](https://slack.dev/bolt-js/) in the `BasePlatformAdapter` contract over
**Socket Mode**: `connect()` starts the Bolt app and caches the bot user id, inbound Slack
`message` events are normalized into a portable `MessageEvent`, and outbound replies go out
via `chat.postMessage`, auto-split at Slack's 4000-character cap. You construct a
`SlackAdapter`, hand it to a `GatewayRunner`, and your handler stays platform-agnostic —
`ctx.reply` routes back to the same channel (and thread, when the inbound was threaded).
## Install
```bash
pnpm add @theokit/gateway-slack @slack/bolt @theokit/gateway @theokit/sdk
```
`@slack/bolt` is a peer dependency — install the version your bot needs. `@theokit/gateway`
and `@theokit/sdk` are peers too, so there is exactly one copy in your app.
Before wiring the adapter, create a Slack app, enable **Socket Mode**, add the bot token
scopes (`chat:write`, `app_mentions:read`, the `*:history` scopes, `users:read`), and
generate an **App-Level Token** with the `connections:write` scope. The adapter needs both a
Bot User OAuth token (`xoxb-...`) and that App-Level token (`xapp-...`).
## Usage
Construct the adapter, resolve an agent per session inside the handler, and reply:
```ts title="server/bot.ts"
import { Agent } from '@theokit/sdk'
import { GatewayRunner, SessionRouter } from '@theokit/gateway'
import { SlackAdapter } from '@theokit/gateway-slack'
const router = new SessionRouter()
const adapter = new SlackAdapter({
botToken: process.env.SLACK_BOT_TOKEN!, // xoxb-...
appToken: process.env.SLACK_APP_TOKEN!, // xapp-... with connections:write
// requireMention defaults to true — public-channel messages without @bot are dropped.
})
const runner = new GatewayRunner({
adapters: [adapter],
handler: async (event, ctx) => {
const agent = await Agent.resume(router.resolveAgentId(event), {
apiKey: process.env.OPENROUTER_API_KEY!,
model: { id: 'openai/gpt-4o-mini' },
})
const run = await agent.send(event.text)
const result = await run.wait()
await ctx.reply(result.result ?? 'no reply')
await agent.dispose()
},
})
await runner.start()
```
`SlackAdapterOptions` is `{ botToken; appToken; transport?: "socket"; requireMention?; logLevel? }`.
`transport` accepts only `"socket"` in v1. When `requireMention` is left at its default
(`true`), public-channel messages that don't `@mention` the bot are dropped before the handler
runs — set it to `false` for FAQ bots that should hear every message. `logLevel`
(`debug` | `info` | `warn` | `error`) is passed straight through to the underlying Bolt app.
### Platform escape hatch
Block Kit, slash commands, and modals are not part of the v1 surface. To reach any of those,
grab the underlying Bolt `App` via `adapter.getApp()` and register handlers on it directly.
`adapter.getBotUserId()` returns the id resolved via `auth.test` on connect.
## What you get
- **`SlackAdapter`** — the `@slack/bolt` wrapper implementing `BasePlatformAdapter`
(`connect` / `disconnect` / `sendMessage` / `onInbound`) over Socket Mode. `connect()`
never throws on a bad token — it cleans up any orphaned Bolt app and returns `false`;
concurrent `connect()` calls share one in-flight start, and `disconnect()` is idempotent.
- **Mention-required channel gating** — `requireMention: true` (default) drops public-channel
messages that don't `@mention` the bot, so a busy channel can't trigger a cost explosion.
Opt out per adapter with `requireMention: false`.
- **Bot-loop guard** — the bot user id is cached via `auth.test` on connect and used to
suppress the adapter's own messages, so a reply can't re-trigger the handler.
- **`splitForSlack(text)`** — splits at the 4000-char `chat.postMessage` cap, preferring
`\n\n` → `\n` → space boundaries with a UTF-16 surrogate-pair guard so a chunk never breaks
a code point. Applied automatically by `sendMessage`; multi-chunk replies stay in the same
thread and the last chunk's `ts` is returned as the `messageId`.
- **Canonical `SendResult` errors** — Slack API failures are mapped to typed results
(`not_connected`, `empty_text`, and mapped Slack errors) rather than thrown.
- **`getApp()` / `getBotUserId()`** — escape hatches for advanced Bolt features (Block Kit,
slash commands, modals) and the cached bot user id.
This adapter is **Socket Mode only** in v1 — there is no HTTP-webhook transport, so the
`transport` option accepts only `"socket"`. That means you must enable Socket Mode on the
Slack app and provide **both** tokens: a Bot User OAuth token (`xoxb-...`) and an App-Level
token (`xapp-...`) carrying the `connections:write` scope. File uploads, Block Kit,
reactions, modals, and slash commands are out of scope for v1 — reach them through
`adapter.getApp()`.
---
# @theokit/gateway-sms
Source: https://docs.usetheo.dev/theokit/gateways/sms
SMS platform adapter for @theokit/gateway — one adapter, three backends (Twilio, Plivo, Vonage) with mandatory inbound webhook signing, E.164 normalization, and grapheme-safe multipart splitting.
`@theokit/gateway-sms` is the SMS platform adapter for `@theokit/gateway`. You construct one
`SMSAdapter`, pick a backend (`twilio`, `plivo`, or `vonage`) via the `backend` field, and hand
it to a `GatewayRunner` — the handler stays platform-agnostic, so `ctx.reply` routes back to the
same phone number. Inbound messages arrive over an Express webhook whose provider signature is
verified **before** any handler runs; outbound replies are normalized to E.164 and split into
`(i/N)` parts when they exceed the carrier limit.
The `backend` discriminated union means the credential shape matches the provider you chose — the
compiler tells you which fields Twilio needs versus Plivo versus Vonage. v0.1 is text-only.
## Install
```bash
pnpm add @theokit/gateway-sms @theokit/gateway @theokit/sdk
# Then install ONE backend SDK you actually use (each is an optional peer):
pnpm add twilio # for backend: "twilio"
pnpm add plivo # for backend: "plivo"
pnpm add @vonage/server-sdk # for backend: "vonage"
```
`@theokit/gateway` and `@theokit/sdk` are peers, so there is exactly one copy in your app. The
backend SDKs (`twilio` / `plivo` / `@vonage/server-sdk`) and `express` (the webhook server) are
**optional** peers — resolved by dynamic import on first use, so only the one you install is
loaded. `libphonenumber-js` (E.164 normalization) is a peer as well.
## Usage
Construct the adapter, hand it to a `GatewayRunner`, then start an Express webhook server so the
provider can POST inbound messages:
```ts title="server/bot.ts"
import { GatewayRunner } from '@theokit/gateway'
import { SMSAdapter, createWebhookServer } from '@theokit/gateway-sms'
const adapter = new SMSAdapter({
backend: 'twilio',
accountSid: process.env.TWILIO_ACCOUNT_SID!,
authToken: process.env.TWILIO_AUTH_TOKEN!, // also the webhook signing secret (required)
fromNumber: process.env.TWILIO_FROM!, // bot's own E.164 number
publicUrl: process.env.PUBLIC_URL!, // e.g. https://abc.ngrok.io — used to verify signatures
})
const runner = new GatewayRunner({
adapters: [adapter],
handler: async (event, ctx) => {
if (event.platform !== 'sms') return
await ctx.reply(`Echo: ${event.text}`)
},
})
await runner.start()
// The provider POSTs inbound to `${path}/${backend}` — here POST /sms/twilio.
const server = await createWebhookServer({ adapter, port: 3000 })
await server.start()
```
Each backend takes its own credential fields on the same union: Plivo uses `authId` + `authToken`,
Vonage uses `apiKey` + `apiSecret` + `signatureSecret`. All three also take `fromNumber`,
`publicUrl`, and an optional `defaultCountry` for parsing unprefixed inbound numbers. Point the
provider's inbound webhook at `https:///sms/`.
## What you get
- **`SMSAdapter`** — one adapter implementing `BasePlatformAdapter` (`connect` / `disconnect` /
`sendMessage` / `onInbound`) over a backend you pick with `backend: "twilio" | "plivo" |
"vonage"`. Backend SDKs are lazily imported, so only the one you installed loads.
- **Mandatory webhook signing** — the constructor throws
`ConfigurationError({ code: "signing_secret_required" })` if the backend's signing secret is
empty, and the webhook responds `401` on a missing or invalid signature **before** any handler
dispatch. A public webhook endpoint with no HMAC validation is refused outright.
- **E.164 normalization** — every phone number (inbound sender, outbound recipient) is normalized
to E.164 via `libphonenumber-js`; a non-parseable number surfaces as an `invalid_phone_number`
send result rather than throwing mid-send.
- **`(i/N)` multipart splitting** — outbound text over 1600 chars is split into `(1/N) …` parts
with `Intl.Segmenter`, so emoji and combining characters are never severed. Parts are sent
sequentially to preserve order; each part is one billable carrier message.
- **`createWebhookServer({ adapter, port })`** — the Express helper that mounts
`POST /sms/`, captures the raw request body verbatim (the signature depends on exact
bytes), verifies the signature, and dispatches. Pass your own `app` to mount it into an
existing server.
The backend signing secret is **mandatory** — `authToken` for Twilio/Plivo, `signatureSecret`
for Vonage. There is no unsigned mode; the constructor refuses to build without it. This is
v0.1, which is **text only**: MMS (image/audio), group SMS, per-message budget, and delivery
status callbacks are deferred to v0.2. For a provider-specific feature meanwhile, reach the
underlying SDK. There is a runnable `examples/sms-bot` in the repo to copy from.
---
# @theokit/gateway-teams
Source: https://docs.usetheo.dev/theokit/gateways/teams
Microsoft Teams adapter for TheoKit gateways — wraps the modern @microsoft/teams.apps v2 SDK in the BasePlatformAdapter contract, mounting a POST /api/messages webhook on your own Express server with SDK-handled JWT validation, mention stripping, and 8000-char message splitting.
`@theokit/gateway-teams` is the Microsoft Teams platform adapter for `@theokit/gateway`. It
wraps the modern [`@microsoft/teams.apps`](https://www.npmjs.com/package/@microsoft/teams.apps)
v2 SDK (ADR D315) in the `BasePlatformAdapter` contract: the SDK validates inbound JWTs,
strips `@bot` mentions, and routes proactive sends, while the adapter normalizes each Teams
`MessageActivity` into a portable `MessageEvent` and auto-splits outbound replies at Teams'
8000-character cap. You construct a `TeamsAdapter`, hand it the SDK's `ExpressAdapter` so it
can register `POST /api/messages` on your own server, and your handler stays platform-agnostic.
## Install
```bash
pnpm add @theokit/gateway-teams @microsoft/teams.apps @theokit/gateway @theokit/sdk
```
`@microsoft/teams.apps` is a peer dependency — it pulls in ~30 MB and is imported lazily, so
you only pay for it when this adapter is used. `@theokit/gateway` and `@theokit/sdk` are peers
too, so there is exactly one copy in your app.
## Usage
Build an Express app, wrap it in the SDK's `ExpressAdapter`, and pass that to the adapter as
`httpServerAdapter` — the SDK registers `POST /api/messages` during `connect()`. Then hand the
adapter to a `GatewayRunner` and reply from a platform-agnostic handler:
```ts title="server/bot.ts"
import express from 'express'
import { ExpressAdapter } from '@microsoft/teams.apps'
import { Agent } from '@theokit/sdk'
import { GatewayRunner, SessionRouter } from '@theokit/gateway'
import { TeamsAdapter } from '@theokit/gateway-teams'
const expressApp = express()
expressApp.use(express.json())
const httpServerAdapter = new ExpressAdapter(expressApp)
const router = new SessionRouter()
const adapter = new TeamsAdapter({
clientId: process.env.TEAMS_CLIENT_ID!,
clientSecret: process.env.TEAMS_CLIENT_SECRET!,
tenantId: process.env.TEAMS_TENANT_ID!,
botDisplayName: process.env.TEAMS_BOT_DISPLAY_NAME ?? 'Theo',
httpServerAdapter,
})
const runner = new GatewayRunner({
adapters: [adapter],
handler: async (event, ctx) => {
const agent = await Agent.resume(router.resolveAgentId(event), {
apiKey: process.env.OPENROUTER_API_KEY!,
model: { id: 'openai/gpt-4o-mini' },
})
const run = await agent.send(event.text)
const result = await run.wait()
await ctx.reply(result.result ?? 'no reply')
await agent.dispose()
},
})
await runner.start()
// The SDK registered POST /api/messages during connect(); now bind the port.
await httpServerAdapter.start(Number(process.env.PORT ?? 3978))
```
`TeamsAdapterOptions` is `{ clientId, clientSecret, tenantId, botDisplayName?, httpServerAdapter? }`.
The three Azure AD ids are required and validated as non-empty strings at construction (fail
fast). `botDisplayName` is an optional fallback for mention stripping. Reply routing uses
`event.channel.id` as the conversation id — the SDK manages the underlying conversation
reference, so there is no ref store to maintain.
### Platform escape hatch
For Teams-only features (Adaptive Cards, message extensions), reach the underlying SDK `App`
via `adapter.getApp()`, or get the mounted HTTP-server adapter with `adapter.getExpressAdapter()`.
## What you get
- **`TeamsAdapter`** — the `@microsoft/teams.apps` v2 wrapper implementing `BasePlatformAdapter`
(`connect` / `disconnect` / `sendMessage` / `onInbound`); `connect()` never throws on bad
credentials (returns `false`), and `disconnect()` swallows errors so shutdown is clean.
- **SDK-handled security** — JWT validation on inbound activities (D319) and built-in mention
stripping (`activity.mentions.stripText`) are configured for you; there is no HMAC/rawBody
middleware to wire (standard `express.json()` is enough).
- **Own-server webhook** — pass `new ExpressAdapter(yourApp)` as `httpServerAdapter` and the SDK
registers `POST /api/messages` on your existing Express server during `connect()` (D316/D326).
- **Portable normalization** — each `MessageActivity` becomes a `TeamsMessageEvent` mapping
`personal` → `dm`, `groupChat`/`channel` → `group`, with `teams` fields (`activityId`,
`conversationId`, `conversationType`, `tenantId`, `channelId`, `teamId`, `raw`) for
platform-specific work.
- **`splitForTeams(text)`** — splits at the 8000-char cap on `\n\n` / `\n` / ` ` boundaries
with a UTF-16 surrogate-pair guard; applied automatically by `sendMessage`.
- **`stripTeamsMentions(text, botDisplayName?)`** — the standalone `…` cleanup helper,
exported as a fallback for callers that consume raw text directly.
This adapter is `0.1.x` (pre-release, ADR D324 — breaking changes allowed within 0.x) and
ships **text only** — no first-class Adaptive Cards (use the `adapter.getApp()` escape hatch)
and no delivery/read receipts. It requires real Azure setup before it can receive anything:
an **Azure AD app registration** (client id, client secret, tenant id), an **Azure Bot
Service** registration, and a **publicly reachable webhook** (ngrok for local dev) set as the
Bot's Messaging Endpoint. There is a runnable `examples/teams-bot` in the repo with an 8-step
Azure walkthrough plus a `smoke` script that validates credentials without standing up Bot
Service.
---
# @theokit/gateway-telegram
Source: https://docs.usetheo.dev/theokit/gateways/telegram
Telegram adapter for TheoKit gateways — wraps grammy in the BasePlatformAdapter contract, with an allow-list filter, group-chat mention gating, and markdown-safe 4096-char message splitting.
`@theokit/gateway-telegram` is the Telegram platform adapter for `@theokit/gateway`. It
wraps [grammy](https://grammy.dev/) in the `BasePlatformAdapter` contract: `connect()`
starts long-polling in the background, inbound Telegram messages are normalized into a
portable `MessageEvent`, and outbound replies are auto-split at Telegram's 4096-character
cap. You construct a `TelegramAdapter`, hand it to a `GatewayRunner`, and your handler
stays platform-agnostic — `ctx.reply` routes back to the same chat.
## Install
```bash
pnpm add @theokit/gateway-telegram grammy @theokit/gateway @theokit/sdk
```
`grammy` is a peer dependency — install the version your bot needs. `@theokit/gateway`
and `@theokit/sdk` are peers too, so there is exactly one copy in your app.
## Usage
Construct the adapter, resolve an agent per session inside the handler, and reply:
```ts title="server/bot.ts"
import { Agent } from '@theokit/sdk'
import { GatewayRunner, SessionRouter } from '@theokit/gateway'
import { TelegramAdapter } from '@theokit/gateway-telegram'
const router = new SessionRouter()
const adapter = new TelegramAdapter({
token: process.env.TELEGRAM_BOT_TOKEN!,
allowedUsers: ['7528967933'], // optional adapter-level allow-list
})
const runner = new GatewayRunner({
adapters: [adapter],
handler: async (event, ctx) => {
const agent = await Agent.resume(router.resolveAgentId(event), {
apiKey: process.env.OPENROUTER_API_KEY!,
model: { id: 'openai/gpt-4o-mini' },
})
const run = await agent.send(event.text)
const result = await run.wait()
await ctx.reply(result.result ?? 'no reply')
await agent.dispose()
},
})
await runner.start()
```
`TelegramAdapterOptions` is exactly `{ token: string; allowedUsers?: readonly string[] }`.
When `allowedUsers` is set, messages from any other sender id are dropped at the adapter
before the handler runs; messages from other bots (`ctx.from.is_bot`) are always ignored.
### Group-chat mention gating
In group chats you usually don't want the bot answering every message. Register
`shouldRespondInChat` as a `pre_inbound` hook: DMs always reply, groups only reply on
`@mention`, reply-to-bot, or a slash command. `stripBotMention` removes the leading
`@bot` from the text:
```ts
import { shouldRespondInChat, stripBotMention } from '@theokit/gateway-telegram'
```
### Platform escape hatch
For Telegram-only features (voice, photos, callback queries), reach the underlying grammy
`Context` via `event.telegram.raw`, or register grammy handlers directly on the bot with
`adapter.getBot()` before calling `runner.start()`.
## What you get
- **`TelegramAdapter`** — the grammy wrapper implementing `BasePlatformAdapter`
(`connect` / `disconnect` / `sendMessage` / `onInbound` / `startTyping`), long-polling
transport that never throws on a bad token (returns `false` from `connect`).
- **Adapter-level allow-list** — `allowedUsers` filters inbound by sender id; bot-to-bot
messages are blocked so loops can't form.
- **`shouldRespondInChat(ctx, policy)` + `stripBotMention(text)`** — group-chat mention
gating as a `pre_inbound` hook (DM = always, group = mention / reply-to-bot / command).
- **`splitForTelegram(text)`** — markdown-safe splitting at the 4096-char cap; breaks on
paragraph/line boundaries and balances markdown pairs so a chunk never breaks `**`,
`__`, `~~`, or `` ` ``. Applied automatically by `sendMessage`.
- **`event.telegram` fields** — `chatId`, `messageId`, optional `threadId`, and `raw`
(the grammy `Context`) for platform-specific work, plus `adapter.getBot()` to register
raw grammy handlers.
This adapter is `0.1.0` (pre-release) and ships **long-polling** only — `connect()`
calls `bot.start()` with `drop_pending_updates`, so updates queued while the bot was
offline are discarded on reconnect. Outbound `parse_mode` is mapped from the message
`format` (`markdown` → `Markdown`, `html` → `HTML`); a Telegram parse failure surfaces
as a `markdown_error` send result rather than throwing. There is a runnable
`examples/telegram-bot` (and `examples/telegram-pro`) in the repo to copy from.
---
# @theokit/gateway-whatsapp
Source: https://docs.usetheo.dev/theokit/gateways/whatsapp
WhatsApp adapter for TheoKit gateways with two backends — the official Meta WhatsApp Business Cloud API and an unofficial whatsapp-web.js subprocess bridge — behind one BasePlatformAdapter with 4096-char splitting.
`@theokit/gateway-whatsapp` is the WhatsApp platform adapter for `@theokit/gateway`. It
ships **two backends** behind one `WhatsAppAdapter` (ADR D303): **cloud** — the official
[Meta WhatsApp Business Cloud API](https://developers.facebook.com/docs/whatsapp/cloud-api)
(Bearer access token, signed inbound webhooks, verify-token handshake); and **web** — an
unofficial [`whatsapp-web.js`](https://wwebjs.dev/) subprocess bridge for personal accounts
that pairs over a QR code. You build a `WhatsAppCloudBackend` or `WhatsAppWebBackend`, hand it
to a `WhatsAppAdapter`, and your handler stays platform-agnostic — inbound WhatsApp messages
normalize into a portable `MessageEvent` and outbound replies auto-split at WhatsApp's
4096-character cap.
## Install
```bash
# Cloud backend (official Meta API) — no extra peer:
pnpm add @theokit/gateway-whatsapp @theokit/gateway @theokit/sdk
# Web backend adds the whatsapp-web.js peer (optional — only for the bridge):
pnpm add whatsapp-web.js
```
`@theokit/gateway` and `@theokit/sdk` are peers, so there is exactly one copy in your app.
`whatsapp-web.js` is an **optional** peer — install it only when you use the web bridge; the
cloud backend never pulls it in.
## Usage
### Cloud backend (official Meta API)
The cloud backend has no persistent connection — inbound is push over a webhook you host, and
send is HTTP. Build the backend, wrap it in a `WhatsAppAdapter`, and drive it with a
`GatewayRunner`:
```ts title="server/whatsapp.ts"
import { Agent } from '@theokit/sdk'
import { GatewayRunner, SessionRouter } from '@theokit/gateway'
import { WhatsAppAdapter, WhatsAppCloudBackend } from '@theokit/gateway-whatsapp'
const router = new SessionRouter()
const backend = new WhatsAppCloudBackend({
accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!, // Meta phone-number-id, not the phone
appSecret: process.env.WHATSAPP_APP_SECRET!, // verifies X-Hub-Signature-256 on inbound
})
const adapter = new WhatsAppAdapter(backend, {
botPhoneId: process.env.WHATSAPP_PHONE_NUMBER_ID!, // used for @mention gating in groups
})
const runner = new GatewayRunner({
adapters: [adapter],
handler: async (event, ctx) => {
const agent = await Agent.resume(router.resolveAgentId(event), {
apiKey: process.env.OPENROUTER_API_KEY!,
model: { id: 'openai/gpt-4o-mini' },
})
const run = await agent.send(event.text)
const result = await run.wait()
await ctx.reply(result.result ?? 'no reply')
await agent.dispose()
},
})
await runner.start()
```
Inbound is delivered by **your** HTTP route calling `backend.handleWebhookPayload(rawBody, sig)`.
Wire two routes: a `GET /webhook` handshake with `verifyWebhookSubscription(query, verifyToken)`,
and a `POST /webhook` that verifies the signature with `verifyWebhookSignature(rawBody, sig, appSecret)`
before dispatching. Preserve the **raw request bytes** — the HMAC is computed over them. See
`examples/whatsapp-bot` in the repo for the full Express wiring.
### Web backend (unofficial bridge)
The web backend spawns the `whatsapp-web.js` bridge as a subprocess; `connect()` prints a QR
code (to stderr) that you scan once from **WhatsApp → Linked devices**. The session persists,
so there is no webhook to host:
```ts title="server/whatsapp-web.ts"
import { WhatsAppAdapter, WhatsAppWebBackend } from '@theokit/gateway-whatsapp'
const backend = new WhatsAppWebBackend({ sessionId: 'my-bot' })
const adapter = new WhatsAppAdapter(backend, {
botPhoneId: process.env.WHATSAPP_BOT_PHONE, // required for group @mention gating in web mode
})
// same adapter.onInbound / adapter.sendMessage (or GatewayRunner) as above
await adapter.connect() // races a 120s timeout so an unattended QR pairing fails fast
```
## What you get
- **`WhatsAppAdapter`** — the `BasePlatformAdapter` façade (`connect` / `disconnect` /
`sendMessage` / `onInbound` / `onStatusReceipt`). It is backend-agnostic: you pass it a
pre-built `WhatsAppCloudBackend` or `WhatsAppWebBackend` plus `{ requireMention?, botPhoneId? }`.
- **`WhatsAppCloudBackend`** — official Meta Cloud API backend. `connect`/`disconnect` are
no-ops (webhook push + HTTP send); inbound flows through `handleWebhookPayload(rawBody, sig)`.
Ships the webhook helpers `verifyWebhookSubscription`, `verifyWebhookSignature`,
`parseWebhookPayload`, `normalizeInboundMessages`, and `normalizeStatusReceipts`.
- **`WhatsAppWebBackend`** — unofficial `whatsapp-web.js` subprocess bridge for personal
accounts. QR pairing, `connectTimeoutMs` (default 120000) and `sendTimeoutMs` (default 30000)
guards, and a `sessionId` that locks the bridge per workspace.
- **Group `@mention` gating** — `requireMention` (default `true`) drops group messages that
don't mention the bot; matching uses digit-only normalization via `digitsOnly(...)`. DMs
always pass. Misconfigured groups (no `botPhoneId`) are dropped silently.
- **`event.whatsapp` fields** — `wamid` (message id: `wamid.xxx` on cloud, serialized id on
web), `phoneNumberId` (cloud only), `contactName`, `backend` (`"cloud" | "web"`), and `raw`
(the backend-specific envelope) for platform-specific work.
- **Status receipts** — `adapter.onStatusReceipt(...)` surfaces `sent` / `delivered` / `read` /
`failed` per `wamid`.
- **`splitForWhatsApp(text)`** — splitting at the fixed 4096-char cap (breaks on `\n\n` → `\n`
→ space, surrogate-pair safe, empty parts filtered). Applied automatically by `sendMessage`.
This adapter is `0.1.0` (pre-release) — pre-1.0 contract per ADR D314, breaking changes
allowed within 0.x. The **web** backend is **unofficial**: it drives a headless
`whatsapp-web.js` session in a subprocess and carries account-ban risk — WhatsApp does not
support automation of personal accounts. Use it for dev/prototypes; use **cloud** for
production. The **cloud** backend requires a Meta app with a WhatsApp Business phone number,
and you host the signed webhook yourself (`verifyWebhookSignature` over the raw request
bytes). There are runnable `examples/whatsapp-bot` (cloud) and `examples/whatsapp-web-bot`
(web) in the repo to copy from.
---
# Advanced
Source: https://docs.usetheo.dev/theokit/goals/advanced
The ephemeral runUntil judge loop and its event stream, goal options, the per-send completion check, and continuation across iteration ceilings.
# Advanced goals
Verified against `@theokit/sdk` (`types/goal-events.ts`, `types/run.ts`).
## The judge loop — `runUntil`
```ts
const iter = agent.runUntil("Ship a green test suite", { maxTurns: 10 });
let ev = await iter.next();
while (!ev.done) {
// observe each transition — ev.value is a GoalEvent
ev = await iter.next();
}
const result: GoalResult = ev.value; // the generator's return value
```
`runUntil(goal, options?)` drives `send → judge → continue` until the auxiliary judge returns `done`,
the judge fails too often, `maxTurns` is hit, or you abort. It is **ephemeral and per-call** — the
goal is passed on every call; there is no durable, thread-scoped objective. A no-goal `runUntil()`
pauses. It yields a `GoalEvent` per transition and returns a `GoalResult` (`status: "completed" |
"failed" | "paused"`, `turnsUsed`, `finalResponse`). **Local runtime only.**
## The event stream — `GoalEvent`
| Event | Meaning |
| --- | --- |
| `turn_start` | A new agent turn begins. |
| `agent_response` | The agent produced a response for this turn. |
| `judge_verdict` | The judge model evaluated the response (`parseFailed: true` on a malformed judge reply). |
| `continuation` | The judge ruled `continue` — carries the follow-up prompt. |
| `status_change` | Overall goal state moved (`active` / `paused` / `completed` / `failed`). |
## `GoalOptions`
```ts
agent.runUntil(goal, {
maxTurns: 20, // hard iteration cap. Default 20
maxConsecutiveJudgeFailures: 3, // bail after N malformed judge replies. Default 3
judgeModel: "openai/gpt-4o-mini", // the auxiliary judge. Default gpt-4o-mini
judgeApiKey, // override env for the judge (default OPENROUTER_API_KEY)
subgoals: ["tests pass", "no lint errors"], // fed to the judge prompt
signal, // AbortController — yields status_change: paused, returns at the next turn
});
```
## Lightweight check — per-send `completionCheck`
For a single-shot "is this done?" without the full loop, pass a `completionCheck` to `send`:
```ts
const result = await (await agent.send(prompt, { completionCheck: { criteria: "…" } })).wait();
result.completionCheck; // the resolved verdict on the run
```
It scores the finished reply against the criteria and surfaces a `CompletionCheckResult` on the run
plus a `completion_check` run-event — a goal check without the continue-loop.
## Continuation across iteration ceilings — `runToCompletion`
Distinct from goals: when a single `send` stops at the loop's iteration cap
(`RunResult.stoppedAtIterationLimit`), `agent.runToCompletion(...)` re-sends a short continuation
prompt (the stateful session preserves context) until a genuine terminal — `done`, `step_limit`
(`maxRounds` exhausted), or `no_progress`. Local agents only.
## Reference
- [`Agent`](/theokit/reference/Agent) · [`GoalOptions`](/theokit/reference/GoalOptions) · [`GoalResult`](/theokit/reference/GoalResult) · [`GoalEvent`](/theokit/reference/GoalEvent) · [`RunToCompletionOptions`](/theokit/reference/RunToCompletionOptions)
---
# Overview
Source: https://docs.usetheo.dev/theokit/goals
Drive an agent toward a goal until an auxiliary judge says it's done — an ephemeral, per-call loop.
# Goals
A normal `send()` runs one turn. A **goal loop** keeps the agent working — `send → judge →
continue` — until an auxiliary judge model returns `done`, the judge fails too often, max turns are
exhausted, or you abort.
```ts
for await (const event of agent.runUntil("Ship a green test suite", { maxTurns: 10 })) {
console.log(event.type); // GoalEvent per state transition
}
```
- **`agent.runUntil(goal, options?)`** — the loop. It is **ephemeral and per-call**: pass the goal to
each call. Yields a `GoalEvent` per transition and returns a `GoalResult`. Local runtime only.
- A **no-goal `runUntil()` pauses** — there is no durable objective to resolve.
- **`isTaskComplete`** (per-send `completionCheck`) scores a finished reply against criteria and
surfaces the verdict on the run — a lightweight goal check without the full loop.
## Next
- [Run until a goal is met](/theokit/goals/set-a-durable-objective) — a runnable `runUntil` example.
- [Advanced](/theokit/goals/advanced) — the `runUntil` judge loop, event stream, and completion checks.
## Reference
- [`Agent`](/theokit/reference/Agent) · [`GoalOptions`](/theokit/reference/GoalOptions) · [`GoalResult`](/theokit/reference/GoalResult) · [`GoalEvent`](/theokit/reference/GoalEvent)
---
# Run until a goal is met
Source: https://docs.usetheo.dev/theokit/goals/set-a-durable-objective
Drive an agent with the ephemeral runUntil judge loop — iterate its GoalEvents and read the final GoalResult.
# Run until a goal is met
`agent.runUntil(goal, options)` drives `send()` in a loop: after each turn an auxiliary judge model
decides `done` vs `continue`, until the goal is met or `maxTurns` is hit. It is an async generator —
iterate the `GoalEvent`s, and the generator's final value is a `GoalResult`. The loop is **ephemeral
and per-call**: pass the goal on every call (a no-goal `runUntil()` pauses). Requires a real LLM
because the judge loop calls a model.
```ts title="run.ts"
import { Agent } from "@theokit/sdk";
const agent = await Agent.create({
apiKey: process.env.OPENROUTER_API_KEY,
model: { id: "openai/gpt-4o-mini" },
systemPrompt: "You are a concise assistant. Do exactly what is asked, one step at a time.",
});
// Explicit goal → ephemeral loop.
const loop = agent.runUntil?.(
"List the first 3 prime numbers, one per line, then say the word DONE.",
{ maxTurns: 4 },
);
let ev = await loop!.next();
while (!ev.done) {
const e = ev.value;
if (e.type === "turn_start") console.log(`turn ${e.turn} → send`);
else if (e.type === "judge_verdict") console.log(`turn ${e.turn} → judge: ${e.verdict}`);
else if (e.type === "status_change") console.log(`status: ${e.status}`);
ev = await loop!.next();
}
const result = ev.value; // GoalResult
console.log("goal status:", result.status);
console.log("turns used: ", result.turnsUsed);
await agent.dispose();
```
## Output
Verified against `openai/gpt-4o-mini` via OpenRouter:
```text
turn 1 → send
turn 1 → judge: done
status: completed
goal status: completed
turns used: 1
```
## What it shows
- **`runUntil(goal, { maxTurns })`** returns an async generator. Each `next()` yields a `GoalEvent`
(`turn_start`, `agent_response`, `judge_verdict`, `continuation`, `status_change`).
- **The generator's return value** (when `done: true`) is a `GoalResult` — `status`
(`completed` / `failed` / `paused`), `turnsUsed`, `finalResponse`.
- **The loop is ephemeral** — the goal is passed to each call; a no-goal `runUntil()` pauses.
- The loop is **local-runtime only**; cloud agents manage continuation server-side.
## Example
Full runnable source:
[`examples/goals-objective`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/goals-objective).
---
# Advanced
Source: https://docs.usetheo.dev/theokit/guardrails/advanced
The full Processor contract, the input/output contexts, violations, the deterministic in-tree processors, and the streaming and cloud caveats.
# Advanced guardrails
Verified against the SDK.
## The `Processor` contract
```ts
interface Processor {
id: string;
processInput?(ctx: InputProcessorContext): string | Promise | void;
processOutput?(ctx: OutputProcessorContext): string | Promise | void;
onViolation?(violation: ProcessorViolation): void; // errors here are swallowed — never break the pipeline
}
```
- Return a string → replace the payload. Return nothing (`void`) → observe-only, payload unchanged.
- `processInput` / `processOutput` may be `async`.
- Processors run **in array order**.
## The contexts
Both contexts extend `ProcessorControls` (`abort(reason): never`, `warn(message, detail?): void`):
```ts
interface InputProcessorContext extends ProcessorControls {
message: string; // the user message for this send
agentId: string;
}
// OutputProcessorContext carries the model's final text plus the same controls.
```
## Violations
`abort()` and `warn()` produce a `ProcessorViolation` delivered to `onViolation`:
- **`abort(reason)`** → the run stops; `RunResult.status = "cancelled"`, `RunResult.tripwire =
{ reason, processorId }`, and a `tripwire` run-event fires. An input abort never reaches the model.
- **`warn(message, detail?)`** → non-blocking; the run continues.
## Deterministic in-tree processors
No LLM, no external service:
| Processor | What it does |
| --- | --- |
| `UnicodeNormalizer.create({ stripControlChars?, collapseWhitespace? })` | Unicode NFC + optional control-char strip / whitespace collapse. |
| `TokenLimiter.create({ limit, strategy? })` | Char-based token estimate (`estimateTokens` is exported); `truncate` cuts to fit, `block` aborts. Caps input or output by placement. |
## LLM-classifier processors — delegated
Moderation / PII / prompt-injection / language classifiers are **not** in core (they churn; the seam
does not). Build them on the same `Processor` seam using a specialist library, or your own model
call. See the SDK's guardrails guide + example for the paved path (the same rationale as
provider-auth delegation).
## Caveats
- **Streaming output redaction** — `processOutput` runs on the buffered `wait()` path in v1; a
token-stream redaction seam is deferred. Redact on `wait()`, or gate the stream at your UI.
- **Cloud** — cloud agents reject processors: function handlers don't serialize across the network.
Guardrails are a local-runtime feature.
## Reference
- [`Processor`](/theokit/reference/Processor) · [`ProcessorViolation`](/theokit/reference/ProcessorViolation) · [`TokenLimiter`](/theokit/reference/TokenLimiter) · [`UnicodeNormalizer`](/theokit/reference/UnicodeNormalizer)
---
# Block a message
Source: https://docs.usetheo.dev/theokit/guardrails/block-a-message
Add an input processor that aborts the run with a tripwire when the message violates a policy — before the model is called.
# Block a message
An `inputProcessor` runs before the LLM. Call `ctx.abort(reason)` and the run stops with a
**tripwire** — no provider call is made, and `RunResult.tripwire` carries the reason.
```ts title="run.ts"
import { Agent } from "@theokit/sdk";
const agent = await Agent.create({
apiKey: process.env.OPENROUTER_API_KEY,
model: { id: "openai/gpt-4o-mini" },
inputProcessors: [
{
id: "no-secrets",
processInput: (ctx) => {
if (/password/i.test(ctx.message)) ctx.abort("blocked: message mentions a password");
},
},
],
});
const result = await (await agent.send("What is my password?")).wait();
console.log("status:", result.status);
console.log("tripwire:", JSON.stringify(result.tripwire));
await agent.dispose();
```
## Output
Deterministic — the processor aborts before the model is ever called:
```text
status: cancelled
tripwire: {"reason":"blocked: message mentions a password","processorId":"no-secrets"}
```
## What it shows
- **`inputProcessors`** run in order, before the LLM. `ctx.message` is the user text.
- **`ctx.abort(reason)`** stops the run with a tripwire: `status: "cancelled"`, and
`result.tripwire = { reason, processorId }`. No provider call — cheap and fail-closed.
- Swap `abort` for **`ctx.warn(...)`** to log a non-blocking violation and let the run continue, or
add a **`processOutput`** to redact the model's reply on the way out.
## Example
Full runnable source:
[`examples/guardrails-basics`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/guardrails-basics).
---
# Overview
Source: https://docs.usetheo.dev/theokit/guardrails
Normalize, validate, block, or redact the user message and the model's reply with an ordered pipeline of processors.
# Guardrails
A **processor** runs on the way in (before the model) or on the way out (before the caller). Use
them to normalize input, block a policy violation, or redact the model's reply — an ordered pipeline
you control.
```ts
await Agent.create({
apiKey, model,
inputProcessors: [ /* run before the LLM */ ],
outputProcessors: [ /* run on the model's reply */ ],
});
```
## A processor
```ts
interface Processor {
id: string; // surfaced on violations + tripwires
processInput?(ctx): string | void; // transform/block the user message
processOutput?(ctx): string | void; // transform/redact the model's reply
onViolation?(violation): void; // fires on abort() and warn()
}
```
Return a (rewritten) string to transform; return nothing to leave the payload unchanged.
## Two controls
Every processor context carries:
- **`ctx.abort(reason)`** — stop the run **immediately** with a tripwire. `RunResult.status` becomes
`"cancelled"` and `RunResult.tripwire` carries `{ reason, processorId }`. An input `abort` never
reaches the model.
- **`ctx.warn(message, detail?)`** — report a **non-blocking** violation (fires `onViolation`); the
run continues.
## Built-in, deterministic processors
No LLM required:
- **`UnicodeNormalizer.create({ … })`** — Unicode NFC + optional control-char strip / whitespace collapse.
- **`TokenLimiter.create({ limit, strategy? })`** — cap input or output size (`truncate` or `block`).
## LLM-classifier guardrails
Moderation, PII, and prompt-injection classifiers are **not** shipped in core — they churn and belong
to specialist libraries. Build them on this same seam (a processor that calls a classifier) — the SDK
ships the paved path and a recommendation rather than a bundled, stale classifier.
## Next
- [Block a message](/theokit/guardrails/block-a-message) — a runnable tripwire example.
- [Advanced](/theokit/guardrails/advanced) — the full contexts, violations, deterministic processors, and streaming.
## Reference
- [`Processor`](/theokit/reference/Processor) · [`ProcessorViolation`](/theokit/reference/ProcessorViolation) · [`TokenLimiter`](/theokit/reference/TokenLimiter)
---
# abort-mid-stream
Source: https://docs.usetheo.dev/theokit/guides/abort-mid-stream
Demonstrates `AbortSignal` end-to-end propagation: caller's signal stops upstream token billing mid-stream.
# abort-mid-stream
Demonstrates `AbortSignal` end-to-end propagation: caller's signal stops upstream token billing mid-stream.
## Run
```bash
# Fixture mode (validates wiring):
pnpm run
# Real LLM (actual token-billing stop visible in OpenRouter dashboard):
OPENROUTER_API_KEY=sk-or-... pnpm run
```
## What it shows
1. **Pre-aborted signal:** `controller.abort()` before send; signal flows to `fetch({ signal })` at LLM client level.
2. **Mid-stream abort (real LLM only):** start a long generation, abort after 200ms; `AgentRunError({ code: "aborted", retriable: false })` thrown, partial assistant message NOT persisted (D320).
3. **`agent.dispose()` lifecycle abort:** the agent owns a `#lifecycleAbortController` that fires on dispose, canceling in-flight sends. Dispose is idempotent (D5).
## Edge runtimes
The SDK ships `anySignal` ponyfill (D324) so `AbortSignal.any` semantics work on Vercel Edge subsets that lack the native method. See `docs.md` "Cancellation" section.
## Code
```ts title="run.ts"
/**
* Production-Readiness #5 — AbortSignal end-to-end example.
*
* Demonstrates:
* - Pass AbortSignal to agent.send via SendOptions.signal
* - Compose user signal with timeout via anySignal-equivalent
* - Agent.dispose() also aborts in-flight sends (lifecycle controller)
*
* To run with a real LLM (recommended to see actual token-billing stop):
* OPENROUTER_API_KEY=sk-or-... pnpm run
*/
import { Agent, AgentRunError } from "@theokit/sdk";
const apiKey = process.env.OPENROUTER_API_KEY ?? "theo_test_abort_example";
const realLlm = apiKey.startsWith("sk-or-");
console.log(`\n== AbortSignal end-to-end example ==`);
console.log(realLlm ? "Mode: real OpenRouter" : "Mode: fixture (no LLM call)");
console.log();
const providers = realLlm
? {
routes: [{ capability: "chat" as const, provider: "openrouter" }],
fallback: ["openrouter"],
}
: undefined;
// ── 1. Pre-aborted signal — send never even hits the LLM ────────────────
const ctrl1 = new AbortController();
ctrl1.abort("pre-aborted before send");
const agent1 = await Agent.create({
apiKey,
model: { id: "openai/gpt-4o-mini" },
local: { cwd: process.cwd() },
...(providers !== undefined ? { providers } : {}),
});
try {
await agent1.send("Reply with: this never runs", { signal: ctrl1.signal });
// Fixture mode short-circuits before checking signal in production path,
// so this is reachable. With real LLM, the loop catches abort early.
console.log(`[1] Send completed (fixture mode short-circuit)`);
} catch (err) {
if (err instanceof AgentRunError && err.code === "aborted") {
console.log(`[1] ✓ Pre-aborted send threw AgentRunError(code="aborted")`);
} else {
console.log(`[1] Unexpected error:`, err);
}
}
await agent1.dispose();
// ── 2. Mid-flight abort (real LLM only) ──────────────────────────────────
if (realLlm) {
const ctrl2 = new AbortController();
const agent2 = await Agent.create({
apiKey,
model: { id: "openai/gpt-4o-mini" },
local: { cwd: process.cwd() },
...(providers !== undefined ? { providers } : {}),
});
// Abort BEFORE the send fires so we get a deterministic aborted result
// (the SDK contract: ctrl2.signal.aborted === true at the moment streamLlmTurn
// runs → fetch throws AbortError → run.wait returns status=error with code=aborted).
console.log(`[2] Aborting BEFORE send to deterministically exercise the path…`);
ctrl2.abort("user clicked stop");
try {
const run = await agent2.send("Write a long essay about jazz", {
signal: ctrl2.signal,
});
// Consume the stream defensively.
for await (const _ of run.stream()) {
void _;
}
const result = await run.wait();
if (result.status === "error" && result.error?.code === "aborted") {
console.log(`[2] ✓ Run aborted with explicit AgentRunError code="aborted"`);
} else if (result.status === "error") {
// The loop's collector detected signal.aborted and surfaced via
// status=error + [aborted] marker in events. Provider request was
// cancelled at fetch level — no more token billing.
console.log(`[2] ✓ Run terminated via abort path (status=error)`);
} else {
console.log(`[2] Run ended with status=${result.status}`);
}
} catch (err) {
if (err instanceof AgentRunError && err.code === "aborted") {
console.log(`[2] ✓ Aborted send threw AgentRunError(code="aborted")`);
} else if (err instanceof Error) {
console.log(`[2] Caught error: ${err.message.slice(0, 100)}`);
} else {
throw err;
}
}
await agent2.dispose();
} else {
console.log(`[2] Skipping mid-flight abort (no OPENROUTER_API_KEY set)`);
}
// ── 3. Dispose triggers lifecycle abort ──────────────────────────────────
const agent3 = await Agent.create({
apiKey,
model: { id: "openai/gpt-4o-mini" },
local: { cwd: process.cwd() },
...(providers !== undefined ? { providers } : {}),
});
console.log(`[3] Disposing agent (lifecycle abort fires)…`);
await agent3.dispose();
console.log(`[3] ✓ Dispose completed — second call is idempotent:`);
await agent3.dispose();
console.log(`[3] ✓ Second dispose returned without error`);
console.log();
console.log(`Done. See docs.md "Cancellation" section for the full contract.`);
```
## Run
```bash
cd examples/abort-mid-stream
cp .env.example .env # fill in keys
pnpm install
pnpm run run
```
## Repository
[examples/abort-mid-stream](https://github.com/usetheodev/theokit-sdk/tree/main/examples/abort-mid-stream)
---
# acp-server
Source: https://docs.usetheo.dev/theokit/guides/acp-server
Minimal `@theokit/acp` example: default-exports a per-session factory that creates a fresh `Agent` per ACP session.
# acp-server
Minimal `@theokit/acp` example: default-exports a per-session factory that creates a fresh `Agent` per ACP session.
## Run
### Local (manual, for testing)
```bash
# Install
pnpm install
# Set provider key
export OPENROUTER_API_KEY=sk-or-...
# Start the ACP server (stdin/stdout become the JSON-RPC channel)
pnpm serve
# OR explicitly:
npx theokit-acp --entry ./src/index.ts
```
The server will block reading from stdin. To talk to it manually, you need to send valid ACP `initialize` JSON-RPC frames. The realistic path is to point a host (Zed/Cursor) at it.
### Zed integration
```bash
mkdir -p ~/.config/zed/external_agents/usetheo-sdk
cp ../../packages/acp/registry/agent.json ~/.config/zed/external_agents/usetheo-sdk/
cp ../../packages/acp/registry/icon.svg ~/.config/zed/external_agents/usetheo-sdk/
```
Edit the `distribution.args` in the copied `agent.json` to point at this example's entry:
```json
{
"distribution": {
"type": "command",
"command": "npx",
"args": ["theokit-acp", "--entry", "/absolute/path/to/examples/acp-server/src/index.ts"],
"env": { "OPENROUTER_API_KEY": "sk-or-..." }
}
}
```
Restart Zed. Open External Agents → `Theokit SDK` should appear. Send a prompt.
## What it does
- ACP `new_session` → `Agent.create({ apiKey, model, local: { cwd } })` per session.
- ACP `prompt` → `agent.send(text).stream()` translated into ACP `agent_message_chunk` notifications.
- ACP `cancel` → fires the session's `AbortController`.
- Tool permissions → default `ask` mode prompts Zed UI; `--permission auto` to disable.
## Permissions
Default is `ask`. To trust the read-only tools and only prompt for writes:
```bash
npx theokit-acp --entry ./src/index.ts --trusted-tools read_file,list_dir,git_diff,search_text
```
## Notes
- Per `.claude/rules/real-llm-validation.md`, this example REQUIRES a real provider key. Fixture mode is not a valid dogfood substitute.
- Set `ACP_EXAMPLE_MODEL` to override the default model.
## Code
```ts title="src/index.ts"
/**
* Minimal ACP server entry point. Default-exports a factory: ACP host
* spawns one process per session and calls `factory(sessionId)` to get a
* fresh `SDKAgent` for each session (D351 — per-session isolation).
*
* Provider precedence:
* 1. `OPENROUTER_API_KEY` → openrouter/ (default `openai/gpt-4o-mini`)
* 2. else falls through to Ollama at `OLLAMA_HOST` (default `localhost:11434`)
* with `ACP_OLLAMA_MODEL` (default `qwen2.5:3b`)
*
* Override either via env: `ACP_EXAMPLE_MODEL`, `ACP_OLLAMA_MODEL`, `OLLAMA_HOST`.
*/
import { Agent, type SDKAgent } from "@theokit/sdk";
export default async function createAgentForSession(sessionId: string): Promise {
const openrouterKey = process.env.OPENROUTER_API_KEY;
if (typeof openrouterKey === "string" && openrouterKey.length > 0) {
return Agent.create({
apiKey: openrouterKey,
model: { id: process.env.ACP_EXAMPLE_MODEL ?? "openai/gpt-4o-mini" },
providers: {
routes: [{ capability: "chat", provider: "openrouter" }],
fallback: ["openrouter"],
},
local: { cwd: process.cwd() },
name: `acp-${sessionId}`,
});
}
// Ollama fallback — 100% local, no remote provider needed.
const ollamaModel = process.env.ACP_OLLAMA_MODEL ?? "qwen2.5:3b";
return Agent.create({
apiKey: "local",
model: { id: `ollama/${ollamaModel}` },
local: { cwd: process.cwd() },
name: `acp-${sessionId}`,
});
}
```
## Run
```bash
cd examples/acp-server
cp .env.example .env # fill in keys
pnpm install
pnpm run run
```
## Repository
[examples/acp-server](https://github.com/usetheodev/theokit-sdk/tree/main/examples/acp-server)
---
# agent-basics
Source: https://docs.usetheo.dev/theokit/guides/agent-basics
The smallest end-to-end path with `@theokit/sdk`: create a local agent against
# agent-basics
The smallest end-to-end path with `@theokit/sdk`: create a local agent against
your own provider key, send one message, await the result, dispose.
Pairs with the docs page **[Agents › Creating an agent](https://docs.usetheo.dev/theokit/agents)**.
## Run
```bash
pnpm install
export OPENROUTER_API_KEY=sk-or-... # https://openrouter.ai/keys — or put it in .env
pnpm run run
```
## What it shows
- `Agent.create({ apiKey, model, name, systemPrompt })` — the canonical factory.
- `agent.send(message)` → `Run`; `await run.wait()` → `RunResult`.
- `result.result` (text), `result.status`, `result.model`, and typed `result.error`.
- `agent.dispose()` for resource cleanup.
## Code
```ts title="run.ts"
/**
* Agents — creating and running an agent (features/agents).
*
* The smallest end-to-end path: create a local agent against your own provider
* key, send one message, await the full result, dispose.
*
* Run:
* pnpm install
* export OPENROUTER_API_KEY=sk-or-... # or put it in .env
* pnpm run run
*/
import { Agent } from "@theokit/sdk";
const apiKey = process.env.OPENROUTER_API_KEY;
if (apiKey === undefined || apiKey.length === 0) {
throw new Error("Set OPENROUTER_API_KEY (env or .env) — see https://openrouter.ai/keys");
}
const agent = await Agent.create({
apiKey,
model: { id: "openai/gpt-4o-mini" },
name: "explainer-bot",
systemPrompt: "You are a concise assistant. Answer in at most two sentences.",
// Local runtime, no sandbox — runs inline in this Node process.
local: { cwd: process.cwd(), sandboxOptions: { enabled: false } },
});
const run = await agent.send("What is an AI agent? Answer for a developer.");
const result = await run.wait();
console.log("Status:", result.status);
console.log("Model: ", result.model);
console.log("Reply: ", result.result);
await agent.dispose();
// Validate: a run that did not finish (auth, rate-limit, model error) is a failure, not a green run.
if (result.status !== "finished" || typeof result.result !== "string" || result.result.length === 0) {
console.error("run did not finish:", JSON.stringify(result.error ?? result.status));
process.exit(1);
}
```
## Run
```bash
cd examples/agent-basics
cp .env.example .env # fill in keys
pnpm install
pnpm run run
```
## Repository
[examples/agent-basics](https://github.com/usetheodev/theokit-sdk/tree/main/examples/agent-basics)
---
# agent-streaming
Source: https://docs.usetheo.dev/theokit/guides/agent-streaming
Iterate `run.stream()` to consume `SDKMessage` events as they arrive, instead of
# agent-streaming
Iterate `run.stream()` to consume `SDKMessage` events as they arrive, instead of
awaiting the whole result.
Pairs with the docs page **[Agents › Streaming](https://docs.usetheo.dev/theokit/agents/streaming)**.
## Run
```bash
pnpm install
export OPENROUTER_API_KEY=sk-or-... # https://openrouter.ai/keys — or put it in .env
pnpm run run
```
## What it shows
- `run.stream()` yields a discriminated union of full `SDKMessage` events:
`system` | `user` | `assistant` | `tool_call` | `thinking` | `status` | …
- `assistant` messages carry `.message.content` as `text` / `tool_use` blocks.
- `run.wait()` after the stream drains resolves to the terminal `RunResult`.
## Code
```ts title="run.ts"
/**
* Agents — streaming a run (features/agents/streaming).
*
* `agent.send()` returns a `Run`. Instead of `await run.wait()` for the whole
* result, iterate `run.stream()` to consume `SDKMessage` events as they arrive.
* The stream is a discriminated union of full messages:
* - "system" — run init metadata
* - "user" — the submitted message, echoed back
* - "assistant" — model output; `.message.content` is (text | tool_use) blocks
* - "tool_call" — a tool the model invoked
* - "thinking" — reasoning content (when the model emits it)
*
* Run:
* pnpm install
* export OPENROUTER_API_KEY=sk-or-... # or put it in .env
* pnpm run run
*/
import { Agent } from "@theokit/sdk";
const apiKey = process.env.OPENROUTER_API_KEY;
if (apiKey === undefined || apiKey.length === 0) {
throw new Error("Set OPENROUTER_API_KEY (env or .env) — see https://openrouter.ai/keys");
}
const agent = await Agent.create({
apiKey,
model: { id: "openai/gpt-4o-mini" },
name: "streamer-bot",
systemPrompt: "You are a concise storyteller.",
local: { cwd: process.cwd(), sandboxOptions: { enabled: false } },
});
const run = await agent.send("Tell me a two-sentence story about a curious robot.");
for await (const msg of run.stream()) {
switch (msg.type) {
case "assistant":
for (const block of msg.message.content) {
if (block.type === "text") process.stdout.write(block.text);
else if (block.type === "tool_use") console.log(`\n[tool_use] ${block.name}`);
}
break;
case "tool_call":
console.log(`\n[calling tool] ${msg.name}`);
break;
default:
// "system" | "user" | "thinking" | "status" | … — ignored here.
break;
}
}
// After the stream drains, wait() resolves to the terminal RunResult.
const result = await run.wait();
console.log(`\n\n[done] status=${result.status}`);
await agent.dispose();
// --- validate output (fail loud) ---
if (result.status !== "finished" || typeof result.result !== "string" || result.result.length === 0) {
console.error("run did not finish:", JSON.stringify(result.error ?? result.status));
process.exit(1);
}
```
## Run
```bash
cd examples/agent-streaming
cp .env.example .env # fill in keys
pnpm install
pnpm run run
```
## Repository
[examples/agent-streaming](https://github.com/usetheodev/theokit-sdk/tree/main/examples/agent-streaming)
---
# agent-structured-output
Source: https://docs.usetheo.dev/theokit/guides/agent-structured-output
Coerce an agent's final answer into a validated, inferred-typed object with a Zod
# agent-structured-output
Coerce an agent's final answer into a validated, inferred-typed object with a Zod
schema via `agent.generate(message, { output: schema })`.
Pairs with the docs page **[Agents › Structured output](https://docs.usetheo.dev/theokit/agents/structured-output)**.
## Run
```bash
pnpm install
export OPENROUTER_API_KEY=sk-or-... # https://openrouter.ai/keys — or put it in .env
pnpm run run
```
## What it shows
- `agent.generate(message, { output: zodSchema })` → `{ object, result, raw, usage }`.
- `object` is typed from the schema (no manual JSON parsing, no casts).
- The SDK forces a single synthetic tool whose schema IS your Zod schema (ADR D33),
then Zod-validates the model's output.
## Code
```ts title="run.ts"
/**
* Agents — structured output (features/agents/structured-output).
*
* `agent.generate(message, { output: schema })` runs the normal tool loop, then
* coerces the final answer into your Zod schema and returns a validated,
* inferred-typed object. Under the hood the SDK forces a single synthetic tool
* whose schema IS your Zod schema (ADR D33), then Zod-parses the result.
*
* Run:
* pnpm install
* export OPENROUTER_API_KEY=sk-or-... # or put it in .env
* pnpm run run
*/
import { Agent } from "@theokit/sdk";
import { z } from "zod";
const apiKey = process.env.OPENROUTER_API_KEY;
if (apiKey === undefined || apiKey.length === 0) {
throw new Error("Set OPENROUTER_API_KEY (env or .env) — see https://openrouter.ai/keys");
}
const Sentiment = z.object({
sentiment: z.enum(["positive", "neutral", "negative"]),
score: z.number().min(0).max(1).describe("Confidence between 0 and 1."),
summary: z.string().describe("One short sentence summarizing the review."),
});
const agent = await Agent.create({
apiKey,
model: { id: "openai/gpt-4o-mini" },
name: "review-analyzer",
systemPrompt: "You analyze product reviews and return structured sentiment.",
local: { cwd: process.cwd(), sandboxOptions: { enabled: false } },
});
const { object } = await agent.generate(
"Review: 'The battery lasts two days and the screen is gorgeous, but it's pricey.'",
{ output: Sentiment },
);
// `object` is typed as { sentiment: "positive" | "neutral" | "negative"; score: number; summary: string }
console.log("sentiment:", object.sentiment);
console.log("score: ", object.score);
console.log("summary: ", object.summary);
await agent.dispose();
// --- validate output (fail loud) ---
if (!object || typeof object.sentiment !== "string") {
console.error("structured output missing sentiment:", JSON.stringify(object));
process.exit(1);
}
```
## Run
```bash
cd examples/agent-structured-output
cp .env.example .env # fill in keys
pnpm install
pnpm run run
```
## Repository
[examples/agent-structured-output](https://github.com/usetheodev/theokit-sdk/tree/main/examples/agent-structured-output)
---
# bedrock-bot
Source: https://docs.usetheo.dev/theokit/guides/bedrock-bot
One-shot Claude prompt via AWS Bedrock (`@theokit/sdk` Adoption Roadmap #8; ADRs D286-D302).
# bedrock-bot
One-shot Claude prompt via AWS Bedrock (`@theokit/sdk` Adoption Roadmap #8; ADRs D286-D302).
## Setup
1. Enable Bedrock model access in your AWS account: [https://console.aws.amazon.com/bedrock/home](https://console.aws.amazon.com/bedrock/home) → "Model access" → request Anthropic models.
2. Generate a Bearer token (one of three paths):
- **Short-term, AWS Console:** IAM → Users → your user → "Security credentials" → "Bedrock API keys" → "Generate API key" (≤12h TTL).
- **Long-term via CLI:**
```bash
aws iam create-service-specific-credential --service-name bedrock.amazonaws.com
```
(only for exploration — AWS docs warn against long-term keys in prod).
- **Auto-refresh via peer dep:**
```bash
pnpm add @aws/bedrock-token-generator
```
Then the SDK refreshes short-term tokens automatically (D287).
3. Copy `.env.example` to `.env` and fill `AWS_BEARER_TOKEN_BEDROCK`.
## Run
```bash
cp .env.example .env
# fill AWS_BEARER_TOKEN_BEDROCK
pnpm install
pnpm run run # default question: "Qual é a capital do Brasil?"
pnpm run run "What's 2+2?" # custom question
```
## Model IDs
Format: `bedrock/{regionPrefix}.anthropic.{model}-v{N}:{rev}`.
Examples:
- `bedrock/us.anthropic.claude-sonnet-4-5-v1:0` — US-region
- `bedrock/eu.anthropic.claude-sonnet-4-5-v1:0` — EU region (different inference profile)
- `bedrock/global.anthropic.claude-opus-4-7-v1:0` — cross-region routed
See [AWS docs](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html) for the full list per region.
## v1 limitations (documented)
- **Non-streaming only** (D302) — the full response arrives at once; chat UX with token-by-token rendering needs v1.x or the escape hatch via `@aws-sdk/client-bedrock-runtime`.
- **Bearer auth only** (D286) — no SigV4 in v1 (D298). Customers in IAM-role-only environments wait for v1.x.
- **InvokeModel only** (D289) — Converse API deferred. Preserves Anthropic prompt-caching + extended-thinking fields.
- **Claude only** — Llama / Cohere / Mistral via Bedrock Converse deferred (D296).
- **Bearer auth doesn't cover** Bedrock Agents / Knowledge Bases / Computer Use (D282-style escape hatch via `adapter.getApp()` not yet wired here).
## Code
```ts title="run.ts"
/**
* AWS Bedrock demo (Adoption Roadmap #8; ADRs D286-D302).
*
* Sends a one-shot prompt to Claude on Bedrock and prints the reply.
* Uses Bearer auth (no SigV4). Non-streaming in v1 (D302).
*
* Run:
* cp .env.example .env # fill AWS_BEARER_TOKEN_BEDROCK
* pnpm install
* pnpm run run
*/
import { Agent } from "@theokit/sdk";
const token = process.env.AWS_BEARER_TOKEN_BEDROCK;
const modelId =
process.env.BEDROCK_MODEL ?? "bedrock/us.anthropic.claude-sonnet-4-5-v1:0";
if (token === undefined || token.length === 0) {
console.log(
"[bedrock] AWS_BEARER_TOKEN_BEDROCK not set — SDK will auto-generate " +
"via @aws/bedrock-token-generator + standard AWS credential chain.",
);
}
const agent = await Agent.create({
apiKey: token ?? "__bedrock_lazy_token__",
model: { id: modelId },
local: { cwd: process.cwd(), sandboxOptions: { enabled: false } as const },
name: "bedrock-bot",
systemPrompt: "You are a concise assistant. Reply in one short sentence.",
});
const question = process.argv[2] ?? "Qual é a capital do Brasil?";
console.log(`[bedrock] model=${modelId} question="${question}"`);
const run = await agent.send(question);
const result = await run.wait();
const errInfo = (result as { error?: { name?: string; message?: string; metadata?: unknown } }).error;
console.log(`[bedrock] status=${result.status} resultLen=${(result.result ?? "").length}`);
if (errInfo !== undefined) {
console.log(`[bedrock] error.name=${errInfo.name}`);
console.log(`[bedrock] error.message=${errInfo.message}`);
console.log(`[bedrock] error.metadata=${JSON.stringify(errInfo.metadata, null, 2)}`);
}
console.log(result.result ?? "(no reply)");
await agent.dispose();
```
## Run
```bash
cd examples/bedrock-bot
cp .env.example .env # fill in keys
pnpm install
pnpm run run
```
## Repository
[examples/bedrock-bot](https://github.com/usetheodev/theokit-sdk/tree/main/examples/bedrock-bot)
---
# cache
Source: https://docs.usetheo.dev/theokit/guides/cache
Demonstrates `Cache.semantic` + `Cache.consult` (Adoption Roadmap #6; ADRs D249-D266).
# cache
Demonstrates `Cache.semantic` + `Cache.consult` (Adoption Roadmap #6; ADRs D249-D266).
## Run (OpenRouter cloud)
```bash
export OPENROUTER_API_KEY=sk-or-...
pnpm install
pnpm run run
```
## What it shows
- `Cache.semantic({ embedder, threshold, ttl, namespace })` factory.
- `cache.consult(prompt)` — direct lookup with `hit: boolean` outcome + `source: "kv" | "semantic"`.
- `cache.remember(prompt, response)` — explicit store after dispatching the LLM yourself.
- `ttl.exclude` regex — time-sensitive prompts (weather, today, now) bypass cache.
- `cache.stats()` — kvHits / semanticHits / misses / excluded counters.
## v1 limitations (documented)
- **Plugin mode provides recall + context inject** — the LLM is still called on hit and pre-loaded with the cached answer. For true short-circuit (skip the LLM call entirely), use `cache.consult()` directly and dispatch your own LLM call only on miss (the demo shows this pattern).
- **No streaming cache** (D256) — only `agent.send` is cached, not `agent.stream`.
- **No adaptive threshold per entry** (D254) — single global threshold; tune via `Cache.semantic({ threshold: 0.95 })` for high-stakes scenarios.
- **No tool-use cache** (D266 / EC-10) — runs that invoked tools are NEVER cached (replay would lose side-effects).
- **Embedder change invalidates** (D258) — `embedder.id` is part of the cache key.
## Pairing with Anthropic prompt_caching (D263)
Cache.semantic resolves paraphrases BEFORE the LLM. Anthropic prompt_caching gives 90%
discount on prefix-identical input AFTER hitting the LLM. They're orthogonal — use both
for compound savings (~95% in ideal workloads):
```
[user query] → Cache.semantic hit? → return cached
→ miss → LLM call with cache_control on system/tools (90% discount)
```
## Code
```ts title="run.ts"
/**
* Semantic Cache demo (Adoption Roadmap #6; ADRs D249-D266).
*
* Shows:
* 1. First query: miss → LLM called → cache.remember stores it
* 2. Paraphrase: semantic hit (no LLM call needed via consult())
* 3. Time-sensitive query: bypassed via exclude regex
* 4. Stats summary
*
* Run:
* export OPENROUTER_API_KEY=sk-or-...
* pnpm install
* pnpm run run
*/
import { Agent, Cache } from "@theokit/sdk";
const OPENROUTER = process.env.OPENROUTER_API_KEY;
if (OPENROUTER === undefined || OPENROUTER.length === 0) {
console.error("OPENROUTER_API_KEY missing — see .env.example");
process.exit(1);
}
// A deterministic toy embedder for the demo (avoids spending OpenAI tokens
// on every prompt — production users plug a real EmbeddingRuntime here).
const toyEmbedder = {
id: "toy-letter",
model: "letter-bag-1",
dimension: 26,
async embed(texts: ReadonlyArray): Promise {
return texts.map((t) => {
const v = new Array(26).fill(0);
const norm = t.toLowerCase().replace(/[^a-z]/g, "");
for (const ch of norm) {
const i = ch.charCodeAt(0) - 97;
if (i >= 0 && i < 26) v[i] += 1;
}
const sum = v.reduce((a, b) => a + b, 0) || 1;
return v.map((x) => x / sum);
});
},
};
async function main(): Promise {
const cache = Cache.semantic({
embedder: toyEmbedder,
threshold: 0.4,
ttl: {
default: process.env.CACHE_TTL ?? "1h",
exclude: /\b(weather|today|now|current|stock)\b/i,
},
namespace: "demo",
modelId: "openai/gpt-4o-mini",
});
const agent = await Agent.create({
apiKey: OPENROUTER,
model: { id: "openai/gpt-4o-mini" },
local: { cwd: process.cwd(), sandboxOptions: { enabled: false } as const },
name: "demo-agent",
plugins: [cache.asPlugin()],
});
console.log("\n=== Query 1: 'What is the capital of France?' (miss expected) ===");
const t1 = Date.now();
const m1 = await cache.consult("What is the capital of France?");
if (m1.hit) {
console.log("(unexpected hit)", m1.response.slice(0, 80));
} else {
const r1 = await agent.send("What is the capital of France? Answer in one short sentence.");
const result1 = await r1.wait();
const text = result1.status === "finished" ? (result1.result ?? "") : "";
await cache.remember("What is the capital of France?", text);
console.log("LLM:", text.slice(0, 200));
}
console.log("Elapsed:", Date.now() - t1, "ms");
console.log("\n=== Query 2: 'Tell me the capital city of France' (semantic hit expected) ===");
const t2 = Date.now();
const m2 = await cache.consult("Tell me the capital city of France");
if (m2.hit) {
console.log("CACHE HIT (", m2.source, ", dist=", m2.distance, ")");
console.log("Cached:", m2.response.slice(0, 200));
} else {
console.log("(unexpected miss)");
}
console.log("Elapsed:", Date.now() - t2, "ms");
console.log("\n=== Query 3: 'What is the weather in SF today?' (excluded by regex) ===");
const t3 = Date.now();
const m3 = await cache.consult("What is the weather in SF today?");
console.log("hit:", m3.hit, "(should always be false due to exclude regex)");
console.log("Elapsed:", Date.now() - t3, "ms");
const s = cache.stats();
console.log("\n=== Stats ===");
console.log(
`entries=${s.entries} kvHits=${s.kvHits} semanticHits=${s.semanticHits} misses=${s.misses} excluded=${s.excluded} evicted=${s.evicted} embedderFailures=${s.embedderFailures}`,
);
await agent.dispose();
}
main().catch((err) => {
console.error("cache demo failed:", err);
process.exit(1);
});
```
## Run
```bash
cd examples/cache
cp .env.example .env # fill in keys
pnpm install
pnpm run run
```
## Repository
[examples/cache](https://github.com/usetheodev/theokit-sdk/tree/main/examples/cache)
---
# Session persistence
Source: https://docs.usetheo.dev/theokit/guides/conversation-storage
How a local agent's conversation persists in v4.0 — the native transcript on disk, with no pluggable adapter.
# Session persistence
In v4.0 a local agent's conversation persists as a **native Claude Code `.jsonl` transcript on
disk** — the file *is* the store. There is no pluggable `ConversationStorageAdapter` and no
session-metadata surface: the transcript is the single source of truth.
The transcript lands at `/projects//.jsonl`, where `baseDir` comes
from `local.baseDir` (default `~/.theokit`; set `~/.claude` for Claude Code CLI `--continue`
interop). Each send appends the whole turn as native records, append-only, with secrets redacted
before disk.
```ts title="run.ts"
import { Agent } from "@theokit/sdk";
const cwd = process.cwd();
const agentId = `agent-${Date.now().toString(36)}`;
// 1. Create + send — the transcript is written under ~/.theokit by default.
const agent = await Agent.create({
apiKey: process.env.OPENROUTER_API_KEY,
model: { id: "openai/gpt-4o-mini" },
agentId,
local: { cwd, baseDir: "~/.theokit" },
});
await (await agent.send("Remember my favorite color is teal. Reply with: OK.")).wait();
await agent.dispose();
// 2. Resume the SAME agentId — the transcript rehydrates the conversation.
const resumed = await Agent.resume(agentId, { local: { cwd, baseDir: "~/.theokit" } });
await (await resumed.send("What is my favorite color?")).wait();
await resumed.dispose();
```
## Multi-host persistence
Because the transcript is a plain file tree, sharing it across hosts is a filesystem concern, not an
SDK adapter: point every host at a **shared `baseDir`** (a network mount), or **replicate the
transcript directory** between hosts. There is no Postgres/Redis/Durable-Object adapter today —
custom backends are out until an adapter contract over the native format ships.
## See also
- [Sessions overview](/theokit/sessions) — the native transcript model and where it lives.
- [Resume a session](/theokit/sessions/manage-sessions) — a runnable resume + `--continue` example.
- [Advanced sessions](/theokit/sessions/advanced) — the record shape, scoped ids, and CLI interop.
---
# custom-provider
Source: https://docs.usetheo.dev/theokit/guides/custom-provider
Register a custom OpenAI-/Anthropic-compatible LLM provider with `defineProvider`
# custom-provider
Register a custom OpenAI-/Anthropic-compatible LLM provider with `defineProvider`
and route to it — no fork required.
```bash
pnpm install
pnpm run # registration + routing only (no LLM call)
GROQ_API_KEY=gsk_... pnpm run # performs a live send through Groq
```
What it shows:
- A `ProviderProfile` is **data only** — declare name, `apiMode` (HTTP dialect),
auth, base URL, fallback models.
- `defineProvider(profile)` returns a `kind: "model-provider"` plugin (mirrors
`defineTool` / `definePlugin`).
- Pass it to `Agent.create({ plugins: [...] })` and route via the `provider/model`
id prefix (`groq/llama-3.1-8b-instant`).
See the "Custom providers (`defineProvider`)" section in `docs.md` for the full
`ProviderProfile` field reference and the supported `apiMode` values.
## Code
```ts title="run.ts"
/**
* Custom LLM provider via `Provider.create`.
*
* Demonstrates:
* - Declare a `ProviderProfile` for any OpenAI-compatible endpoint (here: Groq).
* - Build a `kind: "model-provider"` plugin with `Provider.create`.
* - Route to it via the `provider/model` id prefix on `Agent.create`.
*
* Real-LLM validation (per .claude/rules/real-llm-validation.md):
* The custom-provider REGISTRATION + ROUTING is demonstrated without a live
* call (no key needed). To actually send, set a real Groq key:
* GROQ_API_KEY=gsk_... pnpm run
*/
import { Agent, Provider } from "@theokit/sdk";
const groqKey = process.env.GROQ_API_KEY;
const realLlm = typeof groqKey === "string" && groqKey.startsWith("gsk_");
console.log("\n== Custom provider via Provider.create ==");
console.log(realLlm ? "Mode: real Groq call" : "Mode: registration/routing only (no key)");
// 1. Declare the provider — data only. OpenAI-compatible → apiMode "chat_completions".
const groq = Provider.create({
name: "groq",
apiMode: "chat_completions",
authType: realLlm ? "api_key" : "none",
envVars: ["GROQ_API_KEY"],
baseUrl: "https://api.groq.com/openai/v1",
fallbackModels: ["groq/llama-3.1-8b-instant"],
aliases: ["groq-cloud"],
});
console.log(`Provider plugin: name=${groq.name} kind=${groq.kind}`);
// 2. Create an agent routed to the custom provider via the `groq/...` id prefix.
const agent = await Agent.create({
model: { id: "groq/llama-3.1-8b-instant" },
plugins: [groq],
});
console.log(`Agent created (id=${agent.id}) routing to the custom provider.`);
if (realLlm) {
const run = await agent.send("Reply with exactly: custom provider works");
const result = await run.wait();
console.log(`status=${result.status}`);
console.log(`reply=${result.result ?? "(none)"}`);
} else {
console.log("Set GROQ_API_KEY=gsk_... to perform a live send.");
}
await agent.dispose();
console.log("Done.\n");
```
## Run
```bash
cd examples/custom-provider
cp .env.example .env # fill in keys
pnpm install
pnpm run run
```
## Repository
[examples/custom-provider](https://github.com/usetheodev/theokit-sdk/tree/main/examples/custom-provider)
---
# eval
Source: https://docs.usetheo.dev/theokit/guides/eval
Runs `Eval.create / .run` against a real LLM and prints aggregate +
# eval
Runs `Eval.create / .run` against a real LLM and prints aggregate +
per-row results.
## Run (Ollama, no API keys)
```bash
ollama serve &
ollama pull llama3.2:3b
pnpm install
pnpm run run
```
## Run (OpenRouter cloud)
```bash
export OPENROUTER_API_KEY=sk-or-...
pnpm run run
```
## What it shows
- `Eval.create({...}).run()` returns a populated `EvalRun` shape (D202, D209)
- `Scorers.containsExpected()` + `Scorers.regex()` applied to each row
- Aggregate includes `meanScore`, `passRatio`, `errorRows`, `tokensInTotal`,
`durationMsP50`, `durationMsP95` (D211)
- v1 scale: keep datasets ≤ 10k rows (EC-11 — v1 materializes the dataset
in memory; partition manually for larger evals or wait for streaming v2)
## LLM-as-judge
For subjective scoring, swap the second scorer:
```ts
Scorers.llmJudge({
model: { id: "openai/gpt-4o-mini" },
apiKey: process.env.OPENROUTER_JUDGE_KEY ?? process.env.OPENROUTER_API_KEY ?? "",
criteria: "The answer is concise and accurate.",
rubric: "continuous",
}),
```
**Cost note (EC-12):** `llmJudge` doubles the per-row LLM cost. For
1000 rows × `gpt-4o-mini`, expect ~$1.50 (eval) + ~$1.50 (judge) = $3.00.
The `aggregate.tokensInTotal` only reflects the EVAL agent's tokens, not
the judge's — forecast accordingly.
## Code
```ts title="run.ts"
/**
* Example: `Eval.create / .run` against a real LLM.
*
* pnpm install
* ollama serve & ollama pull llama3.2:3b
* pnpm run run
*
* Or with cloud:
* OPENROUTER_API_KEY=... pnpm run run
*
* Prints the EvalRun JSON (aggregate + rows) to stdout.
*/
import { Eval, Scorers, type EvalRun } from "@theokit/sdk/eval";
const useCloud = typeof process.env.OPENROUTER_API_KEY === "string";
const agent = useCloud
? {
apiKey: process.env.OPENROUTER_API_KEY ?? "",
model: { id: "openai/gpt-4o-mini" },
local: { cwd: process.cwd(), sandboxOptions: { enabled: false } as const },
providers: {
routes: [{ capability: "chat" as const, provider: "openrouter" }],
fallback: ["openrouter"],
},
}
: {
apiKey: "ollama-local",
model: { id: "ollama/llama3.2:3b" },
local: { cwd: process.cwd(), sandboxOptions: { enabled: false } as const },
};
const run: EvalRun = await Eval.create({
name: "smoke-eval",
dataset: [
{ input: "Reply with the single word: ok.", expected: "ok" },
{ input: "Say jazz in one word.", expected: "jazz" },
{ input: "What is 2 + 3? Reply with just the digit.", expected: "5" },
{ input: "Name a primary color. Reply with one word.", expected: "red" },
{ input: "Say hi in one word.", expected: "hi" },
],
scorers: [
Scorers.containsExpected({ caseSensitive: false }),
Scorers.regex(/[a-zA-Z0-9]/),
],
agent,
concurrency: 2,
metadata: { example: "eval-smoke", mode: useCloud ? "cloud" : "ollama" },
}).run();
console.log(JSON.stringify(run, null, 2));
console.log("");
console.log(
`Mean: ${run.aggregate.meanScore.toFixed(3)} | Pass: ${(run.aggregate.passRatio * 100).toFixed(1)}% | Errors: ${run.aggregate.errorRows}/${run.aggregate.totalRows} | Tokens in/out: ${run.aggregate.tokensInTotal}/${run.aggregate.tokensOutTotal}`,
);
```
## Run
```bash
cd examples/eval
cp .env.example .env # fill in keys
pnpm install
pnpm run run
```
## Repository
[examples/eval](https://github.com/usetheodev/theokit-sdk/tree/main/examples/eval)
---
# handoffs
Source: https://docs.usetheo.dev/theokit/guides/handoffs
Triage agent → billing/support specialist. Demonstrates the
# handoffs
Triage agent → billing/support specialist. Demonstrates the
`handoffs: []` declarative API.
## Run (Ollama)
```bash
ollama serve &
ollama pull llama3.2:3b
pnpm install
pnpm run run
```
## Run (OpenRouter cloud)
```bash
export OPENROUTER_API_KEY=sk-or-...
pnpm run run
```
## What it shows
- `Agent.create({ handoffs: [billing, support] })` declares peer-to-peer transfers (ADRs D214-D229).
- `RECOMMENDED_HANDOFF_PROMPT_PREFIX` exported as a constant; including it in the sender's system prompt makes the LLM use the transfer tools reliably.
- `Handoff.create(target, { toolDescription })` shows the customization escape hatch.
- Each handoff is exposed to the LLM as `transfer_to_`; the LLM decides based on the user's intent.
## Model quality dependency (EC-14)
Handoffs require reliable function-calling. Tested combinations:
| Model | Reliability |
|---|---|
| `openai/gpt-4o-mini` (cloud) | ✅ Excellent |
| `anthropic/claude-3-5-haiku` (cloud) | ✅ Excellent |
| `ollama/llama3.2:3b` (local) | ⚠️ Inconsistent — small models often skip the transfer tool |
| `ollama/qwen2.5:7b` (local) | ✅ Good |
| `ollama/llama3.1:8b` (local) | ✅ Good |
| `ollama/mistral:7b` (local) | ✅ Good |
**Rule of thumb:** local models under ~7B params struggle with the handoff
tool-call decision (~30% miss rate observed). For local development, prefer
7B+ models OR test with `Agent.handoffTo` imperative as fallback.
## Cost tradeoff for deep chains (EC-12)
Full conversation history is passed to each receiver by default (D216). For
chains depth > 2, consider `Handoff.create(target, { inputFilter: summarize })`
to bound token cost. Token totals stack across hops; a 3-hop chain on a
5-message history roughly triples the prompt tokens.
## Loop protection
- `maxHandoffDepth: 5` per `send()` (default; D218). Override via `Agent.create({ maxHandoffDepth: N })`.
- Set `maxHandoffDepth: 0` to disable handoffs entirely (EC-8 — tools never fire).
- Pair single-flight (D221): A → B → A within the same `send()` throws `HandoffPairLoopError`. Use a 3rd agent for legitimate "back to triage" patterns.
## Code
```ts title="run.ts"
/**
* Example: triage agent transfers to billing OR support based on intent.
*
* ollama serve & ollama pull llama3.2:3b
* pnpm install
* pnpm run run
*
* Or with cloud:
* export OPENROUTER_API_KEY=sk-or-...
* pnpm run run
*/
import {
Agent,
Handoff,
RECOMMENDED_HANDOFF_PROMPT_PREFIX,
} from "@theokit/sdk";
const useCloud = typeof process.env.OPENROUTER_API_KEY === "string";
const baseAgentConfig = useCloud
? {
apiKey: process.env.OPENROUTER_API_KEY ?? "",
model: { id: "openai/gpt-4o-mini" },
local: { cwd: process.cwd(), sandboxOptions: { enabled: false } as const },
providers: {
routes: [{ capability: "chat" as const, provider: "openrouter" }],
fallback: ["openrouter"],
},
}
: {
apiKey: "ollama-local",
model: { id: "ollama/llama3.2:3b" },
local: { cwd: process.cwd(), sandboxOptions: { enabled: false } as const },
};
// Specialists
const billing = await Agent.create({
...baseAgentConfig,
name: "billing",
systemPrompt:
"You are a billing specialist. Answer questions about invoices, charges, and payments concisely.",
});
const support = await Agent.create({
...baseAgentConfig,
name: "support",
systemPrompt:
"You are a technical support specialist. Answer questions about installation, configuration, and troubleshooting.",
});
// Triage routes to the right specialist
const triage = await Agent.create({
...baseAgentConfig,
name: "triage",
systemPrompt: `${RECOMMENDED_HANDOFF_PROMPT_PREFIX}
You are a triage agent. Listen to the user's question and IMMEDIATELY transfer
the conversation to the right specialist:
- billing questions → transfer_to_billing
- technical/install questions → transfer_to_support
Do NOT answer the user directly. Use exactly ONE transfer_to_* tool per turn.`,
handoffs: [billing, Handoff.create(support, { toolDescription: "Transfer to support for install/config issues" })],
});
console.log(`Triage agent ready (mode: ${useCloud ? "cloud" : "ollama"}).`);
const questions = [
"I have a question about my bill.",
"How do I install the SDK?",
];
for (const q of questions) {
console.log(`\n=== User: ${q}`);
const run = await triage.send(q);
const result = await run.wait();
console.log(`=== Triage status: ${result.status}`);
console.log(`=== Response:`);
console.log(result.result ?? `(${result.status}${result.error ? `: ${result.error.message}` : ""})`);
}
await triage.dispose();
await billing.dispose();
await support.dispose();
console.log("\nDone.");
```
## Run
```bash
cd examples/handoffs
cp .env.example .env # fill in keys
pnpm install
pnpm run run
```
## Repository
[examples/handoffs](https://github.com/usetheodev/theokit-sdk/tree/main/examples/handoffs)
---
# Cookbook
Source: https://docs.usetheo.dev/theokit/guides
Runnable recipes auto-generated from packages/sdk/examples/.
# Cookbook
Every recipe below is a real, runnable example from the SDK repo. Each one has been validated with a real LLM provider before shipping.
- [`abort-mid-stream`](/theokit/guides/abort-mid-stream) — Demonstrates `AbortSignal` end-to-end propagation: caller's signal stops upstream token billing mid-
- [`acp-server`](/theokit/guides/acp-server) — Minimal `@theokit/acp` example: default-exports a per-session factory that creates a fresh `Agent` p
- [`agent-basics`](/theokit/guides/agent-basics) — The smallest end-to-end path with `@theokit/sdk`: create a local agent against
- [`agent-streaming`](/theokit/guides/agent-streaming) — Iterate `run.stream()` to consume `SDKMessage` events as they arrive, instead of
- [`agent-structured-output`](/theokit/guides/agent-structured-output) — Coerce an agent's final answer into a validated, inferred-typed object with a Zod
- [`bedrock-bot`](/theokit/guides/bedrock-bot) — One-shot Claude prompt via AWS Bedrock (`@theokit/sdk` Adoption Roadmap #8; ADRs D286-D302).
- [`cache`](/theokit/guides/cache) — Demonstrates `Cache.semantic` + `Cache.consult` (Adoption Roadmap #6; ADRs D249-D266).
- [Session persistence](/theokit/guides/conversation-storage) — How a local agent's conversation persists in v4.0: the native transcript on disk, with no pluggable adapter.
- [`custom-provider`](/theokit/guides/custom-provider) — Register a custom OpenAI-/Anthropic-compatible LLM provider with `defineProvider`
- [`eval`](/theokit/guides/eval) — Runs `Eval.create / .run` against a real LLM and prints aggregate +
- [`handoffs`](/theokit/guides/handoffs) — Triage agent → billing/support specialist. Demonstrates the
- [`memory-lance`](/theokit/guides/memory-lance) — > Ships with `@theokit/sdk@1.4.0` (lancedb-backend-ship-v1-1 plan). Closes
- [`prompts`](/theokit/guides/prompts) — Instructions are the system prompt — a plain string, a resolver evaluated per send, or a
- [`providers-models`](/theokit/guides/providers-models) — The model is chosen by the `vendor/model` id you pass to `Agent.create` plus the key.
- [`squad-basics`](/theokit/guides/squad-basics) — A sequential `Squad` of two agents: a brainstormer proposes name ideas, a picker chooses the best.
- [`tasks`](/theokit/guides/tasks) — Demonstrates the `Task` namespace from `@theokit/sdk` (Adoption Roadmap gap #2; ADRs D361-D374).
- [`tool-basics`](/theokit/guides/tool-basics) — Give an agent a typed tool with `defineTool` — the model calls it when the prompt
- [`tool-hooks-tracking`](/theokit/guides/tool-hooks-tracking) — Demonstrates `onToolStart` / `onToolEnd` / `onToolError` callbacks for cost tracking, audit log, lat
- [`vertex-bot`](/theokit/guides/vertex-bot) — One-shot Gemini (or Claude) prompt via GCP Vertex AI (`@theokit/sdk` Adoption Roadmap #8; ADRs D286-
- [`workflow-basics`](/theokit/guides/workflow-basics) — A two-step `Workflow`: a `fn` step normalizes the input, an `agentStep` turns it into a
- [`workflows`](/theokit/guides/workflows) — Multi-step pipeline: validate → classify (LLM) → branch (billing/support) → summarize.
## Complex examples (GitHub link)
These examples are too large to inline. View them directly:
- [examples/telegram-pro](https://github.com/usetheodev/theokit-sdk/tree/main/examples/telegram-pro)
---
# memory-lance
Source: https://docs.usetheo.dev/theokit/guides/memory-lance
> Ships with `@theokit/sdk@1.4.0` (lancedb-backend-ship-v1-1 plan). Closes
# memory-lance
> Ships with `@theokit/sdk@1.4.0` (lancedb-backend-ship-v1-1 plan). Closes
> ADR D12 ("LanceDB deferred to v1.1") via fulfillment of D43.
## What this shows
How to opt into the **Lance backend** for `Memory.create` instead of the
default SQLite-vec. Lance scales to >100k embeddings with HNSW-grade
vector search — relevant when SQLite-vec p95 latency starts hurting
(typically above ~10k facts).
Two modes:
1. **Dry-run (default)** — `pnpm run`: prints what the real path would
do, exits 0. No LLM call, no Lance install required. Honored by ADR
D50 — example must work even without the optional dep.
2. **Real** — `LANCE_REAL=1 pnpm run` (with `OPENROUTER_API_KEY` set
and `@lancedb/lancedb` peer dep installed): seeds 3 facts with real
OpenRouter embeddings, runs semantic recall, asserts a non-empty hit.
## Setup (real mode)
```bash
# 1. Install the peer deps (NOT bundled in @theokit/sdk — opt-in).
pnpm add @lancedb/lancedb apache-arrow@^18.1.0
# 2. Get a free OpenRouter key at https://openrouter.ai/keys and copy
# .env.example to .env. Fill OPENROUTER_API_KEY and LANCE_REAL=1.
cp .env.example .env
# Edit .env
# 3. Run.
pnpm run
```
Expected output:
```
=== @theokit/sdk Lance backend example — REAL MODE ===
[1/4] Opening Lance index with real embedder...
[2/4] Seeding 3 synthetic facts...
[3/4] Recalling via semantic search...
Got 3 hits. Top match:
score=0.6XX
snippet="LanceDB is a columnar vector database..."
[4/4] Closing index...
=== SUCCESS — Lance E2E validated with real LLM + real Lance. ===
```
## Gotchas
### Native binding prebuilds (ADR D43 consequences)
`@lancedb/lancedb` ships prebuilt binaries for:
- `linux-x64-gnu`
- `darwin-arm64`
- `darwin-x64`
- `win32-x64-msvc`
**Not covered:** Alpine/musl Linux, ARM-Linux. On those platforms the
peer install attempts a source build via `node-gyp` and fails without
toolchain. **Workaround:** stay on SQLite default (omit `backend: "lance"`).
### Bundler externalization (consumers of SDK)
If your consumer app bundles `@theokit/sdk` (Next.js, Vite, webpack,
rollup), you MUST externalize `@lancedb/lancedb`:
- **Next.js:**
```js
// next.config.js
experimental: { serverComponentsExternalPackages: ["@lancedb/lancedb"] }
```
- **Vite:**
```js
// vite.config.js
optimizeDeps: { exclude: ["@lancedb/lancedb"] },
ssr: { external: ["@lancedb/lancedb"] }
```
- **webpack/rollup:** add to `externals` array.
Without this, the bundler tries to process the `.node` binding and
crashes at build time.
### apache-arrow peer pin
`@lancedb/lancedb@0.30.0` requires `apache-arrow >=15.0.0 <=18.1.0`.
If your app already pins a newer `apache-arrow`, you'll see an
`unmet peer` warning — downgrade to `^18.1.0` or accept the warning
(Lance still works at v21 in practice, but it's not officially supported).
## When to use Lance vs SQLite
| Scale | Recommended backend | Why |
|---|---|---|
| < 1k facts | SQLite-vec (default) | Zero deps, fast startup |
| 1k–10k facts | SQLite-vec (default) | Still under p95 threshold |
| 10k–100k facts | Lance (opt-in) | SQLite-vec p95 > 100ms; Lance HNSW wins |
| > 100k facts | Lance (recommended) | Columnar storage + vector indices designed for this |
See `.claude/knowledge-base/benchmarks/memory-backends-2026-05-31.md`
in the SDK repo for the numerical methodology behind these thresholds.
## Migration SQLite → Lance
If you already have an SQLite memory index and want to migrate:
```bash
# Built-in CLI (shipped with @theokit/sdk).
npx theokit-migrate-memory --from sqlite --to lance
```
See ADR D44 for migration design + `migrate-sqlite-to-lance.ts` for
implementation. Dry-run is the default — no destructive writes without
`--confirm`.
## Code
```ts title="run.ts"
/**
* @theokit/sdk — Lance backend memory example.
*
* Mode A (default — dry-run, no install required): prints a walkthrough
* of what the real path WOULD do. Exit 0.
*
* Mode B (LANCE_REAL=1 + OPENROUTER_API_KEY): runs the actual flow with
* `@lancedb/lancedb` peer + real OpenRouter embeddings + real chat
* completion. Verifies that recall returns at least one seeded fact.
*
* Ships with the lancedb-backend-ship-v1-1 plan (close ADR D12).
* ADR D50 honored: graceful degradation when the peer dep is absent.
*/
import { createRequire } from "node:module";
const LANCE_REAL = process.env.LANCE_REAL === "1";
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY;
function dryRun(): void {
console.log("=== @theokit/sdk Lance backend example — DRY-RUN MODE ===\n");
console.log("This run did NOT touch a real LLM or write to a Lance index.\n");
console.log("In real mode (LANCE_REAL=1 + OPENROUTER_API_KEY), the script:");
console.log(" 1. Confirms `@lancedb/lancedb` peer dep is installed.");
console.log(" 2. Opens a Lance index in a fresh tmpdir.");
console.log(" 3. Seeds 3 facts with real OpenRouter embeddings.");
console.log(" 4. Runs `Memory.recall()` to confirm semantic retrieval.");
console.log(" 5. Disposes the index cleanly.\n");
console.log("To run for real:");
console.log(" $ pnpm add @lancedb/lancedb apache-arrow@^18.1.0");
console.log(" $ cp .env.example .env # then fill OPENROUTER_API_KEY");
console.log(" $ LANCE_REAL=1 pnpm run\n");
console.log("Gotchas (ADR D43 + D50):");
console.log(" - @lancedb/lancedb ships prebuilds for linux-x64-gnu,");
console.log(" darwin-arm64, darwin-x64, win32-x64-msvc. Alpine/musl/ARM");
console.log(" Linux require node-gyp toolchain. SQLite default covers");
console.log(" these cases — use `Memory.create()` without `backend: \"lance\"`.");
console.log(" - Bundlers (Next.js/Vite/webpack) must externalize the");
console.log(" `@lancedb/lancedb` native binding — see SDK CHANGELOG 1.4.0.");
}
function checkPeerInstalled(): boolean {
try {
const require = createRequire(import.meta.url);
require("@lancedb/lancedb");
return true;
} catch {
return false;
}
}
async function realRun(): Promise {
if (OPENROUTER_API_KEY === undefined || OPENROUTER_API_KEY === "") {
console.error("ERROR: LANCE_REAL=1 requires OPENROUTER_API_KEY in env.");
console.error(" Copy .env.example to .env and fill it in.");
process.exit(1);
}
if (!checkPeerInstalled()) {
console.error("ERROR: LANCE_REAL=1 requires @lancedb/lancedb peer dep.");
console.error(" Install: pnpm add @lancedb/lancedb apache-arrow@^18.1.0");
process.exit(1);
}
console.log("=== @theokit/sdk Lance backend example — REAL MODE ===\n");
console.log("Peer dep present + OPENROUTER_API_KEY set. Proceeding...\n");
// Dynamic imports so the dry-run path never executes any of this.
const { mkdtempSync, rmSync } = await import("node:fs");
const { tmpdir } = await import("node:os");
const { join } = await import("node:path");
const { IndexManager } = await import(
"@theokit/sdk/internal/memory/index-manager.js" as string
).catch(async () => {
// The SDK does not (yet) expose IndexManager as a public sub-export.
// For this example we use the public Memory facade if available, OR
// demonstrate the dispatch via direct internal import for educational
// purposes only (NOT a stable API contract).
return await import("../../packages/sdk/src/internal/memory/index-manager.js");
});
const { MEMORY_EMBEDDING_ADAPTERS } = await import(
"../../packages/sdk/src/internal/memory/adapters/catalog.js"
);
const tmp = mkdtempSync(join(tmpdir(), "lance-example-"));
console.log(`Lance storage: ${tmp}/.theokit/memory/lance/\n`);
try {
// For demo purposes we use a deterministic hash-based embedder so the
// example runs reliably without depending on a specific embedding
// provider's availability (OpenRouter embedding endpoints are gated
// by scope; OpenAI requires a separate API key). This proves the
// Lance dispatch + roundtrip end-to-end with REAL Lance.
//
// To use a real embedding provider: replace this with
// `MEMORY_EMBEDDING_ADAPTERS.openai.create({ apiKey: process.env.OPENAI_API_KEY })`.
void MEMORY_EMBEDDING_ADAPTERS; // catalog browseable for users
const { createHash } = await import("node:crypto");
const embedding = {
id: "demo-mock-embedder",
model: "demo",
dimension: 64,
async embed(texts: ReadonlyArray): Promise {
return texts.map((text) => {
const hash = createHash("sha256").update(text).digest();
const v: number[] = new Array(64);
for (let i = 0; i < 64; i++) {
const byte = hash[i % hash.length] as number;
v[i] = (byte / 127.5) - 1;
}
const norm = Math.sqrt(v.reduce((s, x) => s + x * x, 0));
return v.map((x) => x / (norm || 1));
});
},
stats() {
return { cacheHits: 0, cacheMisses: 0, httpCalls: 0, retries: 0 };
},
};
console.log("[1/4] Opening Lance index with real embedder...");
const index = await IndexManager.open({
cwd: tmp,
embedding,
backend: "lance",
});
console.log(" Lance index opened successfully.\n");
console.log("[2/4] Seeding 3 synthetic facts...");
// We use the underlying LanceIndex via the adapter unwrap for direct
// addFacts (the MemoryIndex interface does not expose addFact —
// Memory facade does, via MEMORY.md markdown writes that get synced).
// For this example we want to demonstrate Lance E2E without the
// markdown corpus layer.
const { LanceMemoryAdapter } = await import(
"../../packages/sdk/src/internal/memory/lance-memory-adapter.js"
);
if (!(index instanceof LanceMemoryAdapter)) {
throw new Error("Expected LanceMemoryAdapter — dispatch failed");
}
const lance = index.unwrap();
const now = Date.now();
await lance.addFacts([
{
id: "fact-1",
text: "TypeScript was created by Microsoft and released in 2012.",
source: "memory",
namespace: "default",
scope: "user",
user_id: "demo",
timestamp: now,
},
{
id: "fact-2",
text: "LanceDB is a columnar vector database optimized for embeddings.",
source: "memory",
namespace: "default",
scope: "user",
user_id: "demo",
timestamp: now + 1,
},
{
id: "fact-3",
text: "Apache Arrow is the in-memory format used by Lance for fast IO.",
source: "memory",
namespace: "default",
scope: "user",
user_id: "demo",
timestamp: now + 2,
},
]);
console.log(" 3 facts written with deterministic demo embedder (real Lance).\n");
console.log("[3/4] Recalling via semantic search...");
const hits = await index.search("Which database does Lance use?", {
maxResults: 3,
});
if (hits.length === 0) {
throw new Error("Recall returned ZERO hits — semantic search broken");
}
console.log(` Got ${hits.length} hits. Top match:`);
console.log(` score=${hits[0]?.score?.toFixed(3)}`);
console.log(` snippet="${hits[0]?.snippet}"\n`);
console.log("[4/4] Closing index...");
await index.close();
console.log(" Index closed.\n");
console.log("=== SUCCESS — Lance E2E validated with real LLM + real Lance. ===");
} finally {
rmSync(tmp, { recursive: true, force: true });
}
}
async function main(): Promise {
if (LANCE_REAL) {
await realRun();
} else {
dryRun();
}
}
main().catch((err) => {
console.error("FATAL:", err instanceof Error ? err.message : err);
process.exit(1);
});
```
## Run
```bash
cd examples/memory-lance
cp .env.example .env # fill in keys
pnpm install
pnpm run run
```
## Repository
[examples/memory-lance](https://github.com/usetheodev/theokit-sdk/tree/main/examples/memory-lance)
---
# prompts
Source: https://docs.usetheo.dev/theokit/guides/prompts
Instructions are the system prompt — a plain string, a resolver evaluated per send, or a
# prompts
Instructions are the system prompt — a plain string, a resolver evaluated per send, or a
per-send override.
Pairs with the docs page **[Prompts](https://docs.usetheo.dev/theokit/prompts)**.
## Run
```bash
pnpm install
export OPENROUTER_API_KEY=sk-or-... # https://openrouter.ai/keys — or put it in .env
pnpm run run
```
## What it shows
- `systemPrompt: (ctx) => string` — dynamic instructions from `SystemPromptContext`
(`userMessage`, `model`, `memory`, …).
- `agent.send(msg, { systemPrompt })` — a per-send string override that wins over the resolver.
## Code
```ts title="run.ts"
/**
* Prompts and instructions (features/prompts).
*
* The system prompt is the agent's instructions. It can be a plain string, a
* resolver evaluated per send (with the message + context), or overridden for a
* single send via `SendOptions.systemPrompt`.
*
* Run:
* pnpm install
* export OPENROUTER_API_KEY=sk-or-... # or put it in .env
* pnpm run run
*/
import { Agent } from "@theokit/sdk";
const apiKey = process.env.OPENROUTER_API_KEY;
if (apiKey === undefined || apiKey.length === 0) {
throw new Error("Set OPENROUTER_API_KEY (env or .env) — see https://openrouter.ai/keys");
}
// Dynamic instructions: the system prompt is computed per send from context
// (`ctx.userMessage`, `ctx.model`, recalled `ctx.memory`, …).
const agent = await Agent.create({
apiKey,
model: { id: "openai/gpt-4o-mini" },
systemPrompt: (ctx) =>
`You are a terse assistant. Answer in exactly one sentence. The user asked: "${ctx.userMessage}".`,
});
const dynamic = await (await agent.send("What is TypeScript?")).wait();
console.log("Dynamic: ", dynamic.result);
// Per-send override: a string that wins over the resolver for this send only.
const override = await (
await agent.send("What is TypeScript?", { systemPrompt: "Reply as a pirate, in one sentence." })
).wait();
console.log("Override: ", override.result);
await agent.dispose();
// --- validate output (fail loud) ---
for (const [label, r] of [["dynamic", dynamic], ["override", override]] as const) {
if (r.status !== "finished" || typeof r.result !== "string" || r.result.length === 0) {
console.error(`${label} run did not finish:`, JSON.stringify(r.error ?? r.status));
process.exit(1);
}
}
```
## Run
```bash
cd examples/prompts
cp .env.example .env # fill in keys
pnpm install
pnpm run run
```
## Repository
[examples/prompts](https://github.com/usetheodev/theokit-sdk/tree/main/examples/prompts)
---
# providers-models
Source: https://docs.usetheo.dev/theokit/guides/providers-models
The model is chosen by the `vendor/model` id you pass to `Agent.create` plus the key.
# providers-models
The model is chosen by the `vendor/model` id you pass to `Agent.create` plus the key.
`@theokit/sdk/models` gives you offline helpers to parse an id and look up capabilities.
Pairs with the docs page **[Providers and models](https://docs.usetheo.dev/theokit/providers-models)**.
## Run
```bash
pnpm install
export OPENROUTER_API_KEY=sk-or-... # https://openrouter.ai/keys — or put it in .env
pnpm run run
```
## What it shows
- `parseModelId` / `humanizeModelName` / `resolveModelCapabilities` from `@theokit/sdk/models` — offline, no key.
- `model: { id: "openai/gpt-oss-120b:free" }` — the id + key select the provider (OpenRouter routing).
## Code
```ts title="run.ts"
/**
* Providers and models (features/providers-models).
*
* The model is chosen by the `vendor/model` id you pass to `Agent.create` plus
* the key. `@theokit/sdk/models` also gives you offline helpers to parse a model
* id and look up its capabilities — no key, no network.
*
* Run:
* pnpm install
* export OPENROUTER_API_KEY=sk-or-... # or put it in .env
* pnpm run run
*/
import { Agent } from "@theokit/sdk";
import { humanizeModelName, parseModelId, resolveModelCapabilities } from "@theokit/sdk/models";
const apiKey = process.env.OPENROUTER_API_KEY;
if (apiKey === undefined || apiKey.length === 0) {
throw new Error("Set OPENROUTER_API_KEY (env or .env) — see https://openrouter.ai/keys");
}
// 1. Inspect any model id — offline, pure, no network.
const inspectId = "anthropic/claude-3-5-sonnet";
const { provider, name } = parseModelId(inspectId);
const caps = resolveModelCapabilities(inspectId);
console.log(`Inspect: ${humanizeModelName(inspectId)}`);
console.log(`Provider: ${provider} · name: ${name}`);
console.log(`Context: ${caps.maxContextTokens} tokens · tools: ${caps.supportsToolUse} · vision: ${caps.supportsVision}`);
// 2. Run an agent. The `vendor/model` id + your key route to the provider
// (here OpenRouter routes `openai/…`).
const agent = await Agent.create({
apiKey,
model: { id: "openai/gpt-4o-mini" },
systemPrompt: "You are concise.",
});
const result = await (await agent.send("Name three popular LLM providers, comma-separated.")).wait();
console.log(`\nReply: ${result.result}`);
await agent.dispose();
// --- validate output (fail loud) ---
if (result.status !== "finished" || typeof result.result !== "string" || result.result.length === 0) {
console.error("run did not finish:", JSON.stringify(result.error ?? result.status));
process.exit(1);
}
```
## Run
```bash
cd examples/providers-models
cp .env.example .env # fill in keys
pnpm install
pnpm run run
```
## Repository
[examples/providers-models](https://github.com/usetheodev/theokit-sdk/tree/main/examples/providers-models)
---
# squad-basics
Source: https://docs.usetheo.dev/theokit/guides/squad-basics
A sequential `Squad` of two agents: a brainstormer proposes name ideas, a picker chooses the best.
# squad-basics
A sequential `Squad` of two agents: a brainstormer proposes name ideas, a picker chooses the best.
```bash
OPENROUTER_API_KEY=... pnpm run
```
## Code
```ts title="run.ts"
/**
* Squad basics — a sequential team of agents.
*
* `Squad.create({ agents })` runs its agents in array order: each agent's reply becomes the next
* agent's input. Here a brainstormer proposes name ideas, then a picker chooses the best one.
* `squad.run(input)` returns the terminal `SquadRun` — `result` (last agent's output), `status`,
* and `steps` (one `StepResult` per agent).
*/
import { Agent, Squad } from "@theokit/sdk";
const brainstormer = await Agent.create({
apiKey: process.env.OPENROUTER_API_KEY,
model: { id: "openai/gpt-4o-mini" },
systemPrompt: "List exactly 3 short product name ideas as a comma-separated line. No preamble.",
});
const picker = await Agent.create({
apiKey: process.env.OPENROUTER_API_KEY,
model: { id: "openai/gpt-4o-mini" },
systemPrompt: "From the given list, pick the single best name and reply with just that name and a 6-word reason.",
});
const squad = Squad.create({ agents: [brainstormer, picker] });
const run = await squad.run("a focus timer app for developers");
console.log("Status:", run.status);
console.log("Steps:", run.steps.length);
console.log("Result:", run.result);
await brainstormer.dispose();
await picker.dispose();
// Validate the output — a squad that did not complete is a failure, not a green run.
if (run.status !== "completed" || typeof run.result !== "string" || run.result.length === 0) {
const failed = run.steps.find((s) => s.status === "failed");
console.error(`Squad did not complete: status=${run.status}`, failed?.error ?? "");
process.exit(1);
}
```
## Run
```bash
cd examples/squad-basics
cp .env.example .env # fill in keys
pnpm install
pnpm run run
```
## Repository
[examples/squad-basics](https://github.com/usetheodev/theokit-sdk/tree/main/examples/squad-basics)
---
# tasks
Source: https://docs.usetheo.dev/theokit/guides/tasks
Demonstrates the `Task` namespace from `@theokit/sdk` (Adoption Roadmap gap #2; ADRs D361-D374).
# tasks
Demonstrates the `Task` namespace from `@theokit/sdk` (Adoption Roadmap gap #2; ADRs D361-D374).
## Run (no LLM required)
```bash
pnpm install
pnpm run run
```
The work functions are deterministic — no `OPENROUTER_API_KEY` needed. To use a real LLM, wrap your `agent.send(prompt, { signal: ctx.signal })` call inside the `work` callback.
## What it shows
- `Task.submit(kind, work, options?)` — submit a unit of async work and receive a queued `TaskHandle`.
- `Task.subscribe(id)` — `AsyncIterable` with ring-buffer replay (D372) for late-attach safety.
- Fan-out **batch** pattern — 1 parent task whose work spawns N children with `meta: { item }` provenance.
- Idempotent **cancel** — `Task.cancel` returns `{ cancelled, alreadyTerminal }`; calling twice is safe.
- **JsonFileTaskStore** opt-in via `Task.configure({ store: { backend: "json", dir } })` — handles persist across restarts; inspect them via the CLI:
```bash
THEOKIT_HOME=/tmp/theokit-tasks-example-XXX pnpm exec theokit tasks list
```
## v1 scope cut (documented)
`Agent.send` / `Agent.batch` / `Workflow.run` / `Cron.register` do **NOT** accept a `{ task: true }` option yet — that adapter integration is deferred to v0.2 (see plan v1.2). The user-side pattern in `run.ts` (`Task.submit("kind", async (ctx) => myAsyncWork(ctx))`) covers every observability use case today, with zero coupling to the underlying runtime.
## Code
```ts title="run.ts"
/**
* Example: Task observability registry (ADRs D361-D374, Adoption Roadmap gap #2).
*
* Demonstrates the 5-state lifecycle, subscribe with ring-buffer replay,
* idempotent cancel, fan-out batch pattern, and JsonFileTaskStore
* cross-restart persistence.
*
* No LLM required — the work functions are deterministic so the example
* runs offline and quickly. For a real-LLM equivalent, wrap your
* `agent.send` call inside the `work` callback.
*/
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Task } from "@theokit/sdk";
async function main(): Promise {
// Persist tasks to a temp dir so we can also exercise the CLI verb.
const dir = mkdtempSync(join(tmpdir(), "theokit-tasks-example-"));
Task.configure({
store: { backend: "json", dir: join(dir, "tasks") },
maxConcurrent: 4,
});
console.log(`Using JsonFileTaskStore at ${dir}/tasks\n`);
// ─── 1. Submit a single task + subscribe to progress ──────────────
console.log("1) Single task with progress events");
const handle = await Task.submit("custom", async (ctx) => {
for (let i = 0; i < 3; i++) {
ctx.emit({ step: i });
await sleep(20);
}
return "done";
});
for await (const event of Task.subscribe(handle.id)) {
console.log(" event:", event.type, "type" in event ? JSON.stringify(event) : "");
if (event.type === "finished" || event.type === "errored" || event.type === "cancelled") break;
}
// ─── 2. Fan-out batch with parent + children ──────────────────────
console.log("\n2) Fan-out batch (1 parent + N children)");
const parent = await Task.submit("batch", async (ctx) => {
const items = ["alpha", "beta", "gamma"];
const children = await Promise.all(
items.map((item) =>
Task.submit(
"run",
async (childCtx) => {
childCtx.emit({ item });
await sleep(10);
return `processed:${item}`;
},
{ meta: { item } },
),
),
);
return { childCount: children.length, ids: children.map((h) => h.id) };
});
await waitForTerminal(parent.id);
const parentFinal = await Task.get(parent.id);
console.log(` parent ${parentFinal?.state}, result:`, parentFinal?.result);
// ─── 3. Cancel a running task idempotently ────────────────────────
console.log("\n3) Cancel mid-flight (idempotent)");
const cancellable = await Task.submit("custom", async (ctx) => {
return new Promise((_resolve, reject) => {
ctx.signal.addEventListener("abort", () => reject(new Error("aborted")));
});
});
await sleep(10);
const first = await Task.cancel(cancellable.id);
const second = await Task.cancel(cancellable.id);
console.log(" first cancel:", first);
console.log(" second cancel (idempotent):", second);
// ─── 4. List + filter ──────────────────────────────────────────────
console.log("\n4) List + filter");
const allFinished = await Task.list({ state: "finished" });
const allCancelled = await Task.list({ state: "cancelled" });
console.log(
` ${allFinished.length} finished | ${allCancelled.length} cancelled`,
);
console.log(`\n→ Done. Inspect via:`);
console.log(` THEOKIT_HOME=${dir} pnpm exec theokit tasks list`);
console.log(` THEOKIT_HOME=${dir} pnpm exec theokit tasks inspect `);
}
function sleep(ms: number): Promise {
return new Promise((r) => setTimeout(r, ms));
}
async function waitForTerminal(id: string): Promise {
for (let i = 0; i < 100; i++) {
const h = await Task.get(id);
if (h?.state === "finished" || h?.state === "error" || h?.state === "cancelled") return;
await sleep(10);
}
}
main().catch((err) => {
console.error("example failed:", err);
process.exit(1);
});
```
## Run
```bash
cd examples/tasks
cp .env.example .env # fill in keys
pnpm install
pnpm run run
```
## Repository
[examples/tasks](https://github.com/usetheodev/theokit-sdk/tree/main/examples/tasks)
---
# tool-basics
Source: https://docs.usetheo.dev/theokit/guides/tool-basics
Give an agent a typed tool with `defineTool` — the model calls it when the prompt
# tool-basics
Give an agent a typed tool with `defineTool` — the model calls it when the prompt
calls for it, and the Zod schema validates the arguments first.
Pairs with the docs page **[Tools › Give an agent a tool](https://docs.usetheo.dev/theokit/tools)**.
## Run
```bash
pnpm install
export OPENROUTER_API_KEY=sk-or-... # https://openrouter.ai/keys — or put it in .env
pnpm run run
```
## What it shows
- `defineTool({ name, description, inputSchema, execute })` — the canonical tool factory.
- The Zod `inputSchema` is converted to JSON Schema and validated before `execute` runs.
- `tools: [getWeather]` on `Agent.create` — the agent decides when to call the tool.
## Code
```ts title="run.ts"
/**
* Tools — give an agent a typed tool (features/tools).
*
* `Tool.create` turns a plain async function into a tool the model can call. The
* Zod `inputSchema` is converted to JSON Schema and validated before `execute`
* runs, so the arguments are typed. The agent decides when to call it based on
* the description and the user's message.
*
* Run:
* pnpm install
* export OPENROUTER_API_KEY=sk-or-... # or put it in .env
* pnpm run run
*/
import { Agent, Tool } from "@theokit/sdk";
import { z } from "zod";
const apiKey = process.env.OPENROUTER_API_KEY;
if (apiKey === undefined || apiKey.length === 0) {
throw new Error("Set OPENROUTER_API_KEY (env or .env) — see https://openrouter.ai/keys");
}
const getWeather = Tool.create({
name: "get_weather",
description: "Look up the current weather in a given city.",
inputSchema: z.object({
city: z.string().describe("City name, e.g. 'Tokyo' or 'Brasília'."),
}),
async handler({ city }) {
// A real tool would call a weather API. Mocked here so the example is self-contained.
const mock: Record = {
Tokyo: "18°C, cloudy",
Brasília: "27°C, sunny",
London: "12°C, raining",
};
return mock[city] ?? `No weather data for ${city}.`;
},
});
const agent = await Agent.create({
apiKey,
model: { id: "openai/gpt-4o-mini" },
name: "weather-bot",
systemPrompt: "Use the get_weather tool when the user asks about weather. Answer in one sentence.",
tools: [getWeather],
local: { cwd: process.cwd(), sandboxOptions: { enabled: false } },
});
const run = await agent.send("What's the weather in Tokyo?");
const result = await run.wait();
console.log("Status:", result.status);
console.log("Reply: ", result.result);
await agent.dispose();
// --- validate output (fail loud) ---
if (result.status !== "finished" || typeof result.result !== "string" || result.result.length === 0) {
console.error("run did not finish:", JSON.stringify(result.error ?? result.status));
process.exit(1);
}
```
## Run
```bash
cd examples/tool-basics
cp .env.example .env # fill in keys
pnpm install
pnpm run run
```
## Repository
[examples/tool-basics](https://github.com/usetheodev/theokit-sdk/tree/main/examples/tool-basics)
---
# tool-hooks-tracking
Source: https://docs.usetheo.dev/theokit/guides/tool-hooks-tracking
Demonstrates `onToolStart` / `onToolEnd` / `onToolError` callbacks for cost tracking, audit log, latency telemetry.
# tool-hooks-tracking
Demonstrates `onToolStart` / `onToolEnd` / `onToolError` callbacks for cost tracking, audit log, latency telemetry.
## Run
```bash
# Fixture mode (no tool dispatch fires):
pnpm run
# Real LLM (the only path that actually exercises tool hooks):
OPENROUTER_API_KEY=sk-or-... pnpm run
```
## What it shows
1. Register `onToolStart` / `onToolEnd` / `onToolError` in `AgentOptions`
2. LLM invokes the tool → start hook fires → handler runs → end hook fires
3. Hooks receive `callId` (same value across start/end pair — correlate in logs)
4. `durationMs` measured from start to end (or error) hook fire
5. Hook listener errors are SWALLOWED (D317) — a crashing logger cannot kill the agent
## Use for production
```ts
onToolStart: ({ toolName, callId, conversationId }) => {
metrics.recordToolStart({ toolName, callId, conversationId });
},
onToolEnd: ({ toolName, callId, durationMs, result }) => {
metrics.recordToolEnd({ toolName, callId, durationMs });
},
onToolError: ({ toolName, callId, durationMs, error }) => {
alerts.notify({ toolName, error: error.message });
},
```
See `docs.md` "Tool lifecycle hooks" section for the full contract.
## Code
```ts title="run.ts"
/**
* Production-Readiness #4 — Tool lifecycle hooks example.
*
* Demonstrates onToolStart / onToolEnd / onToolError for cost tracking,
* audit log, latency telemetry. Hook errors are SWALLOWED (D317) — a
* misbehaving listener cannot crash the agent run.
*
* To run with a real LLM (only path that exercises tool dispatch):
* OPENROUTER_API_KEY=sk-or-... pnpm run
*/
import { Agent, Tool } from "@theokit/sdk";
import { z } from "zod";
const apiKey = process.env.OPENROUTER_API_KEY ?? "theo_test_tool_hooks_example";
const realLlm = apiKey.startsWith("sk-or-");
console.log(`\n== Tool lifecycle hooks example ==`);
console.log(realLlm ? "Mode: real OpenRouter" : "Mode: fixture (no LLM call)");
console.log();
// Custom tool that the LLM may invoke.
const getWeatherTool = Tool.create({
name: "get_weather",
description: "Return mock weather for a city",
inputSchema: z.object({ city: z.string() }),
handler: async ({ city }) => {
await new Promise((r) => setTimeout(r, 20)); // simulate latency
if (city === "boom") throw new Error("intentional handler failure");
return JSON.stringify({ city, tempC: 22, condition: "sunny" });
},
});
// In-memory telemetry sink.
const events: Array<{
type: "start" | "end" | "error";
toolName: string;
callId: string;
durationMs?: number;
errorMsg?: string;
}> = [];
const providers = realLlm
? {
routes: [{ capability: "chat" as const, provider: "openrouter" }],
fallback: ["openrouter"],
}
: undefined;
const agent = await Agent.create({
apiKey,
model: { id: "openai/gpt-4o-mini" },
local: { cwd: process.cwd() },
tools: [getWeatherTool],
...(providers !== undefined ? { providers } : {}),
onToolStart: ({ toolName, callId }) => {
events.push({ type: "start", toolName, callId });
},
onToolEnd: ({ toolName, callId, durationMs }) => {
events.push({ type: "end", toolName, callId, durationMs });
},
onToolError: ({ toolName, callId, durationMs, error }) => {
events.push({ type: "error", toolName, callId, durationMs, errorMsg: error.message });
},
});
if (realLlm) {
console.log(`[1] Sending message that should invoke get_weather…`);
const run = await agent.send("What is the weather in Paris? Use the get_weather tool.");
// Consume the stream to drive the conversation forward (real tool dispatch).
let text = "";
for await (const event of run.stream()) {
if (event.type === "assistant") {
for (const part of event.message.content) {
if (part.type === "text") text += part.text;
}
}
}
await run.wait();
console.log(`[1] LLM reply: ${text.slice(0, 200)}`);
} else {
console.log(`[1] Skipping LLM call (no OPENROUTER_API_KEY set)`);
}
console.log();
console.log(`Captured ${events.length} tool lifecycle events:`);
for (const e of events) {
const dur = e.durationMs !== undefined ? ` (${e.durationMs}ms)` : "";
const err = e.errorMsg !== undefined ? ` error="${e.errorMsg}"` : "";
console.log(` [${e.type.padEnd(5)}] ${e.toolName} callId=${e.callId}${dur}${err}`);
}
console.log();
console.log(`Invariants observed:`);
console.log(`- callId is identical across start↔end (or start↔error) pairs`);
console.log(`- durationMs measured from start hook → end (or error) hook`);
console.log(`- error.message is always present on onToolError`);
console.log(`- onToolError fires when validate fails OR handler throws`);
console.log(`- Hook listener throws are swallowed with stderr warn (D317)`);
await agent.dispose();
```
## Run
```bash
cd examples/tool-hooks-tracking
cp .env.example .env # fill in keys
pnpm install
pnpm run run
```
## Repository
[examples/tool-hooks-tracking](https://github.com/usetheodev/theokit-sdk/tree/main/examples/tool-hooks-tracking)
---
# vertex-bot
Source: https://docs.usetheo.dev/theokit/guides/vertex-bot
One-shot Gemini (or Claude) prompt via GCP Vertex AI (`@theokit/sdk` Adoption Roadmap #8; ADRs D286-D302).
# vertex-bot
One-shot Gemini (or Claude) prompt via GCP Vertex AI (`@theokit/sdk` Adoption Roadmap #8; ADRs D286-D302).
## Setup
1. Enable Vertex AI API in your GCP project: [https://console.cloud.google.com/apis/library/aiplatform.googleapis.com](https://console.cloud.google.com/apis/library/aiplatform.googleapis.com).
2. Grant your principal the **Vertex AI User** role (`roles/aiplatform.user`).
3. Authenticate via ADC (Application Default Credentials):
```bash
gcloud auth application-default login
gcloud config set project
```
4. (Optional, production) Use a service account instead:
```bash
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/sa-key.json
```
5. Copy `.env.example` to `.env` and set `GOOGLE_CLOUD_PROJECT`.
## Run
```bash
cp .env.example .env
# fill GOOGLE_CLOUD_PROJECT
pnpm install
pnpm run run # default question
pnpm run run "What's 2+2?"
VERTEX_MODEL=vertex/anthropic/claude-sonnet-4-5@20250929 pnpm run run # Claude on Vertex
```
## Model IDs
- **Gemini (OpenAI-compat path, D291):** `vertex/google/gemini-2.0-flash-001`
- **Claude (`:rawPredict` path, D292):** `vertex/anthropic/claude-sonnet-4-5@20250929`
## Locations
- `us-central1`, `europe-west4`, `asia-southeast1`, etc — regional routing.
- `global` — cross-region for Anthropic (Vertex global endpoint with D293 baseUrl fix).
## v1 limitations (documented)
- **`google-auth-library` required peer dep** (D288) — repo archived Nov 2025 but security-patched.
- **OpenAI-compat path drops unsupported params silently** (D291) — e.g. recursive JSON schemas in `response_format`. Documented in Vertex's own docs.
- **Anthropic on Vertex is non-streaming** in v1 — `:streamRawPredict` deferred to v1.x; v1 always uses `:rawPredict`.
- **No Workload Identity Federation walkthrough** in v1 (D297) — ADC chain resolves it transparently, but the GCP-side setup is out of scope.
- **No Service Account JSON file generation tooling** (D299) — user provides via `GOOGLE_APPLICATION_CREDENTIALS`.
## Code
```ts title="run.ts"
/**
* GCP Vertex AI demo (Adoption Roadmap #8; ADRs D286-D302).
*
* Sends a one-shot prompt to Gemini (or Claude on Vertex) and prints the reply.
* Uses Application Default Credentials via `google-auth-library`.
*
* Run:
* gcloud auth application-default login
* cp .env.example .env # fill GOOGLE_CLOUD_PROJECT
* pnpm install
* pnpm run run
*/
import { Agent } from "@theokit/sdk";
if (process.env.GOOGLE_CLOUD_PROJECT === undefined) {
console.error(
"GOOGLE_CLOUD_PROJECT is required. Set it in .env or run " +
"`gcloud config set project `.",
);
process.exit(1);
}
const modelId =
process.env.VERTEX_MODEL ?? "vertex/google/gemini-2.0-flash-001";
// For Vertex, apiKey isn't strictly used — ADC resolves the OAuth token
// lazily inside the client. We pass an empty placeholder.
const agent = await Agent.create({
apiKey: "vertex-adc",
model: { id: modelId },
local: { cwd: process.cwd(), sandboxOptions: { enabled: false } as const },
name: "vertex-bot",
systemPrompt: "You are a concise assistant. Reply in one short sentence.",
});
const question = process.argv[2] ?? "Qual é a capital do Brasil?";
console.log(
`[vertex] model=${modelId} project=${process.env.GOOGLE_CLOUD_PROJECT} ` +
`location=${process.env.GOOGLE_CLOUD_LOCATION ?? "us-central1"} question="${question}"`,
);
const run = await agent.send(question);
const result = await run.wait();
console.log(`[vertex] status=${result.status} resultLen=${(result.result ?? "").length}`);
console.log(result.result ?? "(no reply)");
await agent.dispose();
```
## Run
```bash
cd examples/vertex-bot
cp .env.example .env # fill in keys
pnpm install
pnpm run run
```
## Repository
[examples/vertex-bot](https://github.com/usetheodev/theokit-sdk/tree/main/examples/vertex-bot)
---
# workflow-basics
Source: https://docs.usetheo.dev/theokit/guides/workflow-basics
A two-step `Workflow`: a `fn` step normalizes the input, an `agentStep` turns it into a
# workflow-basics
A two-step `Workflow`: a `fn` step normalizes the input, an `agentStep` turns it into a
one-sentence fact. Run with a real provider key:
```bash
OPENROUTER_API_KEY=... pnpm run
```
## Code
```ts title="run.ts"
/**
* Workflow basics — declarative multi-step orchestration.
*
* `Workflow.create()` builds a pipeline of steps. A `fn` step is a plain (typed) function;
* an `agentStep` renders a prompt and calls an agent. `.commit()` freezes the workflow, then
* `.run(input)` executes it and returns the terminal `WorkflowRun` (status + output).
*
* NOTE the imports: `Agent` is on the main entrypoint; `Workflow` / `fn` / `agentStep` live on
* the `@theokit/sdk/workflow` sub-path.
*/
import { Agent } from "@theokit/sdk";
import { Workflow, agentStep, fn } from "@theokit/sdk/workflow";
const writer = await Agent.create({
apiKey: process.env.OPENROUTER_API_KEY,
model: { id: "openai/gpt-4o-mini" },
systemPrompt: "You write exactly one concise, factual sentence. No preamble.",
});
const factPipeline = Workflow.create({ name: "topic-fact" })
// Step 1 — a pure function normalizes the input to a clean topic string (no LLM).
.then(fn("normalize", (input: { topic: string }) => input.topic.trim().toLowerCase()))
// Step 2 — an agent step turns the normalized topic into a one-sentence fact. Workflow state
// flows untyped between steps, so the renderer receives `unknown` — coerce it with String().
.then(agentStep("write", writer, (topic) => `Write a one-sentence fact about ${String(topic)}.`))
.commit();
const run = await factPipeline.run({ topic: " The Moon " });
console.log("Status:", run.status);
console.log("Output:", run.output);
await writer.dispose();
// --- validate output (fail loud) ---
if (run.status !== "completed" || run.output == null) {
console.error("workflow did not complete:", JSON.stringify(run.error ?? run.status));
process.exit(1);
}
```
## Run
```bash
cd examples/workflow-basics
cp .env.example .env # fill in keys
pnpm install
pnpm run run
```
## Repository
[examples/workflow-basics](https://github.com/usetheodev/theokit-sdk/tree/main/examples/workflow-basics)
---
# workflows
Source: https://docs.usetheo.dev/theokit/guides/workflows
Multi-step pipeline: validate → classify (LLM) → branch (billing/support) → summarize.
# workflows
Multi-step pipeline: validate → classify (LLM) → branch (billing/support) → summarize.
Demonstrates the `Workflow.create / .then / .branch / .commit / .run` API
(Adoption Roadmap #5; ADRs D230-D248).
## Run (Ollama)
```bash
ollama serve &
ollama pull llama3.2:3b
pnpm install
pnpm run run
```
## Run (OpenRouter cloud)
```bash
export OPENROUTER_API_KEY=sk-or-...
pnpm run run
```
## What it shows
- `Workflow.create({ name }).then(fn(...)).then(agentStep(...)).branch(...).commit()` declarative DSL.
- `fn(id, handler)` for pure-function steps (validation, transformation).
- `agentStep(id, agent, promptTemplate)` for LLM-driven steps.
- `.branch([[predicate, [...]], [...]], { fallback })` first-match-wins routing.
- `WorkflowRun.stepResults` array with per-step status, attempts, duration.
## v1 limitations
- **LocalAgent only** (`CloudAgent` workflow steps throw `UnsupportedRunOperationError`, ADR D244).
- **Saga compensation deferred to v1.2** — `compensate?` slot reserved on `FnStep` but engine not yet implemented (ADR D238).
- **Persistence**: default in-memory. Use `Workflow.create({ persistence: { backend: "json", dir: ".theokit/workflows" } })` for filesystem snapshots.
## Other primitives not shown
- `.parallel([branchA, branchB], { concurrency })` — fan-out N concurrent branches.
- `.foreach("sourceStepId", innerStep, { concurrency })` — map over array output.
- `.dowhile(step, cond, { maxIterations })` — loop until predicate is false (default cap 100).
- `.sleep(ms)` — pause for fixed duration.
- `.suspend({ payloadSchema })` + `Workflow.resume({ runId, payload })` — human-in-the-loop.
## Code
```ts title="run.ts"
/**
* Workflow demo (Adoption Roadmap #5; ADRs D230-D248).
*
* Demonstrates a 4-step refund triage pipeline:
* validate → classify (LLM) → branch (billing vs support) → summarize
*
* Run:
* pnpm install
* pnpm run run
*
* Requires `OPENROUTER_API_KEY` OR a local Ollama daemon listening on
* `OLLAMA_HOST` (default http://localhost:11434).
*/
import { Agent, Workflow, agentStep, fn } from "@theokit/sdk";
const OPENROUTER = process.env.OPENROUTER_API_KEY;
const useOllama = OPENROUTER === undefined || OPENROUTER.length === 0;
const baseConfig = useOllama
? {
model: { id: "ollama/llama3.2:3b" },
local: { cwd: process.cwd(), sandboxOptions: { enabled: false } as const },
}
: {
apiKey: OPENROUTER!,
model: { id: "openai/gpt-4o-mini" },
local: { cwd: process.cwd(), sandboxOptions: { enabled: false } as const },
};
async function main(): Promise {
const classifier = await Agent.create({
...baseConfig,
name: "classifier",
systemPrompt:
"You classify customer support requests. Reply with EXACTLY one word: BILLING, SUPPORT, or OTHER.",
});
const billingExpert = await Agent.create({
...baseConfig,
name: "billing",
systemPrompt:
"You are a billing specialist. Answer concisely (1-2 sentences) about invoices, refunds, charges.",
});
const supportExpert = await Agent.create({
...baseConfig,
name: "support",
systemPrompt:
"You are a technical support specialist. Answer concisely (1-2 sentences) about install, config, troubleshooting.",
});
const wf = Workflow.create<{ claim: string }, string>({ name: "refund-pipeline" })
.then(
fn<{ claim: string }, { claim: string; ts: number }>("validate", (input) => {
if (!input.claim || input.claim.length < 3) {
throw new Error("claim must be at least 3 characters");
}
return { ...input, ts: Date.now() };
}),
)
.then(
agentStep(
"classify",
classifier,
(input) => `Classify: "${(input as { claim: string }).claim}"`,
),
)
.branch(
[
[(out) => String(out).toUpperCase().includes("BILLING"), [
agentStep("billing_resolve", billingExpert, "Handle the billing question."),
]],
[(out) => String(out).toUpperCase().includes("SUPPORT"), [
agentStep("support_resolve", supportExpert, "Handle the support question."),
]],
],
{
id: "decide",
fallback: [
fn("escalate", () => "Escalating to a human agent."),
],
},
)
.commit();
console.log("Running workflow…");
const run = await wf.run({ claim: "I was charged twice last month for the Pro plan." });
console.log("");
console.log("Status:", run.status);
console.log("Duration:", run.durationMs, "ms");
console.log("Steps:");
for (const sr of run.stepResults) {
console.log(` - [${sr.kind}] ${sr.stepId} (${sr.status}, ${sr.attempts} attempt, ${sr.durationMs}ms)`);
}
console.log("");
console.log("Final output:", run.output);
await classifier.dispose();
await billingExpert.dispose();
await supportExpert.dispose();
}
main().catch((err) => {
console.error("workflow demo failed:", err);
process.exit(1);
});
```
## Run
```bash
cd examples/workflows
cp .env.example .env # fill in keys
pnpm install
pnpm run run
```
## Repository
[examples/workflows](https://github.com/usetheodev/theokit-sdk/tree/main/examples/workflows)
---
# Advanced
Source: https://docs.usetheo.dev/theokit/handoffs/advanced
Handoff.create descriptor options, the onHandoff hook, input filtering and tool scoping, depth control, the handoff result, and typed errors.
# Advanced handoffs
Verified against `@theokit/sdk-handoff`.
## `Handoff.create` — a configured descriptor
`asPlugin` accepts bare agents or `HandoffDescriptor`s. Build one with `Handoff.create(target,
options)` to customize the transfer tool:
```ts
import { Handoff } from "@theokit/sdk-handoff";
import { z } from "zod";
const toBilling = Handoff.create(billing, {
toolName: "escalate_to_billing", // default: transfer_to_
toolDescription: "Hand off billing and refund questions.",
inputType: z.object({ reason: z.string() }), // typed structured input the model must supply
onHandoff: (ctx, parsed) => log("handoff", ctx.senderAgentId, "→", ctx.receiverAgentId, ctx.currentDepth, parsed),
inputFilter: (history) => history.slice(-4), // trim what the receiver sees
tools: ["read_invoice"], // scope the receiver to a subset of tools
isEnabled: (ctx) => ctx.currentDepth < 2, // gate the handoff dynamically
});
Agent.create({ …, plugins: [Handoff.asPlugin({ targets: [toBilling] })] });
```
## `HandoffOptions`
| Option | Effect |
| --- | --- |
| `toolName` / `toolDescription` | Override the synthetic transfer tool's name / description. |
| `inputType` (Zod) | Require typed structured input the model fills when transferring. |
| `onHandoff(ctx, parsed)` | Side-effect hook fired at the transfer (telemetry, logging). |
| `inputFilter(history)` | Trim / rewrite the conversation the receiver inherits. |
| `tools` | Restrict the receiver to a subset of tool names. |
| `isEnabled` | Boolean or predicate — enable/disable the handoff per context. |
## Depth control
`Handoff.asPlugin({ maxHandoffDepth })` (or `Agent.create({ maxHandoffDepth })`, default **5**) caps
the transfer chain per `send()`. Set to `0` to disable handoff tools entirely. Exceeding the cap
throws `HandoffLoopError`; a direct A→B→A ping-pong throws `HandoffPairLoopError`.
## The handoff result
Each transfer produces a `HandoffResult` for telemetry:
```ts
interface HandoffResult { from: string; to: string; depth: number; toolName: string; reasonFromLlm?: string; }
```
## Typed errors
- **`HandoffLoopError`** — the chain exceeded `maxHandoffDepth`.
- **`HandoffPairLoopError`** — two agents ping-ponged control.
- **`HandoffNameCollisionError`** — two targets resolved to the same transfer-tool name.
- **`HandoffReceiverDisposedError`** — the target agent was disposed before it could take over.
## Reference
- [`Handoff`](/theokit/reference/Handoff) · [`HandoffDescriptor`](/theokit/reference/HandoffDescriptor) · [`HandoffOptions`](/theokit/reference/HandoffOptions) · [`HandoffResult`](/theokit/reference/HandoffResult) · [`HandoffLoopError`](/theokit/reference/HandoffLoopError)
---
# Overview
Source: https://docs.usetheo.dev/theokit/handoffs
Transfer control from one agent to another — unlike a subagent (delegate-and-return), the receiver takes over the conversation from that point.
# Handoffs
A **handoff** transfers control: agent A decides agent B should take over, and B continues the
conversation from there. Contrast with a [subagent](/theokit/subagents), which delegates a sub-task
and gets the answer *back* — a handoff does not return.
Handoffs ship in the companion package **`@theokit/sdk-handoff`** (`npm i @theokit/sdk-handoff`). The
preferred 2.x wiring is a plugin:
```ts
import { Agent } from "@theokit/sdk";
import { Handoff } from "@theokit/sdk-handoff";
const billing = await Agent.create({ apiKey, model, name: "billing", systemPrompt: "You handle billing." });
const support = await Agent.create({
apiKey, model, name: "support",
systemPrompt: "You are front-line support. Transfer billing questions.",
plugins: [Handoff.asPlugin({ parentAgentId: "support", targets: [billing] })],
});
```
- **`Handoff.asPlugin({ targets, parentAgentId?, maxHandoffDepth? })`** — wraps each target in a
synthetic `transfer_to_` tool the model can call to hand off. The preferred wiring.
- **`Handoff.create(target, options?)`** — build a `HandoffDescriptor` (custom `toolName`,
`inputType`, `onHandoff` hook, tool scoping) to pass in `targets`.
- **`maxHandoffDepth`** — caps the chain per `send()` (default 5); exceeding throws `HandoffLoopError`.
The legacy `Agent.create({ handoffs: [target] })` option still works while `@theokit/sdk-handoff` is
installed, but `plugins: [Handoff.asPlugin(...)]` is the 2.x pattern.
## Subagent vs Handoff vs Squad
| You want | Reach for |
| --- | --- |
| B does a sub-task and returns the answer to A | [Subagent](/theokit/subagents) |
| B **takes over** the conversation from A | **Handoff** (this page) |
| A fixed pipeline A → B → C in order | [Squad](/theokit/squad) |
## Next
- [Transfer control](/theokit/handoffs/transfer-control) — wire a handoff plugin and trigger a transfer.
- [Advanced](/theokit/handoffs/advanced) — `Handoff.create` options, the `onHandoff` hook, depth, and errors.
## Reference
- [`Handoff`](/theokit/reference/Handoff) · [`HandoffDescriptor`](/theokit/reference/HandoffDescriptor) · [`HandoffOptions`](/theokit/reference/HandoffOptions)
---
# Transfer control
Source: https://docs.usetheo.dev/theokit/handoffs/transfer-control
Wire a handoff plugin so a front-line agent transfers a conversation to a specialist — the specialist takes over from that turn.
# Transfer control
Give the front-line agent a `Handoff.asPlugin({ targets })`. The SDK exposes each target as a
`transfer_to_` tool; when the model calls it, control **passes to the receiver**, which answers
the rest of the turn. This drives real models, so it needs a working key.
```ts title="run.ts"
import { Agent } from "@theokit/sdk";
import { Handoff } from "@theokit/sdk-handoff";
// The specialist that will take over.
const billing = await Agent.create({
apiKey: process.env.OPENROUTER_API_KEY,
model: { id: "openai/gpt-4o-mini" },
name: "billing",
systemPrompt: "You are the billing specialist. Answer billing questions precisely.",
});
// Front-line support, wired to hand off to billing.
const support = await Agent.create({
apiKey: process.env.OPENROUTER_API_KEY,
model: { id: "openai/gpt-4o-mini" },
name: "support",
systemPrompt:
"You are front-line support. For any billing or refund question you MUST transfer to the billing agent.",
plugins: [Handoff.asPlugin({ parentAgentId: "support", targets: [billing] })],
});
const result = await (await support.send("I was double-charged — can I get a refund?")).wait();
console.log("Status:", result.status);
console.log("Reply: ", result.result); // answered by billing after the transfer
await support.dispose();
await billing.dispose();
```
## What it shows
- **`Handoff.asPlugin({ targets: [billing] })`** registers a `transfer_to_billing` tool on `support`.
The model calls it to hand off; `support` does not resume — `billing` finishes the turn.
- **The receiver takes over** — unlike a subagent (delegate-and-return), a handoff transfers the
conversation. `RunResult.origin` and the handoff telemetry (`HandoffResult`: `from`, `to`, `depth`)
record who transferred to whom.
- **`maxHandoffDepth`** (default 5) caps how many transfers a single `send()` may chain, so
A→B→A→B… can't loop unbounded — exceeding it throws `HandoffLoopError`.
Handoffs live in **`@theokit/sdk-handoff`** and drive real models, so this example runs from your own
checkout with a provider key rather than the live sandbox. (A release of `@theokit/sdk-handoff`
rebuilt against `@theokit/sdk@3.x` is pending — until then, install it from source.)
---
# Handoffs
Source: https://docs.usetheo.dev/theokit/handoffs
Route a conversation between specialists.
# Handoffs
Typed inter-agent dispatch with loop protection. Ships as its own package and rides on the plugin seam.
| Capability | What it adds |
| --- | --- |
| `@theokit/sdk-handoff` | `Handoff.create` — typed handoff descriptors |
| Loop protection | Guards against ping-pong handoffs |
| `Handoff.asPlugin` | Wire handoffs as a plugin |
## Reference
See the [Handoffs concept](/theokit/concepts/handoffs) for the deep dive.
---
# Add a file-based hook
Source: https://docs.usetheo.dev/theokit/hooks/add-a-file-based-hook
Gate the agent with a shell command from disk — write a .theokit/hooks.json (identical shape to Claude Code), block a dangerous tool call, no plugin code.
# Add a file-based hook
File-based hooks run a **shell command** on a lifecycle event, configured in `.theokit/hooks.json`. The format is **identical to Claude Code's `settings.json` hooks** — same nested shape, same `matcher` / `type: "command"` / `timeout`. Local runtime only.
## 1. Write `.theokit/hooks.json`
```json title=".theokit/hooks.json"
{
"hooks": {
"PreToolUse": [
{
"matcher": "shell",
"hooks": [
{ "type": "command", "command": "node .theokit/policy.js", "timeout": 30 }
]
}
]
}
}
```
| Field | Meaning |
| --- | --- |
| `hooks.` | An array of matcher-groups for that lifecycle event (see the events below). |
| `matcher` | Regex — for `PreToolUse` / `PostToolUse` it matches the **tool name** (`shell`, `write_file`, …). Omit to match every tool. |
| `hooks[]` | The commands to run when the matcher hits. |
| `type` | Always `"command"` (a shell command). |
| `command` | The shell command (run via `sh -c`). |
| `timeout` | Optional, in **seconds** (default 30). |
## 2. Write the command
The command receives the hook **payload as JSON on stdin** — `{ event, tool?, input?, agentId?, runId? }` (`input` is the tool's arguments). **A non-zero exit on `PreToolUse` / `UserPromptSubmit` BLOCKS** the tool/run; the model sees the block and continues. You can also print `{"decision":"deny","reason":"…"}` on stdout for a clean message.
```js title=".theokit/policy.js"
let data = "";
process.stdin.on("data", (c) => (data += c));
process.stdin.on("end", () => {
const { input } = JSON.parse(data);
const cmd = typeof input?.command === "string" ? input.command : "";
if (/(^|\s)(rm|sudo|dd|mkfs|kill)(\s|$)/.test(cmd)) {
process.stderr.write(`Policy denied: ${cmd}`);
process.exit(1); // ← blocks the shell tool call
}
process.exit(0); // allow
});
```
## 3. Enable it
File-based hooks are discovered only when the agent opts into project settings:
```ts
const agent = await Agent.create({
apiKey, model,
local: { cwd: projectRoot, settingSources: ["project"] }, // discovers .theokit/hooks.json
});
```
Edit `.theokit/hooks.json` while the agent is alive? Call `await agent.reload()` to pick up the change without disposing.
## Events
The SDK fires five lifecycle points; use the Claude Code event names in the config:
| Config event | Fires | Can block? |
| --- | --- | --- |
| `PreToolUse` | before a tool call (filtered by `matcher`) | **yes** (non-zero exit) |
| `PostToolUse` | after a tool call | observe |
| `UserPromptSubmit` | before a `send()` starts | **yes** |
| `Stop` | when the run ends | observe |
> A Claude Code event with no SDK firing point (`SessionStart`, `SubagentStop`, `PreCompact`, …) is skipped with a warning rather than silently accepted — the runtime would never fire it.
## When to use which hook surface
- **Just observing tool calls in code?** → the agent-level [`onTool*` callbacks](./observe-the-tool-lifecycle).
- **Vetoing / rewriting / session logic in code?** → a [plugin hook](./advanced) (`ctx.on("pre_tool_call", …)`).
- **Org policy as a shell command, no code?** → `.theokit/hooks.json` (this page).
## Reference
- [`HookName`](/theokit/reference/HookName) · [`Agent`](/theokit/reference/Agent) · [`PreToolCallContext`](/theokit/reference/PreToolCallContext)
---
# Advanced
Source: https://docs.usetheo.dev/theokit/hooks/advanced
The plugin hook names and their contexts, vetoing a tool call, transforming results, session hooks, and file-based shell hooks.
# Advanced hooks
Verified against `@theokit/sdk` (`internal/plugins/types.ts`, `types/agent.ts`).
## Plugin hooks — `ctx.on(name, handler)`
A `Plugin` registers hooks in its `register(ctx)`:
```ts
import { Plugin } from "@theokit/sdk";
const audit = Plugin.create({
name: "audit",
register(ctx) {
ctx.on("pre_tool_call", (c) => { /* c: PreToolCallContext */ });
ctx.on("on_session_start", () => ctx.injectMessage("Session started.", "system"));
},
});
Agent.create({ …, plugins: [audit] });
```
### The hook names
| Hook | When | Can change the run? |
| --- | --- | --- |
| `pre_tool_call` | before a tool runs | **veto** (return `{ block: true, message }`) |
| `post_tool_call` | after a tool returns | observe |
| `pre_llm_call` / `post_llm_call` | around each model call | observe |
| `transform_tool_result` | after a tool result | **rewrite** the result |
| `transform_llm_output` | after the model's output | **rewrite** the output |
| `on_session_start` / `on_session_end` | session boundaries | `injectMessage` (start) |
| `pre_user_send` / `post_assistant_reply` | around a turn (memory-adapter hooks) | inject `` |
### Veto a tool call — `pre_tool_call`
```ts
ctx.on("pre_tool_call", (c) => {
// c: { name, args, agentId, runId }
if (c.name === "delete_file") return { block: true, message: "deletes are not allowed" };
});
```
Returning a `PreToolCallDecision` (`{ block: true, message }`) stops the tool — the model sees the
`message` as the tool result and continues. Return nothing to allow it.
## Plugin capabilities beyond hooks
The same `ctx` also exposes:
- **`ctx.registerTool(tool)`** — add a tool from the plugin (this is how `Handoff.asPlugin` injects
`transfer_to_*` tools).
- **`ctx.registerCommand(name, handler)`** — register a slash-command-style handler.
- **`ctx.injectMessage(content, role?)`** — inject a user/system message into the next turn (v1
supports it from the `on_session_start` context).
## File-based hooks — `.theokit/hooks.json`
With `local: { settingSources: ["project"] }`, a `.theokit/hooks.json` runs a **shell command** on
lifecycle events. The config is the **same shape as Claude Code's** `settings.json` hooks — nested
`{ hooks: { PreToolUse: [{ matcher, hooks: [{ type: "command", command, timeout }] }] } }`, with the
events `PreToolUse` / `PostToolUse` / `UserPromptSubmit` / `Stop` mapped to the SDK's firing points.
Local runtime only; useful for org-wide policy (run a linter on every write, block a command pattern)
without shipping plugin code. (The old `.theokit/hooks/*.md` markdown form is no longer supported — a stray dir warns to migrate.)
**See the step-by-step: [Add a file-based hook](/theokit/hooks/add-a-file-based-hook)** — the JSON
format, the JSON-on-stdin payload, and blocking a tool via a non-zero exit.
## Which surface
- **Just observing tool calls?** → the agent-level `onTool*` callbacks (simplest).
- **Vetoing / rewriting / session logic in code?** → a plugin with `ctx.on`.
- **Org policy as shell commands, no code?** → `.theokit/hooks.json`.
## Reference
- [`Plugin`](/theokit/reference/Plugin) · [`PluginContext`](/theokit/reference/PluginContext) · [`HookName`](/theokit/reference/HookName) · [`PreToolCallContext`](/theokit/reference/PreToolCallContext) · [`PreToolCallDecision`](/theokit/reference/PreToolCallDecision)
---
# Overview
Source: https://docs.usetheo.dev/theokit/hooks
Observe and gate the agent lifecycle — tool-lifecycle callbacks on the agent, plugin hooks over the run, and file-based shell hooks discovered from disk.
# Hooks
Hooks let you **observe and gate** what an agent does — log tool calls, veto a dangerous one, inject
context, react to session boundaries. There are three surfaces:
- **Tool-lifecycle callbacks** — `onToolStart` / `onToolEnd` / `onToolError` on `AgentOptions`. The
simplest surface: fire around every tool call for telemetry.
```ts
const agent = await Agent.create({
apiKey, model, tools: [myTool],
onToolStart: ({ toolName, args }) => log("→", toolName, args),
onToolEnd: ({ toolName, durationMs }) => log("✓", toolName, durationMs),
onToolError: ({ toolName, error }) => log("✗", toolName, error.message),
});
```
- **Plugin hooks** — a `Plugin` registers handlers with `ctx.on(name, handler)` over the run
lifecycle: `pre_tool_call` (can veto), `post_tool_call`, `pre_llm_call`, `post_llm_call`,
`transform_tool_result`, `transform_llm_output`, `on_session_start` / `on_session_end`, and the
memory-adapter hooks `pre_user_send` / `post_assistant_reply`.
- **File-based hooks** — a `.theokit/hooks.json` (the **same shape as Claude Code's** `settings.json`
hooks) discovered from the working directory (with `settingSources: ["project"]`) runs a **shell
command** on lifecycle events. No plugin code; local runtime only.
## Next
- [Observe the tool lifecycle](/theokit/hooks/observe-the-tool-lifecycle) — a runnable `onTool*` example.
- [Add a file-based hook](/theokit/hooks/add-a-file-based-hook) — write a `.theokit/hooks.json` shell hook that blocks a dangerous tool call.
- [Advanced](/theokit/hooks/advanced) — the plugin hook names + contexts, vetoing, and file-based hooks.
## Reference
- [`Agent`](/theokit/reference/Agent) · [`Plugin`](/theokit/reference/Plugin) · [`PreToolCallContext`](/theokit/reference/PreToolCallContext) · [`HookName`](/theokit/reference/HookName)
---
# Observe the tool lifecycle
Source: https://docs.usetheo.dev/theokit/hooks/observe-the-tool-lifecycle
Attach onToolStart / onToolEnd / onToolError to an agent and watch them fire around a tool call — a deterministic lifecycle over a real run.
# Observe the tool lifecycle
The `onToolStart` / `onToolEnd` / `onToolError` callbacks fire around every tool call — the simplest
hook surface. The **set and order** of hooks is deterministic (the same lifecycle every run), even
though the model's reply varies; the model just has to call the tool.
```ts title="run.ts"
import { Agent, Tool } from "@theokit/sdk";
import { z } from "zod";
const events: string[] = [];
const clock = Tool.create({
name: "get_time",
description: "Return the current time in a city.",
inputSchema: z.object({ city: z.string() }),
handler: ({ city }) => `12:00 in ${city}`,
});
const agent = await Agent.create({
apiKey: process.env.OPENROUTER_API_KEY,
model: { id: "openai/gpt-4o-mini" },
systemPrompt: "You MUST call get_time for any time question. Answer in one line.",
tools: [clock],
onToolStart: ({ toolName }) => events.push(`onToolStart:${toolName}`),
onToolEnd: ({ toolName, durationMs }) => events.push(`onToolEnd:${toolName}(${durationMs >= 0 ? "ok" : "?"})`),
onToolError: ({ toolName }) => events.push(`onToolError:${toolName}`),
});
const result = await (await agent.send("What time is it in Tokyo?")).wait();
console.log("hooks fired:", events.join(" -> "));
console.log("status: ", result.status);
await agent.dispose();
```
## Output
The lifecycle is deterministic in structure — start then end around the one tool call:
```text
hooks fired: onToolStart:get_time -> onToolEnd:get_time(ok)
status: finished
```
## What it shows
- **`onToolStart({ toolName, args, callId, conversationId })`** fires before the handler runs;
**`onToolEnd({ …, result, durationMs })`** after it returns successfully.
- **`onToolError({ …, error, attempt })`** fires when the handler throws **or** the args fail schema
validation — `error` is always an `Error`. (Try a handler that throws to see it.)
- These are pure observers — they can't change the tool result. To **veto** or **rewrite**, use a
plugin `pre_tool_call` hook (see [Advanced](/theokit/hooks/advanced)).
## Example
Full runnable source:
[`examples/hooks-lifecycle`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/hooks-lifecycle).
---
# Plugins and hooks
Source: https://docs.usetheo.dev/theokit/hooks
Extend the agent loop.
# Plugins and hooks
`Plugin.create` adds lifecycle hooks — before a tool call, before a send, after a reply. Handoffs and the semantic cache ride on this seam.
| Capability | What it adds |
| --- | --- |
| `Plugin.create` | The plugin factory |
| Hooks | `preToolCall` / `preUserSend` / `postAssistantReply` |
## Reference
See the [Plugins and hooks concept](/theokit/concepts/hooks) for the deep dive.
---
# Introduction
Source: https://docs.usetheo.dev/theokit
Build AI agents in TypeScript — the primitives, every capability you can attach, and the full-stack framework.
# TheoKit
Build AI agents in TypeScript. TheoKit gives you a few **primitives** — Agent, Workflow,
Squad — and a large set of **capabilities** you attach to them. A primitive plus its
capabilities is a component of your agent system. Plain TypeScript: no DSL, no YAML.
Reach for the **SDK** to embed an agent in any Node process — a script, a CI job, a
backend. Reach for the **full-stack framework** (`create-theokit`) when you're building a
whole app. Same primitives underneath.
## Primitives
The things you create.
| Primitive | What it is | |
| --- | --- | --- |
| **Agent** | a model with instructions and tools that runs a task | [Agents](/theokit/agents) |
| **Workflow** | declarative multi-step orchestration (parallel / branch / foreach / suspend) | [Workflows](/theokit/concepts/workflows) |
| **Squad** | a sequential team of agents over a workflow | [Squad](/theokit/concepts/handoffs) |
## Foundations
Start here — the building blocks every agent uses.
| Foundation | What it adds |
| --- | --- |
| **Providers & Models** | Multi-provider access behind one API + an offline capability catalog. `Provider.create` for your own; built-in Bedrock, Vertex, and local (Ollama). |
| **Prompts & Instructions** | Static or per-send system prompts, hierarchical project instructions, and personality presets. |
| **Tools** | `Tool.create` — typed functions the model can call; argument sanitization, typed `ToolError`, multimodal results, and built-in coding tools. |
| **Streaming** | Iterate `run.stream()` over the `SDKMessage` event union, with pure readers and a typed run-event stream. |
## Capabilities
What you attach to a primitive.
| Group | Capabilities |
| --- | --- |
| **Memory & context** | Memory (vector + FTS recall, dreaming consolidation) · Sessions & scopes · Compaction · Conversation storage |
| **Multi-agent** | Subagents (`agent.fork`) · Handoffs (`@theokit/sdk-handoff`) · Agent-to-agent (mailbox + bus) |
| **Structured & generation** | `generateObject` / `generate` · `streamObject` · Batch · Schema normalizer (Zod / JSON Schema / ArkType / Valibot) |
| **Control & safety** | Guardrail processors · Plugins & hooks · Permissions engine · Secret redaction & path safety |
| **Operations** | Cron schedules · Tasks & job queue · Evals & scorers · Budget & cost · Semantic cache · Telemetry (OpenTelemetry) |
## Connections
| Connection | What it does |
| --- | --- |
| **MCP (client)** | Connect an agent to external MCP servers over stdio or HTTP/SSE. |
| **Sandbox** | Sandboxed code execution + repo provisioning. |
| **Filesystem** | A pluggable, boundary-enforced filesystem provider. |
| **ACP** | Drive an agent from Zed / Cursor / Claude Desktop over JSON-RPC (`@theokit/acp`). |
| **Subscriptions** | Typed server RPC over SSE / WebSocket with resume tokens. |
| **HTTP server adapters** | Express / Fastify / Hono handlers + OAuth orchestration. |
## Extensibility
`Provider.create` (bring your own model) · `Tool.create` · `Plugin.create` · filesystem &
inline **Skills** · `AgentBuilder` / `AgentFactory.create`. Every agentic capability ships
as a low-level factory function first.
## Runtimes
Where an agent runs is chosen by the key you pass to `Agent.create()` — your code is
otherwise identical.
| Runtime | What it does | When |
| --- | --- | --- |
| **Local** | runs in-process against your own provider key | dev scripts, CI, your backend |
| **Cloud** | isolated VM (Theo PaaS — **pre-release**) | many agents in parallel, survive caller disconnect |
The cloud surface — `Agent.archive` / `delete`, artifacts, `autoCreatePR`, hosted
`Cron.list`, `Theokit.repositories` — depends on Theo PaaS and is **pre-release**. The
local runtime is the primary, fully-tested path.
## Start here
## Building a full app?
The TheoKit **framework** layers file-based routing, a streaming chat UI, typed server
routes, persistence, and auth on top of the same primitives.
```bash
npx create-theokit my-app
```
---
# Advanced
Source: https://docs.usetheo.dev/theokit/mcp/advanced
Full stdio and HTTP config, OAuth, per-request timeouts, the env-scrub policy, config-file discovery, and the typed MCP error codes.
# Advanced MCP
Verified against `@theokit/sdk` (`types/mcp.ts`, `types/agent.ts`).
## `McpStdioServerConfig`
```ts
{
type: "stdio", // optional; inferred from `command`
command: "npx", // required
args: ["-y", "@scope/server"],
env: { TOKEN: "…" }, // extra vars, merged AFTER the scrub policy (always win)
cwd: "./workspace", // local agents only; cloud rejects this field
requestTimeoutMs: 30_000, // per-request timeout (default 30s)
envPolicy: "inherit-scrubbed", // "inherit-scrubbed" (default) | "all" | "core"
}
```
The spawned server inherits a **secret-scrubbed** environment by default — host vars matching
`*KEY*` / `*SECRET*` / `*TOKEN*` / `*PASSWORD*` / `*_AUTH*` are dropped so a third-party MCP binary
can't exfiltrate host secrets. `env` above is applied after the policy and always wins; pass
`envPolicy: "all"` to restore full inheritance.
## `McpHttpServerConfig`
```ts
{
type: "http" | "sse",
url: "https://mcp.example.com",
headers: { Authorization: `Bearer ${token}` },
auth: { CLIENT_ID: "…", oauth: { /* McpOAuthConfig */ } },
requestTimeoutMs: 30_000,
}
```
- **`headers`** are passed through — `Authorization` works directly for a static token.
- **`auth`** (`McpAuthConfig` → `McpOAuthConfig`) drives an OAuth flow for servers that require it.
- The timeout is enforced via `AbortSignal.timeout` — a slow fetch rejects with `mcp_timeout`.
## Config-file discovery
With `local: { settingSources: ["project"] }`, the SDK loads a project **`.theokit/mcp.json`** (in the
working directory) and merges it with inline `mcpServers`. This keeps server definitions out of code
and shareable across a team.
## Typed errors
MCP failures surface as `NetworkError` with a machine `code`, so you can route them:
| Code | Meaning |
| --- | --- |
| `mcp_timeout` | No reply within `requestTimeoutMs`. |
| `mcp_disconnected` | The server connection dropped mid-run. |
| `mcp_closed` | The stdio server closed its stream. |
| `mcp_crashed` | The spawned stdio process exited abnormally. |
## Reference
- [`McpServerConfig`](/theokit/reference/McpServerConfig) · [`McpStdioServerConfig`](/theokit/reference/McpStdioServerConfig) · [`McpHttpServerConfig`](/theokit/reference/McpHttpServerConfig) · [`McpAuthConfig`](/theokit/reference/McpAuthConfig) · [`McpOAuthConfig`](/theokit/reference/McpOAuthConfig) · [`NetworkError`](/theokit/reference/NetworkError)
---
# Connect an MCP server
Source: https://docs.usetheo.dev/theokit/mcp/connect-an-mcp-server
Wire a stdio or HTTP MCP server via mcpServers so its tools join the agent — with the exact config shape for each transport.
# Connect an MCP server
`mcpServers` is a map of `name → McpServerConfig`. The SDK connects to each server at run time and
adds its tools to the agent's toolset (namespaced `mcp__`). Two transports:
## Stdio — spawn a local server
```ts title="run.ts"
import { Agent } from "@theokit/sdk";
const agent = await Agent.create({
apiKey: process.env.OPENROUTER_API_KEY,
model: { id: "openai/gpt-4o-mini" },
mcpServers: {
filesystem: {
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/data"],
// env?: extra vars for the spawned process · cwd?: working dir (local only)
requestTimeoutMs: 30_000,
},
},
});
const result = await (await agent.send("List the files under /data.")).wait();
console.log(result.result);
await agent.dispose();
```
The SDK spawns `npx …`, speaks MCP over stdio, and exposes the server's tools to the model. The
spawned process's environment is **secret-scrubbed** by default (see [Advanced](/theokit/mcp/advanced)).
## HTTP / SSE — reach a remote server
```ts
mcpServers: {
github: {
type: "http", // or "sse"
url: "https://mcp.example.com",
headers: { Authorization: `Bearer ${token}` }, // Authorization works here
requestTimeoutMs: 30_000,
},
}
```
## Or declare them in a file
Drop a `.theokit/mcp.json` in your project (with `local: { settingSources: ["project"] }`):
```json title=".theokit/mcp.json"
{
"filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"] }
}
```
Inline `mcpServers` merge with the discovered ones.
MCP tools come from a real running server (spawned or remote), so this connects live rather than in
the docs sandbox. Copy the snippet into your own project with a server of your choice.
## What it shows
- **`mcpServers: { name: config }`** — each entry is a stdio (`command`/`args`) or HTTP (`type`/`url`)
server; its tools join `agent.tools` under the `mcp_` namespace.
- **Timeouts are typed** — a stalled MCP request rejects with `NetworkError` (`code: "mcp_timeout"`),
never hanging the loop.
- **Discovery** — a project `.theokit/mcp.json` is merged with inline config.
---
# Overview
Source: https://docs.usetheo.dev/theokit/mcp
Connect Model Context Protocol servers — stdio or HTTP — and their tools become the agent's tools, namespaced under mcp_.
# MCP (Model Context Protocol)
Point an agent at one or more **MCP servers** and their tools become the agent's tools — the model
calls them like any other. Configure them inline via `mcpServers`, or discover them from a
`.theokit/mcp.json` file.
```ts
const agent = await Agent.create({
apiKey, model,
mcpServers: {
filesystem: { command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "/data"] },
github: { type: "http", url: "https://mcp.example.com", headers: { Authorization: `Bearer ${token}` } },
},
});
```
- **Stdio server** — `{ command, args?, env?, cwd? }`. The SDK spawns the process and speaks MCP over
stdio; the spawned env is **secret-scrubbed** by default.
- **HTTP / SSE server** — `{ type: "http" | "sse", url, headers?, auth? }`. Reaches a remote MCP
endpoint (with optional OAuth).
- **Discovered from disk** — a project `.theokit/mcp.json` is picked up automatically; inline
`mcpServers` merge with it.
MCP tools surface under the `mcp_` namespace and appear in `agent.tools`. A request that gets no reply
within the timeout rejects with a typed `NetworkError` (`code: "mcp_timeout"`) instead of hanging the
loop.
## Next
- [Connect an MCP server](/theokit/mcp/connect-an-mcp-server) — the stdio + HTTP config, step by step.
- [Advanced](/theokit/mcp/advanced) — auth, timeouts, the env-scrub policy, config files, and typed errors.
## Reference
- [`McpServerConfig`](/theokit/reference/McpServerConfig) · [`McpStdioServerConfig`](/theokit/reference/McpStdioServerConfig) · [`McpHttpServerConfig`](/theokit/reference/McpHttpServerConfig) · [`McpAuthConfig`](/theokit/reference/McpAuthConfig)
---
# MCP
Source: https://docs.usetheo.dev/theokit/mcp
Connect an agent to external MCP servers.
# MCP
TheoKit is an MCP **client** — it connects an agent to external MCP servers over stdio or HTTP/SSE. (It does not expose an agent as an MCP server; see ACP for that.)
| Capability | What it adds |
| --- | --- |
| `mcpServers` | stdio + HTTP/SSE server configs on `Agent.create` |
| Auth | `McpAuthConfig` for authenticated servers |
## Reference
See the [MCP concept](/theokit/concepts/mcp) for the deep dive.
---
# Advanced
Source: https://docs.usetheo.dev/theokit/memory/advanced
The full MemorySettings, the agent.memory API, third-party adapters, embedding providers, vector backends, active recall, and dreaming.
# Advanced memory
Everything memory exposes, verified against the SDK.
## `MemorySettings` — `AgentOptions.memory`
```ts
memory: {
enabled: true,
namespace, // logical partition
userId, // per-user isolation
scope: "agent" | "user" | "team", // whose memories this agent recalls
storePath, // where the store lives (defaults under cwd)
autoInject: true, // prepend recalled facts as a block (default true)
index: {
tools: true, // register memory_search + memory_get for the model (default true)
backend: "sqlite-vec", // or "lance" (@lancedb/lancedb, optional peer) for scale
embedding: { // omit → FTS-only (lexical) recall
provider: "openai" | "mistral" | "openrouter" | "voyage" | "deepinfra",
model,
},
},
activeRecall: { // opt-in blocking pass before each send()
enabled: true,
queryMode: "message" | "recent" | "full", // what to search on
timeoutMs,
maxSummaryChars,
},
}
```
Without an `embedding` provider the index runs **FTS-only** (keyword). Add one and recall becomes
**vector-based** (sqlite-vec, or LanceDB via `backend: "lance"`).
## The direct API — `agent.memory`
Populated when a memory adapter is registered (via `plugins`). Merges + dedupes across adapters:
```ts
await agent.memory.write("The deploy command is 'make ship'."); // → MemoryId
const facts = await agent.memory.recall("deploy command", undefined, 5); // semantic recall, top-k
await agent.memory.delete(id); // routes to the owning adapter
agent.memory.adapter(); // the first registered adapter, or null
```
`write` fans out to all adapters; `recall` merges + dedupes; `delete` routes by the id's prefix.
## Adapters
| Adapter | Package |
| --- | --- |
| In-memory markdown (dependency-free) | `createInMemoryMarkdownProvider()` — `@theokit/sdk-memory` |
| Honcho | `@theokit/memory-honcho` |
| Mem0 | `@theokit/memory-mem0` |
| Supermemory | `@theokit/memory-supermemory` |
| Your own | implement the `MemoryProvider` port and register it |
Register an adapter's `.asPlugin()` (or the provider) via `Agent.create({ plugins: [...] })`.
## Embedding providers
`@theokit/sdk-memory` ships embedding adapters for **OpenAI, Mistral, OpenRouter, Voyage, DeepInfra,
Ollama** (and more). Configure via `memory.index.embedding.provider`; the same provider keys drive
recall vectors. A circuit breaker guards a flaky embedding endpoint.
## Dreaming — consolidation sweep
Over time, raw memories accumulate. `Memory.runDreamingSweep(options)` runs an **offline**
consolidation: it summarizes and compacts stored memories into higher-level facts (a "dreaming"
phase), returning a `DreamingSweepResult`. Run it on a schedule (see [Schedules](/theokit/schedules))
rather than in the hot path.
```ts
import { Memory } from "@theokit/sdk";
const result = await Memory.runDreamingSweep({ /* DreamingSweepOptions */ });
```
## Active recall query modes
`activeRecall.queryMode` controls what the pre-send pass searches on:
| Mode | Searches on |
| --- | --- |
| `"message"` | Just the current user message (cheapest). |
| `"recent"` | The recent conversation window. |
| `"full"` | The full assembled context (most thorough). |
## Reference
- [`MemorySettings`](/theokit/reference/MemorySettings) · [`AgentMemory`](/theokit/reference/AgentMemory) · [`MemoryProvider`](/theokit/reference/MemoryProvider) · [`DreamingSweepOptions`](/theokit/reference/DreamingSweepOptions)
---
# Overview
Source: https://docs.usetheo.dev/theokit/memory
Give an agent memory that survives the conversation — semantic recall of facts, auto-injected into context, backed by a store you choose.
# Memory
By default an agent only knows the current conversation. Turn on **memory** and it recalls relevant
facts from past sessions — semantically, not just by keyword — and can search them with tools.
```ts
const agent = await Agent.create({
apiKey, model,
local: { cwd }, // where the memory store lives
memory: { enabled: true }, // that's the whole opt-in
});
```
With `memory.enabled`, the SDK persists session summaries, recalls the relevant ones before each
run, and (by default) injects them into the system prompt as a `` block — so the model just
*knows*.
## Two layers
| Layer | What it is |
| --- | --- |
| **Built-in memory** (`AgentOptions.memory`) | A local store (SQLite + FTS5, optionally a vector index) with `memory_search` / `memory_get` tools, active recall, and auto-injection. Zero external services. |
| **Memory adapters** (`agent.memory`) | Plug a third-party memory backend — [Honcho](https://honcho.dev), [Mem0](https://mem0.ai), Supermemory, or your own — via `plugins`, and use the direct `write` / `recall` / `delete` API. |
## What it does
- **Semantic recall** — find facts by meaning. With an embedding provider configured, recall is
vector-based (sqlite-vec or LanceDB); without one, it runs FTS-only (lexical).
- **Auto-inject** — recalled facts prepend to the system prompt (`autoInject`, default `true`).
- **Memory tools** — `memory_search` and `memory_get` let the model query its own memory mid-run.
- **Active recall** — an opt-in blocking pass that runs *before* each `send()` and prepends an
`` block.
- **Dreaming** — an offline consolidation sweep (`Memory.runDreamingSweep`) that summarizes and
compacts stored memories.
- **Scoping** — `scope: "agent" | "user" | "team"`, `namespace`, and `userId` keep memories isolated.
## Next
- [Remember and recall](/theokit/memory/remember-and-recall) — a runnable two-turn example.
- [Advanced](/theokit/memory/advanced) — the full `MemorySettings`, the `agent.memory` API, adapters,
embedding providers, backends, active recall, and dreaming.
## Reference
- [`Agent`](/theokit/reference/Agent) · [`MemorySettings`](/theokit/reference/MemorySettings) · [`AgentMemory`](/theokit/reference/AgentMemory)
---
# Remember and recall
Source: https://docs.usetheo.dev/theokit/memory/remember-and-recall
Turn on memory, tell the agent a fact, then ask for it on a later turn — the agent recalls it from its store.
# Remember and recall
Set `memory: { enabled: true }` and give the agent a `cwd` for its store. It persists what it's told
and recalls the relevant fact on a later turn — no re-stating required.
```ts title="run.ts"
import { Agent } from "@theokit/sdk";
const agent = await Agent.create({
apiKey: process.env.OPENROUTER_API_KEY,
model: { id: "openai/gpt-4o-mini" },
local: { cwd: "./.memory" }, // where the memory store lives
memory: { enabled: true },
systemPrompt: "You are concise. Recall from memory when asked.",
});
// Turn 1 — tell the agent a fact.
const t1 = await (await agent.send("Remember this fact: my project's deploy command is 'make ship'.")).wait();
console.log("Turn 1:", t1.status);
// Turn 2 — a fresh question; memory recalls the fact.
const t2 = await (await agent.send("What is my project's deploy command? Answer with just the command.")).wait();
console.log("Turn 2:", t2.status);
console.log("Recalled:", t2.result);
await agent.dispose();
```
## Output
Verified against `openai/gpt-4o-mini` via OpenRouter:
```text
Turn 1: finished
Turn 2: finished
Recalled: make ship
```
_Model-generated — yours will read differently. Click **Run** above for a fresh one._
## What it shows
- **`memory: { enabled: true }`** — the whole opt-in. The SDK persists session summaries under `cwd`
and recalls the relevant ones before each run.
- **Auto-inject** — the recalled fact is prepended to the system prompt as a `` block, so
turn 2 answers `make ship` without you repeating it.
- **No external service** — the built-in store is local (SQLite + FTS5). Add an embedding provider for
vector recall, or plug a third-party adapter — see [Advanced](/theokit/memory/advanced).
## Example
Full runnable source:
[`examples/memory-basics`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/memory-basics).
EOF
---
# Advanced
Source: https://docs.usetheo.dev/theokit/observability/advanced
Every RunEvent type, and the full TelemetrySettings — exporters, content capture, service name, and auto-detection of Langfuse / Sentry / PostHog.
# Advanced observability
Verified against `@theokit/sdk` (`types/run-events.ts`, `types/agent.ts`).
## Every `RunEvent`
`onRunEvent` receives a discriminated union — branch on `.type`:
| Event | Fires when |
| --- | --- |
| `tool_progress` | A long-running tool reports progress. |
| `tripwire` | An input/output processor aborted (guardrail). |
| `rate_limit` | The provider returned 429 and the SDK is backing off / rotating keys. |
| `permission_denied` | A tool call was denied by the permission engine. |
| `task_started` / `task_updated` / `task_completed` | A background task's lifecycle. |
| `completion_check` | A per-send `completionCheck` verdict resolved. |
| `compact_boundary` | The conversation was compacted mid-run. |
```ts
await agent.send(msg, {
onRunEvent: (ev) => {
switch (ev.type) {
case "rate_limit": backoffUi(); break;
case "permission_denied": auditLog(ev); break;
// …
}
},
});
```
The sink is out-of-band and best-effort — a throwing handler is swallowed so it can never break the
run.
## Tracing — `TelemetrySettings`
```ts
telemetry: {
enabled: true, // master switch (default false)
includeContent: false, // include prompts/responses/tool args as span events (default false)
exporter: "console", // "console" | "otlp" | a custom exporter (default "console")
serviceName: "theokit-sdk", // service name on emitted spans
autoDetect: true, // feature-detect installed OTel exporters (default true)
disable: [], // opt out per adapter: "langfuse" | "sentry" | "posthog"
}
```
- **Spans** wrap the run, each LLM call, and each tool call, with status + token attributes.
- **`includeContent`** is **off by default** — prompts, responses, and tool args are NOT put on spans
unless you opt in (avoid leaking sensitive content to your tracer).
- **Auto-detection** — with `autoDetect: true`, the SDK feature-detects installed observability libs
(**Langfuse**, **Sentry**, **PostHog**) via `createRequire` and registers their exporters; use
`disable` to opt a specific one out.
- **`exporter: "otlp"`** ships spans to any OpenTelemetry collector; a custom exporter object is passed
through unchanged.
## Which to use
- **Reacting to progress in-app** (a UI, custom logs) → `onRunEvent`.
- **Distributed tracing / an observability platform** → `telemetry` (OTel).
They compose — run events for your own UX, spans for your tracing backend.
## Reference
- [`RunEvent`](/theokit/reference/RunEvent) · [`RunEventSink`](/theokit/reference/RunEventSink) · [`TelemetrySettings`](/theokit/reference/TelemetrySettings) · [`RunRateLimitEvent`](/theokit/reference/RunRateLimitEvent) · [`RunPermissionDeniedEvent`](/theokit/reference/RunPermissionDeniedEvent)
---
# Overview
Source: https://docs.usetheo.dev/theokit/observability
See inside a run — a typed runtime-event sink for progress signals, and OpenTelemetry tracing that auto-detects Langfuse, Sentry, and PostHog.
# Observability
Two ways to see what an agent is doing:
- **Runtime events — `onRunEvent`.** A typed, out-of-band sink that receives `RunEvent`s as a run
progresses: `tool_progress`, `rate_limit`, `permission_denied`, `tripwire`, `task_*`,
`completion_check`, `compact_boundary`. Best for reacting to progress in your own UI or logs.
```ts
await agent.send(msg, { onRunEvent: (ev) => console.log(ev.type) });
```
- **Tracing — `telemetry`.** OpenTelemetry spans over the run. Turn it on and the SDK emits spans and
**auto-detects** installed observability libs (Langfuse, Sentry, PostHog).
```ts
const agent = await Agent.create({
apiKey, model,
telemetry: { enabled: true, exporter: "otlp", serviceName: "my-app" },
});
```
A throwing `onRunEvent` sink never breaks the run (best-effort), and telemetry defaults to **off** —
observability is opt-in and never on the critical path.
## Next
- [Subscribe to run events](/theokit/observability/subscribe-to-run-events) — a runnable `onRunEvent` example.
- [Advanced](/theokit/observability/advanced) — every `RunEvent`, and the full `TelemetrySettings`.
## Reference
- [`RunEvent`](/theokit/reference/RunEvent) · [`RunEventSink`](/theokit/reference/RunEventSink) · [`TelemetrySettings`](/theokit/reference/TelemetrySettings)
---
# Subscribe to run events
Source: https://docs.usetheo.dev/theokit/observability/subscribe-to-run-events
Pass onRunEvent to send() and receive typed RunEvents as the run progresses — deterministic tripwire, no LLM.
# Subscribe to run events
`onRunEvent` is a typed sink that receives `RunEvent`s out-of-band while a run executes. Here an input
processor aborts the message before the LLM, emitting a `tripwire` event — so this is deterministic,
no LLM call.
```ts title="run.ts"
import { Agent } from "@theokit/sdk";
const seen: string[] = [];
const agent = await Agent.create({
apiKey: "theo_test_observability",
model: { id: "openai/gpt-4o-mini" },
inputProcessors: [
{
id: "no-secrets",
processInput: (ctx) => {
if (/password/i.test(ctx.message)) ctx.abort("blocked: message mentions a password");
},
},
],
});
const result = await (
await agent.send("What is my password?", { onRunEvent: (ev) => seen.push(ev.type) })
).wait();
console.log("status: ", result.status);
console.log("run events: ", seen.join(", ") || "(none)");
console.log("tripwire: ", JSON.stringify(result.tripwire));
await agent.dispose?.();
```
## Output
Deterministic — the guardrail aborts and emits a `tripwire` event:
```text
status: cancelled
run events: tripwire
tripwire: {"reason":"blocked: message mentions a password","processorId":"no-secrets"}
```
## What it shows
- **`send(msg, { onRunEvent })`** streams typed `RunEvent`s as the run progresses — here just the
`tripwire` from the aborted input.
- **The sink is best-effort** — a throwing `onRunEvent` never breaks the run.
- **The full set** — `tool_progress`, `rate_limit`, `permission_denied`, `tripwire`, `task_started` /
`task_updated` / `task_completed`, `completion_check`, `compact_boundary` — is in
[Advanced](/theokit/observability/advanced).
## Example
Full runnable source:
[`examples/observability-events`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/observability-events).
---
# Observability
Source: https://docs.usetheo.dev/theokit/observability
See what the agent did.
# Observability
OpenTelemetry spans, a typed run-event stream, and trajectory export — plus retry, concurrency, and persistence helpers.
| Capability | What it adds |
| --- | --- |
| Telemetry | OpenTelemetry spans for send / llm / tool |
| Trajectory export | `toShareGptTrajectory` |
| Helpers | Retry · Concurrency · Persistence |
## Reference
See the [Observability concept](/theokit/concepts/telemetry) for the deep dive.
---
# Agent-aware columns
Source: https://docs.usetheo.dev/theokit/orm/concepts/agent-columns
Auto-fill agentId / runId / conversationId from ambient AgentContext on insert and update.
Tables that carry any of the tracked columns — `agentId`, `runId`, `conversationId` — get
those fields **auto-filled** from an ambient `AgentContext` whenever you `insert` or
`update` through a repository. This keeps agent provenance on your rows without threading
context through every call.
## Setting the context
Wrap your work in `withAgentContext`. Any repository write inside the callback (across
`await` boundaries) sees the context:
```ts
import { withAgentContext } from "@theokit/orm";
await withAgentContext({ agentId: "a_1", runId: "r_42", conversationId: "c_7" }, async () => {
await repo.insert({ id: "e1", type: "message" });
// the row is persisted with agentId="a_1", runId="r_42", conversationId="c_7"
});
```
`AgentContext` is `{ agentId?, runId?, conversationId? }` — set only the fields you have.
## Fill rules
On `insert` / `update`, for each tracked column that exists **on the table**:
- fill it from the matching `AgentContext` field, **only** if the value you passed is
`undefined` (an explicit value you provide always wins);
- columns not present on the table are ignored;
- if the table has tracked columns but **no** context is active, the row is written
unchanged — and in non-production a one-time `console.warn` per table reminds you to wrap
the call in `withAgentContext`.
Reading the current context anywhere: `getAgentContext()` returns the active
`AgentContext` or `undefined`. It's backed by `AsyncLocalStorage`, so it's safe under
concurrent requests.
## Example table
```ts
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
export const events = sqliteTable("events", {
id: text("id").primaryKey(),
type: text("type").notNull(),
agentId: text("agent_id"),
runId: text("run_id"),
conversationId: text("conversation_id"),
});
```
Only the columns your table actually declares are filled — a table with just `agentId`
gets only `agentId` populated.
---
# Errors
Source: https://docs.usetheo.dev/theokit/orm/concepts/errors
The typed error hierarchy @theokit/orm throws.
All ORM errors extend a common `OrmError` base, so you can catch the whole family or narrow
by class. Every message carries concrete context — the table, the method, the id, or the
configuration that was wrong.
| Error | Extends | Thrown when |
| --- | --- | --- |
| `OrmError` | `Error` | Base class — catch this to handle any ORM error. |
| `OrmConfigurationError` | `OrmError` | Misconfiguration: no primary key, `forFeature` before `forRoot`, missing `db`, unsupported `dialect`, a driver without `.returning()` / `.transaction()`, or a `@Transactional` method with no bound `DataSource`. |
| `OrmValidationError` | `OrmError` | Bad input at call time: an invalid `id` (null/undefined/empty/non-scalar), or an `insert`/`update` that returned zero rows. |
| `OrmSchemaExportError` | `OrmError` | A table couldn't be introspected during schema export. |
## Catching
```ts
import { OrmError, OrmConfigurationError, OrmValidationError } from "@theokit/orm";
try {
await repo.update(id, patch);
} catch (err) {
if (err instanceof OrmValidationError) {
// caller sent bad input — surface a 400
} else if (err instanceof OrmConfigurationError) {
// wiring/setup bug — fix the configuration, don't retry
} else if (err instanceof OrmError) {
// any other ORM-level failure
}
throw err;
}
```
Configuration errors are **fail-fast setup problems** — they mean the wiring is wrong, not
that a retry might help. Validation errors are **caller input problems** — surface them to
whoever supplied the data.
---
# OrmModule
Source: https://docs.usetheo.dev/theokit/orm/concepts/module
forRoot and forFeature — registering data sources and repositories in the container.
`OrmModule` is a pair of provider factories that plug the ORM into a `@theokit/di`
container. Both return `Provider[]` — spread them into your `providers` array.
## forRoot — register a data source
`forRoot` builds a `DataSource` from your Drizzle setup and registers it under a token.
Call it once per data source, at the root of your provider list.
```ts
OrmModule.forRoot({
schema: { users, posts },
dialect: "sqlite", // "sqlite" | "pg" | "mysql"
db, // the Drizzle db instance
dataSourceName: "default", // optional; defaults to "default"
});
```
It validates its inputs eagerly: a missing `db`, or a `dialect` outside the three supported
values, throws `OrmConfigurationError`.
## forFeature — register repositories
`forFeature` registers a `Repository` provider per entity, each wired to the data source's
`db`:
```ts
OrmModule.forFeature([users, posts]); // default data source
OrmModule.forFeature([events], "analytics"); // a named data source
```
`forFeature` **must** run after `forRoot` for the same data source — it throws
`OrmConfigurationError` if the data source isn't registered yet. Order matters in the
`providers` array.
## Putting it together
```ts
const container = new Container({
providers: [
...OrmModule.forRoot({ schema: { users }, dialect: "sqlite", db }),
...OrmModule.forFeature([users]),
UserService,
],
});
```
## Multiple data sources
Give each `forRoot` a distinct `dataSourceName`, and pass the same name to `forFeature` and
`@InjectRepository`:
```ts
providers: [
...OrmModule.forRoot({ schema: { users }, dialect: "pg", db: pgDb }),
...OrmModule.forRoot({ schema: { events }, dialect: "sqlite", db: liteDb, dataSourceName: "analytics" }),
...OrmModule.forFeature([users]),
...OrmModule.forFeature([events], "analytics"),
]
```
```ts
@Injectable()
class Analytics {
constructor(@InjectRepository(events, "analytics") private readonly repo: Repository) {}
}
```
## DataSource
The value `forRoot` registers is a `DataSource` — `{ name, dialect, schema, db }`. It is
exposed under `ORM_DATA_SOURCE_TOKEN` (or `${ORM_DATA_SOURCE_TOKEN}:` for named
sources), and is what [`@Transactional`](/theokit/orm/concepts/transactional) reads to open
a transaction.
---
# Repository
Source: https://docs.usetheo.dev/theokit/orm/concepts/repository
CRUD methods, the raw query() escape hatch, createRepository, and @InjectRepository.
`Repository` is a thin, typed wrapper over a Drizzle table. It infers its row types from
the table (`InferSelectModel` / `InferInsertModel`) and detects the primary key at
construction.
## CRUD methods
```ts
class Repository {
findById(id: string | number): Promise | null>;
findMany(where?: SQL): Promise[]>;
insert(values: InferInsertModel): Promise>;
update(id: string | number, patch: Partial>): Promise>;
delete(id: string | number): Promise;
query(): /* raw Drizzle select builder */;
}
```
```ts
await repo.insert({ id: "u1", name: "Theo" });
const one = await repo.findById("u1");
const many = await repo.findMany(eq(users.name, "Theo"));
await repo.update("u1", { name: "Théo" });
await repo.delete("u1");
```
- `insert` and `update` use Drizzle's `.returning()` to hand back the persisted row. On a
driver without `.returning()` (e.g. MySQL) they throw a clear `OrmConfigurationError`
pointing you to `repo.query()`.
- `findById`, `update`, and `delete` validate the `id` up front — `undefined`, `null`, an
empty string, or a non-string/number throws `OrmValidationError`.
## The query() escape hatch
`query()` returns the raw Drizzle select builder for the table — reach for it when you need
joins, aggregates, composite keys, or dialect-specific features the CRUD surface doesn't
cover:
```ts
const rows = await repo.query().where(eq(users.name, "Theo")).limit(10);
```
## Primary-key rules
The repository detects the primary key at construction: a column marked `.primaryKey()`, or
a column literally named `id`. If neither exists it throws `OrmConfigurationError` —
v0.1 supports a **single-column** primary key named `id` or marked `.primaryKey()`. For
composite or custom keys, use `repo.query()` and build the predicate yourself.
## createRepository (non-DI)
The `Repository` constructor is already DI-free — `createRepository` just makes that path
explicit and discoverable. No `@theokit/di`, no decorators, no `reflect-metadata`:
```ts
import { createRepository } from "@theokit/orm";
const repo = createRepository(db, users);
```
Works with any Drizzle `db` (including `better-sqlite3`, whose builders are awaitable). Only
[`@Transactional`](/theokit/orm/concepts/transactional) needs a bound `DataSource`.
## @InjectRepository (DI)
Under DI, `@InjectRepository(entity, dataSourceName?)` injects the repository registered by
`OrmModule.forFeature`. It's sugar over `@Inject(getRepositoryToken(entity, dataSourceName))`:
```ts
@Injectable()
class UserService {
constructor(@InjectRepository(users) private readonly repo: Repository) {}
}
```
`getRepositoryToken(entity, dataSourceName?)` computes the token string
(`REPO:` or `REPO::`) — useful if you register or resolve a
repository manually.
---
# Schema export
Source: https://docs.usetheo.dev/theokit/orm/concepts/schema-export
Turn a Drizzle table into JSON Schema (draft 7) for tool inputs, validation, or docs.
`@theokit/orm` can export a Drizzle table as a **JSON Schema (draft 7)** object — handy for
agent tool input schemas, request validation, or generating documentation. These helpers
ship on a dedicated subpath so the core repository surface stays lean.
```ts
import { exportSchema, exportSchemas } from "@theokit/orm/schema-export";
```
## exportSchema
Converts one table to a `JsonSchema7` object — typed `object` with `properties`,
`required`, and `additionalProperties: false`. Column types, nullability, defaults, enums,
and length constraints are mapped across dialects.
```ts
const schema = exportSchema(users);
// {
// type: "object",
// properties: {
// id: { type: "string" },
// name: { type: "string" },
// },
// required: ["id", "name"],
// additionalProperties: false,
// }
```
## exportSchemas
Converts a whole schema map (`Record`) in one call, returning a map of
table-name → `JsonSchema7`:
```ts
const all = exportSchemas({ users, posts });
// { users: {...}, posts: {...} }
```
## Using it for tool inputs
Because the output is standard JSON Schema, it drops straight into anything that consumes
one — for example the input schema of a `@theokit/sdk` tool:
```ts
import { exportSchema } from "@theokit/orm/schema-export";
import { Tool } from "@theokit/sdk";
const createUser = Tool.create({
name: "create_user",
description: "Create a user",
inputSchema: exportSchema(users),
execute: (input) => repo.insert(input),
});
```
A table that can't be introspected throws `OrmSchemaExportError`. The mapping targets the
common column types across SQLite and Postgres; verify the output for exotic
dialect-specific types.
---
# Transactional
Source: https://docs.usetheo.dev/theokit/orm/concepts/transactional
Wrap a method in a Drizzle transaction; nested repository calls join it automatically.
`@Transactional()` wraps a method so every repository call inside it runs in a single
Drizzle transaction. Nested repository calls join the active transaction transparently
through `AsyncLocalStorage` — you don't thread a `tx` handle around.
```ts
import { Injectable } from "@theokit/di";
import { InjectRepository, Repository, Transactional } from "@theokit/orm";
@Injectable()
class BillingService {
constructor(
@InjectRepository(accounts) private readonly accounts: Repository,
@InjectRepository(ledger) private readonly ledger: Repository,
) {}
@Transactional()
async transfer(from: string, to: string, amount: number) {
await this.accounts.update(from, { balance: /* ... */ });
await this.accounts.update(to, { balance: /* ... */ });
await this.ledger.insert({ id: crypto.randomUUID(), from, to, amount });
// all three commit together, or roll back together on throw
}
}
```
## How it works
The decorator reads a `DataSource` bound to the instance, calls `db.transaction(...)`, and
runs your method body inside `withTxContext(tx, ...)`. Every `Repository` checks that
transaction context first: if one is active, it uses the transaction's `db`; otherwise it
falls back to the default `db`. That's why nested repository calls join automatically.
Isolation level is accepted as an option
(`@Transactional({ isolationLevel: "serializable" })`) and passed through to Drizzle where
the driver supports it.
## Requires a DI-managed instance
`@Transactional` reads a `DataSource` bound to the instance, and under DI the `OrmModule`
wiring arranges that binding automatically — a class resolved through the container is
ready to use `@Transactional`. This is the intended path.
Calling a `@Transactional` method with **no** bound `DataSource` throws
`OrmConfigurationError`. So does a `DataSource` whose `db` doesn't expose `.transaction()`
— make sure you passed a real Drizzle db instance.
## Non-DI transactions
The non-DI `Repository` (via [`createRepository`](/theokit/orm/concepts/repository#createrepository-non-di))
covers plain CRUD, but `@Transactional` is designed for DI-managed classes. Outside DI, use
Drizzle's transaction API directly and run repository work against the transaction handle
with `repo.query()`:
```ts
await db.transaction(async (tx) => {
const txRepo = createRepository(tx, accounts);
await txRepo.update("a", { balance: 90 });
await txRepo.update("b", { balance: 110 });
});
```
---
# Getting started
Source: https://docs.usetheo.dev/theokit/orm/getting-started
Install @theokit/orm, define a Drizzle table, register the module, and inject a repository.
## Install
```bash
pnpm add @theokit/orm @theokit/di reflect-metadata drizzle-orm
pnpm add -D drizzle-kit
```
`@theokit/di`, `drizzle-orm`, and `reflect-metadata` are peer dependencies. Enable the
decorator flags in `tsconfig.json`:
```json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
```
## Define a table
Any Drizzle table works. v0.1 requires a single-column primary key named `id` or marked
`.primaryKey()`:
```ts
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: text("id").primaryKey(),
name: text("name").notNull(),
agentId: text("agent_id"), // optional agent-aware column
});
```
## Option A — with DI
Register a data source with `OrmModule.forRoot`, then repositories with `forFeature`, and
inject them via `@InjectRepository`:
```ts
import "reflect-metadata";
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
import { Container, Injectable } from "@theokit/di";
import { OrmModule, Repository, InjectRepository } from "@theokit/orm";
const db = drizzle(new Database(":memory:"), { schema: { users } });
@Injectable()
class UserService {
constructor(@InjectRepository(users) private readonly repo: Repository) {}
create(id: string, name: string) {
return this.repo.insert({ id, name });
}
}
const container = new Container({
providers: [
...OrmModule.forRoot({ schema: { users }, dialect: "sqlite", db }),
...OrmModule.forFeature([users]),
UserService,
],
});
const svc = container.resolve(UserService);
await svc.create("u1", "Theo");
```
`OrmModule.forFeature([...])` must be called **after** `OrmModule.forRoot({...})` — it
throws `OrmConfigurationError` otherwise. Spread both into your `providers` array in that
order.
## Option B — without DI
Skip decorators and `reflect-metadata` entirely — `createRepository(db, table)` gives you
plain CRUD:
```ts
import { createRepository } from "@theokit/orm";
const repo = createRepository(db, users);
await repo.insert({ id: "u1", name: "Theo" });
const user = await repo.findById("u1");
```
## Next
---
# Overview
Source: https://docs.usetheo.dev/theokit/orm
Repository pattern + @Transactional + agent-aware columns over drizzle-orm — @theokit/orm.
**`@theokit/orm`** brings NestJS-flavoured TypeORM DX to the Theo ecosystem — a
**Repository pattern**, `@InjectRepository`, `@Transactional`, agent-aware columns, and
polyglot JSON-Schema export — built on **`drizzle-orm`** and wired through
[`@theokit/di`](/theokit/di).
Built on Drizzle (Apache-2.0, edge-runtime ready, zero codegen). SQLite and Postgres are
first-class in v0.1; MySQL works through `repo.query()` where `.returning()` isn't
available. You can use the `Repository` with **or without** DI.
## Why `@theokit/orm`
- **Repository pattern.** `findById`, `findMany`, `insert`, `update`, `delete`, and an
escape-hatch `query()` that hands you the raw Drizzle builder — typed against your Drizzle
table.
- **DI-native or standalone.** `OrmModule.forRoot` / `forFeature` register repositories in
the container, or call `createRepository(db, table)` for plain CRUD with no decorators and
no `reflect-metadata`.
- **`@Transactional`.** Wrap a method in a Drizzle transaction; nested repository calls
transparently join it through `AsyncLocalStorage`.
- **Agent-aware columns.** Tables with `agentId` / `runId` / `conversationId` are
auto-filled from the ambient `AgentContext` on insert/update.
- **Schema export.** Turn a Drizzle table into JSON Schema (draft 7) for tool inputs,
validation, or docs.
## Quick code
```ts
import "reflect-metadata";
import { createRepository } from "@theokit/orm";
import { drizzle } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3";
import { sqliteTable, text } from "drizzle-orm/sqlite-core";
const users = sqliteTable("users", { id: text("id").primaryKey(), name: text("name").notNull() });
const db = drizzle(new Database(":memory:"), { schema: { users } });
const repo = createRepository(db, users);
await repo.insert({ id: "u1", name: "Theo" });
const user = await repo.findById("u1"); // { id: "u1", name: "Theo" }
```
## Navigate
---
# For AI agents (llms.txt)
Source: https://docs.usetheo.dev/theokit/orm/llms-txt
Machine-readable ground truth of @theokit/orm for LLMs — package metadata, Repository API, OrmModule, @Transactional, agent-aware columns, and schema export. Download it or curl it directly.
`@theokit/orm` ships an `llms.txt` file following the
[llmstxt.org convention](https://llmstxt.org/) — a single Markdown document that
gives any LLM the **factual ground truth** of this package without crawling the site:
- Exact package name, version, license, peers (`@theokit/di`, `drizzle-orm`, `reflect-metadata`)
- Both import paths: the main barrel and the `@theokit/orm/schema-export` subpath
- The full `Repository` API + the DI wiring (`OrmModule.forRoot` / `forFeature`, `@InjectRepository`)
- `@Transactional` (DI-only) + the non-DI transaction workaround
- Agent-aware columns (`withAgentContext` / `getAgentContext`) and their exact fill rules
- Anti-patterns to avoid (`forFeature` before `forRoot`; composite PKs; `bindDataSourceToInstance` is not public)
## Download
## Curl it directly
```bash
# Save to your project root
curl -o theokit-orm-llms.txt https://docs.usetheo.dev/theokit/orm/llms.txt
# Or pipe straight into a prompt
curl -s https://docs.usetheo.dev/theokit/orm/llms.txt | head -120
```
The source files always win. If a bullet in `llms.txt` disagrees with
`packages/orm/src/index.ts` or the per-symbol reference on this site, the code is
correct and the file is stale — regenerate it from the barrel.
---
# AgentContext
Source: https://docs.usetheo.dev/theokit/orm/reference/AgentContext
_No description available._
# `AgentContext`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/orm` source to change this page.
_No description available._
## Signature
```ts
interface AgentContext { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/orm/src/types.ts:26`](https://github.com/usetheodev/theokit-di/blob/main/packages/orm/src/types.ts#L26)
---
# AnyTable
Source: https://docs.usetheo.dev/theokit/orm/reference/AnyTable
_No description available._
# `AnyTable`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/orm` source to change this page.
_No description available._
## Signature
```ts
type AnyTable
```
## Kind
`type`
## Source
[`packages/orm/src/types.ts:3`](https://github.com/usetheodev/theokit-di/blob/main/packages/orm/src/types.ts#L3)
---
# createRepository
Source: https://docs.usetheo.dev/theokit/orm/reference/createRepository
M7-7 — non-DI factory for Repository. The `Repository` constructor is
# `createRepository`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/orm` source to change this page.
M7-7 — non-DI factory for Repository. The `Repository` constructor is
already DI-free (`new Repository(db, table)`); this factory makes the non-DI
path explicit and discoverable, so consumers do NOT need `@theokit/di`,
decorators, or `reflect-metadata` for plain CRUD — only `@Transactional`
requires a bound DataSource. Works with any drizzle `db` (incl. better-sqlite3,
whose query builders are awaitable).
## Signature
```ts
function createRepository(db: unknown, table: T): Repository
```
## Kind
`function`
## Source
[`packages/orm/src/repository.ts:183`](https://github.com/usetheodev/theokit-di/blob/main/packages/orm/src/repository.ts#L183)
---
# DataSource
Source: https://docs.usetheo.dev/theokit/orm/reference/DataSource
_No description available._
# `DataSource`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/orm` source to change this page.
_No description available._
## Signature
```ts
interface DataSource { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/orm/src/types.ts:16`](https://github.com/usetheodev/theokit-di/blob/main/packages/orm/src/types.ts#L16)
---
# Dialect
Source: https://docs.usetheo.dev/theokit/orm/reference/Dialect
_No description available._
# `Dialect`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/orm` source to change this page.
_No description available._
## Signature
```ts
type Dialect
```
## Kind
`type`
## Source
[`packages/orm/src/types.ts:5`](https://github.com/usetheodev/theokit-di/blob/main/packages/orm/src/types.ts#L5)
---
# getAgentContext
Source: https://docs.usetheo.dev/theokit/orm/reference/getAgentContext
_No description available._
# `getAgentContext`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/orm` source to change this page.
_No description available._
## Signature
```ts
function getAgentContext(): unknown
```
## Kind
`function`
## Source
[`packages/orm/src/als-context.ts:10`](https://github.com/usetheodev/theokit-di/blob/main/packages/orm/src/als-context.ts#L10)
---
# getRepositoryToken
Source: https://docs.usetheo.dev/theokit/orm/reference/getRepositoryToken
_No description available._
# `getRepositoryToken`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/orm` source to change this page.
_No description available._
## Signature
```ts
function getRepositoryToken(entity: unknown, dataSourceName: string): string
```
## Kind
`function`
## Source
[`packages/orm/src/tokens.ts:25`](https://github.com/usetheodev/theokit-di/blob/main/packages/orm/src/tokens.ts#L25)
---
# API reference
Source: https://docs.usetheo.dev/theokit/orm/reference
Every public symbol in @theokit/orm, auto-generated from TypeDoc.
# API reference
This section is **auto-generated** from TypeDoc on every build. 18 symbols exported by @theokit/orm.
## Classes
- [`OrmConfigurationError`](/theokit/orm/reference/OrmConfigurationError) — _no description_
- [`OrmError`](/theokit/orm/reference/OrmError) — _no description_
- [`OrmSchemaExportError`](/theokit/orm/reference/OrmSchemaExportError) — _no description_
- [`OrmValidationError`](/theokit/orm/reference/OrmValidationError) — _no description_
- [`Repository`](/theokit/orm/reference/Repository) — _no description_
## Constantes
- [`ORM_DATA_SOURCE_TOKEN`](/theokit/orm/reference/ORM_DATA_SOURCE_TOKEN) — _no description_
- [`OrmModule`](/theokit/orm/reference/OrmModule) — _no description_
## Functiones
- [`createRepository`](/theokit/orm/reference/createRepository) — M7-7 — non-DI factory for Repository. The `Repository` constructor is
- [`getAgentContext`](/theokit/orm/reference/getAgentContext) — _no description_
- [`getRepositoryToken`](/theokit/orm/reference/getRepositoryToken) — _no description_
- [`InjectRepository`](/theokit/orm/reference/InjectRepository) — _no description_
- [`Transactional`](/theokit/orm/reference/Transactional) — _no description_
- [`withAgentContext`](/theokit/orm/reference/withAgentContext) — _no description_
## Interfacees
- [`AgentContext`](/theokit/orm/reference/AgentContext) — _no description_
- [`DataSource`](/theokit/orm/reference/DataSource) — _no description_
- [`OrmRootOptions`](/theokit/orm/reference/OrmRootOptions) — _no description_
## Typees
- [`AnyTable`](/theokit/orm/reference/AnyTable) — _no description_
- [`Dialect`](/theokit/orm/reference/Dialect) — _no description_
---
# InjectRepository
Source: https://docs.usetheo.dev/theokit/orm/reference/InjectRepository
_No description available._
# `InjectRepository`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/orm` source to change this page.
_No description available._
## Signature
```ts
function InjectRepository(entity: unknown, dataSourceName: string): ParameterDecorator
```
## Kind
`function`
## Source
[`packages/orm/src/inject-repository.ts:4`](https://github.com/usetheodev/theokit-di/blob/main/packages/orm/src/inject-repository.ts#L4)
---
# ORM_DATA_SOURCE_TOKEN
Source: https://docs.usetheo.dev/theokit/orm/reference/ORM_DATA_SOURCE_TOKEN
_No description available._
# `ORM_DATA_SOURCE_TOKEN`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/orm` source to change this page.
_No description available._
## Signature
```ts
const ORM_DATA_SOURCE_TOKEN
```
## Kind
`constant`
## Source
[`packages/orm/src/types.ts:24`](https://github.com/usetheodev/theokit-di/blob/main/packages/orm/src/types.ts#L24)
---
# OrmConfigurationError
Source: https://docs.usetheo.dev/theokit/orm/reference/OrmConfigurationError
_No description available._
# `OrmConfigurationError`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/orm` source to change this page.
_No description available._
## Signature
```ts
class OrmConfigurationError {
captureStackTrace(...): ...
prepareStackTrace(...): ...
}
```
## Kind
`class`
## Source
[`packages/orm/src/errors.ts:5`](https://github.com/usetheodev/theokit-di/blob/main/packages/orm/src/errors.ts#L5)
---
# OrmError
Source: https://docs.usetheo.dev/theokit/orm/reference/OrmError
_No description available._
# `OrmError`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/orm` source to change this page.
_No description available._
## Signature
```ts
class OrmError {
captureStackTrace(...): ...
prepareStackTrace(...): ...
}
```
## Kind
`class`
## Source
[`packages/orm/src/errors.ts:1`](https://github.com/usetheodev/theokit-di/blob/main/packages/orm/src/errors.ts#L1)
---
# OrmModule
Source: https://docs.usetheo.dev/theokit/orm/reference/OrmModule
_No description available._
# `OrmModule`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/orm` source to change this page.
_No description available._
## Signature
```ts
const OrmModule
```
## Kind
`constant`
## Source
[`packages/orm/src/module.ts:33`](https://github.com/usetheodev/theokit-di/blob/main/packages/orm/src/module.ts#L33)
---
# OrmRootOptions
Source: https://docs.usetheo.dev/theokit/orm/reference/OrmRootOptions
_No description available._
# `OrmRootOptions`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/orm` source to change this page.
_No description available._
## Signature
```ts
interface OrmRootOptions { /* ... */ }
```
## Kind
`interface`
## Source
[`packages/orm/src/types.ts:7`](https://github.com/usetheodev/theokit-di/blob/main/packages/orm/src/types.ts#L7)
---
# OrmSchemaExportError
Source: https://docs.usetheo.dev/theokit/orm/reference/OrmSchemaExportError
_No description available._
# `OrmSchemaExportError`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/orm` source to change this page.
_No description available._
## Signature
```ts
class OrmSchemaExportError {
captureStackTrace(...): ...
prepareStackTrace(...): ...
}
```
## Kind
`class`
## Source
[`packages/orm/src/errors.ts:13`](https://github.com/usetheodev/theokit-di/blob/main/packages/orm/src/errors.ts#L13)
---
# OrmValidationError
Source: https://docs.usetheo.dev/theokit/orm/reference/OrmValidationError
_No description available._
# `OrmValidationError`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/orm` source to change this page.
_No description available._
## Signature
```ts
class OrmValidationError {
captureStackTrace(...): ...
prepareStackTrace(...): ...
}
```
## Kind
`class`
## Source
[`packages/orm/src/errors.ts:9`](https://github.com/usetheodev/theokit-di/blob/main/packages/orm/src/errors.ts#L9)
---
# Repository
Source: https://docs.usetheo.dev/theokit/orm/reference/Repository
_No description available._
# `Repository`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/orm` source to change this page.
_No description available._
## Signature
```ts
class Repository {
delete(...): ...
findById(...): ...
findMany(...): ...
insert(...): ...
query(...): ...
update(...): ...
}
```
## Kind
`class`
## Source
[`packages/orm/src/repository.ts:79`](https://github.com/usetheodev/theokit-di/blob/main/packages/orm/src/repository.ts#L79)
---
# Transactional
Source: https://docs.usetheo.dev/theokit/orm/reference/Transactional
_No description available._
# `Transactional`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/orm` source to change this page.
_No description available._
## Signature
```ts
function Transactional(_opts: TransactionalOptions): MethodDecorator
```
## Kind
`function`
## Source
[`packages/orm/src/transactional.ts:28`](https://github.com/usetheodev/theokit-di/blob/main/packages/orm/src/transactional.ts#L28)
---
# withAgentContext
Source: https://docs.usetheo.dev/theokit/orm/reference/withAgentContext
_No description available._
# `withAgentContext`
**Auto-generated** from TypeDoc on each build. Edit the JSDoc in `@theokit/orm` source to change this page.
_No description available._
## Signature
```ts
function withAgentContext(ctx: AgentContext, fn: unknown): Promise
```
## Kind
`function`
## Source
[`packages/orm/src/als-context.ts:6`](https://github.com/usetheodev/theokit-di/blob/main/packages/orm/src/als-context.ts#L6)
---
# Advanced
Source: https://docs.usetheo.dev/theokit/permissions/advanced
Argument matchers, the full mode semantics, the canUseTool gate, and the fail-closed guarantees.
# Advanced permissions
Verified against the SDK.
## `PermissionEngine`
```ts
const engine = new PermissionEngine(rules, defaultAction?); // defaultAction default "ask"
engine.evaluate(toolName, args?, mode?): PermissionAction; // "allow" | "deny" | "ask"
```
`evaluate` walks the rules in order; the **first** whose `tool` (string or `RegExp`) — and, if
present, whose `args` matchers — match the call wins. No match ⇒ `defaultAction` (fail-closed `ask`).
## Argument matchers
A rule can gate on the call's arguments, so the same tool resolves differently by *what* it's asked
to do:
```ts
{ tool: "write_file", args: { path: (p) => !String(p).startsWith("/etc") }, action: "allow" }
```
A missing/undefined argument **fails** its predicate (the rule doesn't match) — never throws.
## Mode semantics (`applyMode`)
The per-run `PermissionMode` is threaded into `evaluate`. An **explicit** rule verdict and the
**default** (unmatched) verdict are treated differently:
| Mode | Explicit rule verdict | Unmatched (default) |
| --- | --- | --- |
| `default` | as-is | `ask` (fail-closed) |
| `plan` | only `allow` passes; else `deny` | `deny` |
| `acceptEdits` | honors explicit `ask` | auto-`allow` |
| `bypass` | `allow` — **except** an explicit `deny`, which always wins | `allow` |
The one invariant across all modes: an **explicit `deny` rule is never overridden**.
## Wiring — `PermissionPlugin.create`
```ts
PermissionPlugin.create(engine, {
name, // default "permission-engine"
mode, // the per-run PermissionMode (default "default")
canUseTool, // the ask gate — (toolName, input, ctx) => decision
});
```
- **`canUseTool(toolName, input, ctx)`** is invoked **only** on an `ask` verdict and returns a typed
allow/deny decision — bridge it to a CLI prompt, a UI approval, or a policy service.
- **Fail-closed** — an `ask` verdict with **no** `canUseTool` gate blocks the call. The deprecated
`onAsk` (veto-shaped) is honored only when `canUseTool` is absent.
## Reference
- [`PermissionEngine`](/theokit/reference/PermissionEngine) · [`PermissionRule`](/theokit/reference/PermissionRule) · [`PermissionMode`](/theokit/reference/PermissionMode) · [`PermissionPlugin`](/theokit/reference/PermissionPlugin) · [`PermissionGate`](/theokit/reference/PermissionGate)
---
# Gate tool calls
Source: https://docs.usetheo.dev/theokit/permissions/gate-tool-calls
Build a PermissionEngine from rules and evaluate tool calls across modes — first-match wins, unmatched fails closed.
# Gate tool calls
A `PermissionEngine` resolves a verdict for a tool name (and optional args) against ordered rules,
under a mode. It's pure and deterministic — evaluate it directly to see the policy.
```ts title="run.ts"
import { PermissionEngine } from "@theokit/sdk";
const engine = new PermissionEngine([
{ tool: "delete_file", action: "deny" },
{ tool: /^read_/, action: "allow" },
]);
console.log("delete_file :", engine.evaluate("delete_file"));
console.log("read_file :", engine.evaluate("read_file"));
console.log("send_email (unmatch):", engine.evaluate("send_email"));
console.log("write_file in plan :", engine.evaluate("write_file", undefined, "plan"));
console.log("delete in bypass :", engine.evaluate("delete_file", undefined, "bypass"));
```
## Output
Deterministic:
```text
delete_file : deny
read_file : allow
send_email (unmatch): ask
write_file in plan : deny
delete in bypass : deny
```
## What it shows
- **First match wins** — `delete_file` hits the explicit `deny`; `read_file` hits the `/^read_/`
allow; `send_email` matches nothing, so it falls back to **`ask`** (fail-closed).
- **`plan` mode is read-only** — `write_file` isn't an `allow` rule, so plan resolves it to `deny`.
- **Explicit `deny` is immune to `bypass`** — even in `bypass`, `delete_file` stays `deny`.
In a live agent, wire the engine with `PermissionPlugin.create(engine, { mode, canUseTool })`; the
`ask` verdict then calls your `canUseTool` gate for a human decision. See
[Advanced](/theokit/permissions/advanced).
## Example
Full runnable source:
[`examples/permissions-basics`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/permissions-basics).
---
# Overview
Source: https://docs.usetheo.dev/theokit/permissions
Gate an agent's tool calls with first-match rules, a per-run permission mode, and a human-in-the-loop ask gate — fail-closed by default.
# Permissions
Before a tool runs, the SDK can ask: *is this allowed?* A **permission engine** answers with
`allow` / `deny` / `ask` from ordered rules; a per-run **mode** layers policy on top; and an `ask`
verdict routes to a **human-in-the-loop** gate. Unmatched calls fail closed.
```ts
import { PermissionEngine, PermissionPlugin } from "@theokit/sdk";
const engine = new PermissionEngine([
{ tool: "delete_file", action: "deny" },
{ tool: /^read_/, action: "allow" },
]);
await Agent.create({
apiKey, model, tools,
plugins: [PermissionPlugin.create(engine, { mode: "default", canUseTool })],
});
```
## Rules — first match wins
A `PermissionRule` matches a tool by exact name or `RegExp`, optionally gated on argument values,
and yields an action:
```ts
interface PermissionRule {
tool: string | RegExp;
args?: Record; // match on call arguments too
action: "allow" | "deny" | "ask";
}
```
The first matching rule wins; **no match ⇒ `ask`** (fail-closed).
## Modes
The per-run `PermissionMode` reshapes verdicts — but an explicit `deny` is immune to every mode:
| Mode | Effect |
| --- | --- |
| `default` | Rules decide; unmatched ⇒ `ask`. |
| `plan` | Read-only — only `allow` rules pass; everything else ⇒ `deny` (mutations blocked). |
| `acceptEdits` | Auto-approves unmatched calls, still honoring explicit `ask` rules. |
| `bypass` | Everything ⇒ `allow` **except** an explicit `deny` rule. Never asks. |
## The ask gate — human-in-the-loop
`PermissionPlugin.create(engine, { mode, canUseTool })` wires the engine into the agent. On an `ask`
verdict it calls your **`canUseTool(toolName, input, ctx)`** gate, which returns a typed
allow/deny decision — bridge it to a prompt, a UI approval, a policy service. **No gate on an `ask`
⇒ fail-closed block.**
## Next
- [Gate tool calls](/theokit/permissions/gate-tool-calls) — a runnable rules/modes example.
- [Advanced](/theokit/permissions/advanced) — arg matchers, the full mode semantics, and the gate.
## Reference
- [`PermissionEngine`](/theokit/reference/PermissionEngine) · [`PermissionMode`](/theokit/reference/PermissionMode) · [`PermissionPlugin`](/theokit/reference/PermissionPlugin)
---
# Advanced
Source: https://docs.usetheo.dev/theokit/personalities/advanced
The resolved preset fields, save/reset semantics, tool and model overrides, project vs user precedence, prompt composition, and clearing.
# Advanced personalities
Verified against `@theokit/sdk` (`internal/personality/*`, `types/agent.ts`).
## The resolved preset — `PersonalityPreset`
```ts
interface PersonalityPreset {
name: string;
description: string | undefined;
tools: ReadonlyArray | undefined; // tool allowlist for this personality
model: string | undefined; // model override
tags: ReadonlyArray | undefined;
systemPrompt: string; // the file body
source: "project" | "user"; // where it was found
sourcePath: string; // the resolved file path
}
```
`usePersonality` returns this, or `null` when the personality was cleared.
## save / reset
```ts
await agent.usePersonality("reviewer", { save: true }); // persist across restarts
await agent.usePersonality("reviewer", { reset: true }); // also clear conversation history
```
- **`{ save: true }`** persists the active personality to `$THEOKIT_HOME/personality.json`, so a new
process picks it up. Without it, the switch lasts only for the current process.
- **`{ reset: true }`** clears the conversation history as it switches (default: history is
**preserved** across the switch).
## Tool and model overrides
A preset can carry `tools` (an allowlist) and `model` in its frontmatter — activating it narrows the
agent to those tools and swaps its model for the personality's duration. Omit them and the agent keeps
its base tools and model.
## Prompt composition
The active personality's body is **appended** to your base system prompt:
```
[base system prompt] + separator + [personality body]
```
An empty base means the personality body stands alone (no leading separator); `separator: ""` merges
them directly. The personality augments — it never silently discards your base prompt.
## Project vs user precedence
Presets are discovered from **project** (`.theokit/personalities/*.md` under the cwd) and **user**
(the same path under `$THEOKIT_HOME`) sources. The `source` field on the resolved preset tells you
which won.
## Clearing
Pass a reserved name — `"none"`, `"default"`, or `"neutral"` — to clear the active personality;
`usePersonality` returns `null`. With `{ save: true }`, the cleared state is persisted too.
All of the above is **local-runtime** only. On a cloud agent, `usePersonality` throws
`UnsupportedRunOperationError`.
## Reference
- [`PersonalityPreset`](/theokit/reference/PersonalityPreset) · [`SDKAgent`](/theokit/reference/SDKAgent) · [`UnsupportedRunOperationError`](/theokit/reference/UnsupportedRunOperationError)
---
# Overview
Source: https://docs.usetheo.dev/theokit/personalities
Swap an agent's voice and tool-set at runtime with a personality preset — persisted across restarts if you like.
# Personalities
A **personality preset** bundles a system prompt, an optional tool whitelist, a model, and tags —
activate one for the next `send` without rebuilding the agent.
```ts
const preset = await agent.usePersonality("researcher", { save: true });
```
- **`agent.usePersonality(name, opts?)`** — activate a preset for the next `send`; returns the
resolved `PersonalityPreset` (or `null` when cleared). Reserved names `"none"` / `"default"` /
`"neutral"` clear it.
- **`{ save: true }`** — persist across process restarts (`$THEOKIT_HOME/personality.json`).
- **`{ reset: true }`** — also clear the conversation history on the switch (default: preserved).
- Presets are discovered from **project** and **user** sources. Local runtime only.
## Where they live
Personality files are Markdown with frontmatter at `.theokit/personalities/*.md` — `name` (lowercase
slug), `description?`, `tools?`, `model?`, `tags?` in the frontmatter, the system prompt in the body.
```md
---
name: reviewer
description: A terse, exacting code reviewer.
tags: [engineering, review]
---
You are a senior code reviewer. Be terse. Flag correctness and security issues first.
```
Discovery requires `local: { settingSources: ["project"] }` (same opt-in as
[Skills](/theokit/skills) and [Context](/theokit/context)). Switching composes the prompt as
**`[base system prompt] + separator + [personality body]`** — the preset augments your base prompt.
Personalities are a **local-runtime** feature — cloud agents throw `UnsupportedRunOperationError`.
## Next
- [Switch personality](/theokit/personalities/switch-personality) — a runnable `usePersonality` example.
- [Advanced](/theokit/personalities/advanced) — the resolved preset fields, save/reset, tool + model overrides, and prompt composition.
## Reference
- [`PersonalityPreset`](/theokit/reference/PersonalityPreset) · [`Agent`](/theokit/reference/Agent)
---
# Switch personality
Source: https://docs.usetheo.dev/theokit/personalities/switch-personality
Switch an agent to a named personality with usePersonality and inspect the resolved preset — deterministic, no LLM.
# Switch personality
`agent.usePersonality(name)` resolves a `.theokit/personalities/.md` preset from disk and
activates it for the next `send`. Resolving and inspecting the preset is a local file read — no LLM.
```ts title="run.ts"
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
import { Agent } from "@theokit/sdk";
const here = dirname(fileURLToPath(import.meta.url)); // holds .theokit/personalities/reviewer.md
const agent = await Agent.create({
apiKey: "theo_test_persona", // fixture key — no LLM
model: { id: "openai/gpt-4o-mini" },
local: { cwd: here, settingSources: ["project"] },
});
const preset = await agent.usePersonality?.("reviewer");
console.log("name: ", preset?.name);
console.log("description:", preset?.description);
console.log("tags: ", preset?.tags?.join(", "));
console.log("source: ", preset?.source);
await agent.dispose?.();
```
The `.theokit/personalities/reviewer.md` beside `run.ts`:
```md
---
name: reviewer
description: A terse, exacting code reviewer.
tags: [engineering, review]
---
You are a senior code reviewer. Be terse. Flag correctness and security issues first.
```
## Output
Deterministic — the preset is resolved from disk:
```text
name: reviewer
description: A terse, exacting code reviewer.
tags: engineering, review
source: project
```
## What it shows
- **`usePersonality(name)`** returns the resolved `PersonalityPreset` — `name`, `description`, `tags`,
the `systemPrompt` body, and provenance (`source: "project" | "user"`, `sourcePath`).
- **`settingSources: ["project"]`** is required for the agent to discover project personality files.
- Passing a reserved name — `"none"`, `"default"`, `"neutral"` — clears the active personality and
returns `null`.
## Example
Full runnable source:
[`examples/personality-switch`](https://github.com/usetheodev/theokit-sdk/tree/main/examples/personality-switch).
---
# @theokit/auth-github
Source: https://docs.usetheo.dev/theokit/plugins/auth-github
GitHub OAuth 2.0 provider for the @theokit/sdk Auth.create orchestrator.
`@theokit/auth-github` is the GitHub OAuth 2.0 provider for the `@theokit/sdk` auth
orchestrator (`Auth.create`). It handles the authorization-code flow, state validation,
token exchange, and userinfo fetch — you supply the client credentials and an
`onSignIn` callback.
OAuth 2.0 only — GitHub does not expose OIDC discovery and does not implement PKCE. The
GitHub endpoints are hardcoded but overridable for GitHub Enterprise Server.
## Install
```bash
pnpm add @theokit/auth-github @theokit/sdk theokit
```
Peer dependencies: `@theokit/sdk >= 2.18.0`, `theokit >= 0.2.4`.
## Usage
```ts title="server/auth/index.ts"
import { Auth } from '@theokit/sdk/server/auth'
import { github } from '@theokit/auth-github'
import { sessionManager } from './session.js'
export const auth = Auth.create({
session: sessionManager,
providers: [
github({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
redirectUri: 'https://myapp.com/api/auth/github/callback',
}),
],
onSignIn: async ({ profile }) => {
return { userId: String(profile.id), email: profile.email, login: profile.login }
},
})
```
Register a GitHub OAuth App at **GitHub Settings → Developer settings → OAuth Apps**,
set the callback URL to `https:///api/auth/github/callback`, and put the
Client ID + Client Secret in `.env`. Default scopes are `read:user user:email`
(override via `opts.scopes`).
### GitHub Enterprise Server
Override the four endpoints:
```ts
github({
clientId: '...',
clientSecret: '...',
redirectUri: '...',
authorizationEndpoint: 'https://github.acme.com/login/oauth/authorize',
tokenEndpoint: 'https://github.acme.com/login/oauth/access_token',
userinfoEndpoint: 'https://github.acme.com/api/v3/user',
userEmailsEndpoint: 'https://github.acme.com/api/v3/user/emails',
})
```
## Profile shape
```ts
interface GitHubProfile {
id: number // numeric, preserved as a number (NOT string)
login: string
name?: string | null
email?: string | null // null when scope omits user:email AND user has no public email
avatar_url?: string
}
```
When `scopes` include `user:email`, the provider fetches `/user` first and falls back to
`/user/emails` to pick the primary verified address. When the scope omits `user:email`,
that second fetch is skipped and `email` may be `null`.
Do not assume `email` is always present. When the scope omits `user:email`, `email`
can be `null` even for an active user — handle that case in your `onSignIn` callback.
## Notes
- `id` is preserved as a **number**, not a string — it is GitHub's stable numeric user
identifier.
- Common error codes: `state_mismatch` (CSRF or stale callback), `token_exchange_failed`
(wrong secret, expired code, or mismatched `redirectUri`), `userinfo_fetch_failed`
(often a 403 rate limit), and `missing_id` / `missing_login` (malformed userinfo).
---
# @theokit/auth-google
Source: https://docs.usetheo.dev/theokit/plugins/auth-google
Google OAuth (OIDC) provider for the @theokit/sdk Auth.create orchestrator.
`@theokit/auth-google` is the Google OAuth (OIDC) provider for the `@theokit/sdk` auth
orchestrator (`Auth.create`). It composes OIDC discovery, PKCE (S256), the
authorization-code flow, and the userinfo fetch on top of the `theokit/server/auth`
primitives — with zero runtime dependencies and a roughly 5 KB ESM bundle.
## Install
```bash
pnpm add @theokit/auth-google @theokit/sdk theokit
```
Peer dependencies: `@theokit/sdk >= 2.18.0`, `theokit >= 0.2.4`.
## Usage
```ts title="server/auth/index.ts"
import { Auth } from '@theokit/sdk/server/auth'
import { google } from '@theokit/auth-google'
import { sessionManager } from './session.js'
export const auth = Auth.create({
session: sessionManager,
providers: [
google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectUri: 'https://myapp.com/api/auth/google/callback',
}),
],
onSignIn: async ({ profile }) => {
// profile is GoogleProfile { sub, email, email_verified, name?, picture?, locale? }
return { userId: profile.sub, email: profile.email }
},
})
```
Wire the start and callback routes with `defineRoute`:
```ts title="server/routes/api/auth/google/start.ts"
import { defineRoute } from 'theokit/server'
import { auth } from '../../../auth/index.js'
export const GET = defineRoute({
handler: async ({ req }) => auth.startSignIn('google', req),
})
```
```ts title="server/routes/api/auth/google/callback.ts"
import { defineRoute } from 'theokit/server'
import { auth } from '../../../auth/index.js'
export const GET = defineRoute({
handler: async ({ req, res }) => {
const { returnTo } = await auth.finishSignIn('google', req, res)
return Response.redirect(returnTo ?? '/', 302)
},
})
```
Create an OAuth 2.0 Client ID (type **Web application**) in the Google Cloud Console,
add your callback URI, and set `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` in `.env`.
The `openid`, `profile`, and `email` scopes are added automatically.
## Profile shape
```ts
interface GoogleProfile {
sub: string // OIDC subject — case-sensitive, never lowercased
email: string
email_verified: boolean
name?: string
picture?: string
locale?: string
}
```
`sub` is the canonical Google user identifier and is preserved verbatim. **Never
lowercase, normalize, or trim it** — different casings refer to different accounts.
The `email_verified` boolean is surfaced as-is; you decide whether to gate user
creation on `email_verified === true`.
## Notes
- **Custom scopes** (Drive, Gmail, Calendar) are not yet first-class via `opts.scopes`.
Wrap the provider and post-process the URL in `createAuthorizationURL` to add them;
a `opts.scopes` field lands in a future minor release.
- **Testing** — set `NODE_ENV=test` and `MOCK_GOOGLE_OIDC_BASE_URL=http://localhost:9999`
to route OIDC discovery to a local sidecar. Production builds ignore this env var.
- Common error codes: `missing_pkce_verifier`, `state_mismatch`, `token_exchange_failed`,
`missing_sub` / `missing_email`, and OIDC discovery `403` / `404` (wrong base URL).
---
# @theokit/auth-magic-link
Source: https://docs.usetheo.dev/theokit/plugins/auth-magic-link
Email magic-link (passwordless) provider for the @theokit/sdk Auth.create orchestrator.
`@theokit/auth-magic-link` is the passwordless email magic-link provider for the
`@theokit/sdk` auth orchestrator (`Auth.create`). It has two swappable pieces: a
pluggable token store (`MagicLinkStore` — in-memory for dev, ORM-backed for production)
and a consumer-supplied `sendEmail` callback, so you can use any email transport.
Zero runtime dependencies.
## Install
```bash
pnpm add @theokit/auth-magic-link @theokit/sdk theokit
```
Peer dependencies: `@theokit/sdk >= 2.18.0`, `theokit >= 0.2.4`.
## Quick start (dev)
```ts title="server/auth/index.ts"
import { Auth } from '@theokit/sdk/server/auth'
import { magicLink, createMemoryStore } from '@theokit/auth-magic-link'
import { sessionManager } from './session.js'
export const auth = Auth.create({
session: sessionManager,
providers: [
magicLink({
store: createMemoryStore(), // dev only — see Notes
callbackBaseUrl: 'https://myapp.com',
sendEmail: async ({ to, magicLinkUrl, expiresAt }) => {
console.log(`Magic link for ${to}: ${magicLinkUrl} (expires ${expiresAt.toISOString()})`)
},
}),
],
onSignIn: async ({ profile }) => ({ userId: profile.email, email: profile.email }),
})
```
## Wiring
Magic-link does **not** use the OAuth `startSignIn` flow — call
`provider.startSignIn(req)` directly on the start route, then finish through the
orchestrator on the callback:
```ts title="server/routes/api/auth/magic-link/start.ts"
import { defineRoute } from 'theokit/server'
import { magicLinkProvider } from '../../../auth/providers.js' // your magicLink() instance
export const POST = defineRoute({
handler: async ({ req }) => {
const redirect = await magicLinkProvider.startSignIn(req)
return Response.redirect(redirect, 303)
},
})
```
```ts title="server/routes/api/auth/magic-link/callback.ts"
import { defineRoute } from 'theokit/server'
import { auth } from '../../../auth/index.js'
export const GET = defineRoute({
handler: async ({ req, res }) => {
const { returnTo } = await auth.finishSignIn('magic-link', req, res)
return Response.redirect(returnTo ?? '/', 302)
},
})
```
The default `resolveEmail` reads `?email=` from the URL or the `email` field of a JSON /
form-encoded body; override via `opts.resolveEmail` for custom shapes.
`createMemoryStore()` is **dev-only** — its state lives in one process and is lost on
restart. In production, pass an ORM-backed store (`createOrmStore(...)`) or a custom
`MagicLinkStore`. The store contract requires **atomic single-use** semantics: under
concurrent reads of the same token, exactly one `consumeToken` call wins and the rest
return `null` (SQL adapters use row-level locking).
## Production stores
Implement the store's atomic `consumeAtomically` (e.g. Postgres `UPDATE … RETURNING …
WHERE consumed_at IS NULL`) and pass it via `createOrmStore(...)`. Pair the provider with
[`@theokit/plugin-email`](/theokit/plugins/email) — its `sendMagicLink(provider, opts)`
helper returns a callback that satisfies the `sendEmail` contract directly. Resend,
SendGrid, and Nodemailer/SMTP transports all fit the same callback shape.
## Profile shape
```ts
interface MagicLinkProfile {
email: string // verified by token possession
verifiedAt: Date // when the callback completed
}
```
## Notes
- **Email validated at the boundary** — a malformed email throws
`MagicLinkConfigError(code: 'invalid_email')` *before* any token is created or stored.
- **Token lifecycle** — 32 random bytes (43 base64url chars), 15-minute default lifetime
(`opts.tokenLifetimeMs`), single-use. A second callback with the same token throws
`invalid_or_expired_token`.
- **Cleanup** — expired tokens reject at consume time and can be batch-deleted via
`store.cleanupExpired()`; run it periodically as a cron.
---
# @theokit/plugin-canvas
Source: https://docs.usetheo.dev/theokit/plugins/canvas
Canvas plugin for TheoKit — agent artifact protocol, side-panel UI, and a publish_artifact agent tool.
`@theokit/plugin-canvas` gives your agent a canvas: an auto-opening side panel that
renders nine artifact kinds — `markdown`, `code`, `svg`, `diff`, `whiteboard-scene`,
`slide-deck`, `mermaid`, `html`, and `image`. The agent publishes artifacts through the
`publish_artifact` tool; the panel reacts in real time over SSE.
Each kind has a byte cap and, where relevant, sanitization (SVG strips `