# 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 `