Pydantic AI harnesses: memory, planning and practical research agents
A September 2026 review of Pydantic AI’s Python agent harnesses: persistent memory, to-do lists, web research, thinking, dynamic workflows and background execution.
An AI agent becomes useful when it can keep track of the work, find evidence and recover context without asking the user to repeat everything. Getting a model to call a function is the easy part. Building those surrounding behaviours is where an application starts accumulating infrastructure.
Pydantic AI’s recent harness work tackles that layer in Python. What interests me is the way its capabilities can be combined: memory for continuity, a task list for progress, research tools for evidence and orchestration when one agent is insufficient. You can assemble the parts around an ordinary Agent, rather than adopting an entirely separate application architecture.
How the harness grew over the last year
The harness repository’s release history begins within this review period, rather than spanning the whole twelve months. Its first public release arrived in April 2026 with Code Mode: an approach to coordinating tool calls through code. The following months added the state and ready-made stacks that make longer tasks practical.
| Release milestone | Why it matters |
|---|---|
| April 2026: Harness 0.1.0 | Code Mode established the initial harness offering. |
| June 2026: Harness 0.4.0 | Experimental Planning introduced structured task tracking. |
| June 2026: Pydantic AI 2.0 | The core framework reached its stable second major release. |
| July 2026: Harness 0.7.0 | Memory added persistent agent notebooks. |
| August 2026: Harness 0.15.0 | Planning incorporated the richer task-list features of pydantic-ai-todo. |
| August 2026: Harness 0.21.0 | Coder and Researcher provided composed harnesses. |
These are release milestones, not a claim that every capability below first appeared on those dates. Initial release, Planning release, core 2.0, Memory release, task-list expansion, composed harnesses.
The distinction between packages is useful. Pydantic AI supplies the agent framework and core capabilities such as Thinking, WebSearch and WebFetch. The separate Harness package supplies higher-level capabilities and combinations such as Researcher. Its faster release cadence lets that layer evolve independently. Harness architecture and version policy.
My preference is to start with a narrow agent and add capabilities when a real task requires them. Giving every agent every tool makes its behaviour harder to explain, and its failures harder to reproduce.
Memory: a notebook with boundaries
Memory gives an agent a persistent Markdown notebook. A bounded excerpt of MEMORY.md, together with a listing of other files, can enter the current request. Longer notes stay outside the prompt until the agent reads or searches them. The tool surface includes write_memory, read_memory, delete_memory and search_memory.
This is useful for remembering project conventions, an agreed research scope or a decision already made. It avoids loading the entire history into every conversation. I would persist this notebook in PostgreSQL so application workers share the same durable memory. The Markdown notes are stored as database content rather than local files. Application code resolves the namespace, so the model does not choose another user’s notebook through tool arguments. Memory model and stores.
Here is a single-project example using PostgresMemoryStore and an application-owned asyncpg pool. Install the driver with uv add asyncpg, alongside the pinned Pydantic packages below, and supply the connection string through DATABASE_URL:
import asyncio
import os
import asyncpg
from pydantic_ai import Agent
from pydantic_ai_harness import Memory
from pydantic_ai_harness.memory import PostgresMemoryStore
async def main() -> None:
async with asyncpg.create_pool(os.environ['DATABASE_URL']) as pool:
project_memory = Memory(
PostgresMemoryStore(pool),
max_tokens=2_000,
max_lines=200,
)
agent = Agent('openai:gpt-5', capabilities=[project_memory])
result = await agent.run(
'Remember that this project uses cursor pagination. '
'Record it as a project convention in the notebook.'
)
print(result.output)
asyncio.run(main())
The pool stays open throughout the asynchronous agent run and closes when the context exits. In a web service, I would create it at application startup and close it at shutdown, reusing it across requests. This version of the store initializes its schema on first use, so the database role needs permission to create its tables and sequence. PostgreSQL store implementation.
I would store concise decisions with their source and review date. A note saying “the API uses cursor pagination” is useful; an unexplained copy of an old response is much less useful. Persistent memory can preserve mistakes just as effectively as correct information.
The injected notebook is delimited user-role context, with trusted usage guidance kept separately. Store mutations use optimistic concurrency and idempotency, helping avoid stale overwrites and duplicate writes. Those mechanisms do not replace application authorization. For a shared service, I would resolve scope from authenticated identity, set retention rules and keep credentials out of the notebook. Memory injection and concurrency.
To-do tasks and planning: make progress visible
Planning turns a vague instruction into a structured work list the agent can update. It can write or read a plan, add tasks, change their status and remove them. Subtasks and dependencies are available when enabled. Task-list features from the separate pydantic-ai-todo project are now incorporated here, so older examples using write_todos need translating to write_plan.
The default plan lives in memory for a run. Persistent stores support work spanning sessions, and events let an application expose changes in its interface. The current plan is injected as an ephemeral reminder, rather than repeatedly accumulating in message history. Its cache handling aims to preserve the durable prompt prefix, subject to provider support and capability ordering. Planning tools, persistence and reminders.
For research, I would ask for tasks such as locating release notes, reading the relevant documentation, checking contradictory claims and producing a cited brief. That gives the user something observable while the agent works.
A completed checkbox still reflects what the model believes. I would make important completion criteria independently checkable: a source was fetched, a schema validated or a test actually passed. Memory holds reusable context; the plan records work still required. Keeping those purposes distinct makes both easier to maintain.
Web search finds candidates; web fetch checks them
WebSearch and WebFetch solve different parts of research. Search finds potentially useful pages. Fetch reads the pages that support a claim. A search-result snippet alone is a weak basis for asserting compatibility, a release date or a changed API.
Both core capabilities support provider-native tools. In this snapshot, their defaults require native support; configuring local=True enables fallback implementations when native support is unavailable. Search can fall back to DuckDuckGo, while fetch has a page-to-Markdown implementation. Researcher enables these fallbacks for you. WebSearch behaviour, WebFetch behaviour.
The implementation matters when configuring restrictions: provider-specific search settings and limits are not automatically portable to every fallback. I would verify the chosen model’s behaviour before relying on a domain restriction or usage setting.
My research rule is simple: retain the direct source URL, read the material behind it and distinguish its publication date from the date of the event. Retrieved pages are untrusted input. A page telling an agent to change its instructions should be treated as page content, and a fetcher should operate within the application’s network access policy.
The Researcher stack: a useful starting point
Researcher combines research instructions, WebSearch, WebFetch, sub-agent delegation and tool-output limits. Its default instructions emphasise primary sources, citations and separating evidence from inference. A focused researcher sub-agent is included by default; Researcher(subagents=[]) disables delegation.
I like that this is an inspectable combination of capabilities. You can keep the starting point, change the instructions or replace a component. Tool-output limits also help keep a very large page from consuming the available context. Researcher composition.
For a reproducible example, install the dated versions and the provider extra:
uv add "pydantic-ai-slim[openai]==2.46.0" \
"pydantic-ai-harness[researcher]==0.32.0"
Then define the shape of a research brief:
from pydantic import BaseModel, HttpUrl
from pydantic_ai import Agent
from pydantic_ai.capabilities import Thinking
from pydantic_ai_harness import Planning, Researcher
class Finding(BaseModel):
claim: str
source_url: HttpUrl
class ResearchBrief(BaseModel):
findings: list[Finding]
open_questions: list[str]
agent = Agent(
'openai:gpt-5',
output_type=ResearchBrief,
capabilities=[Researcher(), Planning(), Thinking(effort='high')],
)
result = agent.run_sync(
'Research Pydantic AI harness capabilities available by '
'19 September 2026. Use primary sources, check release dates, '
'and report any claims you cannot verify.'
)
print(result.output.model_dump_json(indent=2))
Running this needs an OpenAI API credential in the environment and network access. The model is an example choice, rather than a requirement of Researcher.
The typed output gives application code a predictable contract. HttpUrl validates the URL’s shape; it does not prove the page supports the claim. For an automated report, I would add an evidence-review step before treating findings as publishable facts.
Thinking is different from planning
Thinking is a core capability that maps a requested effort level to supported provider settings. Different models expose reasoning effort or thinking budgets differently, so the mapping depends on the selected model. It is not a separate researcher or a guarantee of accuracy. Thinking capability.
Planning gives the application an external work ledger. Thinking influences how the model reasons while producing its next response. I would use both for a difficult investigation, but I would not turn up reasoning effort as a substitute for fetching missing evidence.
My bias is to spend effort on the uncertain part of a task. Comparing incompatible migration options may justify more reasoning. Extracting a date from a release page usually needs a good tool result and straightforward validation.
Dynamic Workflow: let the agent coordinate specialists
DynamicWorkflow exposes a run_workflow tool through which an orchestrator writes a sandboxed Python script. Named agents become asynchronous functions in that script. It can fan out independent investigations, chain results or combine typed outputs without returning every intermediate response to the parent model.
For example, one specialist could check release history while another checks API documentation, followed by a reviewer comparing their findings. Calls use a keyword task; typed Pydantic outputs arrive inside the sandbox as dictionaries. Each sub-agent call has isolated context, so its task must contain the information it needs. Dynamic Workflow execution model.
I would use this when the coordination genuinely depends on what the agents discover. A fixed business process with known transitions is often easier to implement directly in application code.
Budgets deserve explicit attention. max_agent_calls limits calls across the parent run; separate sub-agent usage limits are also available. Concurrent calls can complicate aggregate budget enforcement. The Monty sandbox’s execution limits should not be mistaken for an end-to-end deadline that includes waiting for remote agents. I would cap fan-out, retain traces and define what happens when a specialist fails. Workflow limits.
Background tools: concurrency and durability are separate
The named BackgroundTools capability is absent from the 0.32.0 snapshot. There is still useful background execution to discuss at this cutoff: Shell can start a command, return an identifier and let the agent check or stop it while doing other work.
Run-scoped commands are cleaned up when the agent run ends. An opt-in persistent shell tool supports commands that outlive that run. Those are different lifecycles, and neither should be confused with a durable application job queue. Shell background processes and persistent commands.
For a developer agent, starting a test suite and checking its output later is practical. For an operation that must survive a worker restart, I would use durable execution or application-owned job infrastructure with explicit retry and cancellation semantics. Shell command filters are guardrails; untrusted commands still need operating-system isolation.
What I would build first
My starting application would be a focused research assistant: Researcher for sources, Planning for visible progress and typed findings for the output contract. I would add namespaced Memory once repeated conversations make continuity useful, then introduce Dynamic Workflow only when independent investigations benefit from running together.
The evaluation would include stale sources, contradictory documentation, failed fetches and a remembered decision that is now wrong. I would measure supported claims and useful task completion alongside latency and cost. The attraction of these harnesses is that they supply inspectable building blocks; the application still owns evidence quality, permissions and the definition of success.