refactor: graduate capabilities out of experimental (ACP excepted) (#347)

* refactor: graduate capabilities out of `experimental` (ACP excepted)

The `experimental` label suppressed the try->fail->report->improve loop we
want from real usage, so drop it from the harness. Ten capabilities move to
top-level submodules; ACP stays experimental (it may still be reshaped or
moved to core, and can't be generic across agents).

- Old `pydantic_ai_harness.experimental.<name>` paths keep working as
  DeprecationWarning shims (via `warn_moved`), so existing imports don't break.
- Renames to match capability names: authoring -> runtime_authoring,
  overflow -> overflowing_tool_output.
- Capabilities stay in individual submodules with no top-level re-export, so
  importing the root package never pulls in a capability's optional deps.
- Docs drop the experimental framing and the "may be removed in any release"
  language in favor of "API subject to change".

* docs: tell capability authors to ask the user when unsure about a name

Per PR review: a name is a public commitment once shipped, so ask rather than guess.
This commit is contained in:
David SF
2026-07-10 21:31:08 -05:00
committed by GitHub
parent d13cdae9cb
commit 310ffadf09
106 changed files with 680 additions and 468 deletions
+6 -3
View File
@@ -44,9 +44,12 @@ Before implementing or reviewing a capability change:
signatures when needed. Do not assume a contributor's local checkout layout.
5. Use `pydantic_ai_harness.code_mode` as the exemplar for capability shape,
docs, tests, and public exports until another capability becomes a better
example. `code_mode` is a released top-level capability; new capabilities
start under `pydantic_ai_harness.experimental` (see
`agent_docs/capability-authoring.md`, "Experimental Vs Released Exports").
example. Capabilities live in their own top-level submodule
`pydantic_ai_harness/<name>/` (module name = capability name; one module per
capability or strategy) and are not re-exported from the root `__init__.py`,
so each keeps its own optional dependencies. The `experimental` tier is
retired; ACP is the sole remaining experimental capability (see
`agent_docs/capability-authoring.md`, "Capability Submodules And Exports").
## Capabilities API reference
+16 -16
View File
@@ -104,7 +104,7 @@ With it, the model writes one Python script in which each sub-agent is an async
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.dynamic_workflow import DynamicWorkflow
from pydantic_ai_harness.dynamic_workflow import DynamicWorkflow
reviewer = Agent('anthropic:claude-sonnet-4-6', name='reviewer', description='Reviews code for bugs.')
summarizer = Agent('anthropic:claude-sonnet-4-6', name='summarizer', description='Summarizes findings.')
@@ -132,9 +132,9 @@ It composes with the rest of the harness:
- **Budgets**: `max_agent_calls` is an exact, host-enforced ceiling on sub-agent runs (it holds even under concurrent fan-out), and by default the whole tree's token spend lands on the parent run's `usage`.
- **On-demand**: `defer_loading=True` keeps the catalog out of the prompt until the model loads the capability, and `reveal()` adds a sub-agent mid-run without disturbing the prompt cache.
`DynamicWorkflow` ships under `experimental` while planned extensions (structured sub-agent inputs, durable workflows) settle the call contract; importing it emits a `HarnessExperimentalWarning`.
`DynamicWorkflow`'s API is subject to change while planned extensions (structured sub-agent inputs, durable workflows) settle the call contract. Breaking changes ship deprecation warnings where practical.
[Full tutorial →](pydantic_ai_harness/experimental/dynamic_workflow/)
[Full tutorial →](pydantic_ai_harness/dynamic_workflow/)
## Capability matrix
@@ -148,25 +148,25 @@ We studied leading coding agents, agent frameworks, and Claw-style assistants to
| | **Tool search** | Progressive tool discovery for large tool sets | :white_check_mark: [Pydantic&nbsp;AI](https://pydantic.dev/docs/ai/tools-toolsets/toolsets/#deferred-loading) | |
| | **File system** | Read, write, edit, search files with path traversal prevention | :white_check_mark: [Docs](pydantic_ai_harness/filesystem/) | [pydantic-ai-backend](https://github.com/vstorm-co/pydantic-ai-backend) (vstorm&#8209;co) |
| | **Shell** | Execute commands with allowlists, denylists, and timeouts | :white_check_mark: [Docs](pydantic_ai_harness/shell/) | [pydantic-ai-backend](https://github.com/vstorm-co/pydantic-ai-backend) (vstorm&#8209;co) |
| | **Repo context injection** | Auto-load CLAUDE.md/AGENTS.md and repo structure | :white_check_mark: [Docs](pydantic_ai_harness/experimental/context/) (experimental) | [pydantic-deep](https://github.com/vstorm-co/pydantic-deepagents) (vstorm&#8209;co) |
| | **Docs lookup** | On-demand `read_pyai_docs` tool for Pydantic AI docs | :white_check_mark: [Docs](pydantic_ai_harness/experimental/docs/) (experimental) | |
| | **Repo context injection** | Auto-load CLAUDE.md/AGENTS.md and repo structure | :white_check_mark: [Docs](pydantic_ai_harness/context/) | [pydantic-deep](https://github.com/vstorm-co/pydantic-deepagents) (vstorm&#8209;co) |
| | **Docs lookup** | On-demand `read_pyai_docs` tool for Pydantic AI docs | :white_check_mark: [Docs](pydantic_ai_harness/docs/) | |
| | **Verification loop** | Run tests after edits, auto-fix failures | :construction: [PR&nbsp;#169](https://github.com/pydantic/pydantic-ai-harness/pull/169) | |
| **Editor integration** | **ACP** | Serve an agent to editors (Zed, etc.) over the [Agent Client Protocol](https://agentclientprotocol.com) -- streamed text, diff-rendered edits, tool approval | :white_check_mark: [Docs](pydantic_ai_harness/experimental/acp/) (experimental) | |
| **Prompt management** | **Managed prompt** | Back an agent's instructions with a [Logfire](https://pydantic.dev/logfire)-managed prompt, editable without shipping code | :white_check_mark: [Docs](pydantic_ai_harness/logfire/) | |
| **Context management** | **Sliding window** | Trim conversation history to stay within token limits | :white_check_mark: [Docs](pydantic_ai_harness/experimental/compaction/) (experimental) | [summarization-pydantic-ai](https://github.com/vstorm-co/summarization-pydantic-ai) (vstorm&#8209;co) |
| | **Context compaction** | LLM-powered summarization of older messages | :white_check_mark: [Docs](pydantic_ai_harness/experimental/compaction/) (experimental) | [summarization-pydantic-ai](https://github.com/vstorm-co/summarization-pydantic-ai) (vstorm&#8209;co) |
| | **Limit warnings** | Warn agent before hitting context/iteration limits | :white_check_mark: [Docs](pydantic_ai_harness/experimental/compaction/) (experimental) | [summarization-pydantic-ai](https://github.com/vstorm-co/summarization-pydantic-ai) (vstorm&#8209;co) |
| | **Tool output management** | Truncate, summarize, or spill large tool outputs | :white_check_mark: [Docs](pydantic_ai_harness/experimental/overflow/) (experimental) | |
| **Context management** | **Sliding window** | Trim conversation history to stay within token limits | :white_check_mark: [Docs](pydantic_ai_harness/compaction/) | [summarization-pydantic-ai](https://github.com/vstorm-co/summarization-pydantic-ai) (vstorm&#8209;co) |
| | **Context compaction** | LLM-powered summarization of older messages | :white_check_mark: [Docs](pydantic_ai_harness/compaction/) | [summarization-pydantic-ai](https://github.com/vstorm-co/summarization-pydantic-ai) (vstorm&#8209;co) |
| | **Limit warnings** | Warn agent before hitting context/iteration limits | :white_check_mark: [Docs](pydantic_ai_harness/compaction/) | [summarization-pydantic-ai](https://github.com/vstorm-co/summarization-pydantic-ai) (vstorm&#8209;co) |
| | **Tool output management** | Truncate, summarize, or spill large tool outputs | :white_check_mark: [Docs](pydantic_ai_harness/overflowing_tool_output/) | |
| | **System reminders** | Inject periodic reminders to counteract instruction drift | :construction: [PR&nbsp;#181](https://github.com/pydantic/pydantic-ai-harness/pull/181) | |
| **Memory &&nbsp;persistence** | **Memory** | Persistent key-value memory across sessions | :construction: [PR&nbsp;#179](https://github.com/pydantic/pydantic-ai-harness/pull/179) | [pydantic-deep](https://github.com/vstorm-co/pydantic-deepagents) (vstorm&#8209;co) |
| | **Session persistence** | Save and restore full conversation state | :white_check_mark: [Docs](pydantic_ai_harness/experimental/step_persistence/) (experimental) | |
| | **Checkpointing** | Snapshot, resume (`continue_run`), and fork (`fork_run`) a run | :white_check_mark: [Docs](pydantic_ai_harness/experimental/step_persistence/) (experimental) | [pydantic-deep](https://github.com/vstorm-co/pydantic-deepagents) (vstorm&#8209;co) |
| | **Media externalization** | Offload large `BinaryContent` to content-addressed stores (building blocks) | :white_check_mark: [Docs](pydantic_ai_harness/experimental/media/) (experimental) | |
| **Agent orchestration** | **Sub-agents** | Delegate subtasks to specialized child agents | :white_check_mark: [Docs](pydantic_ai_harness/experimental/subagents/) (experimental) | [subagents-pydantic-ai](https://github.com/vstorm-co/subagents-pydantic-ai) (vstorm&#8209;co) |
| | **Dynamic workflow** | Orchestrate sub-agents from a model-written Python script -- fan-out, chaining, voting in one tool call | :white_check_mark: [Docs](pydantic_ai_harness/experimental/dynamic_workflow/) (experimental) | |
| | **Session persistence** | Save and restore full conversation state | :white_check_mark: [Docs](pydantic_ai_harness/step_persistence/) | |
| | **Checkpointing** | Snapshot, resume (`continue_run`), and fork (`fork_run`) a run | :white_check_mark: [Docs](pydantic_ai_harness/step_persistence/) | [pydantic-deep](https://github.com/vstorm-co/pydantic-deepagents) (vstorm&#8209;co) |
| | **Media externalization** | Offload large `BinaryContent` to content-addressed stores (building blocks) | :white_check_mark: [Docs](pydantic_ai_harness/media/) | |
| **Agent orchestration** | **Sub-agents** | Delegate subtasks to specialized child agents | :white_check_mark: [Docs](pydantic_ai_harness/subagents/) | [subagents-pydantic-ai](https://github.com/vstorm-co/subagents-pydantic-ai) (vstorm&#8209;co) |
| | **Dynamic workflow** | Orchestrate sub-agents from a model-written Python script -- fan-out, chaining, voting in one tool call | :white_check_mark: [Docs](pydantic_ai_harness/dynamic_workflow/) | |
| | **Skills** | Progressive tool loading -- search, activate, deactivate | :construction: [PR&nbsp;#183](https://github.com/pydantic/pydantic-ai-harness/pull/183) | [pydantic-ai-skills](https://github.com/DougTrajano/pydantic-ai-skills) (DougTrajano), [pydantic-deep](https://github.com/vstorm-co/pydantic-deepagents) (vstorm&#8209;co) |
| | **Planning** | Break complex tasks into structured plans before execution | :white_check_mark: [Docs](pydantic_ai_harness/experimental/planning/) (experimental) | |
| | **Runtime authoring** | Let an agent author, validate, and load real capabilities at runtime | :white_check_mark: [Docs](pydantic_ai_harness/experimental/authoring/) (experimental) | |
| | **Planning** | Break complex tasks into structured plans before execution | :white_check_mark: [Docs](pydantic_ai_harness/planning/) | |
| | **Runtime authoring** | Let an agent author, validate, and load real capabilities at runtime | :white_check_mark: [Docs](pydantic_ai_harness/runtime_authoring/) | |
| | **Task tracking** | Track tasks, subtasks, and dependencies | :memo: [#65](https://github.com/pydantic/pydantic-ai-harness/issues/65) | [pydantic-ai-todo](https://github.com/vstorm-co/pydantic-ai-todo) (vstorm&#8209;co) |
| | **Teams** | Multi-agent teams with shared state and message bus | :memo: [#195](https://github.com/pydantic/pydantic-ai-harness/issues/195) | [pydantic-deep](https://github.com/vstorm-co/pydantic-deepagents) (vstorm&#8209;co) |
| **Safety &&nbsp;guardrails** | **Input guardrails** | Validate user input before the agent run starts | :white_check_mark: [Docs](pydantic_ai_harness/guardrails/) | [pydantic-ai-shields](https://github.com/vstorm-co/pydantic-ai-shields) (vstorm&#8209;co) |
+26 -12
View File
@@ -31,21 +31,35 @@ Each capability package should normally have:
The root `pydantic_ai_harness/__init__.py` should re-export stable public
capabilities. Keep implementation helpers private unless users need them.
### Experimental Vs Released Exports
### Capability Submodules And Exports
New capabilities land under `pydantic_ai_harness/experimental/`. Each
experimental package calls `warn_experimental('<name>')` in its `__init__.py` so
importing it emits `HarnessExperimentalWarning`. Anything under `experimental`
may change or be removed in any release, without a deprecation period.
The `experimental` tier is retired. ACP is the sole remaining experimental
capability (`pydantic_ai_harness/experimental/acp/`); do not add new capabilities
there.
Promote a capability to a top-level package and a top-level re-export in
`pydantic_ai_harness/__init__.py` only when its API is stable.
New capabilities land as a top-level submodule `pydantic_ai_harness/<name>/`.
They are not re-exported from the root `pydantic_ai_harness/__init__.py`: each
capability keeps its own optional dependencies, so importing the root package
must not pull in a capability's extras. Users import a capability from its
submodule (`from pydantic_ai_harness.<name> import ...`).
Top-level exports in `pydantic_ai_harness/__init__.py` are the intended public
surface. Once an export has shipped in a published release it is a
backward-compatibility commitment: do not move, rename, or break it. (`CodeMode`
is a shipped, released example.) Do not relocate an already-top-level capability
into `experimental`.
Naming: the module name is the capability name, one module per capability or
strategy. Prefer a longer descriptive name over a terse one (e.g.
`overflowing_tool_output`, not `overflow`). A known term is fine as-is (e.g.
`compaction`). If you are unsure what to name a capability, ask the user (via the
ask-user tool) rather than guessing -- a name is a public commitment once shipped.
When a capability's module path changes, keep the old path working as a
`DeprecationWarning` shim so existing imports do not break.
Top-level re-exports in `pydantic_ai_harness/__init__.py` (`CodeMode`,
`FileSystem`, `Shell`, `ManagedPrompt`) are the exception, not the rule. Once an
export has shipped in a published release it is a backward-compatibility
commitment: do not move, rename, or break it. Do not add new top-level
re-exports.
APIs are subject to change between releases; breaking changes ship deprecation
warnings where practical.
## API Design
@@ -36,18 +36,33 @@ Do **not** use this skill for:
`CodeMode` has a full reference below; it is the flagship capability and the one this skill goes deep on.
The rest ship today and each has its own README with API and examples.
**Stable** (imported from `pydantic_ai_harness`):
Each capability lives in its own submodule and is imported from there
(`from pydantic_ai_harness.<module> import ...`). Capabilities are not importable from the top-level
`pydantic_ai_harness` package by design, so each one keeps its own optional dependencies isolated.
`CodeMode`, `FileSystem`, `Shell`, and `ManagedPrompt` also have top-level re-exports (importable directly
from `pydantic_ai_harness`).
| Capability | Description | Reference |
APIs are subject to change between releases; breaking changes ship deprecation warnings where practical.
| Capability | Module | Description |
|---|---|---|
| `CodeMode` | Wraps eligible tools into a single sandboxed `run_code` tool so the model orchestrates them in Python | [Code Mode](./references/CODE-MODE.md) |
| `FileSystem` | Read, write, edit, and search files under a root directory, with traversal prevention | [README](https://github.com/pydantic/pydantic-ai-harness/tree/main/pydantic_ai_harness/filesystem) |
| `Shell` | Run commands in a subprocess with allowlists, a default denylist, timeouts, and env masking | [README](https://github.com/pydantic/pydantic-ai-harness/tree/main/pydantic_ai_harness/shell) |
| `ManagedPrompt` | Back an agent's instructions with a Logfire-managed prompt | [README](https://github.com/pydantic/pydantic-ai-harness/tree/main/pydantic_ai_harness/logfire) |
| `CodeMode` | `pydantic_ai_harness.code_mode` (also top-level) | Wraps eligible tools into a single sandboxed `run_code` tool so the model orchestrates them in Python -- see [Code Mode](./references/CODE-MODE.md) |
| `FileSystem` | `pydantic_ai_harness.filesystem` (also top-level) | Read, write, edit, and search files under a root directory, with traversal prevention |
| `Shell` | `pydantic_ai_harness.shell` (also top-level) | Run commands in a subprocess with allowlists, a default denylist, timeouts, and env masking |
| `ManagedPrompt` | `pydantic_ai_harness.logfire` (also top-level) | Back an agent's instructions with a Logfire-managed prompt |
| `SubAgents` | `pydantic_ai_harness.subagents` | Delegate subtasks to specialized child agents |
| `DynamicWorkflow` | `pydantic_ai_harness.dynamic_workflow` | Orchestrate sub-agents from a model-written Python script |
| `Planning` | `pydantic_ai_harness.planning` | Break complex tasks into structured plans before execution |
| compaction family (`SlidingWindow`, `SummarizingCompaction`, ...) | `pydantic_ai_harness.compaction` | Trim or summarize conversation history to stay within token limits |
| `OverflowingToolOutput` | `pydantic_ai_harness.overflowing_tool_output` | Truncate, summarize, or spill large tool outputs |
| `RepoContext` | `pydantic_ai_harness.context` | Auto-load CLAUDE.md/AGENTS.md and repo structure |
| `StepPersistence` | `pydantic_ai_harness.step_persistence` | Save, restore, resume, and fork run state |
| `PyaiDocs` | `pydantic_ai_harness.docs` | On-demand `read_pyai_docs` tool for Pydantic AI docs |
| `RuntimeAuthoring` | `pydantic_ai_harness.runtime_authoring` | Let an agent author, validate, and load real capabilities at runtime |
| media externalization | `pydantic_ai_harness.media` | Offload large `BinaryContent` to content-addressed stores |
**Experimental** (imported from `pydantic_ai_harness.experimental`, may change in any release): `SubAgents`,
`Planning`, a compaction family (`SlidingWindow`, `SummarizingCompaction`, ...), `OverflowingToolOutput`,
`RepoContext`, `StepPersistence`, `PyaiDocs`, `RuntimeAuthoring`, and an ACP server adapter.
Still experimental: an ACP server adapter, imported from `pydantic_ai_harness.experimental.acp`. Importing it
emits a `HarnessExperimentalWarning`.
The full, current list with links and status is in the
[capability matrix](https://github.com/pydantic/pydantic-ai-harness#capability-matrix).
@@ -1,23 +1,13 @@
# Compaction capabilities
> [!WARNING]
> **Experimental.** These capabilities live under `pydantic_ai_harness.experimental` and may
> change or be removed in any release, without a deprecation period. Import them from the
> experimental path -- there is no top-level export:
> [!NOTE]
> Import these capabilities from their submodule -- there is no top-level `pydantic_ai_harness` re-export:
>
> ```python
> from pydantic_ai_harness.experimental.compaction import TieredCompaction
> from pydantic_ai_harness.compaction import TieredCompaction
> ```
>
> Importing any experimental capability emits a `HarnessExperimentalWarning`. Silence **all**
> harness experimental warnings with a single filter (no per-capability lines needed):
>
> ```python
> import warnings
> from pydantic_ai_harness.experimental import HarnessExperimentalWarning
>
> warnings.filterwarnings('ignore', category=HarnessExperimentalWarning)
> ```
> The API may change between releases. Where practical, breaking changes ship with a deprecation warning.
A menu of strategies for keeping an agent's conversation history within a model's context
window. Each is a Pydantic AI `Capability` that runs in the `before_model_request` hook; edits
@@ -62,7 +52,7 @@ low-entropy repetition, so a head/tail slice loses little.
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.compaction import ClampOversizedMessages
from pydantic_ai_harness.compaction import ClampOversizedMessages
agent = Agent(
'openai:gpt-4o',
@@ -89,7 +79,7 @@ user input should not be silently rewritten, and oversized tool returns are the
Use it as the first tier of `TieredCompaction`, before `ClearToolResults`:
```python
from pydantic_ai_harness.experimental.compaction import (
from pydantic_ai_harness.compaction import (
ClampOversizedMessages,
ClearToolResults,
TieredCompaction,
@@ -113,7 +103,7 @@ that is not enough -- which is exactly what `TieredCompaction` encodes:
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.compaction import (
from pydantic_ai_harness.compaction import (
ClearToolResults,
DeduplicateFileReads,
SummarizingCompaction,
@@ -168,7 +158,6 @@ call is not a file read:
```python
from pydantic_ai.messages import ToolCallPart
def my_file_key(call: ToolCallPart) -> str | None:
if call.tool_name != 'read_file':
return None
@@ -0,0 +1,27 @@
"""Compaction capabilities: keep an agent's conversation history within the context window.
Each capability lives in its own module; shared utilities (token estimation, the
`CompactionStrategy` protocol, tool-pair-safe cutoffs, in-place clearing) live in `_shared`.
"""
from pydantic_ai_harness.compaction._clamp_oversized_messages import ClampOversizedMessages
from pydantic_ai_harness.compaction._clear_tool_results import ClearToolResults
from pydantic_ai_harness.compaction._deduplicate_file_reads import DeduplicateFileReads
from pydantic_ai_harness.compaction._limit_warner import LimitWarner, WarningKind
from pydantic_ai_harness.compaction._shared import CompactionStrategy, estimate_token_count
from pydantic_ai_harness.compaction._sliding_window import SlidingWindow
from pydantic_ai_harness.compaction._summarizing_compaction import SummarizingCompaction
from pydantic_ai_harness.compaction._tiered_compaction import TieredCompaction
__all__ = [
'ClampOversizedMessages',
'ClearToolResults',
'CompactionStrategy',
'DeduplicateFileReads',
'LimitWarner',
'SlidingWindow',
'SummarizingCompaction',
'TieredCompaction',
'WarningKind',
'estimate_token_count',
]
@@ -17,12 +17,11 @@ from pydantic_ai.messages import (
)
from pydantic_ai.tools import RunContext
from pydantic_ai_harness.experimental.compaction._shared import compact_with_span, estimate_text_tokens
from pydantic_ai_harness.compaction._shared import compact_with_span, estimate_text_tokens
if TYPE_CHECKING:
from pydantic_ai.models import ModelRequestContext
_CLAMP_MARKER = '\n[clamped: removed {removed} of {original} characters]\n'
"""Inserted between the head and tail slices of a clamped part. ``{removed}`` and ``{original}``
are filled with character counts."""
@@ -71,7 +70,7 @@ class ClampOversizedMessages(AbstractCapability[AgentDepsT]):
Example:
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.compaction import ClampOversizedMessages
from pydantic_ai_harness.compaction import ClampOversizedMessages
agent = Agent(
'openai:gpt-4o',
@@ -11,7 +11,7 @@ from pydantic_ai.capabilities import AbstractCapability
from pydantic_ai.messages import ModelMessage
from pydantic_ai.tools import RunContext
from pydantic_ai_harness.experimental.compaction._shared import (
from pydantic_ai_harness.compaction._shared import (
compact_with_span,
estimate_token_count,
exceeds,
@@ -43,7 +43,7 @@ class ClearToolResults(AbstractCapability[AgentDepsT]):
Example:
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.compaction import ClearToolResults
from pydantic_ai_harness.compaction import ClearToolResults
agent = Agent(
'openai:gpt-4o',
@@ -11,7 +11,7 @@ from pydantic_ai.capabilities import AbstractCapability
from pydantic_ai.messages import ModelMessage, ToolCallPart
from pydantic_ai.tools import RunContext
from pydantic_ai_harness.experimental.compaction._shared import (
from pydantic_ai_harness.compaction._shared import (
compact_with_span,
exceeds,
iter_tool_pairs,
@@ -39,8 +39,7 @@ class DeduplicateFileReads(AbstractCapability[AgentDepsT]):
```python
from pydantic_ai import Agent
from pydantic_ai.messages import ToolCallPart
from pydantic_ai_harness.experimental.compaction import DeduplicateFileReads
from pydantic_ai_harness.compaction import DeduplicateFileReads
def file_key(call: ToolCallPart) -> str | None:
if call.tool_name != 'read_file':
@@ -48,7 +47,6 @@ class DeduplicateFileReads(AbstractCapability[AgentDepsT]):
args = call.args_as_dict()
return args.get('path')
agent = Agent('openai:gpt-4o', capabilities=[DeduplicateFileReads(file_key=file_key)])
```
"""
@@ -10,7 +10,7 @@ from pydantic_ai.capabilities import AbstractCapability
from pydantic_ai.messages import ModelMessage, ModelRequest, SystemPromptPart, UserPromptPart
from pydantic_ai.tools import RunContext
from pydantic_ai_harness.experimental.compaction._shared import estimate_token_count
from pydantic_ai_harness.compaction._shared import estimate_token_count
if TYPE_CHECKING:
from pydantic_ai.models import ModelRequestContext
@@ -43,7 +43,7 @@ class LimitWarner(AbstractCapability[AgentDepsT]):
Example:
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.compaction import LimitWarner
from pydantic_ai_harness.compaction import LimitWarner
agent = Agent(
'openai:gpt-4o',
@@ -331,7 +331,6 @@ def prepend_first_user_message(
# Tool-pair inspection and in-place clearing
# ---------------------------------------------------------------------------
_CLEARED_TOOL_ARGS = '{}'
"""Replacement for cleared tool-call arguments.
@@ -11,7 +11,7 @@ from pydantic_ai.capabilities import AbstractCapability
from pydantic_ai.messages import ModelMessage
from pydantic_ai.tools import RunContext
from pydantic_ai_harness.experimental.compaction._shared import (
from pydantic_ai_harness.compaction._shared import (
compact_with_span,
exceeds,
find_safe_cutoff,
@@ -37,7 +37,7 @@ class SlidingWindow(AbstractCapability[AgentDepsT]):
Example:
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.compaction import SlidingWindow
from pydantic_ai_harness.compaction import SlidingWindow
agent = Agent(
'openai:gpt-4o',
@@ -20,7 +20,7 @@ from pydantic_ai.messages import (
)
from pydantic_ai.tools import RunContext
from pydantic_ai_harness.experimental.compaction._shared import (
from pydantic_ai_harness.compaction._shared import (
compact_with_span,
exceeds,
find_first_user_message,
@@ -151,7 +151,7 @@ class SummarizingCompaction(AbstractCapability[AgentDepsT]):
Example:
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.compaction import SummarizingCompaction
from pydantic_ai_harness.compaction import SummarizingCompaction
agent = Agent(
'openai:gpt-4o',
@@ -11,7 +11,7 @@ from pydantic_ai.capabilities import AbstractCapability
from pydantic_ai.messages import ModelMessage
from pydantic_ai.tools import RunContext
from pydantic_ai_harness.experimental.compaction._shared import (
from pydantic_ai_harness.compaction._shared import (
CompactionStrategy,
compact_with_span,
estimate_token_count,
@@ -36,7 +36,7 @@ class TieredCompaction(AbstractCapability[AgentDepsT]):
Example:
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.compaction import (
from pydantic_ai_harness.compaction import (
ClearToolResults,
SummarizingCompaction,
TieredCompaction,
@@ -1,23 +1,13 @@
# RepoContext
> [!WARNING]
> **Experimental.** This capability lives under `pydantic_ai_harness.experimental` and may
> change or be removed in any release, without a deprecation period. Import it from the
> experimental path -- there is no top-level export:
> [!NOTE]
> Import this capability from its submodule -- there is no top-level `pydantic_ai_harness` re-export:
>
> ```python
> from pydantic_ai_harness.experimental.context import RepoContext
> from pydantic_ai_harness.context import RepoContext
> ```
>
> Importing any experimental capability emits a `HarnessExperimentalWarning`. Silence **all**
> harness experimental warnings with a single filter (no per-capability lines needed):
>
> ```python
> import warnings
> from pydantic_ai_harness.experimental import HarnessExperimentalWarning
>
> warnings.filterwarnings('ignore', category=HarnessExperimentalWarning)
> ```
> The API may change between releases. Where practical, breaking changes ship with a deprecation warning.
Discover and load a repo's accumulated coding-assistant context engineering (CE).
@@ -37,7 +27,7 @@ idea the rest of the setup exists, so it can neither honor it nor translate it.
from pathlib import Path
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.context import RepoContext
from pydantic_ai_harness.context import RepoContext
agent = Agent(
'anthropic:claude-sonnet-4-6',
+8
View File
@@ -0,0 +1,8 @@
"""Context capability: discover and load a repo's accumulated context engineering."""
from pydantic_ai_harness.context._capability import RepoContext
from pydantic_ai_harness.context._inventory import AgentContextInventory, AssetRoot
from pydantic_ai_harness.context._loader import ContextFile
from pydantic_ai_harness.context._toolset import RepoContextToolset
__all__ = ['AgentContextInventory', 'AssetRoot', 'ContextFile', 'RepoContext', 'RepoContextToolset']
@@ -12,19 +12,18 @@ from pydantic_ai.messages import ToolCallPart
from pydantic_ai.tools import AgentDepsT, RunContext, ToolDefinition
from pydantic_ai.toolsets import AgentToolset
from pydantic_ai_harness.experimental.context._loader import (
from pydantic_ai_harness.context._loader import (
ContextFile,
discover_instruction_files,
find_dir_context_file,
render_context_file,
render_context_files,
)
from pydantic_ai_harness.experimental.context._toolset import RepoContextToolset
from pydantic_ai_harness.context._toolset import RepoContextToolset
if TYPE_CHECKING:
from pydantic_ai._instructions import AgentInstructions
_INVENTORY_HINT = (
'Call `{tool_name}` to map where this repo keeps its coding-assistant setup '
'(instruction dirs, skills, sub-agents, and hooks) so you can read and translate it.'
@@ -63,7 +62,7 @@ class RepoContext(AbstractCapability[AgentDepsT]):
from pathlib import Path
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.context import RepoContext
from pydantic_ai_harness.context import RepoContext
agent = Agent(
'anthropic:claude-sonnet-4-6',
@@ -8,7 +8,7 @@ from pathlib import Path
from pydantic_ai.tools import AgentDepsT
from pydantic_ai.toolsets import FunctionToolset
from pydantic_ai_harness.experimental.context._inventory import AgentContextInventory, scan_assets
from pydantic_ai_harness.context._inventory import AgentContextInventory, scan_assets
class RepoContextToolset(FunctionToolset[AgentDepsT]):
@@ -1,23 +1,13 @@
# PyaiDocs
> [!WARNING]
> **Experimental.** This capability lives under `pydantic_ai_harness.experimental` and may
> change or be removed in any release, without a deprecation period. Import it from the
> experimental path -- there is no top-level export:
> [!NOTE]
> Import this capability from its submodule -- there is no top-level `pydantic_ai_harness` re-export:
>
> ```python
> from pydantic_ai_harness.experimental.docs import PyaiDocs
> from pydantic_ai_harness.docs import PyaiDocs
> ```
>
> Importing any experimental capability emits a `HarnessExperimentalWarning`. Silence **all**
> harness experimental warnings with a single filter (no per-capability lines needed):
>
> ```python
> import warnings
> from pydantic_ai_harness.experimental import HarnessExperimentalWarning
>
> warnings.filterwarnings('ignore', category=HarnessExperimentalWarning)
> ```
> The API may change between releases. Where practical, breaking changes ship with a deprecation warning.
Give an agent a tool that locates and returns Pydantic AI documentation on demand.
@@ -40,7 +30,7 @@ Topics: `capabilities`, `hooks`, `tools`, `tools-advanced`, `toolsets`, `agent`.
from pathlib import Path
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.docs import PyaiDocs
from pydantic_ai_harness.docs import PyaiDocs
agent = Agent(
'anthropic:claude-sonnet-4-6',
+6
View File
@@ -0,0 +1,6 @@
"""Docs capability: an on-demand tool that locates Pydantic AI documentation."""
from pydantic_ai_harness.docs._capability import PyaiDocs
from pydantic_ai_harness.docs._toolset import PyaiDocsToolset, PyaiDocsTopic
__all__ = ['PyaiDocs', 'PyaiDocsToolset', 'PyaiDocsTopic']
@@ -11,7 +11,7 @@ from pydantic_ai.capabilities import AbstractCapability
from pydantic_ai.tools import AgentDepsT
from pydantic_ai.toolsets import AgentToolset
from pydantic_ai_harness.experimental.docs._toolset import PyaiDocsToolset, PyaiDocsTopic
from pydantic_ai_harness.docs._toolset import PyaiDocsToolset, PyaiDocsTopic
if TYPE_CHECKING:
from pydantic_ai._instructions import AgentInstructions
@@ -46,7 +46,7 @@ class PyaiDocs(AbstractCapability[AgentDepsT]):
from pathlib import Path
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.docs import PyaiDocs
from pydantic_ai_harness.docs import PyaiDocs
agent = Agent(
'anthropic:claude-sonnet-4-6',
@@ -2,20 +2,16 @@
Let one agent coordinate a whole team of sub-agents by writing a small Python script.
> **Experimental**
>
> Importing `pydantic_ai_harness.experimental.dynamic_workflow` emits a
> `HarnessExperimentalWarning`. The API can still change in any release. The extensions planned in
> [What is coming](#what-is-coming), structured sub-agent inputs and durable workflows, touch the
> sub-agent call contract itself, so the capability stays experimental until they settle. Once you
> have accepted that, you can silence the warning:
> [!NOTE]
> Import this capability from its submodule -- there is no top-level `pydantic_ai_harness` re-export:
>
> ```python
> import warnings
> from pydantic_ai_harness.experimental import HarnessExperimentalWarning
>
> warnings.filterwarnings('ignore', category=HarnessExperimentalWarning)
> from pydantic_ai_harness.dynamic_workflow import DynamicWorkflow
> ```
>
> The API may change between releases. Where practical, breaking changes ship with a deprecation warning.
> The extensions planned in [What is coming](#what-is-coming), structured sub-agent inputs and durable
> workflows, touch the sub-agent call contract itself.
## The idea
@@ -81,7 +77,7 @@ Let's build the smallest thing that works. Two sub-agents, one orchestrator.
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.dynamic_workflow import DynamicWorkflow
from pydantic_ai_harness.dynamic_workflow import DynamicWorkflow
reviewer = Agent('openai:gpt-5', name='reviewer', description='Reviews code for bugs.')
summarizer = Agent('openai:gpt-5', name='summarizer', description='Summarizes findings.')
@@ -192,7 +188,7 @@ from pydantic import BaseModel
from pydantic_ai import Agent
from pydantic_ai.usage import UsageLimits
from pydantic_ai_harness.experimental.dynamic_workflow import DynamicWorkflow
from pydantic_ai_harness.dynamic_workflow import DynamicWorkflow
# With Logfire configured, the trace shows the orchestrator turn, the run_workflow call (including
# the exact script the model wrote), and every sub-agent run nested underneath it.
@@ -201,12 +197,10 @@ logfire.instrument_pydantic_ai()
MODEL = 'anthropic:claude-sonnet-4-6' # or 'anthropic:claude-opus-4-8'
class Score(BaseModel):
value: int # 0-10
reason: str
drafter = Agent(
MODEL,
name='drafter',
@@ -237,7 +231,6 @@ orchestrator = Agent(
capabilities=[DynamicWorkflow(agents=[drafter, critic, editor])],
)
async def main() -> None:
result = await orchestrator.run(
'Explain, for a new hire, why our service uses idempotency keys on payment requests.',
@@ -245,7 +238,6 @@ async def main() -> None:
)
logfire.info('done', answer=result.output, requests=result.usage.requests)
asyncio.run(main())
```
@@ -401,7 +393,7 @@ want a different name or a different description for one particular workflow, wi
agent itself. Wrap it in a `WorkflowAgent`:
```python
from pydantic_ai_harness.experimental.dynamic_workflow import WorkflowAgent
from pydantic_ai_harness.dynamic_workflow import WorkflowAgent
DynamicWorkflow(
agents=[
@@ -0,0 +1,10 @@
"""Dynamic workflow capability: orchestrate sub-agents from a sandboxed Python script."""
from pydantic_ai_harness.dynamic_workflow._capability import DynamicWorkflow
from pydantic_ai_harness.dynamic_workflow._toolset import (
DynamicWorkflowToolset,
WorkflowAgent,
WorkflowResourceLimits,
)
__all__ = ['DynamicWorkflow', 'DynamicWorkflowToolset', 'WorkflowAgent', 'WorkflowResourceLimits']
@@ -11,7 +11,7 @@ from pydantic_ai.capabilities import AbstractCapability
from pydantic_ai.tools import AgentDepsT
from pydantic_ai.usage import UsageLimits
from pydantic_ai_harness.experimental.dynamic_workflow._toolset import (
from pydantic_ai_harness.dynamic_workflow._toolset import (
DynamicWorkflowToolset,
WorkflowAgent,
WorkflowResourceLimits,
@@ -30,7 +30,7 @@ class DynamicWorkflow(AbstractCapability[AgentDepsT]):
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.dynamic_workflow import DynamicWorkflow
from pydantic_ai_harness.dynamic_workflow import DynamicWorkflow
reviewer = Agent('openai:gpt-5', name='reviewer', description='Review code for bugs.')
summarizer = Agent('openai:gpt-5', name='summarizer', description='Summarize findings.')
+15
View File
@@ -38,3 +38,18 @@ def warn_experimental(feature: str) -> None:
category=HarnessExperimentalWarning,
stacklevel=2,
)
def warn_moved(old: str, new: str) -> None:
"""Emit a `DeprecationWarning` that `experimental.old` now lives at top-level `new`.
Left behind at each old `pydantic_ai_harness.experimental.<name>` path when a capability
graduates out of `experimental`, so existing imports keep working with a clear pointer to
the new location.
"""
warnings.warn(
f'`pydantic_ai_harness.experimental.{old}` has moved to `pydantic_ai_harness.{new}`. '
f'Update your imports; this compatibility shim will be removed in a future release.',
category=DeprecationWarning,
stacklevel=2,
)
@@ -1,16 +1,21 @@
"""Runtime capability authoring: let an agent write, validate, and register real capabilities."""
"""Deprecated import location for `pydantic_ai_harness.runtime_authoring`.
from pydantic_ai_harness.experimental._warn import warn_experimental
from pydantic_ai_harness.experimental.authoring._capability import RuntimeAuthoring
from pydantic_ai_harness.experimental.authoring._store import AuthoredCapability, CapabilityStore
from pydantic_ai_harness.experimental.authoring._toolset import AuthoringToolset
from pydantic_ai_harness.experimental.authoring._validate import (
This capability graduated out of `experimental`; importing from here still works but
emits a `DeprecationWarning`. Import from `pydantic_ai_harness.runtime_authoring` instead.
"""
from pydantic_ai_harness.experimental._warn import warn_moved
from pydantic_ai_harness.runtime_authoring import (
AuthoredCapability,
AuthoringToolset,
CapabilityStore,
CapabilityValidationError,
RuntimeAuthoring,
load_capability_instance,
validate_capability_file,
)
warn_experimental('authoring')
warn_moved('authoring', 'runtime_authoring')
__all__ = [
'AuthoredCapability',
@@ -1,20 +1,24 @@
"""Compaction capabilities: keep an agent's conversation history within the context window.
"""Deprecated import location for `pydantic_ai_harness.compaction`.
Each capability lives in its own module; shared utilities (token estimation, the
`CompactionStrategy` protocol, tool-pair-safe cutoffs, in-place clearing) live in `_shared`.
This capability graduated out of `experimental`; importing from here still works but
emits a `DeprecationWarning`. Import from `pydantic_ai_harness.compaction` instead.
"""
from pydantic_ai_harness.experimental._warn import warn_experimental
from pydantic_ai_harness.experimental.compaction._clamp_oversized_messages import ClampOversizedMessages
from pydantic_ai_harness.experimental.compaction._clear_tool_results import ClearToolResults
from pydantic_ai_harness.experimental.compaction._deduplicate_file_reads import DeduplicateFileReads
from pydantic_ai_harness.experimental.compaction._limit_warner import LimitWarner, WarningKind
from pydantic_ai_harness.experimental.compaction._shared import CompactionStrategy, estimate_token_count
from pydantic_ai_harness.experimental.compaction._sliding_window import SlidingWindow
from pydantic_ai_harness.experimental.compaction._summarizing_compaction import SummarizingCompaction
from pydantic_ai_harness.experimental.compaction._tiered_compaction import TieredCompaction
from pydantic_ai_harness.compaction import (
ClampOversizedMessages,
ClearToolResults,
CompactionStrategy,
DeduplicateFileReads,
LimitWarner,
SlidingWindow,
SummarizingCompaction,
TieredCompaction,
WarningKind,
estimate_token_count,
)
from pydantic_ai_harness.experimental._warn import warn_moved
warn_experimental('compaction')
warn_moved('compaction', 'compaction')
__all__ = [
'ClampOversizedMessages',
@@ -1,11 +1,24 @@
"""Context capability: discover and load a repo's accumulated context engineering."""
"""Deprecated import location for `pydantic_ai_harness.context`.
from pydantic_ai_harness.experimental._warn import warn_experimental
from pydantic_ai_harness.experimental.context._capability import RepoContext
from pydantic_ai_harness.experimental.context._inventory import AgentContextInventory, AssetRoot
from pydantic_ai_harness.experimental.context._loader import ContextFile
from pydantic_ai_harness.experimental.context._toolset import RepoContextToolset
This capability graduated out of `experimental`; importing from here still works but
emits a `DeprecationWarning`. Import from `pydantic_ai_harness.context` instead.
"""
warn_experimental('context')
from pydantic_ai_harness.context import (
AgentContextInventory,
AssetRoot,
ContextFile,
RepoContext,
RepoContextToolset,
)
from pydantic_ai_harness.experimental._warn import warn_moved
__all__ = ['AgentContextInventory', 'AssetRoot', 'ContextFile', 'RepoContext', 'RepoContextToolset']
warn_moved('context', 'context')
__all__ = [
'AgentContextInventory',
'AssetRoot',
'ContextFile',
'RepoContext',
'RepoContextToolset',
]
@@ -1,9 +1,20 @@
"""Docs capability: an on-demand tool that locates Pydantic AI documentation."""
"""Deprecated import location for `pydantic_ai_harness.docs`.
from pydantic_ai_harness.experimental._warn import warn_experimental
from pydantic_ai_harness.experimental.docs._capability import PyaiDocs
from pydantic_ai_harness.experimental.docs._toolset import PyaiDocsToolset, PyaiDocsTopic
This capability graduated out of `experimental`; importing from here still works but
emits a `DeprecationWarning`. Import from `pydantic_ai_harness.docs` instead.
"""
warn_experimental('docs')
from pydantic_ai_harness.docs import (
PyaiDocs,
PyaiDocsToolset,
PyaiDocsTopic,
)
from pydantic_ai_harness.experimental._warn import warn_moved
__all__ = ['PyaiDocs', 'PyaiDocsToolset', 'PyaiDocsTopic']
warn_moved('docs', 'docs')
__all__ = [
'PyaiDocs',
'PyaiDocsToolset',
'PyaiDocsTopic',
]
@@ -1,13 +1,22 @@
"""Dynamic workflow capability: orchestrate sub-agents from a sandboxed Python script."""
"""Deprecated import location for `pydantic_ai_harness.dynamic_workflow`.
from pydantic_ai_harness.experimental._warn import warn_experimental
from pydantic_ai_harness.experimental.dynamic_workflow._capability import DynamicWorkflow
from pydantic_ai_harness.experimental.dynamic_workflow._toolset import (
This capability graduated out of `experimental`; importing from here still works but
emits a `DeprecationWarning`. Import from `pydantic_ai_harness.dynamic_workflow` instead.
"""
from pydantic_ai_harness.dynamic_workflow import (
DynamicWorkflow,
DynamicWorkflowToolset,
WorkflowAgent,
WorkflowResourceLimits,
)
from pydantic_ai_harness.experimental._warn import warn_moved
warn_experimental('dynamic_workflow')
warn_moved('dynamic_workflow', 'dynamic_workflow')
__all__ = ['DynamicWorkflow', 'DynamicWorkflowToolset', 'WorkflowAgent', 'WorkflowResourceLimits']
__all__ = [
'DynamicWorkflow',
'DynamicWorkflowToolset',
'WorkflowAgent',
'WorkflowResourceLimits',
]
@@ -1,28 +1,27 @@
"""Content-addressed media stores for offloading large binary parts.
"""Deprecated import location for `pydantic_ai_harness.media`.
Used by `pydantic_ai_harness.experimental.step_persistence` to keep snapshots small when
messages carry `BinaryContent` payloads. A forthcoming `MediaExternalizer`
capability will reuse these stores for in-flight wire-payload reduction
(rewriting `BinaryContent` to URL parts before the model sees them).
This capability graduated out of `experimental`; importing from here still works but
emits a `DeprecationWarning`. Import from `pydantic_ai_harness.media` instead.
"""
from pydantic_ai_harness.experimental._warn import warn_experimental
from pydantic_ai_harness.experimental.media._s3 import S3MediaStore
from pydantic_ai_harness.experimental.media._store import (
from pydantic_ai_harness.experimental._warn import warn_moved
from pydantic_ai_harness.media import (
DiskMediaStore,
KeyStrategy,
MediaContext,
MediaStore,
PublicUrlResolver,
S3MediaStore,
SqliteMediaStore,
default_key_strategy,
externalize_media,
make_static_public_url,
media_uri_for,
parse_media_uri,
restore_media,
)
from pydantic_ai_harness.experimental.media._walker import externalize_media, restore_media
warn_experimental('media')
warn_moved('media', 'media')
__all__ = [
'DiskMediaStore',
@@ -1,30 +1,26 @@
"""Overflow capability: reduce oversized tool returns at production time.
"""Deprecated import location for `pydantic_ai_harness.overflowing_tool_output`.
`OverflowingToolOutput` intercepts a tool return when it is produced and reduces it --
truncating, spilling to a queryable file, or summarizing -- so an oversized payload does
not persist in history and get re-sent on every later model request. Combine the three
modes through an ordered list of size `bands`.
Spilled payloads are read back on demand through the registered `read_tool_result` tool;
the `OverflowStore` protocol is the seam for a durable backend (the local-file default
ships for single-process runs).
This capability graduated out of `experimental`; importing from here still works but
emits a `DeprecationWarning`. Import from `pydantic_ai_harness.overflowing_tool_output` instead.
"""
from pydantic_ai_harness.experimental._warn import warn_experimental
from pydantic_ai_harness.experimental.overflow._bands import (
from pydantic_ai_harness.experimental._warn import warn_moved
from pydantic_ai_harness.overflowing_tool_output import (
READ_TOOL_NAME,
Action,
Band,
LocalFileStore,
OverflowingToolOutput,
OverflowStore,
Passthrough,
Spill,
Summarize,
SummarizeFunc,
Truncate,
TruncationStrategy,
)
from pydantic_ai_harness.experimental.overflow._capability import READ_TOOL_NAME, OverflowingToolOutput
from pydantic_ai_harness.experimental.overflow._payload import TruncationStrategy
from pydantic_ai_harness.experimental.overflow._store import LocalFileStore, OverflowStore
warn_experimental('overflow')
warn_moved('overflow', 'overflowing_tool_output')
__all__ = [
'READ_TOOL_NAME',
@@ -1,9 +1,22 @@
"""Planning capability: model-owned, cache-friendly task planning for agents."""
"""Deprecated import location for `pydantic_ai_harness.planning`.
from pydantic_ai_harness.experimental._warn import warn_experimental
from pydantic_ai_harness.experimental.planning._capability import Planning
from pydantic_ai_harness.experimental.planning._toolset import PlanItem, PlanningToolset, TaskStatus
This capability graduated out of `experimental`; importing from here still works but
emits a `DeprecationWarning`. Import from `pydantic_ai_harness.planning` instead.
"""
warn_experimental('planning')
from pydantic_ai_harness.experimental._warn import warn_moved
from pydantic_ai_harness.planning import (
PlanItem,
Planning,
PlanningToolset,
TaskStatus,
)
__all__ = ['PlanItem', 'Planning', 'PlanningToolset', 'TaskStatus']
warn_moved('planning', 'planning')
__all__ = [
'PlanItem',
'Planning',
'PlanningToolset',
'TaskStatus',
]
@@ -1,29 +1,29 @@
"""Step-event persistence: append-only event log, continuable snapshots, tool-effect ledger."""
"""Deprecated import location for `pydantic_ai_harness.step_persistence`.
from pydantic_ai_harness.experimental._warn import warn_experimental
from pydantic_ai_harness.experimental.step_persistence._capability import StepPersistence
from pydantic_ai_harness.experimental.step_persistence._helpers import (
This capability graduated out of `experimental`; importing from here still works but
emits a `DeprecationWarning`. Import from `pydantic_ai_harness.step_persistence` instead.
"""
from pydantic_ai_harness.experimental._warn import warn_moved
from pydantic_ai_harness.step_persistence import (
ContinuableSnapshot,
EventKind,
FileStepStore,
InMemoryStepStore,
RunRecord,
SqliteStepStore,
StepEvent,
StepPersistence,
StepStore,
ToolEffectRecord,
ToolEffectStatus,
annotate_tool_effect,
continue_run,
fork_run,
is_provider_valid,
)
from pydantic_ai_harness.experimental.step_persistence._store import (
FileStepStore,
InMemoryStepStore,
SqliteStepStore,
StepStore,
)
from pydantic_ai_harness.experimental.step_persistence._types import (
ContinuableSnapshot,
EventKind,
RunRecord,
StepEvent,
ToolEffectRecord,
ToolEffectStatus,
)
warn_experimental('step_persistence')
warn_moved('step_persistence', 'step_persistence')
__all__ = [
'ContinuableSnapshot',
@@ -1,12 +1,21 @@
"""Sub-agent capability: delegate self-contained tasks to named child agents."""
"""Deprecated import location for `pydantic_ai_harness.subagents`.
from pydantic_ai_harness.experimental._warn import warn_experimental
from pydantic_ai_harness.experimental.subagents._capability import SubAgents, ToolResolver
from pydantic_ai_harness.experimental.subagents._disk import AgentOverride
from pydantic_ai_harness.experimental.subagents._effort import MINIMUM_EFFORT_FLOOR, clamp_effort
from pydantic_ai_harness.experimental.subagents._toolset import SubAgent, SubAgentToolset
This capability graduated out of `experimental`; importing from here still works but
emits a `DeprecationWarning`. Import from `pydantic_ai_harness.subagents` instead.
"""
warn_experimental('subagents')
from pydantic_ai_harness.experimental._warn import warn_moved
from pydantic_ai_harness.subagents import (
MINIMUM_EFFORT_FLOOR,
AgentOverride,
SubAgent,
SubAgents,
SubAgentToolset,
ToolResolver,
clamp_effort,
)
warn_moved('subagents', 'subagents')
__all__ = [
'MINIMUM_EFFORT_FLOOR',
@@ -1,16 +1,19 @@
# Media
> **Experimental.** Imported from `pydantic_ai_harness.experimental.media`, this module can change or be removed in any release. Importing it emits a `HarnessExperimentalWarning`.
```python
from pydantic_ai_harness.experimental.media import (
DiskMediaStore,
S3MediaStore,
SqliteMediaStore,
externalize_media,
restore_media,
)
```
> [!NOTE]
> Import these helpers from their submodule -- there is no top-level `pydantic_ai_harness` re-export:
>
> ```python
> from pydantic_ai_harness.media import (
> DiskMediaStore,
> S3MediaStore,
> SqliteMediaStore,
> externalize_media,
> restore_media,
> )
> ```
>
> The API may change between releases. Where practical, breaking changes ship with a deprecation warning.
Content-addressed stores and walker helpers that move large binary payloads out of message history and put them back on demand.
+38
View File
@@ -0,0 +1,38 @@
"""Content-addressed media stores for offloading large binary parts.
Used by `pydantic_ai_harness.step_persistence` to keep snapshots small when
messages carry `BinaryContent` payloads. A forthcoming `MediaExternalizer`
capability will reuse these stores for in-flight wire-payload reduction
(rewriting `BinaryContent` to URL parts before the model sees them).
"""
from pydantic_ai_harness.media._s3 import S3MediaStore
from pydantic_ai_harness.media._store import (
DiskMediaStore,
KeyStrategy,
MediaContext,
MediaStore,
PublicUrlResolver,
SqliteMediaStore,
default_key_strategy,
make_static_public_url,
media_uri_for,
parse_media_uri,
)
from pydantic_ai_harness.media._walker import externalize_media, restore_media
__all__ = [
'DiskMediaStore',
'KeyStrategy',
'MediaContext',
'MediaStore',
'PublicUrlResolver',
'S3MediaStore',
'SqliteMediaStore',
'default_key_strategy',
'externalize_media',
'make_static_public_url',
'media_uri_for',
'parse_media_uri',
'restore_media',
]
@@ -22,7 +22,7 @@ from datetime import datetime, timezone
from typing import TYPE_CHECKING
from urllib.parse import quote
from pydantic_ai_harness.experimental.media._store import (
from pydantic_ai_harness.media._store import (
_EMPTY_CONTEXT, # pyright: ignore[reportPrivateUsage]
KeyStrategy,
MediaContext,
@@ -36,7 +36,6 @@ from pydantic_ai_harness.experimental.media._store import (
if TYPE_CHECKING:
import httpx
_ALGORITHM = 'AWS4-HMAC-SHA256'
_SERVICE = 's3'
# Conservative subset for x-amz-meta-* keys: ASCII letters/digits/dash; lowercase.
@@ -61,7 +61,6 @@ class MediaContext:
_EMPTY_CONTEXT = MediaContext()
PublicUrlResolver = Callable[[str, MediaContext], 'str | None | Awaitable[str | None]']
"""User-supplied callable that turns a `media+sha256://<hex>` URI into a fetchable URL.
@@ -16,7 +16,7 @@ from __future__ import annotations
import base64
from typing import TypeGuard
from pydantic_ai_harness.experimental.media._store import MediaContext, MediaStore
from pydantic_ai_harness.media._store import MediaContext, MediaStore
_EXTERNAL_MARKER = '__harness_external_media__'
@@ -1,23 +1,13 @@
# Overflow capability
> [!WARNING]
> **Experimental.** This capability lives under `pydantic_ai_harness.experimental` and may
> change or be removed in any release, without a deprecation period. Import it from the
> experimental path -- there is no top-level export:
> [!NOTE]
> Import this capability from its submodule -- there is no top-level `pydantic_ai_harness` re-export:
>
> ```python
> from pydantic_ai_harness.experimental.overflow import OverflowingToolOutput
> from pydantic_ai_harness.overflowing_tool_output import OverflowingToolOutput
> ```
>
> Importing any experimental capability emits a `HarnessExperimentalWarning`. Silence **all**
> harness experimental warnings with a single filter (no per-capability lines needed):
>
> ```python
> import warnings
> from pydantic_ai_harness.experimental import HarnessExperimentalWarning
>
> warnings.filterwarnings('ignore', category=HarnessExperimentalWarning)
> ```
> The API may change between releases. Where practical, breaking changes ship with a deprecation warning.
A tool can return a payload large enough to dominate the context window. Tool returns
persist in history as `ToolReturnPart`s, so an oversized one is re-sent on every later
@@ -60,7 +50,7 @@ that fits wins; anything below the smallest threshold passes through.
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.overflow import (
from pydantic_ai_harness.overflowing_tool_output import (
Band,
OverflowingToolOutput,
Spill,
@@ -0,0 +1,39 @@
"""Overflow capability: reduce oversized tool returns at production time.
`OverflowingToolOutput` intercepts a tool return when it is produced and reduces it --
truncating, spilling to a queryable file, or summarizing -- so an oversized payload does
not persist in history and get re-sent on every later model request. Combine the three
modes through an ordered list of size `bands`.
Spilled payloads are read back on demand through the registered `read_tool_result` tool;
the `OverflowStore` protocol is the seam for a durable backend (the local-file default
ships for single-process runs).
"""
from pydantic_ai_harness.overflowing_tool_output._bands import (
Action,
Band,
Passthrough,
Spill,
Summarize,
SummarizeFunc,
Truncate,
)
from pydantic_ai_harness.overflowing_tool_output._capability import READ_TOOL_NAME, OverflowingToolOutput
from pydantic_ai_harness.overflowing_tool_output._payload import TruncationStrategy
from pydantic_ai_harness.overflowing_tool_output._store import LocalFileStore, OverflowStore
__all__ = [
'READ_TOOL_NAME',
'Action',
'Band',
'LocalFileStore',
'OverflowStore',
'OverflowingToolOutput',
'Passthrough',
'Spill',
'Summarize',
'SummarizeFunc',
'Truncate',
'TruncationStrategy',
]
@@ -16,7 +16,7 @@ from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING
from pydantic_ai_harness.experimental.overflow._payload import TruncationStrategy
from pydantic_ai_harness.overflowing_tool_output._payload import TruncationStrategy
if TYPE_CHECKING:
from pydantic_ai.models import Model
@@ -14,7 +14,7 @@ from pydantic_ai.messages import ToolCallPart, ToolReturn, ToolReturnContent, Us
from pydantic_ai.tools import AgentDepsT, RunContext, ToolDefinition, ToolSelector, matches_tool_selector
from pydantic_ai.toolsets import AgentToolset
from pydantic_ai_harness.experimental.overflow._bands import (
from pydantic_ai_harness.overflowing_tool_output._bands import (
Action,
Band,
Passthrough,
@@ -22,7 +22,7 @@ from pydantic_ai_harness.experimental.overflow._bands import (
Summarize,
Truncate,
)
from pydantic_ai_harness.experimental.overflow._payload import (
from pydantic_ai_harness.overflowing_tool_output._payload import (
is_binary,
json_sketch,
measure,
@@ -31,7 +31,7 @@ from pydantic_ai_harness.experimental.overflow._payload import (
to_text,
truncate_text,
)
from pydantic_ai_harness.experimental.overflow._store import LocalFileStore, OverflowStore
from pydantic_ai_harness.overflowing_tool_output._store import LocalFileStore, OverflowStore
READ_TOOL_NAME = 'read_tool_result'
"""Name of the registered read-back tool. Its own returns are exempt from reduction."""
@@ -96,7 +96,7 @@ class OverflowingToolOutput(AbstractCapability[AgentDepsT]):
Example:
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.overflow import (
from pydantic_ai_harness.overflowing_tool_output import (
Band,
OverflowingToolOutput,
Spill,
@@ -15,7 +15,7 @@ from typing import TypeGuard
from pydantic_ai.messages import ModelMessage, ModelRequest, SystemPromptPart
from pydantic_core import to_json
from pydantic_ai_harness.experimental.compaction._shared import estimate_token_count
from pydantic_ai_harness.compaction._shared import estimate_token_count
class TruncationStrategy(str, Enum):
@@ -1,23 +1,13 @@
# Planning
> [!WARNING]
> **Experimental.** This capability lives under `pydantic_ai_harness.experimental` and may
> change or be removed in any release, without a deprecation period. Import it from the
> experimental path -- there is no top-level export:
> [!NOTE]
> Import this capability from its submodule -- there is no top-level `pydantic_ai_harness` re-export:
>
> ```python
> from pydantic_ai_harness.experimental.planning import Planning
> from pydantic_ai_harness.planning import Planning
> ```
>
> Importing any experimental capability emits a `HarnessExperimentalWarning`. Silence **all**
> harness experimental warnings with a single filter (no per-capability lines needed):
>
> ```python
> import warnings
> from pydantic_ai_harness.experimental import HarnessExperimentalWarning
>
> warnings.filterwarnings('ignore', category=HarnessExperimentalWarning)
> ```
> The API may change between releases. Where practical, breaking changes ship with a deprecation warning.
Give an agent a structured, self-updating task plan -- without ever invalidating the prompt cache.
@@ -36,7 +26,7 @@ So the plan stays current in the model's view while the cached prefix is never i
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.planning import Planning
from pydantic_ai_harness.planning import Planning
agent = Agent('anthropic:claude-sonnet-4-6', capabilities=[Planning()])
@@ -109,7 +99,7 @@ capabilities:
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.planning import Planning
from pydantic_ai_harness.planning import Planning
agent = Agent.from_file('agent.yaml', custom_capability_types=[Planning])
```
+6
View File
@@ -0,0 +1,6 @@
"""Planning capability: model-owned, cache-friendly task planning for agents."""
from pydantic_ai_harness.planning._capability import Planning
from pydantic_ai_harness.planning._toolset import PlanItem, PlanningToolset, TaskStatus
__all__ = ['PlanItem', 'Planning', 'PlanningToolset', 'TaskStatus']
@@ -10,14 +10,13 @@ from pydantic_ai.messages import CachePoint, ModelRequest, ModelResponse, UserPr
from pydantic_ai.tools import AgentDepsT, RunContext
from pydantic_ai.toolsets import AgentToolset
from pydantic_ai_harness.experimental.planning._toolset import PlanningToolset, PlanState, render_plan
from pydantic_ai_harness.planning._toolset import PlanningToolset, PlanState, render_plan
if TYPE_CHECKING:
from pydantic_ai._instructions import AgentInstructions
from pydantic_ai.capabilities.abstract import WrapModelRequestHandler
from pydantic_ai.models import ModelRequestContext
_DEFAULT_GUIDANCE = (
'You have a planning tool, `write_plan`. For multi-step work, call it first to lay out the '
'steps, then call it again to update statuses as you start and finish each step. Pass the '
@@ -41,7 +40,7 @@ class Planning(AbstractCapability[AgentDepsT]):
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.planning import Planning
from pydantic_ai_harness.planning import Planning
agent = Agent('anthropic:claude-sonnet-4-6', capabilities=[Planning()])
```
@@ -1,23 +1,13 @@
# RuntimeAuthoring
> [!WARNING]
> **Experimental.** This capability lives under `pydantic_ai_harness.experimental` and may
> change or be removed in any release, without a deprecation period. Import it from the
> experimental path -- there is no top-level export:
> [!NOTE]
> Import this capability from its submodule -- there is no top-level `pydantic_ai_harness` re-export:
>
> ```python
> from pydantic_ai_harness.experimental.authoring import RuntimeAuthoring
> from pydantic_ai_harness.runtime_authoring import RuntimeAuthoring
> ```
>
> Importing any experimental capability emits a `HarnessExperimentalWarning`. Silence **all**
> harness experimental warnings with a single filter (no per-capability lines needed):
>
> ```python
> import warnings
> from pydantic_ai_harness.experimental import HarnessExperimentalWarning
>
> warnings.filterwarnings('ignore', category=HarnessExperimentalWarning)
> ```
> The API may change between releases. Where practical, breaking changes ship with a deprecation warning.
Let an agent author, validate, and persist real pydantic-ai capabilities at runtime.
@@ -50,7 +40,7 @@ authoring a hook means authoring a capability that overrides one lifecycle metho
from pathlib import Path
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.authoring import RuntimeAuthoring
from pydantic_ai_harness.runtime_authoring import RuntimeAuthoring
authoring = RuntimeAuthoring(directory=Path('.authored'))
agent = Agent('anthropic:claude-sonnet-4-6', capabilities=[authoring])
@@ -0,0 +1,20 @@
"""Runtime capability authoring: let an agent write, validate, and register real capabilities."""
from pydantic_ai_harness.runtime_authoring._capability import RuntimeAuthoring
from pydantic_ai_harness.runtime_authoring._store import AuthoredCapability, CapabilityStore
from pydantic_ai_harness.runtime_authoring._toolset import AuthoringToolset
from pydantic_ai_harness.runtime_authoring._validate import (
CapabilityValidationError,
load_capability_instance,
validate_capability_file,
)
__all__ = [
'AuthoredCapability',
'AuthoringToolset',
'CapabilityStore',
'CapabilityValidationError',
'RuntimeAuthoring',
'load_capability_instance',
'validate_capability_file',
]
@@ -10,13 +10,12 @@ from pydantic_ai.capabilities import AbstractCapability
from pydantic_ai.tools import AgentDepsT
from pydantic_ai.toolsets import AgentToolset
from pydantic_ai_harness.experimental.authoring._store import CapabilityStore
from pydantic_ai_harness.experimental.authoring._toolset import AuthoringToolset
from pydantic_ai_harness.runtime_authoring._store import CapabilityStore
from pydantic_ai_harness.runtime_authoring._toolset import AuthoringToolset
if TYPE_CHECKING:
from pydantic_ai._instructions import AgentInstructions
_DEFAULT_GUIDANCE = (
'You can author new pydantic-ai capabilities at runtime with `author_capability(name, code)`. '
'A capability is a subclass of `pydantic_ai.capabilities.AbstractCapability` that constructs with '
@@ -48,7 +47,7 @@ class RuntimeAuthoring(AbstractCapability[AgentDepsT]):
from pathlib import Path
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.authoring import RuntimeAuthoring
from pydantic_ai_harness.runtime_authoring import RuntimeAuthoring
authoring = RuntimeAuthoring(directory=Path('.authored'))
agent = Agent('anthropic:claude-sonnet-4-6', capabilities=[authoring])
@@ -19,7 +19,7 @@ from typing import Literal
from pydantic import BaseModel, ValidationError
from pydantic_ai.capabilities import AbstractCapability
from pydantic_ai_harness.experimental.authoring._validate import (
from pydantic_ai_harness.runtime_authoring._validate import (
CapabilityValidationError,
load_capability_instance,
validate_capability_file,
@@ -6,7 +6,7 @@ from pydantic_ai.exceptions import ModelRetry
from pydantic_ai.tools import AgentDepsT
from pydantic_ai.toolsets import FunctionToolset
from pydantic_ai_harness.experimental.authoring._store import CapabilityStore
from pydantic_ai_harness.runtime_authoring._store import CapabilityStore
class AuthoringToolset(FunctionToolset[AgentDepsT]):
@@ -1,24 +1,15 @@
# StepPersistence
> [!WARNING]
> **Experimental.** `StepPersistence` and the `media` stores live under
> `pydantic_ai_harness.experimental` and may change or be removed in any release, without a
> deprecation period. Import them from the experimental path -- there is no top-level export:
> [!NOTE]
> Import `StepPersistence` and the `media` stores from their submodules -- there is no top-level
> `pydantic_ai_harness` re-export:
>
> ```python
> from pydantic_ai_harness.experimental.step_persistence import StepPersistence
> from pydantic_ai_harness.experimental.media import S3MediaStore
> from pydantic_ai_harness.step_persistence import StepPersistence
> from pydantic_ai_harness.media import S3MediaStore
> ```
>
> Importing either package emits a `HarnessExperimentalWarning`. Silence **all** harness
> experimental warnings with a single filter (no per-capability lines needed):
>
> ```python
> import warnings
> from pydantic_ai_harness.experimental import HarnessExperimentalWarning
>
> warnings.filterwarnings('ignore', category=HarnessExperimentalWarning)
> ```
> The API may change between releases. Where practical, breaking changes ship with a deprecation warning.
`StepPersistence` records what an agent did at each boundary, separate from
whether the run can be safely resumed. It is the persistence substrate for
@@ -52,7 +43,7 @@ snapshots, and graph-node resume are out of scope and tracked separately
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.step_persistence import StepPersistence, InMemoryStepStore
from pydantic_ai_harness.step_persistence import StepPersistence, InMemoryStepStore
store = InMemoryStepStore()
librarian = Agent(
@@ -128,7 +119,7 @@ context". `StepPersistence` does not introduce a parallel mechanism -- it
exposes one helper that loads the most recent provider-valid snapshot:
```python
from pydantic_ai_harness.experimental.step_persistence import continue_run
from pydantic_ai_harness.step_persistence import continue_run
# Earlier: tag the first turn with a conversation id so the follow-up can find it.
await librarian.run(
@@ -262,7 +253,7 @@ write external state should annotate their in-flight `ToolEffectRecord`
via `annotate_tool_effect`:
```python
from pydantic_ai_harness.experimental.step_persistence import annotate_tool_effect
from pydantic_ai_harness.step_persistence import annotate_tool_effect
@orchestrator.tool
async def set_label(ctx: RunContext[Deps], issue: int, label: str) -> str:
@@ -330,8 +321,8 @@ original bytes.
Override the destination by passing your own `MediaStore`:
```python
from pydantic_ai_harness.experimental.step_persistence import FileStepStore
from pydantic_ai_harness.experimental.media import S3MediaStore
from pydantic_ai_harness.step_persistence import FileStepStore
from pydantic_ai_harness.media import S3MediaStore
store = FileStepStore(
'runs',
@@ -379,7 +370,7 @@ without re-encoding bytes into the request body.
Static base URL (public R2 bucket, CDN):
```python
from pydantic_ai_harness.experimental.media import S3MediaStore, make_static_public_url
from pydantic_ai_harness.media import S3MediaStore, make_static_public_url
store = S3MediaStore(
bucket='my-bucket',
@@ -395,7 +386,7 @@ Presigned / rotating-signature URL -- pass any async callable that takes
`(uri, MediaContext)`:
```python
from pydantic_ai_harness.experimental.media import MediaContext, S3MediaStore
from pydantic_ai_harness.media import MediaContext, S3MediaStore
async def presign(uri: str, ctx: MediaContext) -> str:
key = 'media/' + uri.removeprefix('media+sha256://') + '.bin'
@@ -444,7 +435,7 @@ primary key is the digest, so a user-chosen key would either break
dedup or be a no-op):
```python
from pydantic_ai_harness.experimental.media import DiskMediaStore, MediaContext
from pydantic_ai_harness.media import DiskMediaStore, MediaContext
def by_media_type(uri: str, ctx: MediaContext) -> str:
digest = uri.removeprefix('media+sha256://')
@@ -0,0 +1,41 @@
"""Step-event persistence: append-only event log, continuable snapshots, tool-effect ledger."""
from pydantic_ai_harness.step_persistence._capability import StepPersistence
from pydantic_ai_harness.step_persistence._helpers import (
annotate_tool_effect,
continue_run,
fork_run,
is_provider_valid,
)
from pydantic_ai_harness.step_persistence._store import (
FileStepStore,
InMemoryStepStore,
SqliteStepStore,
StepStore,
)
from pydantic_ai_harness.step_persistence._types import (
ContinuableSnapshot,
EventKind,
RunRecord,
StepEvent,
ToolEffectRecord,
ToolEffectStatus,
)
__all__ = [
'ContinuableSnapshot',
'EventKind',
'FileStepStore',
'InMemoryStepStore',
'RunRecord',
'SqliteStepStore',
'StepEvent',
'StepPersistence',
'StepStore',
'ToolEffectRecord',
'ToolEffectStatus',
'annotate_tool_effect',
'continue_run',
'fork_run',
'is_provider_valid',
]
@@ -15,10 +15,10 @@ from pydantic_ai.models import ModelRequestContext
from pydantic_ai.run import AgentRunResult
from pydantic_ai.tools import AgentDepsT, RunContext, ToolDefinition
from pydantic_ai_harness.experimental.step_persistence._context import current_run_id, snapshot_saved
from pydantic_ai_harness.experimental.step_persistence._helpers import is_provider_valid
from pydantic_ai_harness.experimental.step_persistence._store import InMemoryStepStore, StepStore
from pydantic_ai_harness.experimental.step_persistence._types import (
from pydantic_ai_harness.step_persistence._context import current_run_id, snapshot_saved
from pydantic_ai_harness.step_persistence._helpers import is_provider_valid
from pydantic_ai_harness.step_persistence._store import InMemoryStepStore, StepStore
from pydantic_ai_harness.step_persistence._types import (
ContinuableSnapshot,
EventKind,
RunRecord,
@@ -49,7 +49,7 @@ class StepPersistence(AbstractCapability[AgentDepsT]):
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.step_persistence import StepPersistence, InMemoryStepStore
from pydantic_ai_harness.step_persistence import StepPersistence, InMemoryStepStore
store = InMemoryStepStore()
librarian = Agent(
@@ -124,12 +124,12 @@ class StepPersistence(AbstractCapability[AgentDepsT]):
if backend == 'memory':
return cls(store=InMemoryStepStore(), **kwargs)
if backend == 'file':
from pydantic_ai_harness.experimental.step_persistence._store import FileStepStore
from pydantic_ai_harness.step_persistence._store import FileStepStore
directory = kwargs.pop('directory', '.step-persistence')
return cls(store=FileStepStore(directory), **kwargs)
if backend == 'sqlite':
from pydantic_ai_harness.experimental.step_persistence._store import SqliteStepStore
from pydantic_ai_harness.step_persistence._store import SqliteStepStore
database = kwargs.pop('database', '.step-persistence.db')
return cls(store=SqliteStepStore(database=database), **kwargs)
@@ -5,7 +5,7 @@ from __future__ import annotations
from contextvars import ContextVar
current_run_id: ContextVar[str | None] = ContextVar(
'pydantic_ai_harness.experimental.step_persistence.current_run_id',
'pydantic_ai_harness.step_persistence.current_run_id',
default=None,
)
"""Async-context-local pointer to the active `StepPersistence` `run_id`.
@@ -20,7 +20,7 @@ import.
"""
snapshot_saved: ContextVar[bool] = ContextVar(
'pydantic_ai_harness.experimental.step_persistence.snapshot_saved',
'pydantic_ai_harness.step_persistence.snapshot_saved',
default=False,
)
"""Async-context-local flag: did `after_node_run` already save a snapshot this run?
@@ -13,9 +13,9 @@ from pydantic_ai.messages import (
)
from pydantic_ai.tools import RunContext
from pydantic_ai_harness.experimental.step_persistence._context import current_run_id
from pydantic_ai_harness.experimental.step_persistence._store import StepStore
from pydantic_ai_harness.experimental.step_persistence._types import ToolEffectRecord
from pydantic_ai_harness.step_persistence._context import current_run_id
from pydantic_ai_harness.step_persistence._store import StepStore
from pydantic_ai_harness.step_persistence._types import ToolEffectRecord
def is_provider_valid(messages: list[ModelMessage]) -> bool:
@@ -14,14 +14,14 @@ import anyio.to_thread
from pydantic import TypeAdapter
from pydantic_ai.messages import ModelMessage, ModelMessagesTypeAdapter
from pydantic_ai_harness.experimental.media import (
from pydantic_ai_harness.media import (
DiskMediaStore,
MediaStore,
SqliteMediaStore,
externalize_media,
restore_media,
)
from pydantic_ai_harness.experimental.step_persistence._types import (
from pydantic_ai_harness.step_persistence._types import (
ContinuableSnapshot,
EventKind,
RunRecord,
@@ -1,23 +1,13 @@
# SubAgents
> [!WARNING]
> **Experimental.** This capability lives under `pydantic_ai_harness.experimental` and may
> change or be removed in any release, without a deprecation period. Import it from the
> experimental path -- there is no top-level export:
> [!NOTE]
> Import this capability from its submodule -- there is no top-level `pydantic_ai_harness` re-export:
>
> ```python
> from pydantic_ai_harness.experimental.subagents import SubAgent, SubAgents
> from pydantic_ai_harness.subagents import SubAgent, SubAgents
> ```
>
> Importing any experimental capability emits a `HarnessExperimentalWarning`. Silence **all**
> harness experimental warnings with a single filter (no per-capability lines needed):
>
> ```python
> import warnings
> from pydantic_ai_harness.experimental import HarnessExperimentalWarning
>
> warnings.filterwarnings('ignore', category=HarnessExperimentalWarning)
> ```
> The API may change between releases. Where practical, breaking changes ship with a deprecation warning.
Let an agent delegate self-contained tasks to named child agents.
@@ -31,7 +21,7 @@ A single agent that does everything accumulates a large tool set and a long cont
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.subagents import SubAgent, SubAgents
from pydantic_ai_harness.subagents import SubAgent, SubAgents
researcher = Agent('anthropic:claude-sonnet-4-6', name='researcher', description='Researches a topic and reports findings')
writer = Agent('anthropic:claude-sonnet-4-6', name='writer', description='Turns notes into polished prose')
@@ -71,7 +61,7 @@ Each `SubAgent` carries its own budgets, so one delegate's controls do not touch
```python
from pydantic_ai.usage import UsageLimits
from pydantic_ai_harness.experimental.subagents import SubAgent, SubAgents
from pydantic_ai_harness.subagents import SubAgent, SubAgents
# reproducer and librarian are Agent instances, as in the example above.
orchestrator = Agent(
@@ -115,7 +105,7 @@ A repo's markdown agent definitions become delegates without writing any `Agent`
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.subagents import SubAgents
from pydantic_ai_harness.subagents import SubAgents
orchestrator = Agent(
'anthropic:claude-opus-4-7',
@@ -155,7 +145,7 @@ Frontmatter is read by a small, dependency-free parser limited to those keys (`p
Disk agents inherit the parent run's model by default. Per agent, the caller can override the model and set a thinking/effort level via `agent_overrides`, keyed by the agent's name:
```python
from pydantic_ai_harness.experimental.subagents import AgentOverride, SubAgents
from pydantic_ai_harness.subagents import AgentOverride, SubAgents
SubAgents(
agent_folders='agents',
+16
View File
@@ -0,0 +1,16 @@
"""Sub-agent capability: delegate self-contained tasks to named child agents."""
from pydantic_ai_harness.subagents._capability import SubAgents, ToolResolver
from pydantic_ai_harness.subagents._disk import AgentOverride
from pydantic_ai_harness.subagents._effort import MINIMUM_EFFORT_FLOOR, clamp_effort
from pydantic_ai_harness.subagents._toolset import SubAgent, SubAgentToolset
__all__ = [
'MINIMUM_EFFORT_FLOOR',
'AgentOverride',
'SubAgent',
'SubAgentToolset',
'SubAgents',
'ToolResolver',
'clamp_effort',
]
@@ -14,14 +14,14 @@ from pydantic_ai.settings import ModelSettings
from pydantic_ai.tools import AgentDepsT, RunContext
from pydantic_ai.toolsets import AgentToolset
from pydantic_ai_harness.experimental.subagents._disk import (
from pydantic_ai_harness.subagents._disk import (
AgentOverride,
ParsedAgent,
parse_agent_markdown,
resolve_folders,
)
from pydantic_ai_harness.experimental.subagents._effort import clamp_effort
from pydantic_ai_harness.experimental.subagents._toolset import SubAgent, SubAgentToolset
from pydantic_ai_harness.subagents._effort import clamp_effort
from pydantic_ai_harness.subagents._toolset import SubAgent, SubAgentToolset
if TYPE_CHECKING:
from pydantic_ai._instructions import AgentInstructions
@@ -66,7 +66,7 @@ class SubAgents(AbstractCapability[AgentDepsT]):
```python
from pydantic_ai import Agent
from pydantic_ai_harness.experimental.subagents import SubAgent, SubAgents
from pydantic_ai_harness.subagents import SubAgent, SubAgents
researcher = Agent('anthropic:claude-sonnet-4-6', name='researcher', description='Researches topics')
writer = Agent('anthropic:claude-sonnet-4-6', name='writer', description='Writes prose')
@@ -1,4 +1,4 @@
"""Tests for pydantic_ai_harness.experimental.compaction capabilities."""
"""Tests for pydantic_ai_harness.compaction capabilities."""
from __future__ import annotations
@@ -22,7 +22,7 @@ from pydantic_ai.models import Model, ModelRequestContext, ModelRequestParameter
from pydantic_ai.models.test import TestModel
from pydantic_ai.usage import RunUsage
from pydantic_ai_harness.experimental.compaction import (
from pydantic_ai_harness.compaction import (
ClampOversizedMessages,
ClearToolResults,
DeduplicateFileReads,
@@ -32,11 +32,11 @@ from pydantic_ai_harness.experimental.compaction import (
TieredCompaction,
estimate_token_count,
)
from pydantic_ai_harness.experimental.compaction._clamp_oversized_messages import (
from pydantic_ai_harness.compaction._clamp_oversized_messages import (
_CLAMP_ARGS_KEY,
_CLAMP_MARKER,
)
from pydantic_ai_harness.experimental.compaction._shared import (
from pydantic_ai_harness.compaction._shared import (
_history_changed,
_is_safe_cutoff,
compact_with_span,
@@ -46,7 +46,7 @@ from pydantic_ai_harness.experimental.compaction._shared import (
iter_tool_pairs,
prepend_first_user_message,
)
from pydantic_ai_harness.experimental.compaction._summarizing_compaction import (
from pydantic_ai_harness.compaction._summarizing_compaction import (
_SUMMARY_PREFIX,
_extract_previous_summary,
_extract_system_prompts,
@@ -699,9 +699,9 @@ class TestExtractSystemPrompts:
class TestExports:
def test_exposed_under_experimental_only(self):
def test_exposed_under_submodule_only(self):
import pydantic_ai_harness
import pydantic_ai_harness.experimental.compaction as compaction
import pydantic_ai_harness.compaction as compaction
names = [
'SlidingWindow',
@@ -712,7 +712,7 @@ class TestExports:
'TieredCompaction',
]
for name in names:
# Available from the experimental package...
# Available from the capability submodule...
assert hasattr(compaction, name)
# ...and deliberately NOT from the top-level namespace.
assert not hasattr(pydantic_ai_harness, name)
@@ -1796,7 +1796,7 @@ class TestSummarizingCompactionModel:
assert mock_agent_instance.run.call_args.kwargs['usage'] is ctx.usage
def test_default_prompt_has_structured_sections(self):
from pydantic_ai_harness.experimental.compaction._summarizing_compaction import _DEFAULT_SUMMARY_PROMPT
from pydantic_ai_harness.compaction._summarizing_compaction import _DEFAULT_SUMMARY_PROMPT
for heading in (
'## Intent',
@@ -12,14 +12,14 @@ from pydantic_ai.models.test import TestModel
from pydantic_ai.tools import ToolDefinition
from pydantic_ai.usage import RunUsage
from pydantic_ai_harness.experimental.context import (
from pydantic_ai_harness.context import (
AgentContextInventory,
ContextFile,
RepoContext,
RepoContextToolset,
)
from pydantic_ai_harness.experimental.context._inventory import scan_assets
from pydantic_ai_harness.experimental.context._loader import (
from pydantic_ai_harness.context._inventory import scan_assets
from pydantic_ai_harness.context._loader import (
discover_instruction_files,
find_dir_context_file,
render_context_files,
@@ -2,7 +2,6 @@
from __future__ import annotations
import importlib
from pathlib import Path
import httpx
@@ -12,8 +11,7 @@ from pydantic_ai import Agent
from pydantic_ai.messages import ModelMessage, ModelResponse, TextPart, ToolCallPart, ToolReturnPart
from pydantic_ai.models.function import AgentInfo, FunctionModel
from pydantic_ai_harness.experimental import HarnessExperimentalWarning
from pydantic_ai_harness.experimental.docs import PyaiDocs, PyaiDocsToolset, PyaiDocsTopic
from pydantic_ai_harness.docs import PyaiDocs, PyaiDocsToolset, PyaiDocsTopic
pytestmark = pytest.mark.anyio
@@ -175,9 +173,3 @@ class TestThroughAgent:
if isinstance(part, ToolReturnPart) and part.tool_name == 'read_pyai_docs'
]
assert returns == ['# Capabilities doc']
def test_import_emits_experimental_warning() -> None:
module = importlib.import_module('pydantic_ai_harness.experimental.docs')
with pytest.warns(HarnessExperimentalWarning, match='docs'):
importlib.reload(module)
@@ -29,7 +29,7 @@ from pydantic_ai.usage import RunUsage, UsageLimits
from pydantic_core import core_schema
from pydantic_ai_harness.code_mode import CodeMode
from pydantic_ai_harness.experimental.dynamic_workflow import (
from pydantic_ai_harness.dynamic_workflow import (
DynamicWorkflow,
DynamicWorkflowToolset,
WorkflowAgent,
@@ -995,7 +995,7 @@ async def test_sandbox_panic_after_budget_exhaustion_returns_terminal_result(
await self.dispatch('counted', {'task': 'second'})
raise PanicException('sandbox panic')
monkeypatch.setattr('pydantic_ai_harness.experimental.dynamic_workflow._toolset.MontyExecutor', FakeExecutor)
monkeypatch.setattr('pydantic_ai_harness.dynamic_workflow._toolset.MontyExecutor', FakeExecutor)
ts = DynamicWorkflowToolset[object](agents=[_wf_agent('counted-result', 'counted')], max_agent_calls=1)
out = await _run_script(ts, '1 + 1')
assert isinstance(out, dict)
@@ -0,0 +1,41 @@
"""The old `experimental.<name>` paths still import, with a `DeprecationWarning` to the new path.
Each capability that graduated out of `experimental` leaves a compatibility shim at its former
path. Importing the shim must keep working (re-exporting the moved capability) and must warn.
"""
from __future__ import annotations
import importlib
import warnings
import pytest
# (old experimental subpackage, new top-level module, a public symbol to check re-export)
_SHIMS = [
('authoring', 'runtime_authoring', 'RuntimeAuthoring'),
('overflow', 'overflowing_tool_output', 'OverflowingToolOutput'),
('compaction', 'compaction', 'TieredCompaction'),
('context', 'context', 'RepoContext'),
('docs', 'docs', 'PyaiDocs'),
('dynamic_workflow', 'dynamic_workflow', 'DynamicWorkflow'),
('media', 'media', 'S3MediaStore'),
('planning', 'planning', 'Planning'),
('step_persistence', 'step_persistence', 'StepPersistence'),
('subagents', 'subagents', 'SubAgents'),
]
@pytest.mark.parametrize('old, new, symbol', _SHIMS)
def test_shim_warns_and_reexports(old: str, new: str, symbol: str) -> None:
# Import once quietly so the deprecation fires on the reload we assert on, even if
# another test already imported the shim.
with warnings.catch_warnings():
warnings.simplefilter('ignore')
shim = importlib.import_module(f'pydantic_ai_harness.experimental.{old}')
with pytest.warns(DeprecationWarning, match=rf'experimental\.{old}` has moved'):
importlib.reload(shim)
new_module = importlib.import_module(f'pydantic_ai_harness.{new}')
assert getattr(shim, symbol) is getattr(new_module, symbol)
-42
View File
@@ -1,42 +0,0 @@
"""Tests for the experimental-capability warning convention."""
from __future__ import annotations
import importlib
import warnings
import pytest
from pydantic_ai_harness.experimental import HarnessExperimentalWarning
from pydantic_ai_harness.experimental._warn import warn_experimental
class TestExperimentalWarning:
def test_message_names_feature_and_carries_silence_snippet(self) -> None:
with pytest.warns(HarnessExperimentalWarning) as rec:
warn_experimental('compaction')
assert len(rec) == 1
msg = str(rec[0].message)
assert '`pydantic_ai_harness.experimental.compaction`' in msg
# The message must hand the user the exact, category-wide silence line.
assert "warnings.filterwarnings('ignore', category=HarnessExperimentalWarning)" in msg
def test_one_filter_silences_every_capability(self) -> None:
# A single category filter mutes all experimental warnings — no per-capability lines.
with warnings.catch_warnings():
warnings.simplefilter('error') # baseline: any warning is an error
warnings.filterwarnings('ignore', category=HarnessExperimentalWarning)
warn_experimental('compaction')
warn_experimental('some_future_capability') # also silenced, same filter
@pytest.mark.parametrize('feature', ['compaction', 'subagents', 'dynamic_workflow'])
def test_importing_a_capability_warns(self, feature: str) -> None:
module = importlib.import_module(f'pydantic_ai_harness.experimental.{feature}')
with pytest.warns(HarnessExperimentalWarning):
importlib.reload(module)
@pytest.mark.parametrize('feature', ['step_persistence', 'media'])
def test_importing_step_persistence_or_media_warns(self, feature: str) -> None:
module = importlib.import_module(f'pydantic_ai_harness.experimental.{feature}')
with pytest.warns(HarnessExperimentalWarning, match=feature):
importlib.reload(module)
@@ -24,7 +24,6 @@ import pytest
if TYPE_CHECKING:
from vcr.request import Request as VcrRequest # pyright: ignore[reportMissingTypeStubs]
# Public placeholders baked into the committed cassettes. Tests pass
# these *exact* values when constructing `S3MediaStore`, so the replay
# URI matches the recorded URI even though both were sanitised.
@@ -194,7 +193,7 @@ def s3_store(s3_credentials: dict[str, str]) -> Any:
The key prefix is part of the URL path that lands in the cassette, so
keep it stable across re-records.
"""
from pydantic_ai_harness.experimental.media import S3MediaStore
from pydantic_ai_harness.media import S3MediaStore
return S3MediaStore(
bucket=s3_credentials['bucket'],
@@ -1,4 +1,4 @@
"""Tests for `pydantic_ai_harness.experimental.media`: stores + walker + SigV4 + S3 store."""
"""Tests for `pydantic_ai_harness.media`: stores + walker + SigV4 + S3 store."""
from __future__ import annotations
@@ -10,7 +10,7 @@ from pathlib import Path
import pytest
from httpx import AsyncClient, MockTransport, Request, Response
from pydantic_ai_harness.experimental.media import (
from pydantic_ai_harness.media import (
DiskMediaStore,
MediaContext,
MediaStore,
@@ -22,7 +22,7 @@ from pydantic_ai_harness.experimental.media import (
parse_media_uri,
restore_media,
)
from pydantic_ai_harness.experimental.media._s3 import sign_request
from pydantic_ai_harness.media._s3 import sign_request
pytestmark = pytest.mark.anyio
@@ -387,7 +387,7 @@ class TestS3MediaStoreWithMockTransport:
Otherwise the signed canonical path and the path S3 receives diverge ->
SignatureDoesNotMatch. The wire `raw_path` must equal `_canonical_uri(path)`.
"""
from pydantic_ai_harness.experimental.media._s3 import _canonical_uri # pyright: ignore[reportPrivateUsage]
from pydantic_ai_harness.media._s3 import _canonical_uri # pyright: ignore[reportPrivateUsage]
captured: list[Request] = []
@@ -24,7 +24,7 @@ from __future__ import annotations
import pytest
from pydantic_ai_harness.experimental.media import MediaContext, S3MediaStore, media_uri_for
from pydantic_ai_harness.media import MediaContext, S3MediaStore, media_uri_for
pytestmark = pytest.mark.anyio
@@ -1,4 +1,4 @@
"""Tests for pydantic_ai_harness.experimental.overflow."""
"""Tests for pydantic_ai_harness.overflowing_tool_output."""
from __future__ import annotations
@@ -26,7 +26,7 @@ from pydantic_ai.models.test import TestModel
from pydantic_ai.tools import ToolDefinition
from pydantic_ai.usage import RunUsage
from pydantic_ai_harness.experimental.overflow import (
from pydantic_ai_harness.overflowing_tool_output import (
Band,
LocalFileStore,
OverflowingToolOutput,
@@ -36,7 +36,7 @@ from pydantic_ai_harness.experimental.overflow import (
Truncate,
TruncationStrategy,
)
from pydantic_ai_harness.experimental.overflow._capability import (
from pydantic_ai_harness.overflowing_tool_output._capability import (
READ_TOOL_NAME,
_build_spill_preview,
_handle_key,
@@ -46,7 +46,7 @@ from pydantic_ai_harness.experimental.overflow._capability import (
_Unit,
_with_handles,
)
from pydantic_ai_harness.experimental.overflow._payload import (
from pydantic_ai_harness.overflowing_tool_output._payload import (
is_binary,
json_sketch,
measure,
@@ -55,7 +55,7 @@ from pydantic_ai_harness.experimental.overflow._payload import (
to_text,
truncate_text,
)
from pydantic_ai_harness.experimental.overflow._store import _safe_segment
from pydantic_ai_harness.overflowing_tool_output._store import _safe_segment
# ---------------------------------------------------------------------------
# Helpers
@@ -19,8 +19,8 @@ from pydantic_ai.models import ModelRequestContext, ModelRequestParameters
from pydantic_ai.models.function import AgentInfo, FunctionModel
from pydantic_ai.models.test import TestModel
from pydantic_ai_harness.experimental.planning import PlanItem, Planning, PlanningToolset, TaskStatus
from pydantic_ai_harness.experimental.planning._toolset import PlanState, render_plan
from pydantic_ai_harness.planning import PlanItem, Planning, PlanningToolset, TaskStatus
from pydantic_ai_harness.planning._toolset import PlanState, render_plan
pytestmark = pytest.mark.anyio
@@ -15,13 +15,13 @@ from pydantic_ai.exceptions import ModelRetry
from pydantic_ai.messages import ToolReturnPart
from pydantic_ai.models.test import TestModel
from pydantic_ai_harness.experimental.authoring import (
from pydantic_ai_harness.runtime_authoring import (
AuthoringToolset,
CapabilityStore,
CapabilityValidationError,
RuntimeAuthoring,
)
from pydantic_ai_harness.experimental.authoring._validate import (
from pydantic_ai_harness.runtime_authoring._validate import (
load_capability_instance,
validate_capability_file,
)
@@ -41,7 +41,6 @@ from dataclasses import dataclass
from pydantic_ai.capabilities import AbstractCapability
from pydantic_ai.toolsets import FunctionToolset
class _MarkerToolset(FunctionToolset):
def __init__(self):
super().__init__()
@@ -50,7 +49,6 @@ class _MarkerToolset(FunctionToolset):
async def marker(self) -> str:
return 'marker-result'
@dataclass
class MarkerCapability(AbstractCapability):
def get_instructions(self):
@@ -67,12 +65,10 @@ from dataclasses import dataclass
from pydantic_ai.capabilities import AbstractCapability
@dataclass
class CapA(AbstractCapability):
pass
@dataclass
class CapB(AbstractCapability):
pass
@@ -83,7 +79,6 @@ from dataclasses import dataclass
from pydantic_ai.capabilities import AbstractCapability
@dataclass
class NeedsArg(AbstractCapability):
value: int
@@ -94,7 +89,6 @@ from dataclasses import dataclass
from pydantic_ai.capabilities import AbstractCapability
@dataclass
class BadGetter(AbstractCapability):
def get_toolset(self):
@@ -111,7 +105,6 @@ from dataclasses import dataclass
from pydantic_ai.capabilities import AbstractCapability
@dataclass
class {class_name}(AbstractCapability):
def get_instructions(self):
@@ -124,7 +117,6 @@ from dataclasses import dataclass
from pydantic_ai.capabilities import AbstractCapability
@dataclass
class BadInstructions(AbstractCapability):
def get_instructions(self):
@@ -136,7 +128,6 @@ from dataclasses import dataclass
from pydantic_ai.capabilities import AbstractCapability
@dataclass
class BadModelSettings(AbstractCapability):
def get_model_settings(self):
@@ -25,8 +25,8 @@ from pydantic_ai.messages import (
)
from pydantic_ai.models.test import TestModel
from pydantic_ai_harness.experimental.media import DiskMediaStore, SqliteMediaStore
from pydantic_ai_harness.experimental.step_persistence import (
from pydantic_ai_harness.media import DiskMediaStore, SqliteMediaStore
from pydantic_ai_harness.step_persistence import (
ContinuableSnapshot,
FileStepStore,
RunRecord,

Some files were not shown because too many files have changed in this diff Show More