Skip to main content

py-agent-framework

PyPI version Python versions License CI

A stateful LLM agent framework built on pydantic-ai. It adds what Pydantic AI does not provide out of the box: session-level multi-turn persistence, context-window management, and a lifecycle extension system — with the smallest possible footprint.

Architecture inspired by Pi Agent — its extension-driven lifecycle, progressive-disclosure skills, and reload-by-reconstruction pattern. This is an independent implementation on pydantic-ai, not a port.

Why py-agent-framework?

Pydantic AI gives you Agent, Tool, and Hooks primitives. What it does not give you is memory across turns:

Bare pydantic-ai py-agent-framework
Multi-turn conversation ❌ you re-supply all history every call ✅ session_id in, history out
Long conversations ❌ you hand-roll truncation/summarization ✅ dual-watermark context management
Tool/session lifecycle hooks ⚠️ low-level Hooks only ✅ Extension protocol with 15 events
Storage ❌ none ✅ SQLite / PostgreSQL / stateless

If you were about to write your own "load history → truncate → call model → save" loop, that loop is this library.

Features

  • Session persistence — three backends for three stages of the lifecycle:
    • SingleTurnSessionManager — stateless, zero setup, every run is fresh.
    • LocalSessionManager — SQLite, file-based, local prototyping with full multi-turn memory.
    • PostgresSessionManager — production: connection pooling, JSONB, concurrent access.
    • Reconnect by passing session_id; the stored system prompt lets a prompt-less runner resume without re-supplying it.
  • Context management — dual watermarks keep long conversations inside the window: below the low watermark nothing happens; between watermarks old tool results are truncated; above the high watermark the distant past is summarized by an LLM. A single protect_turns value keeps the last N turns intact through both.
  • Extension system — a Protocol-defined lifecycle: observe and intercept every event (tool calls, token streams, compaction votes, persistence). Tools and skills are declared on AgentRunner; extensions own the lifecycle and may register SDK capabilities.
  • Tools & MCP — tools are plain pydantic-ai objects: Tool, raw callables, or MCPToolset with automatic multi-server disambiguation ({server}_{tool}), partial degradation when a server is down, and mandatory unique server names.
  • Skills — on-demand skill libraries via pydantic-ai harness Skills: the model sees names + descriptions, loads the full body only when needed.

Installation

Requires Python ≥ 3.11.

pip install py-agent-framework

Optional extras:

pip install "py-agent-framework[postgres]"   # PostgreSQL backend

Quick Start

import asyncio
from pydantic_ai.models.openai import OpenAIModel

from py_agent import AgentRunner

model = OpenAIModel("gpt-4o")   # any pydantic-ai model

runner = AgentRunner(
    model=model,
    system_prompt="You are a helpful assistant.",
)

async def main() -> None:
    result = await runner.run("Hello!")
    print(result.output)

asyncio.run(main())

By default the runner is stateless — every run() starts a fresh session. Pass a persistent session_manager to enable multi-turn memory.

Multi-turn Persistence

from py_agent import AgentRunner
from py_agent.session import LocalSessionManager

runner = AgentRunner(
    model=model,
    system_prompt="You are a helpful assistant.",
    session_manager=LocalSessionManager(db_path="sessions.db"),
)

r1 = await runner.run("Remember: the answer is 42.")
r2 = await runner.run("What did I ask you to remember?", session_id=r1.session_id)
# → "You asked me to remember that the answer is 42."

Swap the backend without touching the rest of your code:

from py_agent.session import PostgresSessionManager  # requires: pip install "py-agent-framework[postgres]"

session_manager = PostgresSessionManager(pg_url="postgres://user:pass@localhost/db")

Context Management

Long conversations stay inside your model's context window automatically:

  • Low watermark (default 0.6 × context_window_cap): old tool results are truncated.
  • High watermark (default 0.75 × context_window_cap): the distant past is summarized by an LLM (compaction runs in the background, so your turn is not blocked).
  • protect_turns (default 5) keeps the most recent turns intact through both.

Tune it per runner:

from py_agent import AgentRunner, ContextConfig, SummarizerConfig

runner = AgentRunner(
    model=model,
    system_prompt="You are a helpful assistant.",
    session_manager=LocalSessionManager(db_path="sessions.db"),
    context_config=ContextConfig(
        low_watermark_ratio=0.6,
        high_watermark_ratio=0.75,
        protect_turns=5,
        truncate_chars=1_000,
        context_window_cap=128_000,
    ),
    summarizer_config=SummarizerConfig(),  # defaults: reuse main model
)

Tools, MCP & Skills

from pydantic_ai import Tool
from pydantic_ai.mcp import MCPToolset
from pydantic_ai_harness.skills import Skills

from py_agent import AgentRunner
from py_agent.session import SingleTurnSessionManager


def web_search(q: str) -> str:
    """Search the web."""
    ...


runner = AgentRunner(
    model=model,
    system_prompt="You are a helpful assistant.",
    tools=[
        Tool(web_search),
        MCPToolset("http://search:8000/mcp", id="search"),  # server name required
    ],
    skills=Skills(["skills/"]),
    session_manager=SingleTurnSessionManager(),
)
  • Multi-server MCP tools are auto-prefixed ({server}_{tool}) so identical names never collide.
  • If an MCP server is down, it degrades gracefully — other servers keep working (customize via toolset_failure).

Extensions

Extension is a Protocol with a single hook: on_agent_runner_event(event, data). Subscribe to any of the 15 lifecycle events:

from py_agent import AgentRunner
from py_agent.types import AgentRunnerEvent, Extension


class Audit(Extension):
    async def on_agent_runner_event(self, event, data):
        if event == AgentRunnerEvent.TOOL_CALL:
            audit_log(data["tool_name"], data["args"])
        return None


runner = AgentRunner(..., extensions=[Audit()])

Selected events:

Event When Can modify?
BEFORE_AGENT_RUN before the model sees messages ✅ replace messages (injected messages are persisted)
TOOL_CALL before a tool runs ✅ {"block": true} or rewrite args
TOOL_RESULT after a tool runs ✅ rewrite content
TOKEN_STREAM each token chunk (run_stream) ❌
SESSION_SAVE before persistence ✅ replace delta_messages
COMPACTION_TRIGGER compaction flagged ✅ {"cancel": true} to veto

Streaming extensions: implement on_agent_runner_event_stream(event, data) as an async generator to receive runtime events (TOKEN_STREAM, tool events, lifecycle events) — each chunk it yields is forwarded to the run_stream() consumer. Without such an extension, run_stream() yields only run_end; use run_with_events() (see below) to receive token/tool events with no extension at all.

Public API

from py_agent import AgentRunner, RunResult, ContextConfig, SummarizerConfig
from py_agent.session import SessionManager, SingleTurnSessionManager, LocalSessionManager, PostgresSessionManager
from py_agent.types import Extension, AgentRunnerEvent, MessageRole, ToolsetFailureHandler

AgentRunner also offers run_stream() — an async iterator over runtime events: chunks yielded by streaming extensions (implementing on_agent_runner_event_stream) are forwarded as they happen, and the stream always ends with run_end. A bare consumer receives only run_end.

run_with_events() is a sibling API for bare consumers: it yields normalized events directly, with no extension required. Each event is a dict with a "type" field:

type payload
token {"type": "token", "chunk": str} — one streamed text chunk
tool_call {"type": "tool_call", "tool_name": str, "tool_call_id": str, "args": dict}
tool_result {"type": "tool_result", "tool_name": str, "tool_call_id": str, "content": Any, "is_error": bool}
run_end unchanged — {"type": "run_end", "session_id": str, "output": str, "new_messages": list, "usage": ...}

Tool events precede the token chunk of the text that follows them, and the stream always ends with run_end. User extensions keep observing events under run_with_events() (the internal bridge never mutates chain data).

Events are snapshots taken before chain-mode extension rewrites: tool_call.args and tool_result.content show the values the hooks dispatched, not the rewritten effective values. tool_result.is_error is always False — the hooks never set it (a known limitation).

Known issues

pydantic-ai drops post-tool-call text in thinking + streaming mode

With thinking_enabled=True (the default) and run_stream(), pydantic-ai 2.22+ can terminate the agent loop right after a tool returns (verified on deepseek-v4-flash on both 2.22.0 and 2.25.0): the session's messages contain no second model response, so the post-tool-call text is never generated. run_end.output is not a reliable fallback — it is nondeterministic (truncated on some runs, complete on others). Non-streaming run() is always complete and is the only reliable path when thinking is enabled and the model calls tools.

Workarounds:

  • Prefer run() over run_stream() when thinking is enabled.
  • Set thinking_enabled=False when streaming output matters more than thinking.

AgentRunner emits an on_warning on the first run_stream() or run_with_events() call when thinking_enabled=True (run() never warns — it is not affected by the defect).

Development

Contributors: see CONTRIBUTING.md for setup and the make workflow (make bootstrap → make check).

Contributing

This is a personal project and not yet ready for external contributors. Bug reports and feature discussions via issues are welcome. If you would still like to submit a change, see CONTRIBUTING.md — PRs are required and the project's standards are enforced.

License

MIT © Shunji Wan

Metadata

Release files for py-agent-framework 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for py-agent-framework 0.2.0
File Size Uploaded
py_agent_framework-0.2.0.tar.gz 35.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for py-agent-framework 0.2.0
File Interpreter ABI Platform
py_agent_framework-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 81.0 kB

Release files / py_agent_framework-0.2.0.tar.gz

Download URL py_agent_framework-0.2.0.tar.gz
Size 35.5 kB
Tags Source
SHA-256 checksum
How to use checksums
2f2465e6d2074e9ce8efd1357cde452d6a7e1931802883d8847eccacb6f40399
BLAKE2b-256 checksum
How to use checksums
2756acbe9d4e1bb2fa1d47257104f1d522e8bf7cf3eb16b3cac3e8381c8f4db3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 7, 2026.

Transparency log

Release files / py_agent_framework-0.2.0-py3-none-any.whl

Download URL py_agent_framework-0.2.0-py3-none-any.whl
Size 45.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
79f1b90273addda0bbdbcc11aeb21b9d6bd47f64ad3bb6077319f126e53227f2
BLAKE2b-256 checksum
How to use checksums
ca565dca045364bb8b96b81e20142e569f95fd9582920a9d578e61a52daf618a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 7, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.1

2 release files

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page