agentkit.capabilities¶
Optional collaborators: RequestBuilder, Grounder, Compactor,
Guardrail, Evaluator, Checkpointer, output-schema SchemaAdapter.
See the Capabilities concept for the mental model.
Optional collaborators over the seams.
Grounder
module-attribute
¶
A grounder is anything async-callable as (ctx, task) -> str.
Why a callable and not a MemorySource plus k/where knobs: those knobs
are retrieval mechanics, not prompt-assembly concerns. RequestBuilder should
not know that the grounding came from a vector store at all — only
that some text is available for this task. The caller bakes the
retrieval policy (k, where filter, query derivation, even the choice
of source — vector index, MCP server, static fixtures) in once at
wiring time, then hands the bound callable to the RequestBuilder.
A VectorMemory (or any MemorySource) is adapted to a Grounder
by writing a small async wrapper that queries it and formats the
returned MemoryItems:
async def grounder(ctx, task):
items = await mem.query(task, k=5, ctx=ctx)
return "\n".join(f"[{i.source}] {i.content}" for i in items)
builder = RequestBuilder(prompt=..., grounder=grounder)
For role-specific tuning, the wrapper stays adjacent to the agent definition without leaking into RequestBuilder's signature.
Checkpointer
dataclass
¶
Checkpointer(
port: CheckpointPort, clock: ClockPort | None = None
)
Capability over a CheckpointPort.
snapshot() bumps the version, stamps created_at, and saves. resume() returns the
latest checkpoint (or None if there's nothing to resume from). at_version() is the
time-travel hook — return a specific historical snapshot, or None if it doesn't
exist. delete() clears all versions for a run_id (terminal cleanup).
snapshot
async
¶
snapshot(
run_id: str,
state: dict[str, Any],
*,
status: CheckpointStatus = RUNNING,
metadata: dict[str, Any] | None = None,
ctx: Any = None,
) -> Checkpoint
Persist a new versioned snapshot. Returns the saved Checkpoint so the caller can
log/expose its version. Failures from port.save propagate — the caller decides
whether a checkpoint-save failure should abort the run.
state and metadata are deep-copied at the seam.
Checkpoint is a frozen shell, but its state / metadata
dicts are stored by reference; InMemoryStore returns those
refs on resume. Without the copy, a caller that keeps a live
handle to the scratch dict (or a resumer that pops entries from
cp.state) would mutate the durable record — parity with any
serializing backend (Redis, Postgres) would break silently.
Deep-copy models the wire semantics locally so in-memory tests
stay honest about the durability contract.
ctx is optional: when supplied and it carries a TracePort
via ctx.trace, a checkpointer.snapshot span is opened
around the save so operators can see checkpoint activity in the
trace timeline. Callers without a ctx (bare unit tests, offline
replayers) leave it unset and the span helper degrades to a
nullcontext.
resume
async
¶
resume(
run_id: str, *, include_terminal: bool = False
) -> Checkpoint | None
Return the latest RESUMABLE checkpoint for run_id, or None.
By default, a terminal snapshot (DONE / FAILED) is treated as
"no resumable state" and this returns None — a naive
"resume if any checkpoint exists" wiring MUST NOT re-run a
finished job. Callers that legitimately want to inspect the
terminal snapshot (an audit UI, an operator tool, replay /
time-travel debugging) opt in explicitly with
include_terminal=True.
status on the loaded checkpoint may be a raw string (some
producers, and the Checkpoint shape's own tests, pass strings
directly) rather than a CheckpointStatus enum instance;
because CheckpointStatus is a StrEnum, wrapping the
value normalises both cases without an isinstance branch.
at_version
async
¶
at_version(run_id: str, version: int) -> Checkpoint | None
Return the checkpoint at exactly version, or None if missing.
The time-travel / replay seam.
list_versions
async
¶
Return all known versions for run_id in ascending order.
delete
async
¶
Clear all checkpoints for run_id. Idempotent — deleting an unknown run is a no-op.
Also evicts the per-run asyncio.Lock stashed in
_run_locks — without this, a long-lived Checkpointer serving
many short-lived runs (a shared engine pool, a coordinator
spawning throwaway children) would accumulate one lock per
distinct run_id for the process lifetime. Deletion is the
producer's signal that a run is over, so it's the natural
eviction point. The lock is dropped only AFTER port.delete
succeeds — a failed backend delete leaves the lock in place so a
retry stays serialised with any in-flight snapshot for the same
run.
Compactor
¶
Bases: Protocol
The single seam every compaction policy implements.
A Compactor takes a transcript and returns a (possibly smaller) transcript. Implementations
own their own threshold check — a no-op return on a transcript that is already under budget is
expected, and lets the caller call compact() unconditionally without a guard at the call site.
ImportanceFilteringCompactor
dataclass
¶
ImportanceFilteringCompactor(
filterer: Any,
model: str = "",
max_tokens: int = 12000,
keep_recent: int = 2,
estimate: Callable[
[list[Message]], int
] = _approx_tokens,
)
Uses an LLM to identify and keep only the most important turns.
SlidingWindowCompactor
dataclass
¶
Strictly keeps only the system prompt and the N most recent turns.
SummarizationCompactor
dataclass
¶
SummarizationCompactor(
summarizer: Any,
model: str = "",
max_tokens: int = 12000,
keep_recent: int = 4,
estimate: Callable[
[list[Message]], int
] = _approx_tokens,
)
Summarizes the older middle of the conversation using an LLM.
TruncationCompactor
dataclass
¶
TruncationCompactor(
max_tokens: int = 12000,
keep_recent: int = 4,
estimate: Callable[
[list[Message]], int
] = _approx_tokens,
)
Drops the oldest messages (excluding system prompt) to fit within token limits.
Evaluator
¶
Evaluator(
code_checks: dict[str, Callable[[Any], bool]]
| None = None,
*,
judge_model: str = "",
rubric: str = "",
)
judge
async
¶
Run the judge model through the invoker chain — tracing +
retry + meter all fire. Direct self._judge_llm.complete() would
bypass middleware: a flaky judge couldn't be retried and its cost
wouldn't land on the run's budget. Routing through
ctx.invoker.chat() keeps the judge on the same rails as
every other model call.
Off-hot-path: caller is expected to fire-and-forget on a worker
/ background task. Returns {} on any failure (broken judge
result, JSON parse failure, missing invoker) so monitoring
records the empty result without crashing the run.
OutputCoercionError
¶
Bases: Exception
Raised when a model response can't be coerced into the declared schema.
Carries the validation diagnostics (errors) so the retry middleware can reflect
them back to the model as a user message — "your previous response failed these
checks: …, please try again" — which is the single most effective fix for the
"almost-correct JSON" failure mode. The original raw payload (raw) is preserved
for logging and for the transcript so a human reviewing a failed run can see
exactly what the model emitted.
Wrapping every flavour's native exception (Pydantic ValidationError, stdlib
json.JSONDecodeError, our own missing-field errors) under a single type means
callers don't have to know which adapter is in play to write a retry policy.
raw
instance-attribute
¶
The exact payload (string or dict) the model produced — preserved verbatim for logging, transcripts, and the "show me the failure" UI.
errors
instance-attribute
¶
Per-field validation diagnostics as plain strings. The retry middleware joins these into a user message; the eval harness asserts on them.
SchemaAdapter
¶
Bases: Protocol[T]
Uniform interface over any output-schema flavour the user declares on an Agent.
Implementations exist for Pydantic BaseModel, stdlib dataclass, attrs class, and
raw JSON Schema dict. New flavours plug in by satisfying this protocol — the
adapt() dispatcher is the single place that picks one for a given user input.
name
instance-attribute
¶
Used as the tool name on Anthropic structured-output mode and as the schema
label inside the system-prompt fallback. Defaults to the class __name__ for
Python types; pass-through for raw JSON Schema dicts (caller can name them).
python_type
instance-attribute
¶
The concrete type instances are coerced INTO. dict for the raw-JsonSchema
flavour. Used for AgentResult[T] generic stamping and isinstance checks
at the boundary, so callers can write assert isinstance(result.output, Schema)
without caring which adapter is in play.
json_schema
¶
Render the JSON Schema dict the LLM (or its structured-output mode) will
see. Must be self-contained — no $ref to external documents — because some
providers reject external refs.
parse
¶
Coerce a raw model response into a typed instance. Accepts both:
- str: the model emitted a JSON string (text-mode fallback path).
- dict: a provider's structured-output mode already parsed it for us.
Raises :class:OutputCoercionError (NOT the underlying ValidationError /
json.JSONDecodeError / KeyError) so the retry middleware has a single
exception type to catch and a structured diagnostics list to reflect back to
the model. Never returns None — partial / unfinished outputs go through
:meth:partial_parse instead.
partial_parse
¶
Tolerant parse for streaming. Returns the best-effort partial object
the so-far buffer permits — e.g. a Pydantic model with whatever fields
have already arrived, missing required fields left unset. Returns
None if the buffer has zero useful structure yet (empty, pure
whitespace, a single open bracket the parser can't resolve to a
useful prefix).
Implementations build the partial OBJECT via the type's bypass-init
path (BaseModel.model_construct / object.__new__ + setattr)
so missing required fields don't raise. The streaming output_coerce
middleware calls this on each text delta and lifts the result onto
Delta.partial when it changes — the consumer sees a steadily
more-complete partial without waiting for the terminal delta. The
strict :meth:parse still runs at end-of-stream and remains the
source of truth for the final typed object.
serialize
¶
Round-trip the typed instance back to a JSON-serialisable dict. Used by the durable run-store (so a resumed run can read back the same output the original run produced) and by the transcript replay for evals.
validate
¶
Validate an arbitrary Python value against the schema.
Distinct from :meth:parse (which expects raw JSON or dict from a model
response). validate handles the tool-result case, where the value
coming in is whatever Python object the tool's function produced —
could be an already-instantiated model, a dict the tool built by hand,
or a primitive when the schema wanted an object.
- Already-typed instance (e.g.
MyModel(...)passed whenoutput_schema=MyModel) — fast-path, returns as-is. - Dict → run through coercion (typically via
parse) to build the typed instance. - Anything else (wrong type entirely) → raise :class:
OutputCoercionError.
Used by the tool-result schema check on FunctionTool, where the
tool's return value must match its declared output_schema.
BuiltRequest
dataclass
¶
BuiltRequest(
messages: list[Message],
prompt_version: str,
approx_tokens: int,
)
The output of RequestBuilder.build(). Carries the messages ready
to send AND the cross-cutting signals the caller's invoker and
telemetry need:
prompt_versionlets the caller stamp the agent's output for attribution (so a regression maps back to the exact template).approx_tokensis a crude pre-call estimate the caller can use for budget pre-checks (the authoritative count comes back from the provider inUsage; this is just a guardrail).
RequestBuilder
dataclass
¶
RequestBuilder(
prompt: Prompt,
grounder: Grounder | None = None,
compactor: Compactor | None = None,
reground_every_turn: bool = False,
budget_check: BudgetCheck | None = None,
)
Assembles the LLM input. One instance per agent role per run is typical — the seed prompt and grounding policy don't change inside a run, only the task and the growing transcript do.
Required
prompt: the versioned seed system prompt (an
agentkit.prompts.Prompt).
Optional
grounder: an async callable (ctx, task) -> str injected on
the first turn (or every turn if reground_every_turn).
Empty string means "no grounding for this task" and
produces no grounding message. The caller owns the policy
(k, where, query derivation) — RequestBuilder just calls.
compactor: a Compactor to fold the transcript when it grows.
Applied AFTER the new turn is appended, to the
WorkingContext.messages tail. The cache-stable prefix
is never touched — compactors must not pretend to. Kept-
recent tail always includes the message the LLM is about
to answer.
reground_every_turn: when True, the grounder is invoked on
every turn, and the previous grounding message is dropped
before the new one is appended. The default (False)
preserves the legacy first-turn-only behaviour, which
keeps the prefix cacheable.
budget_check: optional (approx_tokens) -> None callable
invoked at the end of build(). Raise to abort — the
RequestBuilder surfaces no judgement of its own about what's
"too big," that policy lives entirely with the caller.
build
async
¶
build(
task: str,
wc: WorkingContext,
ctx: Ctx,
*,
output_adapter: SchemaAdapter[Any] | None = None,
) -> BuiltRequest
Append a turn to wc and return the messages to send.
First turn (empty prefix AND empty messages): the system
prompt + (optional) grounding land in wc.prefix — the
cache-stable head — and the user task is appended to
wc.messages. Subsequent turns: just append the user task
(and re-ground if reground_every_turn).
output_adapter: when supplied, the adapter's JSON Schema is
rendered into wc.prefix.schema_block on the first turn — a
cache-stable third pinned system message after the prompt and
grounding. Never written into wc.messages (cache invariant).
Mutates wc in place so the caller's running blackboard
stays the source of truth for the transcript. The returned
messages list is a fresh copy of wc.assembled() —
safe to hand to the invoker without worrying about post-call
mutation racing the next turn.