Build agents
One harness for the development loop
AgentHarness wraps any object with invoke(input) and gives every run a stable result shape. create_agent builds the default deep ReAct agent and returns it already wrapped.
Create an agent
from litgraph import create_agent
from litgraph.providers import AnthropicChat
from litgraph.tools import WebFetchTool
harness = create_agent(
AnthropicChat(model="claude-sonnet-4-5"),
tools=[WebFetchTool()],
instructions="Research carefully and cite the supplied sources.",
agents_md_path="AGENTS.md",
skills_dir="skills",
max_iterations=12,
planning=True,
virtual_filesystem=True,
trace_path=".litgraph/traces.jsonl",
)
| Option | Meaning | Default |
|---|---|---|
model |
An explicit chat model or compatible test double. | Required |
tools |
Native, decorated, or adapted tools available to the agent. | Empty |
instructions |
System-level behavior for the agent. | None |
agents_md_path |
Repository instructions loaded into agent context. | None |
skills_dir |
Directory containing reusable agent skills. | None |
max_iterations |
Maximum model/tool loop count; must be positive. | 15 |
planning |
Include planning support in the deep agent. | True |
virtual_filesystem |
Include an in-memory scratch filesystem. | True |
trace_path |
Append lifecycle and agent events as JSONL. | None |
on_event |
Receive each normalized event in process. | None |
The factory keeps provider selection and capabilities explicit. It does not guess a model or discover tools from global state.
Run and inspect
run = harness.run("Prepare a concise incident summary")
print(run.output)
print(run.elapsed_ms)
print(run.run_id)
print(run.success)
Each AgentRun contains:
run_id: a unique hexadecimal identifier.input: the original text passed to the harness.output: normalized text extracted from common agent result shapes.state: the complete native result for advanced inspection.elapsed_ms: wall-clock execution time.successanderror: a uniform outcome pair.
By default, run() re-raises agent exceptions after recording the failed run. Set raise_errors=False when failures should become data:
run = harness.run("risky task", raise_errors=False)
if not run.success:
logger.error("agent failed", extra=run.to_dict())
harness.last_run always points to the latest completed or captured run.
Stream native events
for event in harness.stream("Draft the report"):
if event.get("kind") == "text":
print(event.get("text", ""), end="", flush=True)
stream(input, tokens=True) prefers the agent’s stream_tokens() method and falls back to stream(). Set tokens=False to request the broader event stream directly. The harness forwards every yielded event to the same tracing and hook pipeline used by ordinary runs.
For model-level async streaming, litGraph also exposes typed stream parts and utilities such as broadcast, race, and multiplex. See models and tools.
Add an event hook
events = []
def capture(event):
events.append(event)
harness = create_agent(model, on_event=capture)
harness.run("hello")
assert events[0]["type"] == "run_start"
assert events[-1]["type"] == "run_end"
Lifecycle records receive a Unix timestamp automatically. Event hooks run synchronously, so keep them fast or enqueue work for another consumer.
Persist JSONL traces
harness = create_agent(model, trace_path=".litgraph/traces.jsonl")
harness.run("Investigate the alert")
Each event is serialized as one JSON object per line. Writes are protected by a lock, so multiple harness runs in the same process do not interleave partial records. Inspect a trace with:
pixi run litgraph trace .litgraph/traces.jsonl
pixi run litgraph trace .litgraph/traces.jsonl --json
Use JSONL traces for local debugging and reproducible fixtures; use OpenTelemetry when traces must cross process or service boundaries.
Evaluate the same agent
report = harness.evaluate(
[
{"input": "What is 2 + 2?", "expected": "4"},
{"input": "What is 7 × 6?", "expected": "42"},
],
scorers=[{"name": "exact_match"}],
max_parallel=4,
)
print(report["aggregate"]["means"])
Evaluation invokes the same harness.run(prompt).output path used in production. That prevents test-only adapters from hiding a mismatch in instructions, tools, output normalization, or tracing.
Wrap an existing agent
The harness is not limited to the built-in deep-agent factory:
from litgraph import AgentHarness
from litgraph.agents import ReactAgent
agent = ReactAgent(model, tools, system_prompt="Be concise.")
harness = AgentHarness(agent, trace_path="runs.jsonl")
Any object with a callable invoke(input) is accepted. Streaming is available when it also exposes stream() or stream_tokens().
Test agent behavior deterministically
Use ScriptedChatModel to specify model responses in order. This is the preferred unit-test seam because the actual agent loop, tool execution, and result normalization still run.
from litgraph.testing import ScriptedChatModel
model = ScriptedChatModel(["draft", "revision"])
harness = create_agent(model)
assert harness.run("first").output == "draft"
assert model.calls[0]["messages"][-1]["content"] == "first"
For end-to-end provider tests, keep live credentials and network calls in a separate integration suite. See observe and evaluate.