Cheatsheet¶
Every primitive, tight. Skim in 90 seconds; grab the invocation you need; go. This page has no prose. For the why, follow the links to Concepts or Recipes.
Boot¶
from agentkit import Agent, Scope, RunContext, Services
from agentkit.agents.cognition import SingleCallCognition, ReActCognition
# The composition: identity + prompt + cognition + tools.
agent = Agent(
name="briefer",
model="claude-sonnet-4-6",
prompt="Answer concisely.",
cognition=SingleCallCognition(), # default; ReActCognition(tools=[...]) for a tool loop
)
# The per-run universe. `services=` wires the Invoker + Store + Vector + Trace + ...
ctx = RunContext(
correlation_id="run-42",
scope=Scope(org_id="acme"),
services=Services(invoker=my_invoker), # see the "Invoker" section
autonomy="auto", # "auto" | "gated" | "manual"
)
result = await agent.run("hello", ctx) # collect the stream into one AgentResult
async for ev in agent.stream("hello", ctx): ... # or handle StreamEvents live
Cognition¶
from agentkit.agents.cognition import (
SingleCallCognition,
ReActCognition,
CoordinatorCognition,
ClaudeCliCognition,
)
# One chat call + optional parse-and-repair.
SingleCallCognition()
# Chat ↔ tool loop, HITL suspend/resume, durable resume via Checkpointer.
ReActCognition(tools=[my_tool, another_tool], max_iterations=8)
# Multi-agent: one coordinator + children driven by a Policy.
from agentkit.agents.policies import RoundRobinPolicy, SelectorPolicy
CoordinatorCognition(
children={"planner": planner_agent, "researcher": researcher_agent},
policy=RoundRobinPolicy(max_turns=6),
)
# Delegate the loop to a locally-installed `claude` CLI. No API key.
ClaudeCliCognition(
model="claude-sonnet-4-6",
permission_mode="acceptEdits",
allowed_tools=("Read", "Grep", "Bash"),
max_concurrent=8,
)
Tools¶
from agentkit import tool, FunctionTool, ToolRegistry, FileTool, InMemoryFiles
# The 90% case. `side_effecting=` is REQUIRED — decoration-time TypeError otherwise.
@tool(side_effecting=False)
async def search(query: str) -> str:
"""Search the web for `query`. Returns two bulleted hits."""
return "..."
@tool(side_effecting=True, idempotent=False, requires_approval=True)
async def publish(title: str, body: str) -> str:
"""Publish `title` to the wiki. Not idempotent; always ask a human first."""
return f"published {title}"
# Composite: many tools behind one name lookup.
registry = ToolRegistry.from_tools([search, publish])
# Filesystem-backed memory tool (read/write/list under a confined root).
files_tool = FileTool(backend=InMemoryFiles(), root="/memories")
Prompts¶
from agentkit import Prompt
p = Prompt(
id="briefer.system",
version="1.2.0",
template="You are a terse briefer. Cite every claim.",
inputs=(), # declare template inputs here; render() is a pure fn returning the template
)
# Wire on the Agent — the version travels on every trace + AgentResult.prompt_version.
Agent(name="briefer", prompt=p, cognition=SingleCallCognition())
Memory¶
from agentkit import (
CompositeMemory,
SequentialMemory,
VectorMemory,
ScopedMemory,
)
from agentkit.memory import CachedMemory, JournalMemory, FileMemory, ScratchpadMemory
# Vector store adapter (any VectorPort — pgvector, qdrant, memory-backed, ...).
memory = VectorMemory(vector=my_vector_port)
# Filesystem-backed reads.
memory = FileMemory(files=InMemoryFiles())
# Fan-out across many sources, merge + rerank the top-k.
memory = CompositeMemory(sources=[VectorMemory(...), JournalMemory(...)])
# Fan-in in order (first non-empty wins).
memory = SequentialMemory(sources=[fast_cache_source, slow_vector_source])
# Decorators (compose freely).
memory = ScopedMemory(inner=memory) # enforces ctx.scope at the boundary
memory = CachedMemory(inner=memory, ttl_seconds=60, max_entries=256)
Agent(name="researcher", cognition=ReActCognition(tools=[search]), memory=memory)
Middleware chain¶
from agentkit import chain, BaseMiddleware, Call, Handler, MiddlewareContext
from agentkit.middlewares import (
tracing, retry, meter, fallback, memoize, semantic_memoize,
output_coerce, compaction, security, egress, audit,
)
# The canonical chat chain. Order is deterministic — outermost first.
from agentkit import SlidingWindowCompactor
chat_middleware = [
tracing(), # outermost: one span across everything below
compaction(SlidingWindowCompactor(keep_recent=10)), # shrink transcripts BEFORE meter counts tokens
meter(), # guard/charge Budget + Quota on every call
fallback(models=["gpt-4o", "gpt-4o-mini"]), # rewrite request.model on hard failures
retry(), # re-invoke on transient failures + optional CircuitBreaker
memoize(key=lambda call: call.request.messages[-1].content or ""), # cache identical last-message calls
]
# Custom transform/guard/observe middleware.
class Redact(BaseMiddleware):
async def on_request(self, ctx: MiddlewareContext) -> None:
... # mutate ctx.request
# Custom raw resilience/caching middleware (must re-invoke, skip, or wrap `next`).
async def stopwatch(call: Call, nxt: Handler):
async for item in nxt(call):
yield item
Capabilities¶
from agentkit import (
RequestBuilder,
Grounder,
Checkpointer,
Guardrail,
Evaluator,
SlidingWindowCompactor,
TruncationCompactor,
SummarizationCompactor,
ImportanceFilteringCompactor,
)
from agentkit.adapters.checkpoint import InMemoryCheckpointStore
# Compactors (fold the transcript before it hits the model window).
compactor = SlidingWindowCompactor(keep_recent=10) # dep-free
compactor = TruncationCompactor(max_tokens=12_000) # dep-free
compactor = SummarizationCompactor(summarizer=my_llm) # needs an LLMPort
compactor = ImportanceFilteringCompactor(filterer=my_llm)
# Durable snapshot/resume — powers HITL suspend AND crash-resume.
checkpointer = Checkpointer(port=InMemoryCheckpointStore())
# Production: PostgresCheckpointStore from arc-agentkit[postgres].
# The RequestBuilder assembles a ChatRequest from prompt + memory + tools.
# The Agent builds one automatically from `prompt=` — override to plug grounding.
Concurrency¶
from agentkit import (
CancellationToken, Cancelled,
gather_bounded, gather_best_effort,
run_agents, run_sync,
)
import asyncio
# Fan-out N children, bounded by a semaphore. One failure cancels the rest.
sem = asyncio.Semaphore(4)
results = await gather_bounded([coro1(), coro2(), coro3()], sem=sem)
# Same, but isolate failures into Failure objects (no sibling cancel).
results = await gather_best_effort([coro1(), coro2()], sem=sem)
# Multi-agent: each pair runs under ctx.child(), sharing budget + cancel.
results = await run_agents([(agent_a, "task-a"), (agent_b, "task-b")], ctx)
# Cooperative cancel across the whole subtree.
token = CancellationToken()
ctx.cancel = token
token.cancel() # every check_cancelled() from now on raises Cancelled
# Sync host driving async agentkit — one bridge.
result = run_sync(agent.run("hi", ctx))
Signals + control (multi-agent)¶
from agentkit import (
Suspended, Handoff, DoneSignal, ProgressSignal, EscalateSignal,
CancelSignal, BudgetReducedSignal, RedirectSignal, ContextUpdateSignal,
MergeWithPeerSignal, BlockedSignal, SignalChannel, SignalEnvelope,
ActorBudget, BudgetExhausted,
)
# Frozen result value returned when a run pauses for approval.
assert isinstance(result.evals.get("suspended"), Suspended)
decisions = {tc.id: "approve" for tc in result.evals["suspended"].pending}
final = await agent.resume(result.evals["suspended"].run_id, decisions, ctx)
# Handoff: route to a peer. Consumed by SelectorPolicy / handoff-routing.
signal = Handoff(target="specialist", reason="user asked about billing")
# ActorBudget: per-child slice of the parent's envelope.
parent_budget = ActorBudget(max_tokens=10_000, max_cost_usd=1.0,
max_steps=20, max_wall_seconds=60.0)
Budget + Quota + Meter¶
from agentkit import Budget, Quota, MeterExceeded
from agentkit.middlewares import meter
# Per-run ceiling. Enforced by meter() under an async lock.
budget = Budget(
max_cost_usd=0.50,
max_calls=50,
max_depth=4,
max_concurrency=8,
)
# Per-tenant rolling window, keyed by Scope.
quota = Quota(max_rpm=60, max_tpm=100_000, max_usd=1.0, window=60.0)
# Wire both onto RunContext.meters (Budget goes on RunContext.budget directly).
ctx = RunContext(
correlation_id="run-42",
scope=Scope(org_id="acme"),
budget=budget,
meters=[quota],
services=Services(invoker=Invoker(llm=llm, chat_middleware=[meter()])),
)
try:
await agent.run(task, ctx)
except MeterExceeded as exc:
... # the run halted cleanly at the ceiling
Batteries-included LLM presets¶
from agentkit import claude, openai, deepseek, openrouter
# Each returns a Chat wired with `tracing → meter → retry`.
async with claude(api_key="sk-...", model="claude-sonnet-4-6") as chat:
result = await chat("hi", system="Answer briefly.")
async with openai(api_key="sk-...", model="gpt-4o-mini") as chat:
result = await chat("hi", system="Answer briefly.")
async with deepseek(api_key="sk-...", model="deepseek-chat") as chat:
...
async with openrouter(api_key="sk-...", model="anthropic/claude-3.5-sonnet") as chat:
...
Any HTTP LLM is one LLMPort impl away — bring your own vLLM,
Ollama, or in-house wrapper the same way the presets do.
MCP (Model Context Protocol)¶
# pip install "arc-agentkit[mcp]"
from agentkit.integrations.mcp import (
MCPClient, StdioServer, StreamableHttpServer,
mcp_tools, mcp_resources, mcp_prompts,
)
server = StdioServer(command="uvx", args=("mcp-server-time",))
# Or: StreamableHttpServer(url="https://mcp.example.com/sse")
async with MCPClient(server) as mcp:
tools = await mcp_tools(mcp, prefix="time_") # list[Tool] — drop into ReActCognition
memory = mcp_resources(mcp, name="time_docs") # MemorySource
prompts = await mcp_prompts(mcp) # dict[str, Prompt]
agent = Agent(
name="clock",
prompt=prompts.get("system") or "Answer with the current time.",
cognition=ReActCognition(tools=tools),
memory=memory,
)
Structured output¶
from pydantic import BaseModel
from agentkit import adapt
class Answer(BaseModel):
summary: str
confidence: float
# Output schema is built once; its JSON Schema goes into the prompt prefix
# and output_coerce enforces the shape on the way out. Repairs on parse-fail.
agent = Agent(
name="briefer",
model="gpt-4o-mini",
prompt="Give a summary and a confidence in [0, 1].",
output=Answer, # Pydantic / attrs / dataclass / raw JSON Schema dict
max_repairs=1,
)
result = await agent.run("brief on octopus cognition", ctx)
result.parsed # -> Answer(summary=..., confidence=...)
# Or on a Tool:
@tool(side_effecting=False, output_schema=Answer)
async def brief(topic: str) -> Answer:
"""Return a structured brief for the topic."""
return Answer(summary="...", confidence=0.7)
OpenTelemetry¶
# pip install "arc-agentkit[observability]"
from agentkit.adapters.observability import (
otel_tracer, otel_meter, otel_sampler,
otel_exporter_otlp_http, otel_metrics_exporter_otlp_http,
)
# At process startup — reads OTEL_EXPORTER_OTLP_ENDPOINT etc. from env.
otel_exporter_otlp_http()
otel_metrics_exporter_otlp_http(interval_ms=15_000)
services = Services(
invoker=my_invoker,
trace=otel_tracer(), # pulls the global TracerProvider
metrics=otel_meter(), # pulls the global MeterProvider
sampler=otel_sampler(0.1),
)
Testing kit — for your tests, not for production¶
# The doubles the framework's own suite uses. NEVER wire these into a
# real run: they're for your unit tests and any mock adapters you write.
from agentkit.testing import (
FakeLLM, FakeFetch, FakeSearch, FakeMemory, FakeTool, FakeClock,
FakeGrounder, FakeCompactor, FakeCtx, RecordingTracer, Turn,
make_test_ctx,
)
from agentkit import ToolCall
# One reply.
llm = FakeLLM("42")
# Scripted multi-turn (tool_call → tool_result → final).
llm = FakeLLM.script([
Turn(tool_calls=(ToolCall("c1", "search", {"query": "octopus"}),)),
Turn(content="Octopuses use tools."),
])
# Real RunContext wired around any fake seams (only the LLM is fake).
ctx = make_test_ctx(
llm=llm,
autonomy="gated",
correlation_id="test-42",
)
Test doubles live under agentkit.testing.* on purpose — a
from agentkit import FakeLLM shape would let production code
accidentally pin a fake. The import boundary is the guardrail.