Skip to content

ReactiveAgentBuilder

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

CategoryMethods
ReactiveAgents factorycreate, fromConfig, fromJSON
Core IdentitywithName, withPersona, withSystemPrompt, withEnvironment
Model & ProviderwithModel, withProvider
Reasoning & ContextwithReasoning, withMemory, withContextProfile, withMaxIterations, withMinIterations
Tools & MCPwithTools, withRequiredTools, withMCP, withMetaTools, withSkills, withAgentTool, withDynamicSubAgents, withRemoteAgent
Observability & TelemetrywithObservability, withTelemetry, withCortex, withStreaming, withLogging, withEvents
Safety & ResiliencewithGuardrails, withKillSwitch, withBehavioralContracts, withVerification, withCircuitBreaker, withRateLimiting, withIdentity
Cost & PerformancewithCostTracking, withModelPricing, withDynamicPricing, withCacheTimeout, withRetryPolicy, withTimeout
Lifecycle & HookswithHook, withHealthCheck, withErrorHandler, withFallbacks, withAudit
AdvancedwithOrchestration, withA2A, withGateway, withReactiveIntelligence, withPrompts, withInteraction, withDocuments, withTaskContext, withLayers
Building & Runningbuild, buildEffect, runOnce
Agent Methodsrun, runStream, chat, session, health, cancel, pause, resume, dispose
Result ReferenceAgentResult, AgentDebrief, stream event types
APIDescription
ReactiveAgents.create()New empty builder (defaults: name: "agent", provider: "test").
ReactiveAgents.fromConfig(config)Async — rebuild a builder from an AgentConfig object (agentConfigToBuilder).
ReactiveAgents.fromJSON(json)Async — parse JSON → validate → same as fromConfig.
import { ReactiveAgents } from 'reactive-agents'
// or: import { ReactiveAgents } from "@reactive-agents/runtime";
const builder = ReactiveAgents.create()

On a configured builder:

  • toConfig()AgentConfig (plain object, JSON-serializable except documented exceptions).
  • Use agentConfigToJSON / agentConfigFromJSON from reactive-agents or @reactive-agents/runtime for string round-trips.

All chain methods return this unless noted.

MethodSignatureDescription
withName(name: string) => thisDisplay name / agentId basis
withPersona(persona: AgentPersona) => thisStructured steering: { name?, role?, background?, instructions?, tone? }
withSystemPrompt(prompt: string) => thisCustom system prompt; if persona is set, persona text is prepended
withEnvironment(context: Record<string, string>) => thisExtra key/value context merged into the system prompt (framework already injects date/time/tz/platform)
MethodSignatureDescription
withModel(model: string) => thisSet the LLM model by name (e.g., "claude-sonnet-4-20250514")
withModel(params: ModelParams) => thisSet model with advanced parameters: thinking, temperature, maxTokens
withProvider(provider: "anthropic" | "openai" | "ollama" | "gemini" | "litellm" | "test") => thisSet the LLM provider
interface ModelParams {
model: string // Model identifier (provider-specific)
thinking?: boolean // Enable thinking/reasoning mode (auto-detected if omitted)
temperature?: number // Sampling temperature 0.0–1.0
maxTokens?: number // Maximum output tokens
}
// String form — simple model selection
.withModel("claude-opus-4-20250514")
// ModelParams form — local model with thinking mode
.withModel({ model: "qwen3:14b", thinking: true, temperature: 0.7 })
// ModelParams form — cap token budget
.withModel({ model: "gpt-4o", maxTokens: 2048 })
MethodSignatureDescription
withMemory(options?: MemoryOptions | "1" | "2") => thisEnable memory. Prefer .withMemory() or .withMemory({ tier: "enhanced", ... }). Strings "1" / "2" still work with a deprecation warning ("1" → standard, "2" → enhanced)
FieldTypeDefault / notes
tier"standard" | "enhanced""standard" — enhanced = 4-layer memory + embeddings
dbPathstringSQLite path (default under .reactive-agents/memory/{agentId}/)
maxEntriesnumberCompaction cap
capacitynumberWorking memory slots (default 7)
evictionPolicy"fifo" | "lru" | "importance"Working set eviction
retainDaysnumberEpisodic retention
importanceThresholdnumberSemantic inclusion threshold
MethodSignatureDescription
withMaxIterations(n: number) => thisMax agent loop iterations (default: 10)
withMinIterations(n: number) => thisMinimum iterations before final-answer is permitted — prevents fast-path exit on complex tasks
withContextProfile(profile: Partial<ContextProfile>) => thisModel-adaptive context overrides: compaction thresholds, tool result size limits, budget
withStrictValidation() => thisThrow at build time if required config is missing (provider, model, etc.)
withTimeout(ms: number) => thisExecution timeout in milliseconds. Throws TimeoutError if exceeded
withRetryPolicy(policy: RetryPolicy) => thisRetry on transient LLM failures. { maxRetries: number, backoffMs: number }
withCacheTimeout(ms: number) => thisSemantic cache TTL in milliseconds. Entries older than this are evicted
FieldTypeDescription
tier"local" | "mid" | "large" | "frontier"Model tier — controls which defaults are applied
budgetTokensnumberMax tokens to include in the context window
toolResultMaxCharsnumberTruncate tool results beyond this length
compactionLevel"full" | "summary" | "grouped" | "dropped"How aggressively to compact older steps
maxStepsBeforeCompactionnumberSteps to keep in full detail before compacting
// Lean context for local small models
.withContextProfile({ tier: "local" })
// Manual overrides for a specific task
.withContextProfile({
budgetTokens: 4000,
toolResultMaxChars: 800,
compactionLevel: "grouped",
})

See Context Engineering for full tier defaults.

MethodDescription
withGuardrails(options?)Toggle detectors: { injection?, pii?, toxicity?, customBlocklist? }. All default on when guardrails are enabled.
withKillSwitch()Pause / resume / stop / terminate via KillSwitchService
withBehavioralContracts(contract)Rules such as deniedTools, allowedTools, maxIterations, etc.
withVerification(options?)Post-output checks — toggles and thresholds: semanticEntropy, factDecomposition, multiSource, hallucinationDetection, passThreshold, …
withCostTracking(options?)Budgets in USD: { perRequest?, perSession?, daily?, monthly? } plus cost estimation / routing
withModelPricing(registry)Per-model $/1M tokens: { "model-id": { input, output } }
withDynamicPricing(provider)Remote pricing (openRouterPricingProvider, etc.) fetched at build time
withCircuitBreaker(config?)LLM call circuit breaker (@reactive-agents/llm-provider CircuitBreakerConfig)
withRateLimiting(config?)Throttle LLM requests (requestsPerMinute, tokensPerMinute, concurrency, …)
withReasoning(options?)Strategies + ICS — see ReasoningOptions
withTools(options?)Tool layer — see ToolsOptions below
withDocuments(docs)Chunk + index DocumentSpec[] for RAG (rag-search). Enables tools if needed
withRequiredTools(config)Tools that must run before success — { tools?, adaptive?, maxRetries? }. When adaptive: true, the framework also auto-sets a per-tool call budget of 3 for search-type tools to prevent infinite research loops.
withIdentity()Ed25519 identity + RBAC
withObservability(options?)Metrics dashboard, tracing, verbosity. Options: verbosity ("minimal"|"normal"|"verbose"|"debug"), live (stream phase events), file (JSONL path), logPrefix, logModelIO (when true or when verbosity: "debug", logs the complete FC conversation thread with role labels [USER]/[ASSISTANT]/[TOOL] and raw LLM response for every iteration — essential for debugging prompt issues). Note: observability is enabled at "normal" verbosity by default — you only need .withObservability() to customize the verbosity level or output format.
withCortex(url?)Enable best-effort Cortex reporting. Streams all EventBus events to the Cortex local studio over WebSocket (/ws/ingest). URL priority: explicit url arg → CORTEX_URL env → http://localhost:4321. Connection is non-blocking — if Cortex is unreachable the agent continues normally. See Cortex Studio for the full feature reference.
withStreaming(options?)Default density for agent.runStream(): { density?: "tokens" | "full" }
withTelemetry(config?)Opt-in run telemetry / privacy modes (@reactive-agents/observability TelemetryConfig; default mode isolated if omitted)
withInteraction()Collaboration / approval flows
withPrompts(options?){ templates?: PromptTemplate[] }
withOrchestration()Multi-agent workflows
withExperienceLearning()ExperienceStore cross-agent tips
withMemoryConsolidation(config?)Background consolidation: { threshold?, decayFactor?, pruneThreshold? }
withSelfImprovement()Strategy outcome logging for later bootstrap hints
withAudit()Audit trail
withEvents()Ensures EventBus wiring for agent.subscribe()
withGateway(options?)Heartbeats, crons, webhooks, policies, port, channels, …
withErrorHandler(handler)Observe-only callback on agent.run() failures — does not swallow errors
withFallbacks(config){ providers?, models?, errorThreshold? } fallback chain
withLogging(config)makeLoggerService{ level?, format?, output?: "console" | "file" | WritableStream, filePath?, maxFileSizeBytes?, maxFiles? }
withHealthCheck()Enables agent.health()
withVerificationStep(config?)Post-answer LLM self-review. { mode: "reflect" | "loop", prompt? }. Reflect mode adds one LLM confirmation call; loop mode (V1.1) re-enters the ReAct loop
withOutputValidator(fn, opts?)Validate output before accepting. fn(output) => { valid, feedback? }. Failed validation injects feedback and retries (opts.maxRetries, default 2)
withCustomTermination(fn)Re-run until fn({ output }) === true, up to 3 additional times. For domain-specific completion criteria
withTaskContext(record)Record<string, string> of background facts injected into reasoning context — distinct from system prompt instructions
withProgressCheckpoint(n, opts?)Store checkpoint config every N iterations. { autoResume? }. PlanStore write execution is V1.1
withReactiveIntelligence(false)Disable the Reactive Intelligence layer (enabled by default).
withReactiveIntelligence(options?)Entropy, controller, telemetry, hooks (onEntropyScored, onControllerDecision, …), constraints, autonomy. See Reactive Intelligence
withSkills(config?){ paths?, packages?, evolution?: { mode?, refinementThreshold?, rollbackOnRegression? }, overrides? }
withMetaTools(config?)Conductor meta-tools; pass false to turn off defaults when using .withTools(). See MetaToolsConfig
FieldDescription
tools{ definition: ToolDefinition, handler: (args) => Effect.Effect<unknown> }[] — custom tools (handlers return Effect)
resultCompressionResultCompressionConfig — previews, overflow keys, transforms
allowedToolsIf set, only these tool names are exposed to the model (others filtered)
adaptiveAdaptive tool listing from task text (heuristic), reduces noise for small models
FieldDescription
brief, find, pulse, recallEnable each Conductor meta-tool
harnessSkillboolean, path string, or { frontier?, local? } for harness skill source
findConfig, pulseConfig, recallConfigFine-tuning (scopes, previews, LLM pulse behavior, …)
interface ReasoningOptions {
/**
* Which strategy to use. Defaults to "reactive".
* "adaptive" requires adaptive.enabled: true.
*/
defaultStrategy?:
| 'reactive'
| 'reflexion'
| 'plan-execute-reflect'
| 'tree-of-thought'
| 'adaptive'
/**
* Per-strategy overrides (iterations, temperatures, plan knobs, etc.).
* Each bundle may also set ICS fields (`synthesis`, `synthesisModel`, `synthesisProvider`,
* `synthesisStrategy`, `synthesisTemperature`) — they override the top-level synthesis
* options for that strategy only (see Intelligent Context Synthesis).
*/
strategies?: Partial<{
reactive: ReasoningConfig['strategies']['reactive'] &
StrategySynthesisFields
planExecute: ReasoningConfig['strategies']['planExecute'] &
StrategySynthesisFields
treeOfThought: ReasoningConfig['strategies']['treeOfThought'] &
StrategySynthesisFields
reflexion: ReasoningConfig['strategies']['reflexion'] &
StrategySynthesisFields
}>
/** Adaptive strategy config. Must set enabled: true when defaultStrategy is "adaptive". */
adaptive?: {
enabled?: boolean // Required for adaptive strategy
learning?: boolean // Enable cross-run learning (default: false)
}
/** Max iterations of the reasoning loop (default: 10). */
maxIterations?: number
/**
* Automatically switch to a better-suited strategy when the current one appears stuck
* (repeated tool calls, repeated thoughts, or consecutive think-only steps).
* Default: false.
*/
enableStrategySwitching?: boolean
/**
* Maximum number of strategy switches allowed in a single run.
* Default: 1.
*/
maxStrategySwitches?: number
/**
* When set, bypasses the LLM evaluator and always switches to this strategy on loop
* detection. Useful when you want deterministic switching without the extra LLM call.
* Example: "plan-execute-reflect"
*/
fallbackStrategy?: string
/** ICS default mode: auto (heuristic), fast (templates), deep (LLM), custom, or off. */
synthesis?: 'auto' | 'fast' | 'deep' | 'custom' | 'off'
/** Model for deep synthesis when different from the executing model. */
synthesisModel?: string
/** Provider for the synthesis model when different from the executing provider. */
synthesisProvider?: string
/** Custom synthesis pipeline when `synthesis: "custom"`. */
synthesisStrategy?: SynthesisStrategy
/** Temperature for deep synthesis LLM calls. */
synthesisTemperature?: number
}
/** ICS-only fields allowed on each `strategies.*` bundle (merged with top-level synthesis). */
interface StrategySynthesisFields {
synthesis?: 'auto' | 'fast' | 'deep' | 'custom' | 'off'
synthesisModel?: string
synthesisProvider?: string
synthesisStrategy?: SynthesisStrategy
synthesisTemperature?: number
}

Per-strategy objects under strategies also accept strategy-specific fields from @reactive-agents/reasoning (for example kernelMaxIterations on the reflexion bundle).

At runtime, ReasoningOptions may also include a non-JSON synthesisStrategy function when using synthesis: "custom" (omitted from toConfig() / JSON).

Examples:

// Default: ReAct with no options
.withReasoning()
// Switch to Plan-Execute-Reflect strategy
.withReasoning({ defaultStrategy: "plan-execute-reflect" })
// Adaptive strategy (must set adaptive.enabled)
.withReasoning({ defaultStrategy: "adaptive", adaptive: { enabled: true } })
// Auto-switch when stuck, up to 2 times, via LLM evaluator
.withReasoning({ enableStrategySwitching: true, maxStrategySwitches: 2 })
// Auto-switch deterministically (no extra LLM call) to plan-execute-reflect
.withReasoning({ enableStrategySwitching: true, fallbackStrategy: "plan-execute-reflect" })
// ICS: fast templates globally, but deep LLM synthesis when running ReAct
.withReasoning({
synthesis: "fast",
strategies: { reactive: { synthesis: "deep", synthesisModel: "claude-3-5-haiku-20241022" } },
})

When enableStrategySwitching is active, two EventBus events are emitted around each switch:

  • StrategySwitchEvaluated — after the evaluator runs, before the switch (includes willSwitch, rationale, recommendedStrategy)
  • StrategySwitched — after the new strategy takes over (includes fromStrategy, toStrategy, switchNumber, stepsCarriedOver)

See Automatic Strategy Switching for full details on loop detection triggers, handoff context, and EventBus subscription examples.

interface RequiredToolsConfig {
/** Static list of tool names the agent MUST call before answering. */
tools?: string[]
/** Enable adaptive inference — LLM analyzes task + tools to determine required tools. */
adaptive?: boolean
/** Number of retry loops if required tools are missed (default: 2). */
maxRetries?: number
}

Examples:

// Static required tools — agent must call web-search before answering
.withRequiredTools({ tools: ["web-search"] })
// Adaptive inference — LLM determines which tools are required per-task
.withRequiredTools({ adaptive: true })
// Both — static list as baseline, adaptive for additional inference
.withRequiredTools({ tools: ["web-search"], adaptive: true, maxRetries: 3 })

When adaptive: true, the framework calls the LLM with the task description and available tool schemas to infer which tools are required. The inferred list is merged with any static tools list. A hallucination guard ensures only actual tool names are included.

MethodSignatureDescription
withA2A(options?: A2AOptions) => thisA2A JSON-RPC server — port (default 3000), basePath (default /)
withAgentTool(name: string, agent: { name: string; description?: string; provider?: string; model?: string; tools?: string[]; maxIterations?: number; systemPrompt?: string; persona?: AgentPersona }) => thisStatic sub-agent as a tool
withDynamicSubAgents(options?: { maxIterations?: number }) => thisspawn-agent for runtime sub-agents
withRemoteAgent(name: string, remoteUrl: string) => thisRemote A2A agent as a 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

Use the exported LifecycleHook type from @reactive-agents/runtime. Handlers return Effect.Effect<ExecutionContext, ExecutionError> (import Effect from "effect").

LifecyclePhase values include: bootstrap, guardrail, cost-route, strategy-select, think, act, observe, verify, memory-flush, cost-track, audit, complete.

MethodSignatureDescription
withTestScenario(turns: TestTurn[]) => thisDeterministic test provider. Forces provider: "test". Turns are TestTurn values from @reactive-agents/llm-provider: { text? }, { toolCall? }, { toolCalls? }, { json? }, { error? }, optional match? (regex) per turn

See Testing agents and Configuration for examples.

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.

runStream(input, options?): AsyncGenerator<AgentStreamEvent>

Section titled “runStream(input, options?): AsyncGenerator<AgentStreamEvent>”

Token and phase streaming. Options: { density?: "tokens" | "full", signal?: AbortSignal }. Default density comes from .withStreaming() or "tokens". Ends with StreamCompleted, StreamError, or StreamCancelled.

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

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

Run a task as an Effect for composition (see Effect-TS primer).

MethodDescription
registerTool(definition, handler)Register a tool after build; handler returns Effect
unregisterTool(name)Remove a previously registered custom tool
ingest(content, { source, format?, ... })Ingest text into RAG when tools / withDocuments enabled

chat(message: string, options?: ChatOptions): Promise<ChatReply>

Section titled “chat(message: string, options?: ChatOptions): Promise<ChatReply>”

Conversational Q&A with the agent. Routes automatically:

  • Direct LLM path — for questions, summaries, and status checks (fast, no tools)
  • ReAct loop path — for tool-capable requests (search, fetch, write, create, etc.)

Injects context from the last run’s debrief so the agent can answer “what did you do last time?” accurately.

const reply = await agent.chat('What did you accomplish last run?')
console.log(reply.message)
// Force tool-capable path
const reply2 = await agent.chat('Search for the latest AI news', {
useTools: true,
})
console.log(reply2.toolsUsed) // ["web-search"]
interface ChatReply {
message: string
toolsUsed?: string[] // Set when tools were invoked
fromMemory?: boolean // Set when answered from debrief context
}
interface ChatOptions {
useTools?: boolean // Override auto-routing
maxIterations?: number // Cap for tool-capable path (default: 5)
}

session(options?: SessionOptions): AgentSession

Section titled “session(options?: SessionOptions): AgentSession”

Start a multi-turn conversation session with auto-managed history. Conversation history is forwarded to the LLM on every subsequent turn.

Pass { persist: true, id: "my-session" } to persist conversation history to SQLite via SessionStoreService. Persistent sessions survive process restarts and can be resumed by passing the same id.

// In-memory session (default)
const session = agent.session()
const r1 = await session.chat('What are the key findings from your last run?')
const r2 = await session.chat('Tell me more about the first finding')
// r2 has full context of r1
// Persisted session — survives process restarts
const persistedSession = agent.session({
persist: true,
id: 'research-session-1',
})
await persistedSession.chat('Start researching quantum computing')
// On next run, restore the session:
const restoredSession = agent.session({
persist: true,
id: 'research-session-1',
})
await restoredSession.chat('Continue where we left off')
const history = session.history() // ChatMessage[]
await session.end() // Clears history (and DB record if persisted)
interface SessionOptions {
persist?: boolean // Persist history to SQLite via SessionStoreService
id?: string // Session ID for persistence (auto-generated if omitted)
}
interface AgentSession {
chat(message: string): Promise<ChatReply>
history(): ChatMessage[]
end(): Promise<void>
}

Requires .withHealthCheck() to be enabled.

Returns a structured health snapshot of all agent subsystems. Use for readiness probes, liveness checks, and monitoring dashboards.

const health = await agent.health()
console.log(health.status) // "healthy" | "degraded" | "unhealthy"
for (const check of health.checks) {
console.log(`${check.name}: ${check.status}${check.message}`)
}
interface HealthResult {
status: 'healthy' | 'degraded' | 'unhealthy'
checks: Array<{
name: string
status: 'pass' | 'warn' | 'fail'
message?: string
durationMs?: number
}>
}

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 { Effect } from 'effect'
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
GatewayStartedagentId, timestamp
GatewayStoppedagentId, reason
GatewayEventReceivedagentId, eventId, source, category
ProactiveActionInitiatedagentId, eventId, action
ProactiveActionCompletedagentId, eventId, success, durationMs
ProactiveActionSuppressedagentId, eventId, reason
PolicyDecisionMadeagentId, eventId, action, policyTag
HeartbeatSkippedagentId, consecutiveSkips, reason
EventsMergedagentId, mergedCount, mergeKey
BudgetExhaustedagentId, tokensUsed, dailyBudget
StrategySwitchEvaluatedtaskId, fromStrategy, recommendedStrategy, rationale, willSwitch
StrategySwitchedtaskId, fromStrategy, toStrategy, switchNumber, stepsCarriedOver
ProviderFallbackActivatedtaskId, fromProvider, toProvider, reason, attemptNumber
DebriefCompletedtaskId, agentId, debrief
ChatTurntaskId, sessionId, role, content, routedVia, tokensUsed?
MemorySnapshottaskId, iteration, working, episodicCount, semanticCount, skillsActive
ContextPressuretaskId, utilizationPct, tokensUsed, tokensAvailable, level
AgentHealthReportagentId, status, checks[], uptimeMs
AgentConnectedagentId, runId, cortexUrl
AgentDisconnectedagentId, runId, reason
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
confidence?: 'high' | 'medium' | 'low' // From final-answer tool
}
// Enriched fields (present when reasoning is enabled)
format?: 'text' | 'json' | 'markdown' | 'csv' | 'html' // Output format declared by agent
terminatedBy?:
| 'final_answer_tool' // Exited via the final-answer tool call
| 'final_answer' // Exited via inline FINAL ANSWER: text
| 'max_iterations' // Hit the iteration/llmCalls ceiling
| 'end_turn' // LLM stopped generating (no tool call, no final answer)
| 'llm_error' // LLM request or stream failed (provider error, network, etc.)
llmCalls?: number // Number of LLM calls made during the kernel loop (available when reasoning is enabled)
// Debrief (present when .withMemory() + .withReasoning() are enabled)
debrief?: AgentDebrief
}

A structured post-run synthesis produced automatically when memory is enabled:

interface AgentDebrief {
outcome: 'success' | 'partial' | 'failed'
summary: string // 2-3 sentence narrative
keyFindings: string[]
errorsEncountered: string[]
lessonsLearned: string[] // Auto-fed to ExperienceStore
confidence: 'high' | 'medium' | 'low'
caveats?: string
toolsUsed: { name: string; calls: number; successRate: number }[]
metrics: {
tokens: number
duration: number
iterations: number
cost: number
}
markdown: string // Pre-rendered Markdown version
}

Access it from any run result:

const result = await agent.run('Fetch the latest commits and summarize')
if (result.debrief) {
console.log(result.debrief.summary)
console.log(result.debrief.markdown)
}
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()
.withReasoning({ defaultStrategy: "adaptive", adaptive: { enabled: true } })
.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