Skip to content

Home

The pain

You've written an agent. It ran. Then one of these happened.

Your agent looped 40 times against a model that thought the tool was succeeding. The bill was $217. There was no ceiling.

Your researcher agent errored halfway through a 20-minute run. The worker died with the transcript in memory. You restarted from zero.

Your tool called git push --force on main because there was no approval gate. Nobody was watching. Your teammate lost a day.

agentkit is a low-level framework that makes each of these a one-line fix. Budget(max_cost_usd=5.0) halts the run before it overspends. Checkpointer(port=...) snapshots after every tool iteration so a fresh worker picks up where the last one died. autonomy="gated" on the run suspends before every side-effecting tool and hands control to a human. Nothing is hidden. Every seam is a typed Protocol you can swap.

What you'll actually use

Ontology before tutorial. When you sit down to write, this is the map from "what I want to do" to "which primitive fills that slot."

You want to … Reach for …
Wire one LLM call with retry / cost / trace on top Chat (from claude() / openai() / deepseek() / openrouter())
Build an agent that loops through tools Agent + ReActCognition
Delegate the whole loop to a local claude CLI Agent + ClaudeCliCognition
Orchestrate many child agents Agent + CoordinatorCognition
A fixed multi-step pipeline (author writes the plan) Workflow
Package prompt + cognition + tools + memory as one unit Skill (.as_agent() / .as_tool())
Consume an MCP server's tools MCPClient + mcp_tools()
Pause a tool for human approval Autonomy.GATED on the RunContext + Suspended / Agent.resume()
Cap what the run can spend Budget(max_cost_usd=...) / Quota(max_rpm=..., max_usd=...)
Survive a worker crash Checkpointer(port=...)
Add a cross-cutting concern (tracing, retry, guardrail) Middleware in the chain([...]) on the Invoker

Every row is a slot in the same composition — the primitives on the right are how you fill it, not what agentkit forces you into.

What changes with agentkit

  • Budget halts the run cleanly on overrun — MeterExceeded propagates, the loop unwinds, no over-spend.
  • Checkpointer survives worker crashes — a fresh process resumes from the last snapshot on the same run_id.
  • Autonomy tier gates every side-effecting tool — a human approves before mutation happens.
  • Cancel propagates through the subtree — cancel the parent, every child stops at its next check_cancelled.
  • The middleware chain intercepts every LLM and tool call in one linetracing, retry, meter, memoize, compaction, security — swap or reorder by editing a list.
  • Every seam is a typed Protocol — swap the LLM, the store, the checkpoint backend, the tracer without editing the loop.

The four themes

Every concept in agentkit falls into one of four orthogonal buckets. Learn these four; the rest is which class fills which slot.

Adaptersclaude() / openai() / deepseek() / openrouter() presets, ClaudeCliCognition, MCPClient, and any LLMPort you write — are plug-ins, not the point. They fill the LLM slot in a composition; the composition is what agentkit is about.

First runnable example

An Agent with a ReActCognition, one tool, and a RunContext. The LLM plug-in is swappable — pick whichever tab fits how you'll actually deploy.

"""Requires the `claude` CLI on PATH and one prior `claude login`.
Zero API keys; the CLI's own auth is used.

Install: https://docs.claude.com/en/docs/claude-code
"""

import asyncio

from agentkit import Agent, Scope
from agentkit.agents.cognition import ClaudeCliCognition
from agentkit.runtime import RunContext, Services


async def main() -> None:
    agent = Agent(
        name="briefer",
        prompt="Answer in one short sentence.",
        cognition=ClaudeCliCognition(model="claude-sonnet-4-6"),
    )
    ctx = RunContext(correlation_id="run-1", scope=Scope(), services=Services())

    result = await agent.run("What do we know about octopus cognition?", ctx)
    print(result.output)
    print(f"cost estimate: ${result.usage.cost_usd:.4f}")

    # Expected (real answer varies):
    #   Octopuses appear to plan, use tools, and solve mazes.
    #   cost estimate: $0.0031


if __name__ == "__main__":
    asyncio.run(main())
"""Requires `pip install "arc-agentkit[http]"` and ANTHROPIC_API_KEY.
Same shape; the LLM plug-in is a different one."""

import asyncio
import os

from agentkit import Agent, Scope, Services, RunContext
from agentkit.adapters.llm import providers
from agentkit.runtime import Invoker
from agentkit.middlewares import meter, retry, tracing


async def main() -> None:
    llm = providers.claude(api_key=os.environ["ANTHROPIC_API_KEY"],
                           model="claude-sonnet-4-6")
    services = Services(
        invoker=Invoker(llm=llm, chat_middleware=[tracing(), meter(), retry()]),
    )
    ctx = RunContext(correlation_id="run-1", scope=Scope(), services=services)

    agent = Agent(
        name="briefer",
        model="claude-sonnet-4-6",
        prompt="Answer in one short sentence.",
    )
    result = await agent.run("What do we know about octopus cognition?", ctx)
    print(result.output)
    print(f"cost: ${result.usage.cost_usd:.4f}")


if __name__ == "__main__":
    asyncio.run(main())
"""Requires `pip install "arc-agentkit[http]"` and OPENAI_API_KEY.
Same shape; the LLM plug-in is a different one."""

import asyncio
import os

from agentkit import Agent, Scope, Services, RunContext
from agentkit.adapters.llm import providers
from agentkit.runtime import Invoker
from agentkit.middlewares import meter, retry, tracing


async def main() -> None:
    llm = providers.openai(api_key=os.environ["OPENAI_API_KEY"],
                           model="gpt-4o-mini")
    services = Services(
        invoker=Invoker(llm=llm, chat_middleware=[tracing(), meter(), retry()]),
    )
    ctx = RunContext(correlation_id="run-1", scope=Scope(), services=services)

    agent = Agent(
        name="briefer",
        model="gpt-4o-mini",
        prompt="Answer in one short sentence.",
    )
    result = await agent.run("What do we know about octopus cognition?", ctx)
    print(result.output)
    print(f"cost: ${result.usage.cost_usd:.4f}")


if __name__ == "__main__":
    asyncio.run(main())

Swap the LLM plug-in without touching the Agent, the cognition, or the middleware chain. That's the framework's central bet: composition over the loop.

The run in one picture

flowchart TB
    A[Agent] -->|drives| B[Cognition]
    B -->|invoker.chat / invoker.tool| C[Invoker]
    C -->|walks| D[Middleware chain]
    D -->|tracing → meter → retry → …| E[LLM Port / Tool Port]
    E -->|LLMResult / tool_result| D
    D -->|unwind| C
    C -->|yields events| B
    B -->|StreamEvent| A
    F[RunContext] -.->|budget · cancel · autonomy| B
    F -.->|budget · cancel · autonomy| C
    F -.->|budget · cancel · autonomy| D

Nothing in the picture is optional. Nothing in the picture is a class you can't rewrite.

Where to go next

When agentkit is (probably) not the fit

Frameworks make different bets. Pick the one whose bet matches yours.

  • You want a visual graph you can draw — reach for LangGraph instead. agentkit's loop is Python, not a DAG.
  • You want a batteries-included agent product — hosted memory, built-in personas, one-click deploy — reach for a hosted framework like OpenAI Assistants or a managed offering. agentkit is the runtime, not the product.
  • You want to prototype in a notebook and never leave — anything works, and agentkit's benefits (typed seams, cancel, budget, checkpointing) only start paying off when the code has to keep running under real load.

See Why agentkit for the twelve concrete guarantees and a side-by-side comparison with the alternatives.