cd /news/ai-agents/stop-trusting-your-agent-framework-s… · home topics ai-agents article
[ARTICLE · art-134787] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

Stop Trusting Your Agent Framework. Start Controlling It.

A developer released Reactive Agents, an open source MIT-licensed TypeScript framework now at v0.16, designed to make agent harness behavior explicit rather than hidden inside a black box. Each .with() call enables exactly one capability, and the framework uses model-adaptive context profiles, a tool-call healing pipeline, and an FC-dialect probe so the same code path can run on a local 4B model or a frontier model like Claude Sonnet. The project argues the engineering around the model, not the model itself, is what makes agents reliable.

by read11 min views1 publishedSep 19, 2026

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 started 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.

This is a look at what that turned into.

That founding idea shows up directly in how you build an agent.

import { ReactiveAgents } from "reactive-agents";

const agent = await ReactiveAgents.create()
  .withProvider("anthropic")
  .withModel("claude-sonnet-4-6")
  .withReasoning()
  .withTools({ tools: [getServiceHealth, getRecentDeploys] })
  .build();

const result = await agent.run(
  "The payments-api is alerting. Investigate with the health and " +
  "recent-deploys tools, then tell me the likely cause and what to do."
);

Every .with() call turns on exactly one thing. No memory writes unless you called .withMemory(). No guardrail scanning unless you asked for it. 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.

Here's the idea underneath most of the framework's design decisions: most of what makes an agent capable isn't the model, it's the engineering around it. Better prompts. Better context management. Better memory. Better recovery when something goes slightly wrong. That insight is easiest to see by watching it in action. Take the builder above and change one line:

const agent = await ReactiveAgents.create()
  .withProvider("ollama").withModel("qwen3:4b")              // your laptop, $0
  // .withProvider("anthropic").withModel("claude-sonnet-4-6")  // frontier, same code
  .withReasoning()
  .withTools({ tools: [getServiceHealth, getRecentDeploys] })
  .build();

Both finish the same investigation: call both tools, notice the degraded error rate lines up with a deploy from twelve minutes ago, recommend a rollback. A 4B local model is obviously not as capable as Claude, and nobody working on this framework would tell you otherwise. What's different is narrower and more useful: the harness finishes the loop regardless of which model is behind it, so you can build and iterate for free against a small local model and reach for the frontier one only when a task actually needs the extra reasoning power.

Two things make that 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. And 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.

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

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

That's the whole contract. The framework is built on Effect-TS underneath, which tends to make people wary, so it's worth saying plainly: you don't have to write Effect to use any of this. The builder and every hook you write are ordinary functions. What Effect buys you underneath is a runtime where a failed tool call or a provider timeout is a typed value in an explicit error channel instead of an exception you meet for the first time in production, and where retries, fallbacks, and timeouts compose instead of tangling into nested try/catch blocks.

The same philosophy shows up in what the framework hands back when a run finishes. Every result includes a receipt, a claim-to-evidence record backed by an append-only ledger of what actually happened during the run. Not "trust me," an actual object you can inspect or log:

{
  "verdict": "tool-grounded",
  "method": "heuristic",
  "confidence": 0.91,
  "toolsUsed": ["get_service_health", "get_recent_deploys"],
  "toolCallStats": { "ok": 2, "failed": 0 },
  "deliverables": [
    { "spec": "produce the file ./report.md", "produced": false }
  ]
}

If you asked for three files and only two got produced, deliverables names the one that never landed instead of the agent narrating success anyway. There's a fabrication guard on by default that rejects invented empirical claims not backed by anything the tools actually observed, and a dedicated termination state, terminatedBy: "abstained", for when grounding is structurally impossible, so the agent says why it stopped instead of confidently making something up. You can even have the framework sign that receipt with Ed25519 so it can't be altered after the fact:

const { privateKeyJwk } = await generateReceiptKeyPair();

const agent = await ReactiveAgents.create()
  .withProvider("anthropic")
  .withReceiptSigning({ privateKeyJwk })
  .build();

Long-running agents die mid-task. Processes get rescheduled, containers restart, someone kills the wrong terminal. .withDurableRuns() checkpoints every step to disk, so a fresh process can pick a run back up from its last checkpoint and finish it without re-running the tools that already completed:

// Process A: works, checkpoints each step, then dies.
const a = await build(); // .withDurableRuns({ dir })
for await (const _ of a.runStream(task)) { /* the process gets killed here */ }

// Process B: fresh process, same store.
const b = await build();
const runId = (await b.listRuns({ status: "running" }))[0].runId;
const result = await b.resumeRun(runId);

The same checkpoint machinery backs durable human-in-the-loop approvals. Mark a tool as requiring approval, and when the agent tries to call it, the run s and persists an awaiting-approval state instead of just blocking in memory:

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

A person can approve or deny that action from a completely different process, hours later, and the run resumes exactly where it d. That's a different guarantee than "the agent asks a question and waits," because "waits" usually means the run only survives as long as one process happens to stay alive.

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:

const agent = await ReactiveAgents.create()
  .withProvider("anthropic")
  .withReasoning({ defaultStrategy: "tree-of-thought" })
  .withTools()
  .build();

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, which is the difference between an agent that spins for ten iterations repeating itself and one that notices and adjusts.

Memory is opt-in, off until you call .withMemory(), a deliberate choice made after finding it wasn't earning its cost in every configuration:

const agent = await ReactiveAgents.create()
  .withProvider("anthropic")
  .withMemory({ tier: "standard" })
  .withReasoning()
  .build();

Turn it on and you get four layers, working, episodic, semantic (vector search plus full-text search), and procedural, backed by SQLite with background consolidation. A Living Skills system, SKILL.md-compatible and LLM-refined over time, lets an agent accumulate know-how across sessions without you hand-growing a prompt to hold it all.

Letting an agent touch anything real means answering questions a demo never has to: 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.

const agent = await ReactiveAgents.create()
  .withProvider("anthropic")
  .withGuardrails()                          // injection, PII, toxicity
  .withBudget({ tokenLimit: 100_000 })       // hard spend cap, survives restarts
  .withTools()
  .build();

Agent identity is backed by real Ed25519 certificates with role-based access and delegation chains, not a naming convention. On the cost side, a multi-factor complexity router can send each run to the cheapest model capable of handling it, and a semantic cache skips redundant LLM calls for similar queries. Verification runs on the output side of that same concern: semantic entropy checks, fact decomposition, and NLI-based hallucination detection catch a confident answer that isn't actually backed by anything, before it reaches a user.

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, and race() returns whichever finishes first. agentFn() wraps a builder into a lazy, callable primitive so these compose cleanly:

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 gives you Agent Cards, a JSON-RPC 2.0 server and client, SSE streaming, and agent-as-tool, so one agent can discover and delegate to another the same way it would call a regular tool. Sub-agents can also be spawned dynamically under a depth limit, for tasks where the shape of the work isn't known up front.

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() gives you 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, started with .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. It's the same event stream the framework publishes internally, just rendered, so there's nothing it can show you that isn't also available to your own code through the hooks and EventBus.

An agent that only runs from a script isn't much use to most products, so the integration surface is real. @reactive-agents/ui-core is a headless, framework-agnostic core with a versioned wire protocol and a resumable stream client, and @reactive-agents/react, vue, and svelte build hooks and components on top of it. All of them consume agent.runStream() through AgentStream.toSSE(), so wiring an agent into a Next.js, SvelteKit, or Nuxt route is a one-line SSE endpoint rather than a bespoke streaming protocol.

On the inbound side, 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, which is what turns an agent from something you call into something that runs on its own. Tools aren't limited to hand-written functions either; MCP servers plug in through .withMCP(), with container lifecycle handled for you.

Worth being 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, that's LangChain or Mastra, not this. Reactive Agents is younger and the community is smaller.

If you need something proven across a large number of production deployments today, say so honestly: it's actively developed, with a real test suite (well over nine thousand tests) and a typed foundation throughout, but it's still v0.16. Better to say that up front than have someone find out three weeks into a migration.

bun add reactive-agents

The point of building this in the open is that the harness is yours to reshape. If a phase doesn't behave the way your use case needs, the hook is right there. If none of the eight reasoning strategies fit, register your own. If you build something with it, or push a local model past what the healing pipeline can currently repair, that's genuinely useful to hear about. Issues and feedback welcome.

── more in #ai-agents 4 stories · sorted by recency
── more on @reactive agents 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/stop-trusting-your-a…] indexed:0 read:11min 2026-09-19 ·