Skip to content

ReactiveAgentBuilder

The ReactiveAgentBuilder is the primary entry point for creating agents. It provides a fluent API for composing capabilities.

Creates a new builder instance.

import { ReactiveAgents } from "reactive-agents";
// or: import { ReactiveAgents } from "@reactive-agents/runtime";
const builder = ReactiveAgents.create();

All methods return this for chaining.

MethodSignatureDescription
withName(name: string) => thisSet the agent’s name (used as agentId)
withPersona(persona: AgentPersona) => thisSet a structured persona for behavior steering. Fields: { name?, role?, background?, instructions?, tone? }
withSystemPrompt(prompt: string) => thisSet a custom system prompt. When combined with persona, the persona is prepended
MethodSignatureDescription
withModel(model: string) => thisSet the LLM model (e.g., "claude-sonnet-4-20250514")
withProvider(provider: "anthropic" | "openai" | "ollama" | "gemini" | "litellm" | "test") => thisSet the LLM provider
MethodSignatureDescription
withMemory(tier: "1" | "2") => thisEnable memory. Tier 1: FTS5. Tier 2: FTS5 + KNN vectors
MethodSignatureDescription
withMaxIterations(n: number) => thisMax agent loop iterations (default: 10)
MethodDescription
withGuardrails()Injection, PII, toxicity detection on input
withKillSwitch()Per-agent and global emergency halt capability via KillSwitchService
withBehavioralContracts(contract)Enforce typed behavioral boundaries: deniedTools, allowedTools, maxIterations. Throws BehavioralContractError on violation
withVerification()Semantic entropy, fact decomposition, and multi-source (LLM + Tavily) on output
withCostTracking()Budget enforcement, complexity routing, semantic caching
withReasoning(options?)Structured reasoning (ReAct, Reflexion, Plan-Execute, ToT, Adaptive). Options: { defaultStrategy?, strategies?, adaptive?: { enabled?: boolean, learning?: boolean } }. Set adaptive.enabled: true to auto-select strategy per task
withTools(options?)Tool registry with sandboxed execution (subprocess isolation via Bun.spawn). Options: { tools?: [{ definition, handler }], resultCompression?: ResultCompressionConfig }. See Tool Result Compression
withIdentity()Agent certificates (real Ed25519 keys) and RBAC
withObservability(options?)Distributed tracing, metrics, structured logging. Options: { verbosity?: "minimal" | "normal" | "verbose" | "debug", live?: boolean, file?: string }
withInteraction()5 interaction modes with adaptive transitions
withPrompts(options?)Version-controlled prompt template engine. Options: { templates?: PromptTemplate[] }
withOrchestration()Multi-agent workflow coordination
withSelfImprovement()Cross-task self-improvement: logs StrategyOutcome per task and retrieves relevant past outcomes at bootstrap to guide strategy selection
withAudit()Compliance audit trail logging
MethodSignatureDescription
withA2A(config: { port?: number }) => thisEnable A2A server capability
withAgentTool(name: string, agent: { name: string; description?: string; persona?: AgentPersona; systemPrompt?: string; ... }) => thisRegister a local agent as a callable tool. Subagent personas are supported for specialized behavior
withDynamicSubAgents(options?: { maxIterations?: number }) => thisEnable spawn-agent tool to dynamically create subagents at runtime with optional persona parameters
withRemoteAgent(name: string, remoteUrl: string) => thisRegister a remote A2A agent as a callable tool
MethodSignatureDescription
withMCP(config: MCPServerConfig | MCPServerConfig[]) => thisConnect to MCP servers. Accepts a single config or array. Automatically enables .withTools().
FieldTypeTransportDescription
namestringallUnique name for this server. Tool names are prefixed {name}/
transport"stdio" | "streamable-http" | "sse" | "websocket"allProtocol to use. Use "streamable-http" for modern remote servers, "stdio" for local subprocesses
commandstringstdioExecutable to launch ("bunx", "docker", "python", absolute path, etc.)
argsstring[]stdioArguments passed to command. Includes package names, flags, Docker image, etc.
envRecord<string, string>stdioExtra env vars merged on top of the parent process environment. Use for per-server secrets
cwdstringstdioWorking directory for the subprocess. Defaults to parent process cwd
endpointstringstreamable-http, sse, websocketHTTP/WebSocket URL ("https://mcp.example.com", "ws://localhost:8000/mcp")
headersRecord<string, string>streamable-http, sseHTTP headers sent on every request. Use for Authorization, x-api-key, etc.

Examples:

// stdio: npm package via bunx
{ name: "filesystem", transport: "stdio", command: "bunx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "."] }
// stdio: with per-server secret
{ name: "github", transport: "stdio", command: "bunx",
args: ["-y", "@modelcontextprotocol/server-github"],
env: { GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GH_TOKEN ?? "" } }
// stdio: Docker container with networking
{ name: "my-server", transport: "stdio", command: "docker",
args: ["run", "-i", "--rm", "--network", "host", "ghcr.io/org/mcp-server"] }
// streamable-http: modern cloud server with Bearer auth
{ name: "stripe", transport: "streamable-http",
endpoint: "https://mcp.stripe.com",
headers: { Authorization: `Bearer ${process.env.STRIPE_KEY}` } }
// sse: legacy remote server with API key
{ name: "legacy", transport: "sse",
endpoint: "https://api.example.com/mcp",
headers: { "x-api-key": process.env.API_KEY ?? "" } }
MethodSignatureDescription
withHook(hook: LifecycleHook) => thisRegister a lifecycle hook
interface LifecycleHook {
phase: ExecutionPhase;
timing: "before" | "after" | "on-error";
handler: (ctx: ExecutionContext) => Effect.Effect<ExecutionContext>;
}
type ExecutionPhase =
| "bootstrap" | "guardrail" | "cost-route" | "strategy-select"
| "think" | "act" | "observe"
| "verify" | "memory-flush" | "cost-track" | "audit" | "complete";
MethodSignatureDescription
withTestResponses(responses: Record<string, string>) => thisSet canned test responses (uses "test" provider)
MethodSignatureDescription
withLayers(layers: Layer<any, any>) => thisAdd custom Effect Layers to the runtime
async build(): Promise<ReactiveAgent>

Creates the agent, resolving the full Layer stack. Returns a ReactiveAgent instance.

buildEffect(): Effect.Effect<ReactiveAgent, Error>

Creates the agent as an Effect for composition in Effect programs.

runOnce(input: string): Promise<AgentResult>

Section titled “runOnce(input: string): Promise<AgentResult>”

Builds the agent, runs a single task, disposes all resources, and returns the result — in one call. Use this for one-shot scripts where you don’t need to hold a reference to the agent.

const result = await ReactiveAgents.create()
.withProvider("anthropic")
.withReasoning()
.runOnce("Summarize the README in one paragraph");
console.log(result.output);
// Resources are already cleaned up

The facade returned by build().

Agents that use MCP servers (stdio transport) or other subprocess-based resources must be disposed after use, otherwise the process will hang on open pipes. Three patterns are available:

Uses the Explicit Resource Management protocol introduced in TypeScript 5.2. The agent is disposed automatically when the enclosing block exits, whether normally or via an exception.

await using agent = await ReactiveAgents.create()
.withProvider("anthropic")
.withMCP({ name: "filesystem", transport: "stdio", command: "npx", args: ["@modelcontextprotocol/server-filesystem", "."] })
.withReasoning()
.build();
const result = await agent.run("List the project files.");
console.log(result.output);
// agent.dispose() is called automatically here

Requires "lib": ["ES2022", "ESNext"] or "target": "ES2022" in your tsconfig.json.

If you only need a single result and don’t want to manage the agent handle at all, use the builder’s runOnce() method. It builds, runs, and disposes in one call.

const result = await ReactiveAgents.create()
.withProvider("anthropic")
.withMCP({ name: "filesystem", transport: "stdio", command: "npx", args: ["@modelcontextprotocol/server-filesystem", "."] })
.withReasoning()
.runOnce("List the project files.");
console.log(result.output);
// Resources already cleaned up

Call dispose() manually in a finally block when you need to reuse the agent across multiple calls before cleaning up.

const agent = await ReactiveAgents.create()
.withProvider("anthropic")
.withReasoning()
.build();
try {
const r1 = await agent.run("First task");
const r2 = await agent.run("Second task");
console.log(r1.output, r2.output);
} finally {
await agent.dispose();
}
PatternWhen to use
await usingGeneral purpose — automatic cleanup, works with try/catch
runOnce()Single-shot scripts and one-liners
dispose()Multiple sequential runs before teardown

Run a task with the given input. Returns the result with output and metadata.

runEffect(input: string): Effect.Effect<AgentResult, Error>

Section titled “runEffect(input: string): Effect.Effect<AgentResult, Error>”

Run a task as an Effect for composition.

Cancel a running task by its ID.

getContext(taskId: string): Promise<unknown>

Section titled “getContext(taskId: string): Promise<unknown>”

Get the execution context of a running or completed task.

Requires .withKillSwitch() to be enabled.

MethodSignatureDescription
pause()() => Promise<void>Pause execution at the next phase boundary. Blocks until resume() is called
resume()() => Promise<void>Resume a paused agent
stop(reason)(reason: string) => Promise<void>Graceful stop — signals intent; agent completes current phase then exits
terminate(reason)(reason: string) => Promise<void>Immediate termination (also triggers kill switch)

Requires an EventBus to be wired (any feature that enables it, e.g., .withObservability()).

subscribe is overloaded — pass a tag for type-narrowed access, or omit it for a catch-all:

// ── Tag-filtered: event is narrowed to the exact payload type ──────────────
const unsub = await agent.subscribe("AgentCompleted", (event) => {
// TypeScript knows event has: taskId, agentId, success, totalIterations,
// totalTokens, durationMs — no _tag check, no cast needed
console.log(`Done in ${event.durationMs}ms, ${event.totalTokens} tokens`);
});
unsub();
// ── Catch-all: receives the full AgentEvent union ──────────────────────────
const unsub2 = await agent.subscribe((event) => {
// Discriminate via event._tag when handling multiple types in one handler
if (event._tag === "ToolCallStarted") console.log(`Tool: ${event.toolName}`);
if (event._tag === "LLMRequestStarted") console.log(`Model: ${event.model}`);
});
unsub2();

TypeScript signatures:

// Tag-filtered — event type is automatically narrowed
subscribe<T extends AgentEventTag>(
tag: T,
handler: (event: Extract<AgentEvent, { _tag: T }>) => void,
): Promise<() => void>;
// Catch-all — full AgentEvent union
subscribe(handler: (event: AgentEvent) => void): Promise<() => void>;

The AgentEventTag and TypedEventHandler<T> helpers are exported from @reactive-agents/core for use in your own service code:

import type { AgentEventTag, TypedEventHandler } from "@reactive-agents/core";
// Build a typed handler outside of an inline callback
const onStepComplete: TypedEventHandler<"ReasoningStepCompleted"> = (event) => {
// event.thought, event.action, event.observation — all typed
return Effect.log(`Step ${event.step}: ${event.thought ?? event.action}`);
};
yield* eventBus.on("ReasoningStepCompleted", onStepComplete);

Subscribable event tags:

TagPayload fields
AgentStartedtaskId, agentId, provider, model, timestamp
AgentCompletedtaskId, agentId, success, totalIterations, totalTokens, durationMs
LLMRequestStartedtaskId, requestId, model, provider, contextSize
LLMRequestCompletedtaskId, requestId, tokensUsed, durationMs
ReasoningStepCompletedtaskId, strategy, step, thought|action|observation
ToolCallStartedtaskId, toolName, callId
ToolCallCompletedtaskId, toolName, callId, success, durationMs
FinalAnswerProducedtaskId, strategy, answer, iteration, totalTokens
GuardrailViolationDetectedtaskId, violations, score, blocked
ExecutionPhaseEnteredtaskId, phase
ExecutionPhaseCompletedtaskId, phase, durationMs
ExecutionHookFiredtaskId, phase, timing
ExecutionCancelledtaskId
MemoryBootstrappedagentId, tier
MemoryFlushedagentId
AgentPausedagentId, taskId
AgentResumedagentId, taskId
AgentStoppedagentId, taskId, reason
TaskCompletedtaskId, success
interface AgentResult {
output: string; // The agent's response
success: boolean; // Whether the task completed successfully
taskId: string; // Unique task identifier
agentId: string; // Agent that ran the task
metadata: {
duration: number; // Execution time in milliseconds
cost: number; // Estimated cost in USD
tokensUsed: number; // Total tokens consumed across all LLM calls
strategyUsed?: string; // Reasoning strategy used (if reasoning enabled)
stepsCount: number; // Number of reasoning steps / iterations
};
}
import { ReactiveAgents } from "reactive-agents";
import { Effect } from "effect";
// await using — agent is disposed automatically when this block exits
await using agent = await ReactiveAgents.create()
.withName("research-assistant")
.withProvider("anthropic")
.withModel("claude-sonnet-4-20250514")
.withPersona({
role: "CRISPR Research Specialist",
background: "Expert in gene editing and molecular biology",
instructions: "Provide detailed technical analysis with citations",
tone: "professional",
})
.withMemory("1")
.withReasoning({ defaultStrategy: "adaptive" })
.withTools() // Built-in tools (web search, file I/O, etc.)
.withGuardrails()
.withVerification()
.withCostTracking()
.withObservability()
.withAudit()
.withInteraction()
.withMaxIterations(15)
.withHook({
phase: "think",
timing: "after",
handler: (ctx) => {
console.log(`Iteration ${ctx.iteration}, tokens: ${ctx.tokensUsed}`);
return Effect.succeed(ctx);
},
})
.build();
// Run a task
const result = await agent.run("Research the latest advances in CRISPR gene editing");
console.log(result.output);
console.log(`Cost: $${result.metadata.cost.toFixed(4)}`);
console.log(`Tokens: ${result.metadata.tokensUsed}`);
console.log(`Strategy: ${result.metadata.strategyUsed}`);
// agent.dispose() is called automatically here