Skip to content

Resilience & Caching

Reactive Agents includes multiple resilience layers that protect your agent workflows from provider outages, redundant API calls, and unsafe code execution.

The LLM provider layer includes a circuit breaker that protects against cascading failures when a provider is experiencing issues.

const agent = await ReactiveAgents.create()
.withProvider("anthropic")
.withReasoning()
.build();
// Circuit breaker is automatically enabled for all LLM calls

The circuit breaker has three states:

StateBehavior
CLOSED (normal)Requests pass through. Failures increment the counter
OPEN (tripped)Requests fail immediately without calling the provider. Resets after timeout
HALF_OPEN (probing)A limited number of requests pass through. Success resets to CLOSED; failure returns to OPEN

When consecutive LLM call failures exceed the failure threshold, the circuit opens and subsequent calls fail fast — preventing wasted tokens and API quota during outages. After a configurable reset timeout, the circuit moves to half-open and probes with limited requests.

An LRU + TTL cache sits in front of all embedding API calls, avoiding redundant requests for previously-embedded text.

const agent = await ReactiveAgents.create()
.withProvider("anthropic")
.withMemory("2") // Tier 2 enables semantic memory with embeddings
.build();
// Embedding cache is automatically active when memory tier 2 is enabled

Repeated embedding calls for identical text return cached vectors instantly — useful for agents that re-embed the same context across reasoning iterations.

PropertyValue
EvictionLRU (least recently used)
TTLConfigurable per instance
ScopePer-agent session

Budget state is persisted to SQLite, so cost tracking survives agent restarts:

const agent = await ReactiveAgents.create()
.withProvider("anthropic")
.withCostTracking() // Budget state persisted to SQLite
.build();

When the agent starts, the budget enforcer loads the most recent spend from the database and continues tracking from where it left off. Daily and monthly budgets are enforced across restarts without resetting.

Tool execution results are cached to avoid redundant calls for identical inputs within a session:

const agent = await ReactiveAgents.create()
.withProvider("anthropic")
.withReasoning()
.withTools() // Tool result caching is built-in
.build();

When the same tool is called with the same arguments, the cached result is returned immediately. This is especially valuable in reasoning loops where the agent may re-invoke a tool with identical parameters across iterations.

  • Keyed by tool name + JSON-serialized arguments
  • Scope is per-session (not persisted across agent.run() calls)
  • TTL configurable via ToolResultCacheConfig

For code execution tools, the Docker sandbox provides container-level isolation:

const agent = await ReactiveAgents.create()
.withProvider("anthropic")
.withReasoning()
.withTools() // Code execution uses Docker sandbox when available
.build();

Code snippets execute in isolated Docker containers with resource limits:

LimitDefault
MemoryConfigurable per container
CPUConfigurable CPU shares
TimeoutPer-execution timeout
NetworkIsolated by default

The Docker sandbox prevents:

  • File system escapes
  • Environment variable leakage (API keys are not inherited)
  • Resource exhaustion (CPU/memory caps)
  • Network access to internal services

When Docker is not available, code execution falls back to Bun.spawn() subprocess isolation with a minimal environment (PATH only).

Ensure your agent calls critical tools before producing a final answer:

const agent = await ReactiveAgents.create()
.withProvider("anthropic")
.withReasoning()
.withTools()
.withRequiredTools({
tools: ["web-search"], // Must call web-search before answering
maxRetries: 2, // Retry up to 2 times if tool is missed
})
.build();

Instead of a static tool list, let the LLM determine which tools are required per-task:

.withRequiredTools({ adaptive: true })

The framework calls the LLM with the task description and available tool schemas. A hallucination guard filters the inferred list against actual tool names, ensuring only real tools are required.

Use both a static baseline and adaptive inference:

.withRequiredTools({
tools: ["web-search"], // Always required
adaptive: true, // Plus LLM-inferred requirements
maxRetries: 3,
})
  1. Before execution, the required tools list is determined (static, adaptive, or both)
  2. The kernel runner tracks which tools are called during reasoning
  3. After the kernel produces a final answer, the runner checks if all required tools were called
  4. If any are missing, a nudge message is injected and the kernel re-enters the loop
  5. This repeats up to maxRetries times before accepting the answer as-is