# Brain (/docs/brain) API guide for the `Brain` class. For the role Brain plays in the system, see [Concepts — Architecture](./concepts#architecture). Creating a Brain [#creating-a-brain] Only two things are required — `prompt` (what to learn about) and `model` (which LLM to use): ```typescript import { Brain } from '@unbody-io/adapt' import { openai } from '@ai-sdk/openai' const brain = await Brain.create({ prompt: 'Track user coding patterns and development philosophy.', model: openai('gpt-4o'), }) ``` With just these two fields, the Brain will auto-decompose the prompt into neurons, store everything in memory, and enable evolution — all with sensible defaults. When you need more control, you can configure persistence, learning thresholds, and evolution behavior: ```typescript import { Brain } from '@unbody-io/adapt' import { SQLiteBrainStore } from '@unbody-io/adapt/sqlite' const brain = await Brain.create({ prompt: 'Track user coding patterns and development philosophy.', model: openai('gpt-4o'), // Persist state to disk (default: in-memory, lost on exit). // Per-neuron data is derived from this store automatically // (e.g. ./brain.db → sibling files for each neuron). store: new SQLiteBrainStore('./brain.db'), learning: { // Synthesize understanding after every 5 observations instead of the default 10 understand: { thresholds: { maxObservations: 5 } }, }, // Evolution is on by default — shown here for clarity evolution: { enabled: true }, }) ``` For Bun, import `SQLiteBrainStore` from `@unbody-io/adapt/sqlite/bun` instead of `@unbody-io/adapt/sqlite`. See [Configuration](./configuration) for the full config reference. Fresh vs Restored [#fresh-vs-restored] Brain has two construction verbs that map to two distinct intents: ```typescript // Fresh — runs LLM decomposition, persists to the store. // Throws if the store already contains a brain. const brain = await Brain.create({ prompt, model, store }) // Restore — loads the previously persisted brain. Throws if the store is empty. // Pass a runtime so persisted models can be rehydrated. const brain = await Brain.restore('./brain.db', { model: openai('gpt-4o') }) const brain = await Brain.restore(myBrainStore, { model: openai('gpt-4o') }) ``` > **Pass a runtime to `Brain.restore`:** persisted models are stored as `{provider, modelId}` refs and rehydrate through the LLM plugin you supply at restore time. Pick one form: > > * `model: openai('gpt-4o')` — covers the 90% case (single AI SDK provider). > * per-slot overrides — `init: { model }`, `query: { model }`, `evolution: { model }`, `learning: { ... }` — when the brain was created with multi-slot model config. > * `llm: customPlugin` — for BYO custom runtimes (Effect, in-house clients, etc.). > > Skipping the runtime is only safe for in-memory restores within the same process that already cached the live model from a prior `Brain.create` call. There is no Gateway-string fallback — calls will throw a clear "register a provider" error if no usable runtime is wired up. `Brain.create` and `Brain.restore` are the only public entry points — the constructor is private. Both methods fully initialize the brain (no separate `init()` call required). BYO LLM runtime [#byo-llm-runtime] The default plugin (`createAiSdkLLM`) wraps the AI SDK and is wired up automatically when you pass `model: openai(...)` (or any other AI SDK model). For runtimes outside the AI SDK — Effect, in-house clients, anything implementing the `AdaptLLMPlugin` contract — pass `llm` instead: ```typescript import { Brain, createAiSdkLLM } from '@unbody-io/adapt' import { openai } from '@ai-sdk/openai' // Custom AI SDK plugin with explicit provider registration (lets the // plugin resolve persisted model refs back to live models on restore). const llm = createAiSdkLLM({ providers: { openai } }) const brain = await Brain.create({ prompt: '...', model: openai('gpt-4o'), llm, }) const restored = await Brain.restore('./brain.db', { llm }) ``` Plugin error contract [#plugin-error-contract] If you're implementing your own `AdaptLLMPlugin`, there's one rule that's easy to miss and breaks repair behavior when violated: **structured-output failures must be returned, not thrown.** When the model produces output that doesn't fit the requested schema (malformed JSON, missing fields, raw text where an object was asked for), your plugin's `call` should return: ```typescript { text: rawTextFromModel, json: undefined, finishReason: 'error', // ...usage, toolCalls, etc. } ``` Adapt's core then runs its repair pipeline (markdown-fence stripping, syntactic recovery via `jsonrepair`, schema validation) on the `text` and produces a usable structured value. If you throw instead, repair never gets a chance and the call fails outright. Throws are reserved for **system errors** — transport failures, auth errors, rate limits, provider 5xx, abort signals. Those propagate out of `generate()` / `streamText()` and the caller decides what to do. The same rule applies to `streamCall`: structured-output recovery happens after the stream finalizes via the optional `output: Promise` on `AdaptStreamResult` (populated by core, not the plugin). Throw only when the stream itself can't start. The default `createAiSdkLLM` follows this contract: it catches the AI SDK's `NoObjectGeneratedError` internally and surfaces `error.text` as the result — no AI-SDK-specific error types leak into Adapt's core. For stubborn structured-output failures, Adapt also supports an opt-in `repairWithFeedback` hook on `generate()` / `streamText()` calls and as a `Brain.create()` / restore runtime default. Core runs the normal repair pipeline first; the hook only runs if the repaired JSON still fails schema validation. In streaming, the extra feedback repair happens after the stream finalizes, through `AdaptStreamResult.output`. Injecting Data [#injecting-data] ```typescript // Array or single item — anything serializable await brain.inject([ { type: 'note', text: 'Users prefer dark mode' }, { type: 'commit', message: 'refactor: move to composition API' }, ]) // With custom ID await brain.inject(data, { id: 'session-42' }) ``` Items are batched by `ingest.batchSize` (default: 20). Routing is parallel — every neuron sees every item. Batches dismissed by *all* neurons get tracked as coverage gaps for [evolution](./evolution). What observers see [#what-observers-see] The observer receives your data as `JSON.stringify(data, null, 2)`. It sees the raw structure — keys, values, nesting. Structure your data so the observer can reason about it: ```typescript // Good — structured, self-describing, rich context await brain.inject([ { type: 'bookmark', url: 'https://example.com/local-first', title: 'Local-First Software', highlights: ['CRDTs enable...', 'Offline-first is...'], tags: ['architecture', 'sync'], savedAt: '2025-03-01T10:30:00Z', }, ]) // Bad — opaque, no context for the observer to reason about await brain.inject(['https://example.com/local-first']) ``` The observer does **not** see the neuron's accumulated knowledge when filtering — it only uses the neuron's identity (derived from `instructions`) to decide relevance. This is intentional: it keeps observation fast and stateless, but it also means the observer can't filter based on "I already know this." That trade-off is handled downstream during synthesis, where the neuron integrates new observations with existing knowledge. **Timestamps matter.** If your use case involves temporal patterns, include timestamps in the data. The observer and synthesizer will see them and can reason about time if the neuron's instructions ask for it. Querying [#querying] ```typescript const result = await brain.ask('What patterns do you see?') result.insight // Synthesized answer from all neurons result.sources // [{ neuronId, relevance, confidence, insight }] result.gaps // Knowledge gaps across all neurons ``` Two query modes: * **`direct`** (default) — All neurons are queried in parallel (one LLM call each), then a single synthesis call combines their answers. Fast and predictable. * **`deep`** — An LLM agent drives the query interactively. It decides which neurons to consult, what to ask each one, and whether to ask follow-up questions based on what it learns. It can also consult [internal neurons](#consulting-internal-neurons) (gap tracking, cross-domain patterns) to build a more complete answer. Slower, but better for complex questions that benefit from multi-step reasoning. ```typescript // Default — fast, parallel const result = await brain.ask('What patterns do you see?') // Agentic — multi-step, selective const deep = await brain.ask('What patterns do you see?', { mode: 'deep' }) // Override model per-call const result = await brain.ask('...', { model: openai('gpt-4o') }) ``` Streaming [#streaming] All query and evaluation methods have streaming variants that return an Adapt-shaped `AdaptStreamResult` — the same surface area regardless of which LLM plugin is active. ```typescript // Stream a brain query const stream = await brain.askStream('What patterns do you see?') for await (const chunk of stream.textStream) { process.stdout.write(chunk) // incremental text } // Or iterate all events (tool calls visible in deep mode) const stream = await brain.askStream('What patterns?', { mode: 'deep' }) for await (const part of stream.fullStream) { if (part.type === 'text-delta') process.stdout.write(part.text) if (part.type === 'tool-call') console.log(`Tool: ${part.toolCall.toolName}`) } // Resolved promises available after stream completes const text = await stream.text const usage = await stream.usage const toolCalls = await stream.toolCalls ``` Consulting Internal Neurons [#consulting-internal-neurons] The four internal neurons (introduced in [Concepts — Evolution](./concepts#evolution)) are queryable via `brain.consult()`. The table below is the practical "when to consult which" reference. | Internal Neuron | Type | What it tracks | When to consult it | | -------------------------- | ---- | ----------------------------------------------- | ------------------------------------------- | | Global Understanding | text | Cross-domain patterns from all neuron knowledge | "What themes connect my different domains?" | | Global Query Understanding | list | Query topics, frequency, clusters | "What are users asking about most?" | | Injection Gaps | text | Data no neuron could process | "What data am I not capturing?" | | Query Gaps | text | Questions no neuron could answer well | "Where are my blind spots?" | Query them via `consult()`: ```typescript const meta = await brain.consult('What cross-domain patterns have emerged?') // Target a specific internal neuron const gaps = await brain.consult('What knowledge gaps exist?', { neuron: '__internal_injection_gaps', }) ``` All internal neurons are enabled by default. Toggle them: ```typescript const brain = await Brain.create({ // ... internalNeurons: { globalUnderstanding: true, // enabled (default) globalQueryUnderstanding: false, // disabled injectionGaps: { governance: { maxTokens: 4000 } }, // enabled with overrides queryGaps: true, }, }) ``` Inspecting the Brain [#inspecting-the-brain] `inspect()` is an agentic read-only method that answers questions about the brain's structure and knowledge. An LLM agent browses neuron metadata, reads understanding summaries, and consults internal neurons to build its answer. ```typescript // What is the brain set up to track? (works even before any data is injected) const result = await brain.inspect('What are you learning and tracking?') console.log(result.insight) // Deeper questions about accumulated knowledge const health = await brain.inspect('Which neurons have the most gaps?') ``` Unlike `ask()` (which queries neuron knowledge) or `consult()` (which queries internal self-knowledge), `inspect()` can reason across both — and works on a fresh brain by falling back to neuron configs when no understanding exists yet. Managing Neurons [#managing-neurons] There are two ways to manage neurons: **basic management** (you provide explicit configs) and **evolution management** (the LLM designs neurons from natural language guidance). Basic management is always available. Evolution management requires `evolution.enabled` (on by default). **Basic management** — you specify exactly what to create or change: ```typescript // Add with explicit config const neuron = await brain.addNeuron({ id: 'ui-patterns', type: 'text', name: 'UI Patterns', description: 'Tracks UI/UX design patterns', instructions: 'Track user interface patterns, component choices, and design decisions.', }) // Adjust with natural language — incremental, preserves knowledge await brain.adjustNeuron('ui-patterns', 'Focus more on accessibility patterns') // Remove await brain.removeNeuron('ui-patterns') // Inspect brain.getNeurons() // all external neurons brain.getNeuron('ui-patterns') // specific neuron ``` **Evolution management** — the LLM designs neurons from natural language guidance. Use this when you know *what* you want but want the system to figure out the specifics (name, instructions, type, schema): ```typescript // LLM designs the neuron from guidance const neuron = await brain.createNeuron('Track emerging frontend frameworks') // Merge overlapping neurons const merged = await brain.mergeNeurons( ['react-neuron', 'vue-neuron'], 'Combine into unified frontend framework tracker' ) // Split overloaded neuron const parts = await brain.splitNeuron( 'broad-neuron', 'Separate into technical patterns vs team dynamics' ) // LLM-driven update await brain.updateNeuron('neuron-x', 'Narrow scope to React hooks only') // Delete via evolution await brain.deleteNeuron('neuron-y') ``` Pausing Neurons [#pausing-neurons] Pause a neuron to stop including it in `inject()` fan-out without losing its accumulated knowledge. Paused neurons stay queryable via `ask()` and `consult()` — pause is about ingestion, not visibility. ```typescript await brain.pauseNeuron('ui-patterns') brain.getNeuronStatus('ui-patterns') // 'inactive' await brain.resumeNeuron('ui-patterns') brain.getNeuronStatus('ui-patterns') // 'active' ``` Status survives restarts via the brain store. Every transition emits a `brain:neuron:status:changed` event with `previousStatus` and `newStatus`. Update vs Adjust [#update-vs-adjust] These are different operations: **`brain.adjustNeuron(id, directive)`** — Natural language steering. The LLM sees the neuron's current state and evolves it incrementally. Preserves all existing observations and understanding. Think "steering." ```typescript await brain.adjustNeuron('topics', 'Be stricter about what counts as a distinct topic') await brain.adjustNeuron('patterns', 'Also track testing patterns going forward') ``` **`brain.update(config)`** — Config replacement. Changes to mechanical fields (like models, thresholds, governance) take effect immediately across all neurons. Changes to semantic fields (like `prompt`) trigger an evolution evaluation, because changing the Brain's purpose may require restructuring its neurons. ```typescript // Mechanical: cascades immediately to all neurons await brain.update({ learning: { understand: { thresholds: { maxObservations: 20 } } }, }) // Semantic: triggers evolution evaluation await brain.update({ prompt: 'Track design systems instead of coding patterns.' }) ``` **Standalone neuron equivalents:** ```typescript // adjust() — incremental, LLM sees current state await neuron.adjust('Also track performance metrics') // update() — replace config, regenerate from scratch await neuron.update({ instructions: 'Track only React performance patterns.', understand: { thresholds: { maxObservations: 5 } }, }) ``` # Changelog (/docs/changelog) The canonical release history for Adapt lives in the root `CHANGELOG.md`. * [Repository changelog](https://github.com/unbody-io/adapt/blob/main/CHANGELOG.md) * [GitHub Releases](https://github.com/unbody-io/adapt/releases) For upcoming work, see the `Unreleased` section in the root changelog. # Concepts (/docs/concepts) This page is the canonical mental model for Adapt. The guide pages assume you've read it — they jump straight into API usage rather than re-explaining the concepts here. Architecture [#architecture] Adapt has two building blocks: **Brain** (an orchestrator) and **Neurons** (specialists). A Brain contains neurons, routes data to them, and synthesizes answers across them. Each neuron independently learns from the data it sees and builds its own understanding. ```text ┌──────────────────────────────────────────────────────────────────────┐ │ Brain │ │ Routes data · Synthesizes answers · Evolves structure │ │ │ │ ┌────────────────┐ ┌──────────────────────┐ ┌──────────────────┐ │ │ │ Evaluator │ │ Evolution │ │ Internal │ │ │ │ Watches for │ │ Orchestrator │ │ Neurons │ │ │ │ structural │ │ Acts on evaluator │ │ Track meta- │ │ │ │ problems and │ │ decisions: create, │ │ knowledge: │ │ │ │ suggests │ │ merge, split, │ │ gaps, query │ │ │ │ changes │ │ update, delete │ │ patterns, │ │ │ │ │ │ neurons │ │ cross-domain │ │ │ │ │ │ │ │ understanding │ │ │ └────────────────┘ └──────────────────────┘ └──────────────────┘ │ └───────────────────────────────┬────────────────────────────────────────┘ │ ┌───────────────┼───────────────┐ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ │Neuron A │ │Neuron B │ │Neuron N │ │ (Text) │ │ (List) │ │ (Text) │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ Observe → Buffer → Understand │ └───────────────┬───────────────┘ │ ┌──────────┼──────────┐ ▼ ▼ ┌───────────┐ ┌───────────┐ │ LLM │ │ Storage │ │ (ai-sdk) │ │ (Memory/ │ └───────────┘ │ SQLite) │ └───────────┘ ``` Learning Pipeline [#learning-pipeline] When data flows into a neuron (via `learn()` on a standalone neuron, or `inject()` on a Brain), it goes through two phases: 1. **Observe** — the neuron decides what's relevant. An LLM reads each item against the neuron's purpose and filters out what doesn't belong. Relevant items become observations, each scored by importance (0–1). Irrelevant items are dismissed but tracked, so the system can detect coverage gaps over time. Observations are buffered until there are enough to synthesize. 2. **Understand** — once enough observations have accumulated (controlled by configurable thresholds — observation count or token count), the neuron synthesizes them into knowledge. How this works depends on the neuron type. Each synthesis produces a significance rating (`routine`, `notable`, `critical`). **Querying** is separate from learning: when you ask a question, the LLM reads the neuron's accumulated understanding and answers with relevance and confidence scores. It doesn't re-process raw data. Brain vs Standalone [#brain-vs-standalone] | | Use this when | | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | **Brain** (`Brain.create`) | You want auto-decomposition into multiple specialists, parallel routing, multi-neuron synthesis, and evolution. | | **Standalone neuron** (`TextNeuron.create`, `ListNeuron.create`) | You only need one domain and want direct control. No orchestration, no evolution. | Standalone neurons are first-class — they expose the same `learn` / `query` / `update` / `adjust` surface a Brain calls into. Most examples on this site use a Brain because that's the more common starting point. Neuron Types [#neuron-types] | | TextNeuron | ListNeuron | | ------------------------ | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------- | | **Understanding format** | Narrative prose (string) | Structured collection (items with schema) | | **How synthesis works** | LLM integrates observations into one evolving text | LLM agent uses add/update/remove tools to manage items | | **Schema** | N/A | Auto-generated from instructions (or provided manually) | | **Confidence scoring** | LLM-judged per query ("how well can I answer this?") | Evidence-based — items referenced more often score higher | | **Pattern recognition** | Detects confirmation, contradiction, recurrence, intensification, avoidance, and other dynamics | N/A — item management only | | **Growth control** | Strategy (`continuous`, `cumulative`, `decay`) + token limit | Deduplication + max items + pruning policy | **Rule of thumb:** narrative answer → TextNeuron. Table or list of items → ListNeuron. When Brain auto-decomposes a prompt, the LLM chooses per domain and defaults to TextNeuron when in doubt. Evolution [#evolution] The neuron structure isn't static. As data arrives and queries flow in, neurons can become overloaded, overlap, or leave gaps. Evolution is the loop that lets a Brain reshape itself: **signal → evaluate → act**. * **Signals** flag that something might need to change. Three sources: automatic (neuron health — high dismissal, low confidence, stagnation), coverage gaps (no neuron could process / answer), and developer-injected via `brain.signal({ source, description })`. * **Evaluator** is an LLM agent that triggers when signals accumulate (default threshold: 5). It inspects neurons, queries their knowledge, and consults internal neurons before deciding what to do. * **Actions** the evaluator can take: **create**, **merge**, **split**, **update**, or **delete** neurons. Brain also runs four **internal neurons** that track meta-knowledge about the system itself — cross-domain patterns (Global Understanding), query topics (Global Query Understanding), data nothing could process (Injection Gaps), questions nothing could answer (Query Gaps). The evaluator consults them; you can too via `brain.consult()`. Steering [#steering] Three distinct verbs cover the steering surface. They are not interchangeable: | Verb | What it feeds | When to use | | ------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------ | | `inject(data)` | Data — runs through observe → understand | Routine — you have new information | | `signal({ source, description })` | Metadata — feeds the evolution evaluator | "We need a neuron for X" / "These two overlap" / "Restructure now" | | `adjustNeuron(id, directive)` / `neuron.adjust()` | Natural-language steering for one neuron | "Stop tracking desktop" / "Be stricter about distinct topics" | | `update(config)` | Mechanical config replacement (models, thresholds, prompt) | Threshold tuning, model swap, semantic prompt change | Data goes through `inject`. Structural change goes through `signal`. Per-neuron behavior tweaks go through `adjust`. Mechanical config changes go through `update`. # Configuration (/docs/configuration) Reference for `BrainConfig` and the model cascade. For per-operation LLM call counts, see [Cost & Performance](./cost). BrainConfig [#brainconfig] ```typescript interface BrainConfig { prompt: string // What to track and learn model: LanguageModel // Default model for all operations blueprintModel?: LanguageModel // Schema/config generation (falls back to model) llm?: AdaptLLMPlugin // BYO LLM runtime (default: AI SDK plugin) autoSetup?: boolean // LLM decomposition on init (default: true) neurons?: GeneratedNeuronConfig[] // Explicit neuron definitions store?: BrainStore // Brain persistence (default: MemoryBrainStore) onEvent?: UnifiedHandler // Pre-init event subscription (catches init events) init?: { model?: LanguageModel } // Decomposition model query?: { model?: LanguageModel } // ask() synthesis model ingest?: { batchSize?: number } // Items per batch (default: 20) learning?: { // Per-neuron stores are derived from `store` automatically via // BrainStore.getNeuronStore(neuronId). No factory required. observer?: { model?: LanguageModel // Observe phase model blueprintModel?: LanguageModel // Observer prompt generation model } understand?: { model?: LanguageModel // Understand phase model blueprintModel?: LanguageModel // Understand prompt generation model thresholds?: { maxObservations?: number // default: 10 maxTokens?: number // default: 8000 minImportance?: number // default: 0.5 } } query?: { model?: LanguageModel } // Per-neuron query model governance?: { strategy?: 'continuous' | 'cumulative' | 'decay' // default: 'cumulative' maxTokens?: number // default: 16000 } } evolution?: { enabled?: boolean // default: true model?: LanguageModel // Evolution evaluation model evaluatorSignalThreshold?: number // Signals before auto-eval (default: 5) autoEvaluate?: boolean // Auto-trigger on threshold (default: true) coverageGap?: { relevanceThreshold?: number // Below this = "not relevant" (default: 0.3) gapCountThreshold?: number // Gaps before signaling (default: 5) windowSize?: number // Rolling window (default: 20) } } internalNeurons?: { globalUnderstanding?: boolean | Partial globalQueryUnderstanding?: boolean | Partial injectionGaps?: boolean | Partial queryGaps?: boolean | Partial } dismissedBatchBuffer?: { maxSize?: number // default: 100 } } ``` Model Cascade [#model-cascade] Different operations have different cost/quality trade-offs. Observation runs on every inject and scales linearly with neurons — a cheap, fast model works well here. Synthesis and querying run less often but need higher quality output. The model cascade lets you assign different models to different operations, with each level falling back to its parent if not explicitly set: ```text brain.model (default for everything) ├── brain.blueprintModel (schema/prompt generation, falls back to model) ├── brain.init.model (decomposition, falls back to blueprintModel) ├── brain.query.model (ask synthesis, falls back to model) ├── brain.evolution.model (evolution evaluation, falls back to model) └── learning.* (applied to all neurons): ├── learning.observer.model → observe phase ├── learning.observer.blueprintModel → observer prompt generation ├── learning.understand.model → understand phase ├── learning.understand.blueprintModel → understand prompt generation └── learning.query.model → per-neuron query ``` **Cost-optimized setup** — fast model for high-volume observation, smart model for synthesis: ```typescript import { openai } from '@ai-sdk/openai' const fast = openai('gpt-4o-mini') const smart = openai('gpt-4o') const brain = await Brain.create({ prompt: '...', model: fast, // Default: cheap model blueprintModel: smart, // Schema generation: smart model init: { model: smart }, // Decomposition: smart model query: { model: smart }, // ask() synthesis: smart model learning: { observer: { model: fast }, // Observation: cheap model (high volume) understand: { model: smart }, // Synthesis: smart model (critical) query: { model: smart }, // Per-neuron query: smart model }, }) ``` For a per-operation breakdown of how many LLM calls each method makes — and where to spend a smarter model vs a cheaper one — see [Cost & Performance](./cost). Using Different Providers [#using-different-providers] ```typescript import { openai } from '@ai-sdk/openai' import { anthropic } from '@ai-sdk/anthropic' import { google } from '@ai-sdk/google' import { createOpenRouter } from '@openrouter/ai-sdk-provider' // Direct providers await Brain.create({ model: openai('gpt-4o'), ... }) await Brain.create({ model: anthropic('claude-sonnet-4-20250514'), ... }) await Brain.create({ model: google('gemini-2.0-flash'), ... }) // OpenRouter (multi-provider gateway) const openrouter = createOpenRouter({ apiKey: process.env.OPENROUTER_API_KEY }) await Brain.create({ model: openrouter('google/gemini-2.0-flash-001'), ... }) ``` Any `LanguageModel` from any `@ai-sdk/*` provider works. See [Vercel AI SDK providers](https://sdk.vercel.ai/providers) for the full list. Custom LLM Runtimes [#custom-llm-runtimes] If you need a runtime outside the AI SDK — Effect, an in-house client, or anything implementing the `AdaptLLMPlugin` contract — pass `llm` in `BrainConfig` (or to `Brain.restore` / `TextNeuron.create` / `ListNeuron.create`): ```typescript import { Brain, createAiSdkLLM } from '@unbody-io/adapt' import { openai } from '@ai-sdk/openai' // Pre-register providers so the plugin can rehydrate persisted model refs. const llm = createAiSdkLLM({ providers: { openai } }) const brain = await Brain.create({ prompt: '...', model: openai('gpt-4o'), llm, }) ``` The default plugin (`createAiSdkLLM`) is wired up automatically when you pass any AI SDK model in `model` — you only need to set `llm` explicitly if you want a non-default plugin or to opt into AI Gateway via `createAiSdkLLM({ gateway: true })`. See [Brain — BYO LLM runtime](./brain#byo-llm-runtime). Model Requirements [#model-requirements] Adapt requires models that support **structured output** and **tool calling**. Not all operations need both — here's the breakdown: | Operation | Structured Output | Tool Calling | | ------------------------------------- | ----------------- | --------------------------------------------------------- | | Observe phase | Yes | No | | TextNeuron understand | Yes | No | | ListNeuron understand | No | **Yes** (CRUD tools: add/update/remove/list/search items) | | Direct query (`ask`) | Yes | No | | Agentic query (`ask` deep mode) | No | **Yes** (cognitive tools + done tool) | | Brain synthesis (direct) | Yes | No | | Brain synthesis (deep) | No | **Yes** (specialist query tools) | | Evaluator | No | **Yes** (inspect, query, review tools) | | Brain inspect | No | **Yes** (introspection tools) | | Skill/schema generation (neuron init) | Yes | No | | Evolution handlers | Yes | No | | Governance compression | No | No (plain text) | If your model doesn't support tool calling, **TextNeuron in direct mode** still works end-to-end. ListNeuron, deep mode, evolution, and inspect require tool-calling models. > **Practical tip:** Use a tool-calling model (GPT-4o, Claude Sonnet, Gemini Flash) as the default `model`. If you want to use a cheaper/local model for observation, set it on `learning.observer.model` only — observation doesn't need tool calling. Understand Thresholds [#understand-thresholds] After observations are buffered, synthesis doesn't happen immediately — it triggers when enough observations have accumulated. Three settings control this: | Threshold | Default | Effect | | ----------------- | ------- | ------------------------------------------------------ | | `maxObservations` | 10 | Trigger synthesis after N buffered observations | | `maxTokens` | 8000 | Trigger synthesis when buffered tokens exceed this | | `minImportance` | 0.5 | Observations below this importance (0–1) are discarded | Synthesis triggers when **either** `maxObservations` or `maxTokens` is exceeded. **Tuning guidance:** * **Small `maxObservations` (3–5):** Frequent synthesis, fresher understanding, more LLM calls, higher cost. * **Large `maxObservations` (20–50):** Less frequent synthesis, better batching, understanding stays stale longer. * **Low `minImportance` (0.1–0.3):** Buffer almost everything — noisy but comprehensive. * **High `minImportance` (0.7–0.9):** Only buffer highly significant data — clean but may miss subtle patterns. Governance [#governance] **TextNeuron governance** controls how understanding grows over time: * **`continuous`** — Understanding grows indefinitely. No compression. Use for low-volume domains where you want full detail. * **`cumulative`** (default) — Understanding grows until `maxTokens`, then LLM compresses it to a \~500-token seed summary. The seed becomes the foundation for the next cycle. * **`decay`** — Understanding is organized into temporal sections (Current State / Recent Developments / Historical Context). When `maxTokens` is approached, older content is progressively compressed while recent stays detailed. **ListNeuron governance** is mechanical post-processing after each synthesis: * **`deduplication: 'strict'`** (default) — Items with identical data are merged. Merges `touchCount` from both, keeps max confidence, combines signals, preserves earliest `firstSeen`. * **`maxItems: 200`** (default) — Hard cap on collection size. * **`pruning: 'oldest'`** (default) — When over limit, remove oldest items first. Also: `'least-confident'` or `'none'`. How Thresholds and Governance Interact [#how-thresholds-and-governance-interact] Thresholds control *when* synthesis happens. Governance controls *what happens to understanding* after synthesis. Together they shape how a neuron learns. **Thresholds are the intake valve.** `minImportance` filters what gets buffered. `maxObservations` and `maxTokens` control how much accumulates before the LLM synthesizes. Tight thresholds (high importance, low observation count) mean frequent, focused synthesis. Loose thresholds mean bigger, noisier batches synthesized less often. **Governance is the memory policy.** It determines whether understanding grows forever (`continuous`), consolidates periodically (`cumulative`), or prioritizes recency (`decay`). This matters because understanding is what the neuron reads when answering queries — if it's too large, the LLM loses focus; if it's too compressed, nuance is lost. **Common combinations:** | Use case | Thresholds | Governance | Why | | ----------------------------------------- | ------------------------------------------- | ------------------------------ | ----------------------------------------------------------- | | High-volume stream (logs, events) | `minImportance: 0.3`, `maxObservations: 20` | `decay` | Accept most data, keep recent detail, compress history | | Low-volume, high-value (design decisions) | `minImportance: 0.5`, `maxObservations: 5` | `continuous` | Be selective, synthesize often, keep everything | | Long-running tracker (feature requests) | `minImportance: 0.3`, `maxObservations: 10` | `cumulative` + `maxItems: 200` | Periodic consolidation, bounded collection | | Session-scoped (single conversation) | `minImportance: 0.1`, `maxObservations: 5` | `continuous` | Capture everything, no need to compress — session ends soon | # Cost & Performance (/docs/cost) Adapt's cost is dominated by LLM calls. This page gives you the exact call count per operation so you can estimate spend and latency, and points to the levers that move the most. Quick Reference [#quick-reference] Typical cost for common workflows: | Scenario | Neurons | Calls per `inject` | Calls per `ask` | | -------------------------------------------- | ------- | ------------------ | --------------- | | Simple (3 neurons, no understand trigger) | 3 | 3 | 5 | | Medium (5 neurons, understand triggers on 1) | 5 | 6 | 7 | | Large (10 neurons, understand triggers on 3) | 10 | 13 | 12 | | Deep mode ask (5 neurons) | 5 | — | 2–12 | > **Cost tip:** The observe phase runs on every inject and scales linearly with neurons. This is where a fast/cheap model pays off the most. Understand and query run less frequently but need higher quality — use a smarter model there. See [Configuration — Model Cascade](./configuration#model-cascade) for the cost-optimized cascade setup. Detailed Call Counts [#detailed-call-counts] N = number of neurons, B = number of batches. Core Operations [#core-operations] | Operation | LLM Calls | Model Slot | Notes | | ------------------------------------- | --------- | --------------------- | ------------------------------------------------------------------------------------------------------------ | | **`inject(data)`** | | | | | → Observe | `N × B` | `learning.observer` | 1 call per neuron per batch. Skipped if `skipObservation: true` | | → Understand | `0 – N` | `learning.understand` | Only triggers when buffer exceeds `maxObservations` or `maxTokens`. Always skipped if `skipUnderstand: true` | | **`ask(question)`** | | | | | → Neuron selection | `1` | `query` | Skipped if only 1 neuron has knowledge | | → Neuron queries | `N` | `learning.query` | Parallel. N = relevant neurons, not all | | → Synthesis | `1` | `query` | Combines neuron results into final answer | | **`ask(question, { mode: 'deep' })`** | `2–12` | `query` | Agentic — LLM decides which neurons to query and when to stop | | **`query(question)`** | `1` | `learning.query` | Standalone neuron query (single call) | Lifecycle Operations [#lifecycle-operations] | Operation | LLM Calls | Model Slot | Notes | | ------------------------- | --------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`Brain.create()`** | | | | | → Decomposition | `1` | `init` | Only with `autoSetup: true`. Determines neuron structure | | → Prompt parsing | `1` | `blueprintModel` | Extracts purpose and synthesis directives | | → Neuron init | `0 – N` | `blueprintModel` | Up to 1 call per neuron. TextNeuron: cognitive-skill customization — always, unless `skipUnderstand` (then `0`). ListNeuron: schema generation — `0` when a custom `observationSchema` is supplied. Observe and understand prompts are assembled deterministically — no LLM call. | | **`adjust(directive)`** | `1–3` | `blueprintModel` | 1 classify + (if config changes) 1 instruction-rewrite + 1 prompt/schema regen. Observe and understand prompts are re-assembled deterministically — TextNeuron spends the extra call on skill regen, ListNeuron on schema regen | | → + Understanding rewrite | `+1–15` | `learning.understand` | Only if directive changes what the neuron knows, not just how it behaves | | **`update(config)`** | `0–1` | `blueprintModel` | 0 if mechanical (model/threshold changes). 1 if instructions changed (schema/skill regen — the observe/understand prompts themselves regenerate without an LLM call) | Evolution Operations [#evolution-operations] | Operation | LLM Calls | Model Slot | Notes | | --------------------- | ----------- | ---------------- | --------------------------------------------------------- | | **`signal()`** | `0` | — | Buffers only. No immediate LLM call | | **Evaluator trigger** | `1–12` | `evolution` | Agentic — inspects neurons, reviews gaps, makes decisions | | → Create N neurons | `1 + N` | `blueprintModel` | 1 generation + 1 per neuron init | | → Merge neurons | `1` | `blueprintModel` | Single generation call | | → Split into N | `1 + (N-1)` | `blueprintModel` | 1 generation + 1 init per new neuron | | → Update neuron | `1–15` | `blueprintModel` | 1 guidance + optional adjust cascade | | → Delete neuron | `0` | — | Mechanical removal | Where to spend the smarter model [#where-to-spend-the-smarter-model] Routing the right model to the right slot is the cheapest performance win. Some heuristics: * **`learning.observer`** — runs on every batch, every neuron. Use the cheapest model that can follow the observation schema. A relevance-classification call doesn't need a frontier model. * **`learning.understand`** — runs less often but is what determines knowledge quality. Use a smart model. * **`learning.query`** — runs once per ask per neuron. Smart model recommended; the answer the user sees is shaped here. * **`query` (brain ask synthesis)** — final integration step. Smart model. * **`init` / `blueprintModel`** — one-time and rare (decomposition, per-neuron skill/schema generation). Smart model. Cost is amortized. * **`evolution`** — runs when signals accumulate. Smart model with tool-calling support is required. See [Configuration — Model Cascade](./configuration#model-cascade) for the full cascade and the `Cost-optimized setup` example. Latency [#latency] Per-call latency is dominated by the model and provider, not Adapt. A few observations: * Observe phase runs in parallel across neurons within a batch. * Synthesize runs once per neuron when its buffer crosses a threshold — not every batch. * Internal neurons run a second pass after user-facing neurons. They use queries that resolve asynchronously and often complete on the next inject. * Deep-mode ask is variable: 2–12 calls depending on the agent's path. For a step-by-step lifecycle with timing estimates, see [Events — Brain lifecycle timing](./events#timing-expectations). # Events & Lifecycle (/docs/events) Brain emits events at every pipeline stage. Use them for real-time UIs, debugging, side effects, or forwarding to external systems. Neuron events are forwarded through Brain — subscribe on Brain. Subscribing [#subscribing] ```typescript // Specific event (typed) brain.on('neuron:synthesized', (payload) => { console.log(`${payload.neuronId}: ${payload.significance} — ${payload.evolution}`) }) // All events brain.on((event) => { console.log(`[${event.type}] ${event.id} at ${event.timestamp}`) }) ``` Forwarding to a UI [#forwarding-to-a-ui] ```typescript // Forward all brain events to an Electron renderer brain.on((event) => { window.webContents.send('brain:event', { type: event.type, ...event.payload, }) }) ``` See [Recipes — SSE Event Broadcasting](./recipes#sse-event-broadcasting) for a server-sent events example. Catching init events [#catching-init-events] `brain.on(...)` can only see events fired *after* it runs. But `brain:init:*` and `neuron:init:*` events fire *during* `Brain.create()` / `Brain.restore()` — before you hold a `brain` reference to subscribe on. To observe the init sequence, pass an `onEvent` handler in the config (or restore runtime options). It is attached via `.on(...)` before initialization starts: ```typescript const brain = await Brain.create({ prompt: '...', model: openai('gpt-4o'), onEvent: (event) => console.log(`[${event.type}]`), // sees brain:init:started onward }) ``` `onEvent` is also accepted by `Brain.restore()` runtime options, and by `TextNeuron`/`ListNeuron` `create` configs and `restore` options for standalone-neuron `neuron:init:*` events. Lifecycle [#lifecycle] A Brain's lifecycle has two phases: ```text BIRTH LEARN (repeats per inject) ───── ───── Config generation Observe (parallel) Neuron setup (sequential) Synthesize Internal neuron setup Internal cascade Ready Evolve (optional) ``` Birth happens once. Learning repeats every time you call `inject()`. > Internal event names use the `brain:init:*` / `neuron:init:*` prefixes. The public callsite is `Brain.create(config)` for fresh-init and `Brain.restore(pathOrStore, runtime?)` for restore — there is no separate `brain.init()` step. Birth [#birth] `Brain.create()` runs three stages (skipped on `Brain.restore()`, which loads from the store with no LLM calls): **1. Config generation** — the Brain decomposes your prompt into neuron configurations. Skipped if you provide explicit `neurons`. ```text brain:init:started brain:init:config:generating brain:init:config:generated → N neuron configs ``` **2. Neuron setup** — each neuron is initialized **sequentially**. The observe and understand system prompts are assembled deterministically from the verbatim instructions (no LLM call). Each neuron makes one init LLM call — a TextNeuron for cognitive-skill customization, a ListNeuron for schema generation (skipped when a custom schema is supplied). ```text neuron:init:started → neuron 1 neuron:init:completed brain:neuron:added → neuron 1 visible neuron:init:started → neuron 2 neuron:init:completed brain:neuron:added → neuron 2 visible ``` `brain:neuron:added` fires when a neuron becomes visible to the outside world. **3. Internal neuron setup** — Brain initializes its four [internal neurons](./brain#consulting-internal-neurons). Sequentially, but no `brain:neuron:added` (they're internal). ```text neuron:init:started / completed ×4 brain:init:completed → Brain is ready ``` Learning [#learning] Each `brain.inject()` call processes data through observe → synthesize. Injections are split into batches. ```text brain:inject:started └─ Batch 1 ───────────────────────────────────── neuron:observe:started (×N) ← parallel neuron:observe:thinking ← per neuron neuron:observed ← observation recorded neuron:synthesize:started ← begins when observe completes neuron:synthesized ← understanding updated neuron:health:updated ← always follows synthesize brain:inject:batch:completed └─ Batch 2 (internal cascade) ────────────────── neuron:observe:started (×M) ← internal neurons neuron:synthesize:started neuron:synthesized neuron:health:updated neuron:query:started ← internal global query brain:inject:batch:completed brain:inject:completed ``` Key invariants: * **Observe is parallel.** All neurons in a batch begin observing at the same time. * **Synthesize follows observe.** A neuron synthesizes as soon as its buffer crosses a threshold — it doesn't wait for siblings. * **`neuron:health:updated` always follows `neuron:synthesized`.** Reliable pair. * **Internal neurons run in a second batch.** They observe the user-facing neurons' updated state, then synthesize their own meta-knowledge. This is why you see two batches per injection. Async cascade [#async-cascade] Internal neurons issue queries that complete asynchronously — often during the *next* injection rather than the current one: ```text Injection 1: neuron:synthesized neuron:query:started ← internal query begins brain:inject:completed ← injection finishes, query still running Injection 2: brain:inject:started neuron:observe:started (×N) neuron:query:completed ← previous query finishes mid-injection ... ``` This overlap is normal. Internal queries don't block injections. Evolution signals [#evolution-signals] When neurons detect structural problems (high dismissal, coverage gaps, low relevance), signals fire: ```text neuron:synthesized brain:signal:received ← evolution signal neuron:health:updated ``` Signals accumulate. When enough build up (default threshold: 5), the evaluator triggers automatically and may restructure the brain. See [Evolution](./evolution). Timing expectations [#timing-expectations] Rough ranges based on typical LLM latency. Actual times vary by model, provider, and data volume. | Phase | Typical duration | | ------------------------ | ---------------- | | Config generation | 2–4s | | Per neuron init | 3–10s | | Full birth (2–3 neurons) | 30–60s | | Observe (per batch) | 2–4s | | Synthesize (per neuron) | 2–10s | | Internal query | 3–6s | | Single injection | 8–30s | The largest variance comes from synthesis — a neuron integrating many observations into a complex understanding takes longer than one processing routine data. Injections that trigger evolution take longer still. Event Reference [#event-reference] Events are grouped by phase. The most-used ones are `neuron:synthesized` (a neuron updated its knowledge), `brain:ask:completed` (a query finished), and `evaluator:evaluation:completed` (the evolution system made decisions). Brain Events [#brain-events] | Phase | Events | Key payload fields | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | **Init** | `brain:init:started`, `brain:init:config:generating`, `brain:init:config:generated`, `brain:init:completed`, `brain:init:failed` | `configs[]`, `neuronIds[]`, `usage` | | **Inject** | `brain:inject:started`, `brain:inject:batch:started`, `brain:inject:batch:completed`, `brain:inject:completed`, `brain:inject:failed` | `injectId`, `itemCount`, `batchCount`, `results[]` | | **Ask** | `brain:ask:started`, `brain:ask:synthesis:started`, `brain:ask:completed`, `brain:ask:failed` | `queryId`, `query`, `insight`, `sources[]`, `gaps[]`, `usage` | | **Neuron Mgmt** | `brain:neuron:added`, `brain:neuron:removed`, `brain:neuron:status:changed` | `neuronId`, `name`, `instructions`, `previousStatus`, `newStatus` | | **Signals** | `brain:signal:received` | `source`, `description`, `timestamp` | | **Evaluator** | `evaluator:evaluation:started`, `evaluator:evaluation:completed`, `evaluator:evaluation:failed` | `signalCount`, `decisions[]`, `reasoning` | | **Evolution** | `evolution:action:started`, `evolution:action:executed`, `evolution:action:failed` | `action`, `targets[]`, `reasoning`, `result` | | **Config** | `brain:config:updated` | `updates`, `changedFields[]` | Neuron Events [#neuron-events] | Phase | Events | Key payload fields | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | | **Init** | `neuron:init:started`, `neuron:init:completed`, `neuron:init:failed` | `neuronId`, `systemPrompt`, `usage` | | **Observe** | `neuron:observe:started`, `neuron:observe:thinking`, `neuron:observed`, `neuron:observe:dismissed`, `neuron:observe:error` | `neuronId`, `output[]`, `importance`, `bufferCount`, `gaps[]` | | **Synthesize** | `neuron:synthesize:started`, `neuron:synthesize:thinking`, `neuron:synthesized`, `neuron:synthesize:dismissed`, `neuron:synthesize:error` | `neuronId`, `newUnderstanding`, `significance`, `evolution` | | **Learn** | `neuron:learn:failed` | `neuronId`, `error` | | **Query** | `neuron:query:started`, `neuron:query:completed`, `neuron:query:failed`, `neuron:query:thinking` | `neuronId`, `insight`, `relevance`, `confidence`, `gaps[]` | | **Health** | `neuron:health:updated` | `neuronId`, `activation`, `status`, `previousStatus` | | **Signals** | `neuron:signal` | `neuronId`, `description`, `metrics` | | **Config** | `neuron:config:updated`, `neuron:prompts:regenerated`, `neuron:understanding:set`, `neuron:understanding:adjusted` | `neuronId`, `changedFields[]`, `directive`, `significance` | Thinking events [#thinking-events] `neuron:observe:thinking`, `neuron:synthesize:thinking`, and `neuron:query:thinking` fire when the LLM produces intermediate reasoning. Payload: `thoughts: string[]` and `usage: TokenUsage`. Useful for debugging (seeing *why* a neuron made a decision) and real-time UI feedback (streaming the thought process to users). # Evolution (/docs/evolution) Configuration and manual-control reference for evolution. For the conceptual overview, see [Concepts — Evolution](./concepts#evolution). Signal triggers [#signal-triggers] Default firing conditions for the three signal sources: * **Automatic** — neuron emits when dismissal rate >80%, query relevance avg \<0.3, confidence avg \<0.3, or observation stagnation. * **Coverage gaps** — query where all neurons score below `coverageGap.relevanceThreshold`, or injection batches dismissed by all neurons. * **Developer** — `brain.signal({ source, description })` from application logic. Pass `bypass: true` to force immediate evaluation regardless of threshold. Evolution Config [#evolution-config] ```typescript evolution: { enabled: true, // Master switch (default: true) model: openai('gpt-4o'), // Evaluation model (falls back to brain.model) evaluatorSignalThreshold: 5, // Signals before auto-evaluation (default: 5) autoEvaluate: true, // Auto-trigger on threshold (default: true) coverageGap: { relevanceThreshold: 0.3, // Below this = "not relevant" (default: 0.3) gapCountThreshold: 5, // Gaps before signaling (default: 5) windowSize: 20, // Rolling window (default: 20) }, } ``` Manual Control [#manual-control] ```typescript // Inject custom signal brain.signal({ source: 'analytics', description: 'Users asking about deployment patterns but no neuron covers it', }) // Force immediate evaluation (bypass threshold) brain.signal({ source: 'admin', description: 'Restructure neurons for new product direction', bypass: true, }) // Manually trigger evaluation const { decisions } = await brain.evaluateEvolution() // Dry run — see what the evaluator would decide without executing const { decisions: preview } = await brain.evaluateEvolution({ dryRun: true }) // Stream evolution evaluation const { stream, decisions } = await brain.evaluateEvolutionStream({ dryRun: true }) for await (const part of stream.fullStream) { // See evaluator tool calls: inspectSpecialist, querySpecialist, finalizeDecisions, etc. } const { decisions: d, results } = await decisions // resolves after stream completes ``` The evaluator is an LLM agent with tools to inspect neurons, query their knowledge, consult [internal neurons](./brain#consulting-internal-neurons) (gap tracking, cross-domain patterns), review dismissed data, and review past decisions — all before making structural choices. Evolution vs Basic Neuron Management [#evolution-vs-basic-neuron-management] | Operation | Requires Evolution | What happens | | ----------------------------- | ------------------ | -------------------------------------------- | | `addNeuron(config)` | No | Add neuron with explicit config | | `removeNeuron(id)` | No | Remove neuron and dispose store | | `adjustNeuron(id, directive)` | No | LLM-driven incremental steering | | `createNeuron(guidance)` | Yes | LLM designs the neuron from natural language | | `deleteNeuron(id)` | Yes | Delete via evolution system | | `mergeNeurons(ids, guidance)` | Yes | Merge 2+ neurons into one | | `splitNeuron(id, guidance)` | Yes | Split neuron into multiple | | `updateNeuron(id, guidance)` | Yes | LLM-driven config update | When to Enable/Disable [#when-to-enabledisable] **Enable** (default) when the Brain is long-lived and the domain may shift over time. Evolution lets the system adapt — creating neurons for uncovered domains, merging overlaps, splitting overloaded neurons — without you having to monitor and restructure manually. **Disable** when the neuron structure is known upfront and shouldn't change. For example: session-scoped brains that process a single conversation, or brains with carefully hand-crafted neurons where automatic restructuring would be counterproductive. ```typescript // Long-term brain: let it evolve const clientBrain = await Brain.create({ prompt: '...', model, evolution: { enabled: true }, }) // Session brain: fixed structure const sessionBrain = await Brain.create({ prompt: '...', model, evolution: { enabled: false }, autoSetup: false, neurons: [...], }) ``` # Getting Started (/docs) Adapt is a TypeScript library for building AI systems that learn and evolve from data. This guide walks you through installation and building your first Brain in under 5 minutes. Install [#install] ```bash npm install @unbody-io/adapt ``` Adapt ships dual ESM and CommonJS builds — `import { Brain } from '@unbody-io/adapt'` works in modern bundlers and `require('@unbody-io/adapt')` works in Electron's main process or any other CJS consumer. Adapt connects to LLMs through [Vercel AI SDK](https://sdk.vercel.ai) providers. Install the one that matches your LLM service — for example, `@ai-sdk/openai` for OpenAI or `@ai-sdk/anthropic` for Claude: ```bash npm install @ai-sdk/openai ``` See [Configuration — Using Different Providers](./configuration#using-different-providers) for the full list of supported providers. Quick Start [#quick-start] Brain [#brain] A Brain takes a prompt describing what to learn, automatically creates specialized neurons to cover different aspects of that domain, and coordinates them. You feed it data, it routes to all neurons, and when you ask a question it synthesizes answers from all of them. ```typescript import { Brain } from '@unbody-io/adapt' import { openai } from '@ai-sdk/openai' const brain = await Brain.create({ prompt: 'Track my coding patterns and development philosophy.', model: openai('gpt-4o'), }) await brain.inject([ { type: 'commit', message: 'refactor: extract validation into pure functions' }, { type: 'review', comment: 'Too heavy — factory functions work fine for our scale.' }, ]) const result = await brain.ask('What is my coding philosophy?') console.log(result.insight) ``` Standalone Neuron [#standalone-neuron] If you don't need multi-domain orchestration, neurons work independently without a Brain. You get direct control over a single learning domain: ```typescript import { TextNeuron, MemoryNeuronStore } from '@unbody-io/adapt' import { openai } from '@ai-sdk/openai' const neuron = await TextNeuron.create({ model: openai('gpt-4o'), instructions: 'Track product design principles and philosophy.', store: new MemoryNeuronStore(), }) await neuron.learn([ 'User said: simplicity over features', 'Team decided: no dark patterns, ever', ]) const result = await neuron.query('What are our design principles?') console.log(result.insight) ``` Explicit Neurons [#explicit-neurons] By default, Brain uses the LLM to decompose your prompt into neurons automatically. If you already know what neurons you want, you can define them explicitly: ```typescript const brain = await Brain.create({ prompt: 'Track cooking knowledge.', model: openai('gpt-4o'), autoSetup: false, neurons: [ { id: 'techniques', name: 'Cooking Techniques', type: 'text', description: 'Culinary methods and approaches', instructions: 'Track cooking techniques, methods, and principles.', }, { id: 'recipes', name: 'Recipe Collection', type: 'list', description: 'Tracked recipes and ingredients', instructions: 'Track recipes with cuisine type, ingredients, and difficulty level.', }, ], }) ``` Both `autoSetup` and `neurons` can coexist — Brain will auto-generate additional neurons alongside your explicit ones. SQLite Persistence [#sqlite-persistence] By default, all state is held in memory and lost when the process exits. To persist knowledge across sessions, use SQLite. Two construction verbs handle the two cases — `Brain.create` for the first run, `Brain.restore` for every run after. **Node.js** ```bash npm install better-sqlite3 ``` ```typescript import { Brain } from '@unbody-io/adapt' import { SQLiteBrainStore } from '@unbody-io/adapt/sqlite' import { openai } from '@ai-sdk/openai' // First run — fresh brain, persists to disk const brain = await Brain.create({ prompt: 'Track my coding patterns.', model: openai('gpt-4o'), store: new SQLiteBrainStore('./brain.db'), }) // Subsequent runs — restore from disk, no LLM calls during init const brain = await Brain.restore('./brain.db', { model: openai('gpt-4o') }) ``` Per-neuron data is written to sibling files derived from the brain DB path automatically (e.g. `./brain.db` → `./brain..db`). The `model` argument rehydrates persisted model refs through the LLM plugin — see [Brain — Fresh vs Restored](./brain#fresh-vs-restored) for runtime options including custom plugins and per-slot model overrides. **Bun** ```typescript import { Brain } from '@unbody-io/adapt' import { SQLiteBrainStore } from '@unbody-io/adapt/sqlite/bun' import { openai } from '@ai-sdk/openai' // First run const brain = await Brain.create({ prompt: 'Track my coding patterns.', model: openai('gpt-4o'), store: new SQLiteBrainStore('./brain.db'), }) // Subsequent runs — Bun callers must pass an explicit store instance const brain = await Brain.restore( new SQLiteBrainStore('./brain.db'), { model: openai('gpt-4o') }, ) ``` The path-string sugar `Brain.restore('./brain.db', ...)` always uses the Node SQLite adapter via dynamic import; Bun callers construct `SQLiteBrainStore` from `@unbody-io/adapt/sqlite/bun` and pass the instance instead. Next Steps [#next-steps] * [Concepts](./concepts) — architecture and mental model * [Brain](./brain) — full Brain API guide * [Neurons](./neurons) — TextNeuron and ListNeuron in depth * [Prompt Design](./prompt-design) — the most important input to a neuron # Models (/docs/models) Adapt requires models that support tool calling and structured JSON output. The table below lists models we've tested. The list will be updated as we go. The **Compatible** column reflects the non-streaming path (`brain.ask`, `neuron.query`, `brain.evaluateEvolution`). Streaming (`brain.askStream`, `neuron.queryStream`, `brain.evaluateEvolutionStream`) has its own caveats — see the **Streaming** column. Where the two diverge it usually comes down to weaker structured-output enforcement on the provider's streaming endpoint. | Provider | Model | Compatible | Streaming | Notes | | ----------- | ---------------------------------------------------- | ---------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | OpenAI | `gpt-4o-mini` | ✓ | ✓ | | | Google | `gemini-2.5-flash` | ✓ | ✓ | | | Anthropic | `claude-haiku-4-5` | ✓ | ✓ | | | OpenRouter | `google/gemini-2.0-flash-001` | ✓ | ✓ | | | OpenRouter | `google/gemma-4-31b-it` | ✓ | ✓ | | | OpenRouter | `x-ai/grok-4.3` | ✓ | ✓ (slow) | Streaming size-scaled output \~1 min on a 3 KB schema; works, but plan for higher latency. | | OpenRouter | `~openai/gpt-mini-latest` | ✓ | ✓ | | | OpenRouter | `~moonshotai/kimi-latest` | ✓ | ✓ (very slow) | Streaming size-scaled output took **8 minutes** on a \~6 KB schema — usable but document the latency for users. | | OpenRouter | `mistralai/ministral-3b-2512` | ✓ | ✓ | | | OpenRouter | `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free` | ✗ | ✗ | No endpoint supports `tool_choice`. | | OpenRouter | `~anthropic/claude-haiku-latest` | ✗ | ✗ | Provider error on structured output. | | OpenRouter | `qwen/qwen3.6-flash` | ✗ | ✗ | Provider error on both calls. | | Together.ai | `deepseek-ai/DeepSeek-V3.1` | ✗ | ✗ | Together doesn't enforce structured output — model truncates large JSON. Same failure in streaming. | | Together.ai | `Qwen/Qwen3.5-397B-A17B` | ✓ | ✗ | **Streaming-only regression:** size-scaled structured output fails to parse on the streaming endpoint. | | Together.ai | `LiquidAI/LFM2-24B-A2B` | ✓ | ✗ | **Streaming-only regression:** size-scaled structured output schema mismatch on the streaming endpoint. | | Together.ai | `Qwen/Qwen3.5-9B` | ✓ | ✗ | **Streaming-only regression:** size-scaled structured output fails to parse on the streaming endpoint. | | Together.ai | `google/gemma-4-31B-it` | ✓ | ✗ | **Streaming-only regression:** stream terminates mid-response on the size-scaled schema. | | LM Studio | `liquid/lfm2.5-1.2b` | ✓ | ⚠ unstable | Disable the "Structured Output" toggle — server forces json-mode otherwise. Streaming risks runaway generation on the stateful tool flow (eval blew Node's heap on this model). Document a wall-clock guard for users running tiny local models. | | llama.cpp | `LiquidAI/LFM2.5-1.2B-Instruct-GGUF` | ✗ | ? | Tool calling works; default `llama-server` doesn't constrain JSON to the schema. Streaming not yet verified. | # Neurons (/docs/neurons) API guide for `TextNeuron` and `ListNeuron`. For when to pick each, see [Concepts — Neuron Types](./concepts#neuron-types). TextNeuron [#textneuron] Only `model`, `instructions`, and `store` are required: ```typescript import { TextNeuron, MemoryNeuronStore } from '@unbody-io/adapt' import { openai } from '@ai-sdk/openai' const neuron = await TextNeuron.create({ model: openai('gpt-4o'), instructions: 'Track product design principles and user research insights.', store: new MemoryNeuronStore(), }) await neuron.learn([ 'User testing showed: 3-click navigation preferred over hamburger menu', 'Design review: dark mode should be default for evening users', ]) const understanding = await neuron.getUnderstanding() // string const result = await neuron.query('What are the key design principles?') ``` You can control how the neuron grows and when it synthesizes. **Governance** determines how understanding evolves over time (see [Governance Strategies](#governance-strategies) below). **Thresholds** control how many observations accumulate before synthesis triggers: ```typescript const neuron = await TextNeuron.create({ model: openai('gpt-4o'), instructions: 'Track product design principles and user research insights.', store: new MemoryNeuronStore(), governance: { strategy: 'decay', maxTokens: 8000 }, understand: { thresholds: { maxObservations: 5, minImportance: 0.3 } }, }) ``` To restore a previously persisted neuron from disk, use `TextNeuron.restore`: ```typescript import { SQLiteNeuronStore } from '@unbody-io/adapt/sqlite' // Path-string sugar (Node SQLite) const neuron = await TextNeuron.restore('./neuron.db', { model: openai('gpt-4o') }) // Or pass an explicit NeuronStore instance const neuron = await TextNeuron.restore( new SQLiteNeuronStore('./neuron.db'), { model: openai('gpt-4o') }, ) // Standalone neurons can carry their own id when restoring from a shared store const neuron = await TextNeuron.restore('./neuron.db', { id: 'design-principles', model: openai('gpt-4o'), }) ``` The `model` argument rehydrates persisted model refs through the LLM plugin. See [Brain — Fresh vs Restored](./brain#fresh-vs-restored) for the full runtime semantics — it applies identically to neuron `restore`. `ListNeuron` exposes the same `create` / `restore` shape. Constructors are private — `create` and `restore` are the only public entry points, and both fully initialize the neuron (no separate `init()` call). Phase Instructions [#phase-instructions] A neuron's `instructions` feed both the observe and understand phases verbatim. You can override either phase independently, and skip a phase entirely: ```typescript const neuron = await TextNeuron.create({ model: openai('gpt-4o'), instructions: 'Track product design principles and user research insights.', store: new MemoryNeuronStore(), observeInstructions: 'Keep any data about design decisions, user research, or accessibility.', understandInstructions: 'Synthesize into a coherent set of design principles with evidence.', focus: 'design systems and component libraries', }) ``` | Field | Phase | Effect | | ------------------------ | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `instructions` | both | Required. Shared verbatim instructions used by observe and understand. | | `observeInstructions` | observe | Optional. Overrides the verbatim instructions for the observe prompt. Falls back to `instructions` when omitted or `null`. | | `understandInstructions` | understand | Optional. Overrides the verbatim instructions for the understand prompt. Falls back to `instructions` when omitted or `null`. | | `focus` | observe | Optional. Appended to the observe instructions to narrow relevance filtering. | | `skipObservation` | — | When `true`, the observe phase is skipped — data goes straight into the understanding buffer unfiltered. | | `skipUnderstand` | — | When `true`, the observe phase runs and observations are retained, but understanding is **never** synthesized — not on threshold, not on `forceSynthesize`. The symmetric counterpart of `skipObservation`. | A `skipUnderstand` neuron is a pure observation collector: query it, inspect its buffer, and drive synthesis externally via `setUnderstanding()` if needed. No understand prompt is built for it. Cognitive Skills [#cognitive-skills] A TextNeuron's synthesizer applies two built-in skill sets when integrating observations — automatic, not configurable. Instructions influence *what domain* the skills are applied to (see [Prompt Design](./prompt-design)). | Skill Set | Skills | What the neuron asks itself | | ------------ | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | **Compare** | `confirms`, `contradicts`, `extends`, `new` | "Does this observation reinforce, challenge, add to, or introduce something new relative to what I already know?" | | **Dynamics** | `recurs`, `intensifies`, `fades`, `shifts`, `avoids` | "Is this pattern repeating? Getting stronger? Declining? Changing direction? Being avoided?" | These skills produce grounded output ("cancelled gym 6 times in 3 weeks") rather than vague summaries ("sometimes skips gym"). Governance Strategies [#governance-strategies] `governance.strategy` controls what happens when accumulated understanding gets large enough to risk losing the LLM's focus during queries. | Strategy | How it works | Good for | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | | `continuous` | Understanding grows indefinitely with no compression | Low-volume domains where you want every detail preserved (e.g., design decisions) | | `cumulative` (default) | Grows until `maxTokens`, then the LLM compresses it to a \~500-token seed summary and starts a new cycle. The seed carries forward the most important patterns | General-purpose learning — bounded size with periodic consolidation | | `decay` | Organizes understanding into temporal sections (Current / Recent / Historical). As it grows, older content is progressively compressed while recent stays detailed | Domains where recency matters — the latest observations get the most detail | ListNeuron [#listneuron] Same required fields as TextNeuron — `model`, `instructions`, `store`: ```typescript import { ListNeuron, MemoryNeuronStore } from '@unbody-io/adapt' import { openai } from '@ai-sdk/openai' const neuron = await ListNeuron.create({ model: openai('gpt-4o'), instructions: 'Track restaurants with cuisine type, location, price range, and rating.', store: new MemoryNeuronStore(), }) await neuron.learn([ 'Had amazing ramen at Ichiran in Shibuya — rich tonkotsu broth, ¥1200', 'Tried the new Italian place on 5th — mediocre pasta, overpriced', ]) const items = await neuron.getUnderstanding() // ListItem[] ``` The LLM generates the data schema from your instructions. For "track restaurants with cuisine, location, price range, and rating," it produces fields like `name`, `cuisine`, `location`, `priceRange`, `rating`. During synthesis, the LLM works like an agent with tools: it reads the buffered observations and decides how to update the collection using `addItem`, `updateItem`, `removeItem`, and other collection management tools. For example, if an observation mentions a restaurant the neuron already tracks, the LLM calls `updateItem` to revise it rather than creating a duplicate. **Schema generation depends on your instructions.** The fields in the schema come directly from what you describe. If your instructions say "track whether it's been rejected by the PM," the schema will have a rejection field. If you don't mention it, it won't exist — and that data will be lost even if it appears in observations. See [Prompt Design](./prompt-design) for guidance. Custom Schemas [#custom-schemas] You can bypass LLM schema generation entirely by providing `observationSchema` and/or `understandingSchema` in the neuron config: ```typescript const brain = await Brain.create({ prompt: 'Track therapy sessions.', model: openai('gpt-4o'), autoSetup: false, neurons: [{ id: 'relationships', type: 'list', name: 'Relationships', description: 'Key people, client descriptions, shifts in perception', instructions: 'Track the key people in this client\'s life and how perception evolves.', governance: { deduplication: 'strict', maxItems: 100, pruning: 'least-confident' }, observationSchema: { type: 'object', properties: { person_name: { type: 'string' }, relationship_to_client: { type: 'string' }, description: { type: 'string' }, }, required: ['person_name', 'description'], }, understandingSchema: { type: 'object', properties: { person_name: { type: 'string' }, relationship_to_client: { type: 'string' }, emotional_charge: { type: 'string', enum: ['positive', 'negative', 'ambivalent', 'neutral'] }, role_in_client_patterns: { type: 'string' }, perception_shift_observed: { type: 'boolean' }, }, required: ['person_name', 'relationship_to_client'], }, }], }) ``` When provided, schemas are used as-is — no LLM call, fully deterministic. This works for both `TextNeuron` and `ListNeuron`, and for both standalone neurons and Brain-managed explicit neurons. Item Structure [#item-structure] ```typescript { id: string data: Record // Fields matching the schema metadata: { confidence: number // 0–1, mechanical: touchCount / maxTouchCount touchCount: number // How many times this item was referenced in observations firstSeen: string // ISO 8601 lastUpdated: string // ISO 8601 signals: string[] // Accumulated tags } } ``` **Confidence tells you how much evidence backs an item.** It's calculated mechanically, not by the LLM: each time an observation references an item (via `updateItem`), its `touchCount` increments. After each synthesis, confidence is normalized across all items as `touchCount / maxTouchCount`. The most-referenced item always has confidence 1.0. Items mentioned only once will have low confidence — this helps you distinguish well-established items from one-off mentions. **Deduplication is automatic.** When the LLM calls `addItem` during synthesis, the system searches existing items for similar matches. If it finds any, it returns them to the LLM and suggests using `updateItem` instead — preventing the same entity from appearing multiple times. List Governance [#list-governance] | Option | Values | Default | | --------------- | ------------------------------------------- | ---------- | | `deduplication` | `'strict'` / `'none'` | `'strict'` | | `maxItems` | number | `200` | | `pruning` | `'oldest'` / `'least-confident'` / `'none'` | `'oldest'` | Common Neuron API [#common-neuron-api] Both `TextNeuron` and `ListNeuron` share: ```typescript // Learning await neuron.learn(batch) // LearnOutput await neuron.learn(batch, { forceSynthesize: true }) // Force understand phase // Querying const result = await neuron.query('...') // QueryResult const stream = await neuron.queryStream('...') // AdaptStreamResult // Understanding await neuron.getUnderstanding() // string (text) or ListItem[] (list) await neuron.setUnderstanding(value) // Set directly await neuron.getSummary() // Prose summary await neuron.hasKnowledge() // Has any understanding? // Introspection neuron.getHealth() // { activation, status, signalThresholds } neuron.getMetrics() // { ingestion: { dismissalRate, ... }, query: { ... } } await neuron.getEvolution() // EvolutionRecord[] neuron.getObservationSchema() // JSON Schema for observations neuron.getUnderstandingSchema() // JSON Schema for understanding neuron.getMetadata() // NeuronMetadata // Buffer await neuron.getBufferState() // { count, avgImportance, totalTokens } await neuron.getBufferedObservations() // Array<{ text, importance }> — pending only, condensed // Observations (full history, full ObservationRecord shape) await neuron.getObservations() // ObservationRecord[] await neuron.getObservations({ status: 'pending' }) // filter await neuron.getObservations({ status: 'processed' }) await neuron.setObservations(records) // bulk replace await neuron.updateObservation(id, patch) await neuron.removeObservation(id) // Config await neuron.adjust('natural language directive') await neuron.update({ instructions: '...' }) // Identity neuron.id // string neuron.name // string neuron.instructions // string — shared verbatim instructions neuron.observeInstructions // string | null — observe-phase override neuron.understandInstructions // string | null — understand-phase override neuron.description // string neuron.type // 'text' | 'list' neuron.focus // string | null neuron.origin // 'prompt' | 'developer' | 'emergent' ``` Learn Output [#learn-output] `learn()` returns a discriminated union: ```typescript const result = await neuron.learn(data) switch (result.status) { case 'observed': // Observations buffered, threshold not met yet console.log(`Buffered ${result.output.length} observations`) break case 'synthesized': // Understanding updated console.log(`Significance: ${result.significance}`) // routine | notable | critical console.log(`What changed: ${result.evolution}`) break case 'observe:dismissed': // Data not relevant to this neuron console.log(`Gaps: ${result.gaps}`) break case 'observe:error': case 'synthesize:dismissed': case 'synthesize:error': // Error or LLM chose not to update break } ``` # Prompt Design (/docs/prompt-design) Prompts and instructions are the single most important inputs to the system. They determine everything downstream: ```text Brain prompt → Decomposition (what neurons are created, how many) → Evolution context (how the brain reshapes itself) Neuron instructions → Observe prompt (what data is relevant?) → Schema generation (ListNeuron: what fields exist?) → Understand prompt (how to synthesize?) → Query behavior (how to answer questions?) ``` A vague prompt produces a vague system. A specific prompt with clear decomposition guidance produces focused specialists that build deep, evidence-backed knowledge. > **Instructions reach the LLM verbatim.** A neuron's `instructions` are not paraphrased or compressed — they are inserted directly into the observe and understand system prompts under a `## Developer Instructions` heading. The runtime prompt is assembled deterministically in three layers: (1) a static framework role frame, (2) your verbatim instructions, (3) framework mechanics (JSON envelope, importance scale, cognitive skills). No LLM call rewrites your text. This makes the wording of your instructions directly load-bearing — what you write is what the model sees. Brain Prompt [#brain-prompt] The brain prompt has two jobs: describe **what** to learn and guide **how** to decompose. Describe the domain [#describe-the-domain] Be specific about the domain and what matters: ```text Track my coding patterns across git commits, code reviews, and technical discussions. I want to understand my evolving development philosophy, preferred tools, and antipatterns I avoid. ``` Not: ```text Be a good brain that learns stuff. ``` Guide the decomposition [#guide-the-decomposition] The decomposition LLM decides how many neurons to create. Its built-in heuristic says "start with the minimum number that provides meaningful separation" — which means without guidance, it often collapses a multi-dimensional domain into a single broad neuron. If your domain has separable concerns, tell the brain to specialize: ```text Learn about a person's behavioral patterns from their digital activity. You start knowing nothing — everything must be discovered from the data. When you start recognizing distinct concerns, prioritize specialization over generalization. Create dedicated specialists for each distinct thing you discover rather than lumping things together. The kinds of things to watch for as they emerge: - Avoidance patterns: things they postpone, cancel, or make excuses about - Emotional patterns: frustration, anxiety, excitement — and triggers - Browsing behaviors: timing patterns, depth vs breadth, categories - The gap between stated intentions and actual actions Do not generalize. If two things could be separate specialists, they should be. The goal is maximum coverage with maximum depth. ``` Without the decomposition guidance, this same domain description produces 1 neuron. With it, you get 4–6 focused specialists that each build deep understanding and can answer targeted queries. The difference is dramatic in cross-neuron synthesis quality — `brain.ask()` can draw on multiple independent perspectives instead of one monolithic view. Key phrases that influence decomposition [#key-phrases-that-influence-decomposition] | Phrase | Effect | | --------------------------------------------------------------- | ---------------------------------------------------- | | *"Prioritize specialization over generalization"* | Pushes toward more, focused neurons | | *"If two things could be separate specialists, they should be"* | Explicit anti-merge signal | | *"Maximum coverage with maximum depth"* | Sets the optimization target | | *"The kinds of things to watch for as they emerge"* | Lists dimensions without mandating upfront structure | | *"You start knowing nothing"* | Prevents the LLM from over-assuming structure | | *"Do not generalize"* | Reinforces specialization | When NOT to guide decomposition [#when-not-to-guide-decomposition] * **Unified domain** — If the domain is genuinely about one thing (e.g., "track one person's coffee preferences"), a single neuron is correct. Don't force decomposition where it doesn't belong. * **Known structure** — If you know exactly what neurons you want, skip auto-decomposition entirely and define them with `neurons` + `autoSetup: false`. Decomposition guidance is for the middle ground — you know the domain has structure but want the LLM to discover the right boundaries. Real-World Example: R&D Standups [#real-world-example-rd-standups] A brain that learns from daily standup transcripts, discovering team dynamics over time: ```text You are learning from raw daily standup meeting transcripts of a software team. You start knowing nothing — no team members, no projects, no context. Everything must be discovered from the data as it arrives. As transcripts come in, stay open and attentive. Let the data reveal who the people are, what they're working on, how they interact, and what matters to them. Do not rush to conclusions — let understanding build incrementally with each new piece of data. When you start recognizing distinct concerns, prioritize specialization over generalization. Create dedicated specialists for each distinct thing you discover rather than lumping things together. The kinds of things to watch for as they emerge: - Individual team members — communication style, expertise, how they evolve - Projects and initiatives — scope, decisions, blockers, trajectory - Interpersonal dynamics — collaboration patterns, decision-making, conflicts - Cultural signals — humor, morale, shared references, how the team handles stress - Ideas and proposals that surface in conversation - Technical decisions and architectural patterns These are not instructions to create upfront — they are dimensions that may reveal themselves over time. Some may never appear. Others you haven't anticipated will. Follow the data. Do not generalize. If two things could be separate specialists, they should be. The goal is maximum coverage with maximum depth. ``` Notice: the prompt lists dimensions as possibilities, not mandates. It tells the brain *how to think* about decomposition without prescribing the exact neurons. This produces different decompositions depending on the data — a team of 3 might not need individual-member neurons, while a team of 12 might. Neuron Instructions [#neuron-instructions] Lead with the questions the neuron should be able to answer. These root questions orient everything — what the observer filters for, how the synthesizer reasons, and what the query layer prioritizes. ```text Track product design principles and user research insights. Track answers to: - What are the team's core design principles? - How do user needs inform design choices? - Where do design principles conflict with each other? Watch for: - Design decisions and their rationale - User testing results and behavioral patterns - Accessibility considerations and standards applied ``` The "Track answers to" section is the most important part. It gives the neuron a purpose beyond collecting data. Per-phase instructions [#per-phase-instructions] By default the shared `instructions` field feeds both the observe phase (relevance filtering) and the understand phase (synthesis). When a phase needs different wording, override it: * **`observeInstructions`** — replaces the verbatim instructions used by the observe prompt only. Use it when the relevance criteria should read differently from the synthesis directive. * **`understandInstructions`** — replaces the verbatim instructions used by the understand prompt only. * **`focus`** — observe-only. Appended to the observe instructions under a `Focus:` heading to narrow what data is kept. When an override is omitted or set to `null`, that phase falls back to the shared `instructions`. These fields exist on `TextNeuron`/`ListNeuron` configs and on Brain explicit-neuron configs. Instructions for TextNeuron [#instructions-for-textneuron] TextNeuron instructions shape how cognitive skills are applied to your domain. The neuron automatically detects confirmation, contradiction, recurrence, intensification, avoidance, etc. — your instructions determine *what* it applies these skills to. **Ask for specifics if you want them.** The synthesizer defaults to abstract patterns unless instructions push for grounding: ```text // Vague — produces "Alex exercises sometimes" Track Alex's daily habits. // Specific — produces "Alex cancelled gym 6 times with excuses, // rescheduled dentist 4 times, dodged promotion conversation at 4 separate 1:1s" Track Alex's behavioral patterns across daily activities. Track answers to: - What does Alex consistently avoid, and how? (approximate counts, timeframes) - What topics recur most frequently? How many times, over what period? - Where do stated intentions contradict actual behavior? ``` The difference is that specific instructions trigger the dynamics skills (recurs, avoids, shifts) to gather evidence — counts, timeframes, concrete instances — rather than just labeling patterns. Real-World Example: Clinical Trajectory [#real-world-example-clinical-trajectory] From a therapy copilot that tracks client patterns across sessions: ```text You are the narrative memory of this therapeutic relationship — the through-line. Track answers to: - **Themes**: What topics recur? How has the client's language and framing around each topic shifted? Which carry the most emotional charge? - **Emotional patterns**: What emotions come up most frequently and in what contexts? What triggers emotional shifts? How does the client regulate? - **Avoidance**: What does the client consistently steer away from? How does avoidance manifest — topic changes, humor, intellectualization, somatic complaints, silence? - **Language shifts**: How is the client's language evolving? Self-descriptions changing? Emotional vocabulary deepening? Agency language emerging? - **Overall arc**: Where is this client right now? What is concretely shifting? What remains stuck despite effort? Ground everything in the client's own language and specific interactions. A claim without a quote or concrete example is not a claim. ``` Notice: specific sub-questions, explicit dimensions to track, and a grounding instruction at the end. Instructions for ListNeuron [#instructions-for-listneuron] ListNeuron instructions directly control schema generation. **The fields in your schema come from what you describe in instructions.** **Name the fields you want.** If you say "track cuisine type, location, and price range," the schema will have those fields. If you don't mention a field, it won't exist. ```text // Missing status field — if a PM rejects a feature, there's nowhere to record it Track feature requests with name, description, and customer segment. // Has status field — deprioritization data is captured Track feature requests for a SaaS product. Each item is a distinct feature request. For each feature request, track: - The feature name and description - Which customer segments are asking (enterprise, SMB, startup) - Whether it's been deprioritized or rejected by the PM ``` **Describe what each item IS.** "Each item is a distinct feature request" is better than "Track features" — it tells the synthesizer the granularity you expect, which affects deduplication behavior. **Don't ask the LLM to count.** If your instructions say "track how many sources requested this," the LLM will manage a `request_count` field — but it drifts over time (LLMs are bad at arithmetic across batches). Use `metadata.touchCount` instead, which is mechanically accurate. Specificity and Signal-to-Noise [#specificity-and-signal-to-noise] Instructions control how much data the observer lets through: * **Narrow instructions** → observer dismisses most data → high precision, may miss related patterns * **Broad instructions** → observer accepts most data → comprehensive but noisy, more synthesis needed This is a design choice, not a quality issue. A neuron tracking "React performance antipatterns" will be precise but miss general coding philosophy. A neuron tracking "coding patterns" will be comprehensive but need more synthesis cycles to find structure. Neuron Granularity [#neuron-granularity] **Few broad neurons vs many narrow ones?** Start with 3–7 neurons covering broad domains. Let evolution split them as data arrives. Reasons: * Each neuron makes independent LLM calls during observe, understand, and query. More neurons = linear cost increase per inject/ask. * Evolution is designed for this — it detects when a neuron is overloaded (high dismissal, low confidence) and splits it. * Narrow neurons miss cross-cutting patterns. A "React hooks" neuron won't notice your general preference for functional patterns. **When to go narrow:** If you know your domains upfront and they're distinct (e.g., "recipes" vs "workout tracking" vs "journal entries"), define explicit neurons. If domains emerge from usage, let `autoSetup: true` and evolution handle it. **Practical limits:** Brain processes all neurons in parallel per inject/ask call. 10–20 neurons is comfortable. 50+ will work but increases latency and cost proportionally. The bottleneck is LLM calls, not Brain itself. Adjust Directives [#adjust-directives] Natural language steering for `adjustNeuron()` / `neuron.adjust()`: ```typescript // Expand scope await brain.adjustNeuron('design', 'Also track accessibility patterns') // Narrow scope await brain.adjustNeuron('tech', 'Focus only on React, stop tracking Vue') // Change behavior await brain.adjustNeuron('patterns', 'Be stricter about what counts as a distinct pattern') // Shift emphasis await brain.adjustNeuron('trends', 'Weight recent observations more heavily') ``` The LLM sees the neuron's current raw instruction fields (`instructions`, `observeInstructions`, `understandInstructions`, `focus`), then evolves them incrementally — preserving existing text unless the directive clearly changes it. The updated instructions are re-inserted verbatim into the observe and understand prompts. If the directive is ambiguous, it preserves more rather than less. # Recipes (/docs/recipes) Common patterns for building with Adapt. Each recipe addresses a specific problem you'll likely encounter. Proactive Insights [#proactive-insights] **Problem:** By default, insights only come when you ask for them via `ask()`. But sometimes you want the system to proactively surface important findings — for example, notifying a user when a critical pattern emerges. **Solution:** Subscribe to `neuron:synthesized` events and trigger a query when significance is high: ```typescript brain.on('neuron:synthesized', async (payload) => { if (payload.significance === 'critical' || payload.significance === 'notable') { const insight = await brain.ask('What new patterns or tensions have emerged?') if (insight.sources.some(s => s.confidence > 0.7)) { notifyUser(insight) } } }) ``` Quality Gating [#quality-gating] **Problem:** Not every query produces a good answer. If neurons don't have enough knowledge, you'll get low-confidence, speculative responses. You don't want to surface these to users. **Solution:** Filter results by confidence and relevance before showing them: ```typescript const result = await brain.ask(query) const strong = result.sources.filter(s => s.confidence > 0.6 && s.relevance > 0.5) if (strong.length === 0) return null // Nothing worth surfacing return { insight: result.insight, gaps: result.gaps } ``` Cross-Domain Connections [#cross-domain-connections] **Problem:** Each neuron is an independent specialist. But sometimes the most interesting insights come from *connecting* knowledge across domains — patterns that no single neuron can see on its own. **Solution:** Just ask. The synthesis step in `brain.ask()` sees all neuron responses together and can draw connections between them: ```typescript const result = await brain.ask('What connects my interest in calm tech with my wedding planning?') ``` User-Steerable Taxonomy [#user-steerable-taxonomy] **Problem:** Your users may want to control how knowledge is organized — merging categories that feel redundant, splitting broad ones that are too noisy, or adjusting what a neuron pays attention to. **Solution:** Expose the evolution management API to your users, letting them reshape the Brain's structure at runtime: ```typescript await brain.mergeNeurons(['eink', 'paper-displays'], 'Combine under hardware') await brain.splitNeuron('ai-neuron', 'Separate into AI tools vs AI research') await brain.adjustNeuron('categories', 'Stop categorizing things as inspiration') ``` Dual-Brain Architecture [#dual-brain-architecture] **Problem:** Some use cases need both long-term memory (patterns across weeks or months) and short-term processing (extracting observations from a single session). A single Brain can't easily serve both — long-term brains have accumulated knowledge that biases observation, while session brains need to start fresh. **Solution:** Use two separate brains. A session brain processes the immediate data with a fixed, lightweight structure. After the session, transfer its knowledge into the long-term brain: ```typescript import { Brain } from '@unbody-io/adapt' import { SQLiteBrainStore } from '@unbody-io/adapt/sqlite' // Long-term brain: evolves over time, persists everything const longTermBrain = await Brain.create({ prompt: 'Track patterns across all interactions.', model, store: new SQLiteBrainStore('./long-term.brain.db'), evolution: { enabled: true }, }) // Session brain: fixed structure, short-lived const sessionBrain = await Brain.create({ prompt: 'Extract observations from this session.', model, autoSetup: false, neurons: sessionNeuronDefs, evolution: { enabled: false }, }) // Process data through the session brain await sessionBrain.inject(sessionData) // Transfer session knowledge to the long-term brain for (const neuron of sessionBrain.getNeurons()) { const understanding = await neuron.getUnderstanding() if (understanding) { await longTermBrain.inject({ source: neuron.name, content: understanding }) } } ``` On Bun, import the same adapters from `@unbody-io/adapt/sqlite/bun`. Event-Driven Synchronization [#event-driven-synchronization] **Problem:** `brain.inject()` returns after observation completes, but synthesis may still be running. If you need to guarantee that all neurons have finished processing before continuing (e.g., before querying), you need to wait for the full pipeline. **Solution:** Listen for the `brain:inject:completed` event, which fires after all neurons finish both observation and synthesis: ```typescript const injectDone = new Promise((resolve) => { brain.on('brain:inject:completed', () => resolve()) }) await brain.inject(data) await injectDone // Block until all neurons finish processing ``` Multi-Provider Model Setup [#multi-provider-model-setup] **Problem:** You want to minimize cloud API costs, but local models aren't good enough for synthesis and querying. **Solution:** Use the [model cascade](./configuration#model-cascade) to assign a local model for high-volume observation and a cloud model for the operations that need quality: ```typescript import { openai } from '@ai-sdk/openai' import { createOpenAICompatible } from '@ai-sdk/openai-compatible' const local = createOpenAICompatible({ baseURL: 'http://localhost:11434/v1', name: 'ollama', }) const brain = await Brain.create({ prompt: '...', model: local('llama3.1'), blueprintModel: openai('gpt-4o'), init: { model: openai('gpt-4o') }, query: { model: openai('gpt-4o') }, learning: { observer: { model: local('llama3.1') }, understand: { model: openai('gpt-4o') }, }, }) ``` SSE Event Broadcasting [#sse-event-broadcasting] **Problem:** You're building a web app and want to stream Brain activity (observation progress, synthesis results, evolution decisions) to the browser in real time. **Solution:** Forward Brain events over Server-Sent Events. The event system emits structured payloads that map cleanly to SSE: ```typescript // Forward all brain events to connected clients brain.on((event) => { for (const send of sseClients) { send(event.type, event.payload) } }) ``` The SSE transport layer is framework-dependent (Express, Hono, Fastify, etc.) — the pattern above works with any setup that gives you a `send(event, data)` callback per client. # Stores (/docs/stores) Brain has two independent storage layers: | Layer | Interface | Purpose | | ---------------- | ------------- | ------------------------------------------------------------------ | | **Brain Store** | `BrainStore` | Brain state, neuron registry, evolution history, dismissed batches | | **Neuron Store** | `NeuronStore` | Per-neuron observations, understanding, evolution, state | You only configure the brain store directly. Per-neuron stores are derived from it through `BrainStore.getNeuronStore(neuronId)` — same code path on `Brain.create` and `Brain.restore`, so the two cannot disagree on where each neuron's data lives. Memory Stores (default) [#memory-stores-default] ```typescript import { Brain, MemoryBrainStore } from '@unbody-io/adapt' const brain = await Brain.create({ prompt: '...', model: openai('gpt-4o'), store: new MemoryBrainStore(), }) ``` Ephemeral — data lost on process exit. Good for development and testing. `MemoryBrainStore` caches a `MemoryNeuronStore` per neuron id internally. SQLite Stores [#sqlite-stores] Adapt ships runtime-specific SQLite adapters. Core Adapt remains runtime-agnostic; only the adapter import changes. | Adapter | Runtime | Driver | | ----------------------------- | ------- | ---------------- | | `@unbody-io/adapt/sqlite` | Node.js | `better-sqlite3` | | `@unbody-io/adapt/sqlite/bun` | Bun | `bun:sqlite` | Node.js [#nodejs] **Install:** ```bash npm install better-sqlite3 ``` ```typescript import { Brain } from '@unbody-io/adapt' import { SQLiteBrainStore } from '@unbody-io/adapt/sqlite' const brain = await Brain.create({ prompt: '...', model: openai('gpt-4o'), store: new SQLiteBrainStore('./brain.db'), }) ``` Persistent via `better-sqlite3`. Each neuron's data is written to a sibling file derived from the brain DB path (e.g. `./brain.db` → `./brain..db`). On the next session, restore with `Brain.restore('./brain.db')` — no LLM calls on restore. Bun [#bun] ```typescript import { Brain } from '@unbody-io/adapt' import { SQLiteBrainStore } from '@unbody-io/adapt/sqlite/bun' const brain = await Brain.create({ prompt: '...', model: openai('gpt-4o'), store: new SQLiteBrainStore('./brain.db'), }) ``` Persistent via Bun's built-in `bun:sqlite`. Restore behavior matches Node — pass a `SQLiteBrainStore` instance to `Brain.restore()`: ```typescript import { SQLiteBrainStore } from '@unbody-io/adapt/sqlite/bun' const brain = await Brain.restore(new SQLiteBrainStore('./brain.db')) ``` (The path-string sugar `Brain.restore('./brain.db')` always uses the Node SQLite adapter via dynamic import, so Bun callers must pass an explicit store instance.) Standalone neurons follow the same shape: `TextNeuron.create(config)` writes to the configured store; `TextNeuron.restore(pathOrStore)` reads it back. See [Neuron API → Lifecycle](./reference/neuron-api#lifecycle). Hierarchical Persistence [#hierarchical-persistence] For apps with multiple entities, scope a brain DB path per entity. Per-neuron files are derived as siblings of that brain DB automatically: ```typescript const brain = await Brain.create({ prompt: '...', model, store: new SQLiteBrainStore(`./${entityId}/brain.db`), }) ``` This lets you cleanly delete all data for a single entity by removing its directory. Persistence Across Sessions [#persistence-across-sessions] ```typescript import { Brain } from '@unbody-io/adapt' import { SQLiteBrainStore } from '@unbody-io/adapt/sqlite' // Session 1: create and learn const brain = await Brain.create({ prompt: '...', model: openai('gpt-4o'), store: new SQLiteBrainStore('./brain.db'), }) await brain.inject(data) await brain.dispose() // Session 2: restore and continue const brain2 = await Brain.restore('./brain.db', { model: openai('gpt-4o') }) await brain2.ask('What do you know?') // full knowledge from session 1 ``` The `model` argument rehydrates persisted model refs through the LLM plugin. See [Brain — Fresh vs Restored](./brain#fresh-vs-restored) for the full runtime options (custom plugins, per-slot overrides). Custom Stores [#custom-stores] If you need a backend other than in-memory or SQLite (e.g., PostgreSQL, Redis, a cloud database), you can implement your own stores. Both interfaces follow a simple collection-based pattern — each namespace is a CRUD collection for a specific type of record. **NeuronStore** — one per neuron, holds that neuron's observations, understanding, evolution history, and state: ```typescript interface NeuronStore { observations: NeuronCollection understanding: NeuronCollection evolution: NeuronCollection state: NeuronCollection dispose(): Promise } ``` Observation lifecycle [#observation-lifecycle] Observations are persistent, not ephemeral buffered input. Each `ObservationRecord` carries a `metadata_status` field that moves through two states: * **`pending`** — written during the observe phase of `learn()`. These are the only rows `getBufferedObservations()` and `getBufferState()` return, and they're what synthesis will consume on the next understand pass. * **`processed`** — set after synthesis completes. The record stays in the collection; it's *not* deleted. With `SQLiteNeuronStore`, both pending and processed observations survive process restarts. This means the full history of what a neuron has seen remains queryable via `neuron.store.observations.list(...)`. If you need only processed history, filter by `{ metadata_status: 'processed' }`. The neuron class itself only exposes the pending buffer today — for anything else, go through the store collection directly. **BrainStore** — one per brain, holds the brain's state, neuron registry, internal neuron registry, evolution history, and dismissed batches. Also resolves per-neuron stores via `getNeuronStore(id)`: ```typescript interface BrainStore { state: BrainCollection neurons: BrainCollection internalNeurons: BrainCollection evolution: BrainCollection dismissedBatches: BrainCollection getNeuronStore(neuronId: string): NeuronStore dispose(): Promise } ``` `getNeuronStore(neuronId)` is the single seam for per-neuron persistence — Brain calls into it on both `create` and `restore`, so the two paths cannot disagree on where a neuron's data lives. A custom `BrainStore` implementation must return a `NeuronStore` for any neuron id (typical strategy: cache one per id, derive a path/key from the id for persistent backends). Both `NeuronCollection` and `BrainCollection` implement the same CRUD interface. Each method does what you'd expect — the important one to note is `search()`, which should support full-text search (used by ListNeuron's deduplication during synthesis): ```typescript interface NeuronCollection { add(item: T): Promise get(id: string): Promise list(filter?: Record): Promise update(id: string, changes: Partial>): Promise delete(id: string): Promise clear(): Promise count(filter?: Record): Promise search(query: string): Promise addBatch(items: T[]): Promise } ``` Complete custom neuron store example [#complete-custom-neuron-store-example] ```typescript class MapNeuronCollection implements NeuronCollection { private items = new Map() async add(item: T): Promise { if (this.items.has(item.id)) throw new Error(`Record with id "${item.id}" already exists`) this.items.set(item.id, item) } async get(id: string): Promise { return this.items.get(id) } async list(filter?: Record): Promise { const values = [...this.items.values()] if (!filter) return values return values.filter((item) => Object.entries(filter).every(([key, value]) => (item as Record)[key] === value) ) } async update(id: string, changes: Partial>): Promise { const existing = this.items.get(id) if (!existing) throw new Error(`Record with id "${id}" not found`) this.items.set(id, { ...existing, ...changes }) } async delete(id: string): Promise { if (!this.items.delete(id)) throw new Error(`Record with id "${id}" not found`) } async clear(): Promise { this.items.clear() } async count(filter?: Record): Promise { return (await this.list(filter)).length } async search(query: string): Promise { const normalized = query.toLowerCase() return (await this.list()).filter((item) => JSON.stringify(item).toLowerCase().includes(normalized) ) } async addBatch(items: T[]): Promise { for (const item of items) await this.add(item) } } class MapNeuronStore implements NeuronStore { observations = new MapNeuronCollection() understanding = new MapNeuronCollection() evolution = new MapNeuronCollection() state = new MapNeuronCollection() async dispose(): Promise { // no-op } } ``` # Brain API Reference (/docs/reference/brain-api) Complete API reference for the `Brain` class, covering lifecycle management, data operations, neuron management, evolution controls, and all associated result types. Lifecycle [#lifecycle] | Method | Returns | Description | | -------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Brain.create(config)` | `Promise` | Construct a fresh brain. Throws if the store already contains a brain. | | `Brain.restore(pathOrStore, runtime?)` | `Promise` | Restore an existing brain. Path-string sugar uses Node SQLite via dynamic import; Bun callers pass an explicit `BrainStore`. `runtime` carries the LLM context the brain needs after restore. Throws if the store is empty. | | `dispose()` | `Promise` | Walk neuron stores and close the brain store. | Constructors are private — `Brain.create` and `Brain.restore` are the only public entry points. Both fully initialize the brain; there is no separate `init()` step. **`Brain.restore()` runtime options:** ```typescript interface BrainRuntimeOptions { model?: LanguageModel // Live AI SDK model — covers the 90% case blueprintModel?: LanguageModel init?: { model?: LanguageModel } query?: { model?: LanguageModel } evolution?: { model?: LanguageModel } learning?: LearningConfig llm?: AdaptLLMPlugin // Custom plugin — for BYO runtimes onEvent?: UnifiedHandler // Subscribed before restore init runs } ``` All fields are optional. See [Brain — Fresh vs Restored](../brain#fresh-vs-restored) for the canonical write-up of when to use each form. Pre-init event subscription (`onEvent`) [#pre-init-event-subscription-onevent] `BrainConfig.onEvent` (for `Brain.create`) and `BrainRuntimeOptions.onEvent` (for `Brain.restore`) register an event handler via `.on(...)` **before** initialization runs. Without it, the first events you can observe are post-init — `brain:init:*` events fire during `Brain.create` / `Brain.restore` and would be missed by a handler attached afterward. Pass `onEvent` to capture the full init sequence: ```typescript const brain = await Brain.create({ prompt: '...', model: openai('gpt-4o'), onEvent: (event) => console.log(`[${event.type}]`), // sees brain:init:started, neuron:init:*, ... }) ``` Data [#data] | Method | Returns | Description | | ---------------------------- | ---------------------------- | -------------------------------------- | | `inject(data, options?)` | `Promise` | Route data to all neurons | | `ask(query, options?)` | `Promise` | Query all neurons, synthesize response | | `askStream(query, options?)` | `Promise` | Streaming variant of `ask()` | | `consult(query, options?)` | `Promise` | Query internal neurons | | `inspect(query, options?)` | `Promise` | Agentic read-only introspection | **`ask()` options:** `{ model?: LanguageModel, mode?: 'direct' | 'deep' }` + AI SDK `CallSettings` **`consult()` options:** `{ neuron?: string }` — target a specific internal neuron by ID Neuron Management [#neuron-management] | Method | Returns | Description | | ----------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `addNeuron(config)` | `Promise` | Add neuron with explicit config | | `removeNeuron(id)` | `Promise` | Remove neuron and dispose store | | `adjustNeuron(id, directive)` | `Promise<{ neuron, result }>` | Natural language steering (preserves knowledge) | | `getNeuron(id)` | `BaseNeuron \| undefined` | Get neuron by ID | | `getNeurons()` | `BaseNeuron[]` | Get all external neurons | | `getInternalNeuron(id)` | `BaseNeuron \| undefined` | Get internal neuron by ID | | `pauseNeuron(id)` | `Promise` | Set neuron status to `'inactive'`. Inject fan-out skips paused neurons; query path is unaffected. | | `resumeNeuron(id)` | `Promise` | Set neuron status to `'active'`. | | `getNeuronStatus(id)` | `NeuronStatus \| undefined` | Returns `'active'` / `'inactive'`, or `undefined` if neuron not found. Defaults to `'active'` for records that predate the status field. | Evolution [#evolution] Requires `evolution.enabled` (default: `true`). | Method | Returns | Description | | ----------------------------------- | --------------------------------- | ----------------------------------- | | `createNeuron(guidance)` | `Promise` | Create neuron from natural language | | `deleteNeuron(id)` | `Promise` | Delete neuron via evolution | | `mergeNeurons(ids, guidance)` | `Promise` | Merge 2+ neurons into one | | `splitNeuron(id, guidance)` | `Promise` | Split neuron into multiple | | `updateNeuron(id, guidance)` | `Promise` | LLM-driven config update | | `evaluateEvolution(options?)` | `Promise<{ decisions, results }>` | Trigger evolution evaluation | | `evaluateEvolutionStream(options?)` | `Promise<{ stream, decisions }>` | Streaming variant | **`evaluateEvolution()` options:** `{ dryRun?: boolean }` Signals & Config [#signals--config] | Method | Returns | Description | | ------------------------------------------- | ---------------------------- | --------------------------------------------------- | | `signal({ source, description, bypass? })` | `void` | Inject signal for evolution | | `triggerEvolution({ source, description })` | `Promise` | Trigger evolution directly, bypassing signal buffer | | `update(updates)` | `Promise` | Update brain config | | `on(type, handler)` | `this` | Subscribe to specific event | | `on(handler)` | `this` | Subscribe to all events | Accessors [#accessors] | Property | Type | Description | | ------------------ | ------------------------- | -------------------------------------------- | | `prompt` | `string` | Brain's purpose prompt | | `promptContext` | `object \| undefined` | Decomposed prompt context | | `evolutionContext` | `string \| null` | Evolution guidance from prompt decomposition | | `config` | `ResolvedBrainConfig` | Current resolved config (computed view) | | `neurons` | `Map` | Map of external neurons | | `internalNeurons` | `Map` | Map of internal neurons | | `store` | `BrainStore` | Persistence layer | Result Types [#result-types] BrainAskResult [#brainaskresult] ```typescript { insight: string // Synthesized answer sources: Array<{ neuronId: string relevance: number confidence: number insight: string }> gaps: string[] // Aggregated knowledge gaps } ``` BrainInjectResult [#braininjectresult] ```typescript { id: string // Inject ID batches: Array<{ id: string index: number results: Array<{ neuronId: string result: LearnOutput // See neuron-api.md }> }> } ``` BrainUpdateResult [#brainupdateresult] ```typescript { changedFields: string[] config: ResolvedBrainConfig neuronResults: Array<{ neuronId: string changedFields: string[] }> evolutionResults?: { // Present when semantic changes trigger evaluation decisions: EvolutionDecision[] created: string[] updated: string[] deleted: string[] merged: string[] split: string[] } } ``` # Neuron API Reference (/docs/reference/neuron-api) Shared API for `TextNeuron` and `ListNeuron` (both extend `BaseNeuron`). Lifecycle [#lifecycle] | Method | Returns | Description | | ------------------------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `TextNeuron.create(config, parentModels?)` | `Promise` | Construct a fresh TextNeuron and persist its config. Throws if the store already contains a neuron. | | `TextNeuron.restore(pathOrStore, runtime?)` | `Promise` | Restore a previously persisted TextNeuron. Path-string sugar uses Node SQLite via dynamic import. Throws if the store is empty. | | `ListNeuron.create(config, parentModels?)` | `Promise` | Same shape — fresh ListNeuron. | | `ListNeuron.restore(pathOrStore, runtime?)` | `Promise` | Same shape — restored ListNeuron. | | `dispose()` | `Promise` | Close the neuron's own store. | | `isInitialized()` | `boolean` | True once `init` has completed (an explicit internal `initialized` flag is set at the end of init). For legacy persisted neurons that predate the flag, it also returns true when `understand_prompt` is present. Works for `skipObservation` and `skipUnderstand` neurons. | Constructors are private — the static `create` / `restore` methods are the only public entry points. Both fully initialize the neuron; there is no separate `init()` step on the public surface. `create` runs prompt + schema generation via LLM and persists everything. `restore` rehydrates identity, prompts, and schemas from the store via DB reads only — zero LLM calls during init. **`restore()` runtime options:** ```typescript { id?: string // Pin the neuron id (useful when the store holds multiple) model?: LanguageModel // Live AI SDK model — covers the common case llm?: AdaptLLMPlugin // Custom plugin — for BYO runtimes onEvent?: UnifiedHandler // Subscribed before restore init runs — catches neuron:init:* events } ``` All fields are optional. See [Brain — Fresh vs Restored](../brain#fresh-vs-restored) for the canonical write-up — the same semantics apply to neuron `restore`. Learning [#learning] | Method | Returns | Description | | ------------------------ | ---------------------- | ----------------------------------- | | `learn(batch, options?)` | `Promise` | Process data (observe → understand) | **`learn()` options:** `{ forceSynthesize?: boolean }` LearnOutput [#learnoutput] Discriminated union — check `status` to determine the outcome: | Status | Fields | Meaning | | ---------------------- | --------------------------------------------------------------- | ---------------------------------------- | | `observed` | `output[], usage?` | Observations buffered, threshold not met | | `synthesized` | `newUnderstanding, significance, evolution, reasoning?, usage?` | Understanding updated | | `observe:dismissed` | `output[], gaps[], usage?` | Data not relevant | | `observe:error` | `error` | Observer failed | | `synthesize:dismissed` | `output, usage?` | LLM chose not to update | | `synthesize:error` | `error` | Synthesizer failed | **`significance`**: `'routine' | 'notable' | 'critical'` Querying [#querying] | Method | Returns | Description | | --------------------------------- | ---------------------------- | ---------------------- | | `query(question, options?)` | `Promise` | Query neuron knowledge | | `queryStream(question, options?)` | `Promise` | Streaming variant | QueryResult [#queryresult] ```typescript { relevant: boolean relevance: number // 0–1 confidence: number // 0–1 insight: string gaps: string usage: TokenUsage } ``` Understanding [#understanding] | Method | Returns | Description | | ------------------------- | ------------------ | ---------------------------------------------------------------- | | `getUnderstanding()` | `Promise` | Current knowledge (`string` for text, `ListItem[]` for list) | | `setUnderstanding(value)` | `Promise` | Set knowledge directly | | `getSummary()` | `Promise` | Prose summary (returns `'(no understanding yet)'` pre-synthesis) | | `hasKnowledge()` | `Promise` | Has any understanding | **Pre-synthesis return values.** Before synthesis has produced any understanding (fresh neuron, or enough `learn()` calls buffered but threshold not yet crossed), `getUnderstanding()` resolves to an empty value of `T` — never `undefined`, `null`, or a throw: * `TextNeuron` → `''` (empty string) * `ListNeuron` → `[]` (empty array) For a clean "is there any understanding yet?" check, prefer `hasKnowledge()` over inspecting the return of `getUnderstanding()`. Buffer [#buffer] | Method | Returns | Description | | --------------------------- | ------------------------------------------------ | ------------------------------------------ | | `getBufferState()` | `Promise<{ count, avgImportance, totalTokens }>` | Pending observation metrics | | `getBufferedObservations()` | `Promise>` | Pending observations only, condensed shape | Observations [#observations] Full-history access to the underlying observation collection. Returns full `ObservationRecord` shape (id, data, importance, status, tokens, created\_at). | Method | Returns | Description | | ------------------------------ | ------------------------------ | ------------------------------------------------------------------------- | | `getObservations(filter?)` | `Promise` | All observations. Optional `{ status: 'pending' \| 'processed' }` filter. | | `setObservations(records)` | `Promise` | Bulk-replace the entire observation collection. | | `updateObservation(id, patch)` | `Promise` | Patch a single observation's fields. | | `removeObservation(id)` | `Promise` | Delete a single observation. | Introspection [#introspection] | Method | Returns | Description | | ----------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------- | | `getHealth()` | `NeuronHealth` | Activation, status, signal thresholds | | `getMetrics()` | `NeuronMetrics` | `{ ingestion: { dismissalRate, ... }, query: { relevanceScores, confidenceScores, gaps } }` | | `getMetadata()` | `NeuronMetadata` | Combined metadata | | `getEvolution()` | `Promise` | Change history | | `getObservationSchema()` | `Record \| null` | JSON Schema for observations | | `getUnderstandingSchema()` | `Record \| null` | JSON Schema for understanding | | `getObserveSystemPrompt()` | `string \| null` | Generated observe system prompt | | `getUnderstandSystemPrompt()` | `string \| null` | Generated understand system prompt | | `getUnderstandThresholds()` | `{ maxObservations?, maxTokens?, minImportance? }` | Current thresholds | Config [#config] | Method | Returns | Description | | ------------------- | ---------------------------- | ---------------------------------- | | `update(updates)` | `Promise<{ changedFields }>` | Replace config, regenerate prompts | | `adjust(directive)` | `Promise` | Incremental LLM-driven adjustment | AdjustResult [#adjustresult] ```typescript { changedFields: string[] adjustedConfig: boolean adjustedUnderstanding: boolean } ``` Identity [#identity] | Property | Type | Description | | ------------------------ | ---------------- | ----------------------------------------------------------------------- | | `id` | `string` | Neuron ID | | `type` | `string` | `'text'` or `'list'` | | `name` | `string` | Display name | | `description` | `string` | What this neuron tracks | | `instructions` | `string` | Shared verbatim instructions (feed both observe and understand) | | `observeInstructions` | `string \| null` | Observe-phase instruction override (`null` = inherit `instructions`) | | `understandInstructions` | `string \| null` | Understand-phase instruction override (`null` = inherit `instructions`) | | `focus` | `string \| null` | Observe-only focus narrowing | | `origin` | `NeuronOrigin` | `'prompt'`, `'developer'`, or `'emergent'` | Config Fields [#config-fields] Accepted by `TextNeuron.create()` / `ListNeuron.create()` (and, where noted, by Brain explicit-neuron configs). Required: `model`, `instructions`, `store`. | Field | Type | Description | | ------------------------ | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `model` | `LanguageModel` | Default model for all phases. | | `instructions` | `string` | Shared verbatim instructions. Inserted directly into the observe and understand prompts — not paraphrased. | | `observeInstructions` | `string \| null` | Optional. Overrides the verbatim instructions for the observe prompt only. | | `understandInstructions` | `string \| null` | Optional. Overrides the verbatim instructions for the understand prompt only. | | `focus` | `string \| null` | Optional. Observe-only — narrows what data is kept. | | `skipObservation` | `boolean` | Optional. Skip the observe phase — data goes straight to the understanding buffer. | | `skipUnderstand` | `boolean` | Optional. Retain observations but never synthesize understanding — not on threshold, not on `forceSynthesize`. No understand prompt is built. Symmetric counterpart of `skipObservation`. | | `observationSchema` | `Record` | Optional. Custom observation JSON Schema — skips LLM schema generation. | | `understandingSchema` | `Record` | Optional. Custom understanding JSON Schema — skips LLM schema generation. | | `onEvent` | `UnifiedHandler` | Optional. Subscribed via `.on(...)` before `init()` runs, so `neuron:init:*` events are observable. Also accepted in `restore()` runtime options. |