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).

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.1

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.1
File Size Uploaded
py_agent_framework-0.2.1.tar.gz 35.5 kB Details

Built distribution (wheel)

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

Total release size: 81.0 kB

Release files / py_agent_framework-0.2.1.tar.gz

Download URL py_agent_framework-0.2.1.tar.gz
Size 35.5 kB
Tags Source
SHA-256 checksum
How to use checksums
ae1ed3552ba978d27fa17de68104bcd2abca72c9963325128ea0c48360e402bd
BLAKE2b-256 checksum
How to use checksums
94d851b5c68b7609374b7e80d3dd3b334034f52992d90fa7b76d530b005b46eb
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 8, 2026.

Transparency log

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

Download URL py_agent_framework-0.2.1-py3-none-any.whl
Size 45.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8964d6c9f6269f45e4d4c7170c6d3043ec00c01fd31ebea6ecd7600a36dc644b
BLAKE2b-256 checksum
How to use checksums
51bb938be784c828de85c81a58901f57b3a809e1a0b561231144db18880006cc
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 8, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 release files

0.2.0

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