Skip to content
agentFast
The production layer

Architecture

The four layers, the adapter seam, and the hole at L1.

In short: agentFast is built in four layers. The bottom one — how your agent thinks — is deliberately left empty for you to fill with CrewAI, LangGraph, or whatever you prefer. agentFast owns the three above it. That one decision explains everything else on this page.

agentFast is a framework with one deliberate hole in it. Understanding where the hole is explains every other design decision.

The four layers

L4  PRODUCT SURFACE   playground · dashboard · HITL review UI · CLI
L3  PRODUCTION LAYER  observability · evals · guardrails · HITL/durable-exec
                      · memory · tools(MCP) · streaming        ← SDK-agnostic. The product.
L2  ADAPTER SEAM      on_llm_call · on_tool_call · checkpoint · resume · ~200 LOC per SDK
L1  ORCHESTRATION     LangGraph · CrewAI · Claude Agent SDK
                      · OpenAI Agents SDK · vanilla            ← NOT abstracted. Idiomatic per SDK.

Most frameworks own L1 and leave you to build L3. agentFast inverts that: it owns L3 and leaves L1 to you.

That's the whole positioning in one sentence. CrewAI and LangGraph frame how your agent thinks — roles, graphs, hand-offs, the shape of a turn. agentFast frames how it survives: what happens when a tool fails, when a human has to sign off, when the process dies mid-run, when someone asks what it cost.

NoteWhy leave L1 empty

A grand Agent interface across five SDKs would be a lowest-common-denominator wrapper — it would have to hide whatever any one of them does uniquely, which is usually the reason you picked it. Leaving the hole means your LangGraph code stays real LangGraph code and there's nothing new to learn at the layer you already understand.

The seam

Every adapter translates its SDK's native lifecycle into the same six calls:

ctx    = await runtime.start_run(agent, sdk, session_id, ...)
req    = await runtime.on_llm_call(ctx, req)          # memory injection + guardrails in
res    = await runtime.on_llm_result(ctx, req, res)   # guardrails out + cost + trace
result = await runtime.execute_tool(ctx, call)        # policy → rate limit → HITL → run → trace
         await runtime.checkpoint(ctx, snapshot)      # after EVERY iteration
snap   = await runtime.resume(run_id)                 # after a restart
         await runtime.end_run(ctx, "completed", ...)

The runtime never owns the orchestration loop — the SDK does. It also never imports an SDK or a database: persistence goes through a RunStore protocol, and model-side effects arrive as plain values. That's why almost the entire test suite runs in memory without Docker.

Composition, not inheritance

The Runtime takes its collaborators as optional constructor arguments:

Runtime(
    store=run_store,          # required — the durable seam
    tools=registry,
    guardrails=guardrails,
    tracer=telemetry_hub,
    memory=memory,
    rate_limiter=limiter,
    pricing=pricing_table,
    classifier=complexity,
    stream=stream_port,
)

Absent collaborators no-op. A bare Runtime(store) is fully functional, which is what makes the production layer testable a piece at a time — and what means enabling streaming or guardrails changes nothing about how a run executes.

Where the data lives

| Concern | Where | Why it matters | |---|---|---| | Runs, steps, approvals | Postgres | The audit trail and the dashboard's source of truth | | Checkpoints | Postgres | What survives a process kill | | Long-term memory | pgvector (or Chroma) | Recall across sessions | | Live stream | In-process bus | Deliberately not durable — see Streaming |

The split is the point: anything a run's correctness depends on is in Postgres, and anything that only affects your view of it is not. Losing the live stream costs you a view. Losing a checkpoint would cost you the run, so checkpoints are never in memory.

Next