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-20250514") .build();This creates a minimal agent with:
- LLM provider (Anthropic)
- In-memory SQLite for memory (Tier 1)
- Direct LLM loop (no reasoning strategy)
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-20250514") .withMemory("1") // Tier 1: FTS5 full-text search .build();Tier 1 gives you working memory, semantic storage, episodic logging, and full-text search — all backed by bun:sqlite.
Tier 2 adds vector embeddings for semantic similarity search (requires an embedding provider).
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-20250514") .withMemory("1") .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-20250514") .withMemory("1") .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 the test provider for deterministic tests:
const agent = await ReactiveAgents.create() .withName("test-agent") .withProvider("test") .withTestResponses({ "capital of France": "Paris is the capital of France.", "quantum": "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");