Your First Agent
This guide walks through building a research assistant agent with memory, reasoning, and guardrails.
The Builder Pattern
Section titled “The Builder Pattern”Every agent starts with ReactiveAgents.create():
import { ReactiveAgents } from "reactive-agents";
const agent = await ReactiveAgents.create() .withName("research-assistant") .withProvider("anthropic") .withModel("claude-sonnet-4-6") .build();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:
const agent = await ReactiveAgents.create() .withName("research-assistant") .withProvider("anthropic") .withModel("claude-sonnet-4-6") .withMemory() // tier: "standard" (default) .build();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 |
.withMemory({ 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 ReactiveAgents.create() .withName("research-assistant") .withProvider("anthropic") .withModel("claude-sonnet-4-6") .withMemory() .withReasoning() // ReAct loop: Think -> Act -> Observe .build();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
Adding Safety
Section titled “Adding Safety”Guardrails protect against prompt injection, PII leakage, and toxic content:
const agent = await ReactiveAgents.create() .withName("research-assistant") .withProvider("anthropic") .withModel("claude-sonnet-4-6") .withMemory() .withReasoning() .withGuardrails() // Input/output safety .withCostTracking() // Budget controls .build();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";
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 Effect.succeed(ctx); }, }) .build();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:
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");