Stop Trusting Your Agent Framework. Start Controlling It.
DEV Community

Stop Trusting Your Agent Framework. Start Controlling It.

The Problem with Black Box Agent Frameworks

Most agent frameworks ask you to trust a black box. You hand it a model and a prompt, it hands back an answer, and everything in between-the reasoning, the tool selection, the context management-happens somewhere you can't see and can't touch. That works fine until it doesn't, and when it doesn't, you're debugging a system that was never designed to be debugged.

Reactive Agents starts from a different premise: you shouldn't need to trust a bigger model to paper over a weak harness, or a proprietary runtime you can't see inside. It's an open source TypeScript framework, MIT licensed, now at v0.16, built on the idea that the engineering around the model is what makes an agent reliable, and that engineering should be visible and yours to shape, not hidden behind someone else's abstraction.

Reactive Agents: A Different Approach

Nothing runs that you didn't ask for. The founding idea shows up directly in how you build an agent. Each .with() call turns on exactly one thing. There are no memory writes unless you call .withMemory(). There is no guardrail scanning unless you asked for it. There is no hidden system prompt doing work you didn't sign up for.

If you've ever inherited an agent built on a framework where you genuinely don't know what's happening inside a single .invoke() call, that's the exact discomfort this API is designed to remove.

Core Design Principles

The insight that shaped everything else is that the harness makes the model smart. Most of what makes an agent capable isn't the model itself-it's the engineering around it. Better prompts. Better context management. Better memory. Better recovery when something goes slightly wrong.

Two things make this possible:

  • Model-adaptive context profiles tune prompt density and compaction per model tier, since a small model drowns in the same verbose prompt a frontier model handles easily.
  • A healing pipeline sits in front of every tool call, catching the small ways smaller models get it almost right-a tool name off by a naming convention, a parameter sent under an alias, a malformed path. Instead of the loop dying on "invalid tool," the call gets repaired and runs.

Underneath both, an FC-dialect probe picks native function-calling where a provider supports it and falls back to a tiered text-parsing driver where it doesn't, which is the actual reason a small open model and Claude can share one code path at all.

Key Features

Lifecycle & Hooks

Every agent run moves through a fixed, named sequence of phases: bootstrap, guardrail, cost-route, think, act, observe, verify, and on through termination. Every phase exposes hooks before and after it runs. For example:

const agent = await ReactiveAgents.create().withHook({
  phase: "act",
  timing: "after",
  handler: (ctx) => {
    const last = ctx.toolResults.at(-1);
    console.log("tool called:", last?.toolName);
    return ctx;
  }
});

Result Receipts & Verification

Every result includes a receipt, an actual object you can inspect or log-not "trust me." The receipt contains structured fields like verdict, method, confidence, toolsUsed, and toolCallStats. If you asked for three files and only two were produced, the delivery report correctly identifies the missing one instead of fabricating success.

Verification runs on the output side with semantic entropy checks, fact decomposition, and NLI-based hallucination detection to catch confident answers that aren't actually backed by anything before they reach a user.

Durable Runs & Checkpointing

Long-running agents die mid-task because processes get rescheduled, containers restart, or someone kills the wrong terminal. With .withDurableRuns(), every step is checkedpointe d to disk, so a fresh process can pick up a run from its last checkpoint and finish it without re-running completed tools.

// Checkpointing example
await build();

// Resuming a paused run
const runId = (await build().listRuns({ status: "running" }))[0].runId;
const result = await build().resumeRun(runId);

Approval Policies

Mark a tool as requiring approval, and when the agent tries to call it, the run pauses and persists an awaiting-approval state instead of just blocking in memory. This allows a person to approve or deny an action from a completely different process, hours later, and the run resumes exactly where it paused-providing a different guarantee than simply waiting.

.withTools({
  tools: ["deleteRecordsTool"],
  withDurableRuns({ dir })
}).withApprovalPolicy({
  tools: ["delete-records"],
  mode: "detach"
});

Composable Reasoning Strategies

Reasoning was never meant to be a single fixed algorithm. Eight strategies live in the strategy registry today: ReAct, Blueprint (a plan-once-execute-in-parallel strategy), Reflexion, Plan-Execute, Tree-of-Thought, Adaptive (a meta-strategy that picks among the others), Direct, and an experimental Code-Action strategy.

Swapping one in is a builder call:

.withReasoning({ defaultStrategy: "tree-of-thought" })

A reactive controller also watches for stalls, loops, and context pressure mid-run and can trigger early-stop, compression, or a strategy switch on its own-the difference between an agent that spins for ten iterations repeating itself and one that notices and adjusts.

Memory Management

Memory is opt-in, off until you call .withMemory(). When enabled, it provides four layers: working, episodic, semantic (vector search plus full-text search), and procedural, backed by SQLite with background consolidation.

Guardrails, Identity & Cost Control

Letting an agent touch anything real means addressing questions a demo never has to answer: can it be prompt-injected, does it leak PII, who is allowed to invoke it, what happens if it runs away and burns through your budget. These concerns are handled via guardrails, identity backed by real Ed25519 certificates with role-based access and delegation chains, and a multi-factor complexity router that sends each run to the cheapest model capable of handling it.

Agent Composition

A single agent handles a lot, but some tasks are naturally a pipeline or a fan-out. Functional combinators let you build those without hand-rolling orchestration:

  • pipe() - chains agents so one's output feeds the next's input
  • parallel() - runs several concurrently and collects the results
  • race() - returns whichever finishes first
import { agentFn, pipe } from "reactive-agents";

const research = agentFn(() =>
  ReactiveAgents.create().withProvider("anthropic").withTools()
);

const summarize = agentFn(() =>
  ReactiveAgents.create().withProvider("anthropic")
);

const pipeline = pipe(research, summarize);

const result = await pipeline("Find recent TypeScript runtime benchmarks and summarize them");

For agents that need to call each other across process or network boundaries, the A2A protocol implementation provides Agent Cards (JSON-RPC 2.0 server and client), SSE streaming, and agent-as-tool, enabling sub-agents to be spawned dynamically under a depth limit.

Interaction Modes

Not every interaction is a single run() call. agent.chat() handles one-shot Q&A against an agent's prior run context, and agent.session() provides a proper multi-turn conversation with its own history:

const session = agent.session();
await session.chat("What did the investigation find?");
await session.chat("Now draft a Slack message about it");

For watching a run happen rather than just reading its output afterward, Cortex Studio is a local dev UI (available via .withCortex() or rax run --cortex) that shows a live agent canvas, an entropy signal, per-step token usage, and a full execution trace with an AI-generated debrief when a run finishes.

Integration Ecosystem

An agent that only runs from a script isn't very useful to most products. The integration surface is extensive:

  • @reactive-agents/ui-core - a headless, framework-agnostic core with a versioned wire protocol and a resumable stream client
  • @reactive-agents/react, vue, and svelte - build hooks and components on top of it
  • All consume agent.runStream() through AgentStream.toSSE(), so wiring an agent into Next.js, SvelteKit, or Nuxt requires a one-line SSE endpoint rather than a bespoke streaming protocol
  • A persistent gateway package handles adaptive heartbeats, cron scheduling, webhook ingestion with a GitHub adapter, and a composable policy engine for routing events to the right agent
  • MCP servers plug in through .withMCP(), with container lifecycle handled automatically

When Reactive Agents Isn't the Right Fit

It's important to be direct about this rather than glossing over it. If you're on one provider with a simple, mostly linear loop, use that vendor's own Agent SDK-you don't need a harness underneath you, and reaching for one here adds weight for no reason. If you want the largest ecosystem and the most tutorials on the internet right now, consider LangChain or Mastra instead.

Reactive Agents is younger and the community is smaller. If you need something proven across a large number of production deployments today, be honest: it's actively developed, with a real test suite (well over nine thousand tests) and a typed foundation throughout.

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.