This guide walks through building a research assistant agent with memory, reasoning, and guardrails.
The front door: createAgent(config)
Section titled “The front door: createAgent(config)”Every agent starts from a declarative config object — the shape you know from the Vercel AI SDK and OpenAI SDK:
import { createAgent } from "reactive-agents";
const agent = await createAgent({ name: "research-assistant", provider: "anthropic", model: "claude-sonnet-4-6",});This creates a minimal agent with:
- LLM provider (Anthropic, Claude Sonnet 4)
- Direct LLM loop (no reasoning strategy, no memory, no tools)
Adding Memory
Section titled “Adding Memory”Memory persists context across conversations:
import { createAgent } from "reactive-agents";
const agent = await createAgent({ name: "research-assistant", provider: "anthropic", model: "claude-sonnet-4-6", memory: { tier: "standard" }, // 4-layer memory (default tier)});The 4-layer memory system has two tiers:
| Tier | Layers active | When to use |
|---|---|---|
"standard" | Working + Episodic + FTS5 keyword search | Conversational agents, default for most apps |
"enhanced" | All 4 layers + vector embeddings (semantic recall) | Research agents, long-running tasks needing semantic similarity |
memory: { tier: "enhanced", dbPath: "./data/memory.db" } // Full 4-layer"enhanced" requires an embedding provider — set EMBEDDING_PROVIDER=openai or EMBEDDING_PROVIDER=ollama in .env.
Adding Reasoning
Section titled “Adding Reasoning”The reasoning layer gives your agent structured thinking:
const agent = await createAgent({ name: "research-assistant", provider: "anthropic", model: "claude-sonnet-4-6", memory: { tier: "standard" }, reasoning: {}, // ReAct loop: Think -> Act -> Observe (pass { defaultStrategy } to switch)});With reasoning enabled, the agent uses a ReAct loop instead of a simple LLM call. It can:
- Break tasks into steps
- Request tool calls
- Observe results and adjust
To switch strategy, pass reasoning: { defaultStrategy: ... } — valid values are "reactive" (the default), "plan-execute-reflect", "reflexion", "tree-of-thought", "adaptive", "direct", "code-action", and "blueprint". See Choosing a Strategy.
Adding Safety
Section titled “Adding Safety”Guardrails protect against prompt injection, PII leakage, and toxic content:
const agent = await createAgent({ name: "research-assistant", provider: "anthropic", model: "claude-sonnet-4-6", memory: { tier: "standard" }, reasoning: {}, guardrails: {}, // Input/output safety costTracking: {}, // Budget controls});Running the Agent
Section titled “Running the Agent”const result = await agent.run("Explain the difference between TCP and UDP");
console.log(result.output); // The agent's responseconsole.log(result.success); // trueconsole.log(result.metadata); // { duration, cost, tokensUsed, stepsCount }Using the Effect API
Section titled “Using the Effect API”For advanced use cases, use the Effect-based API:
import { Effect } from "effect";import { ReactiveAgents } from "reactive-agents";
const program = Effect.gen(function* () { const agent = yield* ReactiveAgents.create() .withName("research-assistant") .withProvider("anthropic") .withReasoning() .buildEffect();
const result = yield* agent.runEffect("Explain quantum entanglement"); return result;});
const result = await Effect.runPromise(program);Lifecycle Hooks
Section titled “Lifecycle Hooks”Observe and modify agent behavior at any phase:
const agent = await ReactiveAgents.create() .withName("research-assistant") .withProvider("anthropic") .withHook({ phase: "think", timing: "after", handler: (ctx) => { console.log(`[think] Response: ${ctx.metadata.lastResponse}`); return ctx; }, }) .build();Effect is optional here — a hook handler can return a plain value (or nothing, to just observe), a Promise, or an Effect; no effect import needed.
Available phases: bootstrap, guardrail, cost-route, strategy-select, think, act, observe, verify, memory-flush, cost-track, audit, complete.
Each phase supports before, after, and on-error timing.
Testing
Section titled “Testing”Use withTestScenario() for deterministic tests — it automatically sets the provider to "test", so no API key or real LLM call is involved:
import { expect, test } from "bun:test";import { ReactiveAgents } from "reactive-agents";
test("answers deterministically", async () => { const agent = await ReactiveAgents.create() .withName("test-agent") .withTestScenario([ { match: "capital of France", text: "Paris is the capital of France." }, { match: "quantum", text: "Quantum mechanics describes nature at the atomic scale." }, ]) .build();
const result = await agent.run("What is the capital of France?"); expect(result.output).toContain("Paris");});