Top level (agentlens)¶
AgentLens: regression testing and evaluation toolkit for LLM agents.
Record agent runs as traces, replay them deterministically, score them with deterministic checks and LLM-as-judge, and catch regressions in CI.
Redactor
module-attribute
¶
Takes a JSON payload and returns it with secrets masked. Must not mutate its input.
Evaluator ¶
Bases: Protocol
Anything that scores one trace of one case (docs/design.md §5).
name is what ends up in EvalResult.evaluator; it is declared as a
read-only property so both a plain attribute and a @property satisfy the
protocol. evaluate should return an EvalResult; exceptions are
turned into failed results by run_evaluator.
SpanKind ¶
Bases: StrEnum
What a span represents (FR-T2).
Trace ¶
Bases: SchemaModel
A complete agent run: its input, final output and the tree of spans.
Derived metrics (total_cost, total_tokens, latency_ms,
tool_calls, n_steps) are plain properties, not serialised fields:
they are always recomputed from the spans, so a stored trace can never hold
stale totals.
error
class-attribute
instance-attribute
¶
Set when the agent raised; the run is still recorded (FR-T5).
metadata
class-attribute
instance-attribute
¶
Run context: git sha, agent version, case id, repetition index, ...
total_cost
property
¶
Sum of known span costs in USD.
None when no span carries a cost (unknown is not the same as free).
latency_ms
property
¶
Wall-clock duration of the whole run, or None if it has not ended.
tool_calls
property
¶
Names of the tools called, in call order (used by tool_sequence).
n_steps
property
¶
Number of agent steps.
Counts AGENT_STEP spans when the agent records them; otherwise falls
back to the number of LLM calls, since each LLM call is one turn of the
agent loop.
PatternRedactor ¶
PatternRedactor(patterns: Iterable[str | Pattern[str]] = DEFAULT_PATTERNS, *, keys: Iterable[str] = DEFAULT_SENSITIVE_KEYS, replacement: str = REDACTED)
Redactor masking sensitive mapping keys and secret-looking substrings.
It walks nested lists and dicts and returns a new value; the input is never mutated.
.. code-block:: python
redactor = PatternRedactor(
patterns=(*DEFAULT_PATTERNS, r"\bINTERNAL-[0-9]{6}\b"),
keys=DEFAULT_SENSITIVE_KEYS | {"session_id"},
)
agentlens.run("agent", sink=store.save_trace, redactor=redactor)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
patterns
|
Iterable[str | Pattern[str]]
|
Regular expressions matched inside every string. A group
named |
DEFAULT_PATTERNS
|
keys
|
Iterable[str]
|
Mapping keys whose values are always masked. Keys ending in
|
DEFAULT_SENSITIVE_KEYS
|
replacement
|
str
|
The text that replaces each secret. |
REDACTED
|
evaluator ¶
evaluator(fn: Callable[..., Any]) -> FunctionEvaluator
evaluator(*, name: str | None = None, threshold: float = DEFAULT_THRESHOLD, register: bool = True, replace: bool = False, registry: EvaluatorRegistry | None = None) -> Callable[[Callable[..., Any]], FunctionEvaluator]
evaluator(fn: Callable[..., Any] | None = None, /, *, name: str | None = None, threshold: float = DEFAULT_THRESHOLD, register: bool = True, replace: bool = False, registry: EvaluatorRegistry | None = None) -> FunctionEvaluator | Callable[[Callable[..., Any]], FunctionEvaluator]
Turn fn(trace, case, **params) into a registered FunctionEvaluator.
Usable bare or with options:
@agentlens.evaluator
def mentions_order(trace, case) -> bool:
return "order" in str(trace.output)
@agentlens.evaluator(name="short_answer", threshold=0.8)
def short(trace, case, max_words: int = 50) -> tuple[float, str]:
words = len(str(trace.output).split())
return min(1.0, max_words / max(words, 1)), f"{words} words"
The function may return an EvalResult, a bool, a score in
[0, 1] (passes when >= threshold), (score_or_bool, reason) or a
mapping with score/passed (see to_eval_result).
Exceptions become failed results instead of propagating.
Parameters after (trace, case) are filled from the suite spec:
{short_answer: {max_words: 20}} or, through the scalar shorthand,
{short_answer: 20} (the first such parameter; threshold if there is none).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str | None
|
Registry/result name; defaults to the function name. |
None
|
threshold
|
float
|
Pass threshold for bare float scores. |
DEFAULT_THRESHOLD
|
register
|
bool
|
Add it to the registry so suites can refer to it by name. |
True
|
replace
|
bool
|
Override an existing evaluator with the same name. Re-defining the same function (a re-run notebook cell) replaces it anyway. |
False
|
registry
|
EvaluatorRegistry | None
|
Registry to use instead of |
None
|
Raises:
| Type | Description |
|---|---|
TypeError
|
if |
DuplicateEvaluatorError
|
if the name is taken by another evaluator. |
get_client ¶
get_client(spec: str | None = None, *, pricing: Pricing | str | PathLike[str] | None = None, **overrides: Any) -> LLMClient
Build an LLM client from "<provider>:<model>" (default: $AGENTLENS_MODEL).
Providers and the environment variables they read (overrides win):
openai:<model>—OpenAICompatClient;OPENAI_API_KEY,OPENAI_BASE_URL(defaulthttps://api.openai.com/v1).anthropic:<model>—AnthropicClient;ANTHROPIC_API_KEY,ANTHROPIC_BASE_URL.ollama:<model>—OLLAMA_BASE_URL(defaulthttp://localhost:11434/v1), no key.vllm:<model>—VLLM_BASE_URL(defaulthttp://localhost:8000/v1), optionalVLLM_API_KEY.lmstudio:<model>—LMSTUDIO_BASE_URL(defaulthttp://localhost:1234/v1).openai-compat:<model>— any other compatible server:OPENAI_COMPAT_BASE_URL(required, orbase_url=), optionalOPENAI_COMPAT_API_KEY.mock/mock:<name>—MockLLMreporting model<name>(default"mock") that echoes the last user message; passscript=(andcycle=,default=) to script it. Costs 0 and needs no network.
Local servers never receive OPENAI_API_KEY. Every client except the mock is
wrapped in a PricedClient, which fills usage.cost_usd from pricing:
a Pricing, a YAML file merged over the packaged table, or None for
Pricing.load (packaged table + $AGENTLENS_PRICING).
Raises:
| Type | Description |
|---|---|
LLMConfigError
|
No spec and no |
current_trace ¶
current_trace() -> Trace | None
The trace being recorded in this context, or None outside run.
redact_trace ¶
Return a copy of trace with every payload passed through redactor.
Redacted fields: the trace's input, output, metadata and
error and each span's input, output, attributes and
error. Ids, names, timings and usage are kept as they are. The original
trace is not modified.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
trace
|
Trace
|
The trace to redact. |
required |
redactor
|
Redactor | None
|
Defaults to |
None
|
Raises:
| Type | Description |
|---|---|
Exception
|
whatever |
run ¶
run(name: str, *, input: object = None, metadata: Mapping[str, object] | None = None, sink: TraceSink | None = None, redactor: Redactor | None = DEFAULT_REDACTOR) -> Iterator[Trace]
Record one agent run as a Trace.
.. code-block:: python
with agentlens.run("support-agent", input=question) as trace:
trace.output = agent(question)
print(trace.latency_ms, trace.tool_calls)
Spans created inside the block (by trace, span or the LLM
wrappers) are attached to the yielded trace. When the block exits the trace
gets its end_time (and error if the block raised) and a redacted
copy is passed to sink; the yielded trace itself is not redacted.
Exceptions from the block are re-raised after recording.
Runs may be nested; an inner run records an independent trace.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Trace name, e.g. the agent or suite name. |
required |
input
|
object
|
The agent input; converted to JSON ( |
None
|
metadata
|
Mapping[str, object] | None
|
Run context such as git sha, case id or repetition. |
None
|
sink
|
TraceSink | None
|
Called once with the finished trace (e.g. |
None
|
redactor
|
Redactor | None
|
Masks secrets in the copy given to |
DEFAULT_REDACTOR
|
span ¶
span(name: str, *, kind: SpanKind | str = CUSTOM, input: object = None, attributes: Mapping[str, object] | None = None) -> Iterator[Span | None]
Record a block of code as a span.
Yields the running Span (set span.output to
record a result), or None when no run is active.
.. code-block:: python
with agentlens.span("search", kind="retrieval", input=query) as s:
docs = index.search(query)
if s is not None:
s.output = [d.id for d in docs]
trace ¶
trace(*, kind: SpanKind | str = ..., name: str | None = ...) -> Callable[[Callable[P, R]], Callable[P, R]]
trace(func: Callable[P, R] | None = None, /, *, kind: SpanKind | str = CUSTOM, name: str | None = None) -> Callable[P, R] | Callable[[Callable[P, R]], Callable[P, R]]
Decorator recording each call of a sync or async function as a span.
The span input is the call's arguments by parameter name (without
self/cls), the output is the return value, and an exception is
stored in span.error before being re-raised.
.. code-block:: python
@agentlens.trace(kind="tool_call")
def lookup_order(order_id: str) -> dict: ...
@agentlens.trace # kind=custom, name=function name
async def plan(question: str) -> list[str]: ...
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kind
|
SpanKind | str
|
The |
CUSTOM
|
name
|
str | None
|
Span name; defaults to the function's |
None
|
Raises:
| Type | Description |
|---|---|
TypeError
|
if applied to a generator function (not supported yet). |
ValueError
|
if |
wrap_llm ¶
wrap_llm(client: LLMClient, *, name: str | None = None, provider: str | None = None, include_raw: bool = False) -> TracedLLM
Wrap an LLM client so every call inside run is recorded.
Each complete/acomplete call becomes an llm_call span:
- name:
nameif given, else the requested model, else the client'smodelattribute, else the client class name; - input: the
LLMRequestas JSON (fields left at their defaults omitted); - output: the
LLMResponseas JSON, withoutusage(stored inspan.usage) and without the provider'srawpayload unlessinclude_raw=True; - usage: the response's token counts and cost (
Noneif unreported); - attributes:
client(class path),provider(if known),model,temperature,max_tokens,n_messages,n_tools,tool_names,response_format,request_hash(cassette key, see ADR 0004) and, once answered,response_modelandn_tool_calls.
Errors raised by the client are stored in span.error and re-raised.
Outside a run the wrapper calls straight through. Wrapping an already
wrapped client returns it unchanged.
.. code-block:: python
llm = agentlens.wrap_llm(MockLLM(["Hello!"]))
with agentlens.run("agent") as trace:
llm.complete(LLMRequest(messages="Hi"))
assert trace.spans[0].usage is not None
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
client
|
LLMClient
|
Any object implementing |
required |
name
|
str | None
|
Fixed span name instead of the model name. |
None
|
provider
|
str | None
|
|
None
|
include_raw
|
bool
|
Also record |
False
|
wrap_tool ¶
wrap_tool(fn: Callable[P, R] | None = None, /, *, name: str | None = None) -> Callable[P, R] | Callable[[Callable[P, R]], Callable[P, R]]
Record each call of a tool (sync or async callable) as a tool_call span.
Use it as a plain wrapper or as a decorator, with or without a name:
.. code-block:: python
search = agentlens.wrap_tool(index.search, name="search_docs")
@agentlens.wrap_tool
def lookup_order(order_id: str) -> dict[str, str]: ...
@agentlens.wrap_tool(name="refund")
async def issue_refund(order_id: str, amount: float) -> bool: ...
The span name is the tool name (name, else the function's
__name__), which is what Trace.tool_calls and the
tool_sequence evaluator compare. The input is the call's arguments by
parameter name (self/cls dropped), the output the return value; an
exception is recorded in span.error and re-raised. Outside a run the
tool is called directly. The returned function exposes the original as
__wrapped__ and the tool name as __agentlens_tool__; wrapping it
again returns it unchanged, or re-wraps the original under a new name
(never two nested spans).
Raises:
| Type | Description |
|---|---|
TypeError
|
if |