Skip to content

Your First Agent

Last updated 1 day ago · 5ad31ba

Updated yesterday

"docs: first-timer onboarding fixes — broken first command, zero-key path, funnel order" · 5ad31ba · 2026-07-21

  • ## Adding Safety
  • ## Testing

This guide walks through building a research assistant agent with memory, reasoning, and guardrails.

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)

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:

TierLayers activeWhen to use
"standard"Working + Episodic + FTS5 keyword searchConversational 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.

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.

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
});
const result = await agent.run("Explain the difference between TCP and UDP");
console.log(result.output); // The agent's response
console.log(result.success); // true
console.log(result.metadata); // { duration, cost, tokensUsed, stepsCount }

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);

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.

Use withTestScenario() for deterministic tests — it automatically sets the provider to "test", so no API key or real LLM call is involved:

src/agent.test.ts
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");
});