pyagentic-core 2.13.0a1__tar.gz → 2.14.1.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyagentic_core-2.13.0a1/pyagentic_core.egg-info → pyagentic_core-2.14.1.dev1}/PKG-INFO +1 -1
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/index.md +3 -3
- pyagentic_core-2.14.1.dev1/docs/policies/built-in.md +211 -0
- pyagentic_core-2.14.1.dev1/docs/policies/custom.md +342 -0
- pyagentic_core-2.14.1.dev1/docs/policies/index.md +246 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/states.md +5 -5
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/mkdocs.yml +4 -1
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1/pyagentic_core.egg-info}/PKG-INFO +1 -1
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic_core.egg-info/SOURCES.txt +3 -1
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic_core.egg-info/scm_file_list.json +3 -1
- pyagentic_core-2.14.1.dev1/pyagentic_core.egg-info/scm_version.json +8 -0
- pyagentic_core-2.13.0a1/docs/policies.md +0 -601
- pyagentic_core-2.13.0a1/pyagentic_core.egg-info/scm_version.json +0 -8
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/.github/workflows/docs.yml +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/.github/workflows/release.yml +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/.github/workflows/testing.yml +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/.gitignore +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/CHANGELOG.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/LICENSE +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/README.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/agent-linking.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/api/creating-an-app.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/api/deploying.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/api/index.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/api/jobs.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/api/running.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/diagrams/declaration.svg +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/diagrams/instantiation.svg +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/diagrams/runtime.svg +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/diagrams/source/README.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/diagrams/source/declaration.d2 +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/diagrams/source/instantiation.d2 +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/diagrams/source/runtime.d2 +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/execution-modes.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/getting-started.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/images/langfuse.png +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/inheritance.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/observability.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/phases.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/reference/architecture.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/reference/modules.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/responses.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/structured-output.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/tools.md +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/__init__.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/__init__.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_agent/__init__.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_agent/_agent.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_agent/_agent_linking.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_agent/_agent_state.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_depends.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_exceptions.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_info.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_mcp.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_metaclasses.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_ref.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_spec.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_state.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_tool.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_validation.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_utils/_typing.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_utils/_warnings.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_version_scheme.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/__init__.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/_app.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/_build.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/_config.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/_docker.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/_mcp_server.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/_models.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/_sessions.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/jobs/__init__.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/jobs/_models.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/jobs/_orchestrator.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/jobs/_routes.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/jobs/backends/__init__.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/jobs/backends/_base.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/jobs/backends/_in_process.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/jobs/store/__init__.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/jobs/store/_base.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/jobs/store/_sqlite.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/llm/__init__.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/llm/_anthropic.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/llm/_gemini.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/llm/_mock.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/llm/_openai.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/llm/_provider.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/logging.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/models/llm.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/models/response.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/models/tracing.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/policies/__init__.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/policies/_events.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/policies/_list.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/policies/_policy.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/policies/messages.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/tracing/__init__.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/tracing/_basic.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/tracing/_langfuse.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/tracing/_tracer.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/updates.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic_core.egg-info/dependency_links.txt +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic_core.egg-info/requires.txt +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic_core.egg-info/top_level.txt +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyproject.toml +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/setup.cfg +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/setup.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/__init__.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/__init__.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_agent.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_agent_forking.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_agent_inheritance.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_agent_linking.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_agent_message_policies.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_agent_provider.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_params.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_phases.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_policy_list.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_state.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_tool.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_validator.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/__init__.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/jobs/__init__.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/jobs/test_orchestrator.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/jobs/test_routes.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/jobs/test_store_sqlite.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/test_app.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/test_build_agent.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/test_config.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/test_construct_model.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/test_dependencies_integration.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/test_docker.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/test_mcp_server.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/test_metaclass_models.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/test_router.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/test_sessions.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/conftest.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/llm/__init__.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/llm/test_message_conversion.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/models/test_messages.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/models/test_responses.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/policies/__init__.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/policies/test_message_policies.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/tracing/__init__.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/tracing/test_basic_tracer.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/tracing/test_models.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/tracing/test_tracer.py +0 -0
- {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/uv.lock +0 -0
|
@@ -48,13 +48,13 @@ Dive deeper into PyAgentic's powerful features:
|
|
|
48
48
|
|
|
49
49
|
[:octicons-arrow-right-24: Learn about phases](phases.md)
|
|
50
50
|
|
|
51
|
-
- :material-shield-check: **[Policies](policies.md)**
|
|
51
|
+
- :material-shield-check: **[Policies](policies/index.md)**
|
|
52
52
|
|
|
53
53
|
---
|
|
54
54
|
|
|
55
|
-
React to state changes with validation,
|
|
55
|
+
React to state and message-context changes with validation, transformation, context management, and custom behaviors.
|
|
56
56
|
|
|
57
|
-
[:octicons-arrow-right-24: Learn about policies](policies.md)
|
|
57
|
+
[:octicons-arrow-right-24: Learn about policies](policies/index.md)
|
|
58
58
|
|
|
59
59
|
- :material-chat-processing: **[Agent Responses](responses.md)**
|
|
60
60
|
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Built-in Policies
|
|
2
|
+
|
|
3
|
+
PyAgentic ships four ready-to-use policies, all focused on **context management** —
|
|
4
|
+
keeping the message history sent to the LLM small so long-running agents don't
|
|
5
|
+
blow up their context window (or your token bill).
|
|
6
|
+
|
|
7
|
+
All of them live in `pyagentic.policies`:
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from pyagentic.policies import (
|
|
11
|
+
ToolOutputClipPolicy,
|
|
12
|
+
ToolEvictionPolicy,
|
|
13
|
+
SlidingWindowPolicy,
|
|
14
|
+
CompactionPolicy,
|
|
15
|
+
)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Attach them to an agent with the `__message_policies__` class attribute:
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
class ResearchAgent(BaseAgent):
|
|
22
|
+
__system_message__ = "You research topics using tools."
|
|
23
|
+
__message_policies__ = [
|
|
24
|
+
ToolOutputClipPolicy(max_chars=8000),
|
|
25
|
+
ToolEvictionPolicy(keep_last_n=5),
|
|
26
|
+
CompactionPolicy(max_input_tokens=80_000),
|
|
27
|
+
]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
They compose — the list above clips oversized tool outputs the moment they enter
|
|
31
|
+
context, stubs out stale tool results as the conversation moves on, and
|
|
32
|
+
summarizes old history if the context still crosses the token threshold.
|
|
33
|
+
|
|
34
|
+
!!! note
|
|
35
|
+
Policies only ever shape the **working context** (`state._context`) that
|
|
36
|
+
providers consume. The raw history (`state._messages` / `state.raw_messages`)
|
|
37
|
+
always keeps every message untouched, for debugging and auditing. See the
|
|
38
|
+
[Policies overview](index.md#dual-history-raw-log-vs-working-context) for
|
|
39
|
+
how the dual history works.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## ToolOutputClipPolicy
|
|
44
|
+
|
|
45
|
+
Clips oversized tool results **at append time**, so a huge output never occupies
|
|
46
|
+
context in the first place. This is the cheapest, highest-impact guard — a single
|
|
47
|
+
tool call returning a large JSON payload is the most common cause of context
|
|
48
|
+
explosion.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
ToolOutputClipPolicy(
|
|
52
|
+
max_chars=8000, # max content length for a tool result
|
|
53
|
+
suffix="\n…[output clipped]", # marker so the model knows it saw a slice
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
| Parameter | Default | Description |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `max_chars` | `8000` | Maximum content length for a `ToolResultMessage`. |
|
|
60
|
+
| `suffix` | `"\n…[output clipped]"` | Appended to clipped content. |
|
|
61
|
+
|
|
62
|
+
**Behavior**
|
|
63
|
+
|
|
64
|
+
- Runs on `on_append`; only `ToolResultMessage` (and its subclass
|
|
65
|
+
`AgentResultMessage`) are affected — user/assistant messages pass through.
|
|
66
|
+
- The raw, unclipped result is still recorded in the raw history.
|
|
67
|
+
|
|
68
|
+
**When to use:** always, essentially. Any agent whose tools can return
|
|
69
|
+
unbounded output (API calls, file reads, search results).
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## ToolEvictionPolicy
|
|
74
|
+
|
|
75
|
+
Evicts old tool results from the context, keeping only the most recent N intact.
|
|
76
|
+
Old results are usually only useful for the turn they served — after that they're
|
|
77
|
+
dead weight.
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
ToolEvictionPolicy(
|
|
81
|
+
keep_last_n=5, # recent results kept intact
|
|
82
|
+
stub="[tool result evicted to save context]", # replacement content
|
|
83
|
+
include_agent_results=True, # also evict linked-agent results
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
| Parameter | Default | Description |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `keep_last_n` | `5` | Number of most-recent tool results kept verbatim. |
|
|
90
|
+
| `stub` | `"[tool result evicted to save context]"` | Replacement content for evicted results. |
|
|
91
|
+
| `include_agent_results` | `True` | Whether `AgentResultMessage`s are also subject to eviction. |
|
|
92
|
+
|
|
93
|
+
**Behavior**
|
|
94
|
+
|
|
95
|
+
- Runs on `on_compile`, right before each inference.
|
|
96
|
+
- **Stubs, never deletes**: the message and its `tool_call_id` survive, because
|
|
97
|
+
providers reject histories with orphaned call/result pairs.
|
|
98
|
+
- Idempotent — already-stubbed results are left alone, so it does no repeated
|
|
99
|
+
work on later turns.
|
|
100
|
+
- Set `include_agent_results=False` to exempt linked-agent responses (useful
|
|
101
|
+
when sub-agent findings need to stay in context longer than raw tool output).
|
|
102
|
+
|
|
103
|
+
**When to use:** tool-heavy agents with many calls per conversation. Pair it
|
|
104
|
+
with `ToolOutputClipPolicy` — clipping bounds each result, eviction bounds how
|
|
105
|
+
many results linger.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## SlidingWindowPolicy
|
|
110
|
+
|
|
111
|
+
Bounds the context to the most recent `max_messages` messages, dropping from the
|
|
112
|
+
front.
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
SlidingWindowPolicy(max_messages=50)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
| Parameter | Default | Description |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| `max_messages` | `50` | Maximum number of messages kept in context. |
|
|
121
|
+
|
|
122
|
+
**Behavior**
|
|
123
|
+
|
|
124
|
+
- Runs on `on_compile`.
|
|
125
|
+
- The cut is **pair-boundary-safe**: it advances past any tool results whose
|
|
126
|
+
calls were dropped, so no result ever survives without the call that produced
|
|
127
|
+
it.
|
|
128
|
+
- Blunt but predictable — older turns disappear entirely rather than being
|
|
129
|
+
summarized.
|
|
130
|
+
|
|
131
|
+
**When to use:** simple bounded-memory agents where old turns genuinely stop
|
|
132
|
+
mattering (chat companions, per-session assistants). Prefer `CompactionPolicy`
|
|
133
|
+
when older context contains facts the agent must not forget.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## CompactionPolicy
|
|
138
|
+
|
|
139
|
+
Summarizes older history into a single `CompactionSummaryMessage` when the
|
|
140
|
+
context grows past a token threshold — the "keep the gist, drop the transcript"
|
|
141
|
+
strategy.
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
CompactionPolicy(
|
|
145
|
+
max_input_tokens=100_000, # threshold that triggers compaction
|
|
146
|
+
keep_recent=10, # most-recent messages kept verbatim
|
|
147
|
+
summary_prompt=DEFAULT_COMPACTION_PROMPT,
|
|
148
|
+
)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
| Parameter | Default | Description |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| `max_input_tokens` | `100_000` | Input-token threshold that triggers compaction. |
|
|
154
|
+
| `keep_recent` | `10` | Number of most-recent messages kept verbatim. |
|
|
155
|
+
| `summary_prompt` | `DEFAULT_COMPACTION_PROMPT` | System prompt for the summarization call. |
|
|
156
|
+
|
|
157
|
+
**Behavior**
|
|
158
|
+
|
|
159
|
+
- Runs on `on_compile`. The trigger is the **previous inference's reported
|
|
160
|
+
input tokens** (`event.last_usage`) — no tokenizer dependency. When usage
|
|
161
|
+
isn't available, it falls back to a chars/4 estimate.
|
|
162
|
+
- Splits history at `len - keep_recent`, nudged forward so no tool call/result
|
|
163
|
+
pair straddles the boundary, renders the older half into a transcript, and
|
|
164
|
+
summarizes it with **one LLM call** via the same provider the agent uses.
|
|
165
|
+
- The result replaces the old messages with a single
|
|
166
|
+
`CompactionSummaryMessage(compacted_count=N)`. Because the compiled context
|
|
167
|
+
is written back, compaction fires **once per threshold crossing**, not every
|
|
168
|
+
turn, and prior summaries are folded into the next one.
|
|
169
|
+
- Override `summary_prompt` to control what survives compaction (e.g. "always
|
|
170
|
+
preserve file paths and error messages" for a coding agent). The default
|
|
171
|
+
prompt (`pyagentic.policies.DEFAULT_COMPACTION_PROMPT`) preserves key facts,
|
|
172
|
+
user goals, decisions, tool findings, and open tasks.
|
|
173
|
+
|
|
174
|
+
**When to use:** long-running agents where old context carries facts that must
|
|
175
|
+
survive (research, planning, multi-session work). Costs one extra LLM call per
|
|
176
|
+
compaction.
|
|
177
|
+
|
|
178
|
+
!!! tip "State survives compaction for free"
|
|
179
|
+
Anything stored in typed `State` fields is rendered into the system prompt
|
|
180
|
+
from state, not from message history — so it is never lost to compaction.
|
|
181
|
+
Put durable facts in state; let the transcript be compactable.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Choosing and combining
|
|
186
|
+
|
|
187
|
+
| Goal | Policy |
|
|
188
|
+
|---|---|
|
|
189
|
+
| One tool call returns megabytes | `ToolOutputClipPolicy` |
|
|
190
|
+
| Many tool calls accumulate | `ToolEvictionPolicy` |
|
|
191
|
+
| Hard cap on history length, forgetting is fine | `SlidingWindowPolicy` |
|
|
192
|
+
| Long sessions, must remember the gist | `CompactionPolicy` |
|
|
193
|
+
| Linked-agent responses flooding the parent | `ToolOutputClipPolicy` / `ToolEvictionPolicy` (they match `AgentResultMessage` too) |
|
|
194
|
+
|
|
195
|
+
A sensible default stack for a tool-using agent:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
__message_policies__ = [
|
|
199
|
+
ToolOutputClipPolicy(max_chars=8000), # bound each result (append time)
|
|
200
|
+
ToolEvictionPolicy(keep_last_n=5), # bound how many linger (compile time)
|
|
201
|
+
CompactionPolicy(max_input_tokens=80_000), # last resort when context still grows
|
|
202
|
+
]
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Order matters at compile time: policies run in declaration order, each seeing
|
|
206
|
+
the previous one's output. Put cheap, targeted policies before expensive,
|
|
207
|
+
sweeping ones so compaction sees an already-slimmed context.
|
|
208
|
+
|
|
209
|
+
All built-in policies are **stateless** (config-only), as required — policy
|
|
210
|
+
instances are shared across agent instances and forks. To write your own, see
|
|
211
|
+
[Writing Custom Policies](custom.md).
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# Writing Custom Policies
|
|
2
|
+
|
|
3
|
+
A policy is a plain class implementing the `Policy` protocol. Subclass it,
|
|
4
|
+
implement **only the handlers you need**, and take configuration in
|
|
5
|
+
`__init__` — every handler you don't implement is skipped.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from pyagentic.policies import Policy
|
|
9
|
+
from pyagentic.policies._events import (
|
|
10
|
+
GetEvent, SetEvent, AppendEvent, CompileEvent,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
class Policy(Protocol[T]):
|
|
14
|
+
# sync — block and may transform or veto
|
|
15
|
+
def on_get(self, event: GetEvent, value: T) -> T | None: ...
|
|
16
|
+
def on_set(self, event: SetEvent, value: T) -> T | None: ...
|
|
17
|
+
def on_append(self, event: AppendEvent, item: Any) -> Any | None: ...
|
|
18
|
+
|
|
19
|
+
# async — runs before each LLM inference, result written back
|
|
20
|
+
async def on_compile(self, event: CompileEvent, items: list) -> list | None: ...
|
|
21
|
+
|
|
22
|
+
# async — fire-and-forget side effects after the operation
|
|
23
|
+
async def background_get(self, event: GetEvent, value: T) -> None: ...
|
|
24
|
+
async def background_set(self, event: SetEvent, value: T) -> None: ...
|
|
25
|
+
async def background_append(self, event: AppendEvent, item: Any) -> None: ...
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Three return conventions, everywhere:
|
|
29
|
+
|
|
30
|
+
| Handler returns | Meaning |
|
|
31
|
+
|---|---|
|
|
32
|
+
| a value | Replace the value/item/list with it |
|
|
33
|
+
| `None` | No change |
|
|
34
|
+
| *raises* | Veto — the write is aborted / the appended item is skipped |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## State policy examples
|
|
39
|
+
|
|
40
|
+
### Validation (veto by raising)
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
class RangePolicy(Policy[int]):
|
|
44
|
+
"""Keep an int field within [min_val, max_val]."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, min_val: int, max_val: int):
|
|
47
|
+
self.min_val, self.max_val = min_val, max_val
|
|
48
|
+
|
|
49
|
+
def on_set(self, event: SetEvent, value: int) -> int | None:
|
|
50
|
+
if not (self.min_val <= value <= self.max_val):
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"{event.name} must be between {self.min_val} and {self.max_val}"
|
|
53
|
+
)
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class GameAgent(BaseAgent):
|
|
58
|
+
__system_message__ = "You run a text adventure."
|
|
59
|
+
|
|
60
|
+
score: State[int] = spec.State(
|
|
61
|
+
default=0,
|
|
62
|
+
access="write", # autogenerates a set_score tool
|
|
63
|
+
policies=[RangePolicy(0, 100)],
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
When the LLM calls `set_score(150)`, the policy raises, the write is aborted,
|
|
68
|
+
and the tool error propagates back to the LLM — which can retry with a valid
|
|
69
|
+
value:
|
|
70
|
+
|
|
71
|
+
```mermaid
|
|
72
|
+
sequenceDiagram
|
|
73
|
+
participant LLM
|
|
74
|
+
participant State as AgentState
|
|
75
|
+
participant Policy as RangePolicy
|
|
76
|
+
|
|
77
|
+
LLM->>State: set score = 150
|
|
78
|
+
State->>Policy: on_set(event, 150)
|
|
79
|
+
Policy--xState: raise ValueError("score must be between 0 and 100")
|
|
80
|
+
State--xLLM: Tool error (state unchanged)
|
|
81
|
+
|
|
82
|
+
LLM->>State: set score = 100
|
|
83
|
+
State->>Policy: on_set(event, 100)
|
|
84
|
+
Policy-->>State: None (keep)
|
|
85
|
+
State-->>LLM: ✓ "Score updated to 100"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Transformation (return a replacement)
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
class NormalizeTagPolicy(Policy[str]):
|
|
92
|
+
"""Store tags in a canonical form."""
|
|
93
|
+
|
|
94
|
+
def on_set(self, event: SetEvent, value: str) -> str:
|
|
95
|
+
return value.strip().lower().replace(" ", "-")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Background persistence (side effects, non-blocking)
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import json, asyncio
|
|
102
|
+
from pathlib import Path
|
|
103
|
+
|
|
104
|
+
class JSONAuditPolicy(Policy):
|
|
105
|
+
"""Append every change to an audit file, off the hot path."""
|
|
106
|
+
|
|
107
|
+
def __init__(self, filepath: str):
|
|
108
|
+
self.filepath = Path(filepath)
|
|
109
|
+
|
|
110
|
+
async def background_set(self, event: SetEvent, value) -> None:
|
|
111
|
+
record = {
|
|
112
|
+
"ts": event.timestamp.isoformat(),
|
|
113
|
+
"field": event.name,
|
|
114
|
+
"old": str(event.previous),
|
|
115
|
+
"new": str(value),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
def write():
|
|
119
|
+
existing = (
|
|
120
|
+
json.loads(self.filepath.read_text()) if self.filepath.exists() else []
|
|
121
|
+
)
|
|
122
|
+
existing.append(record)
|
|
123
|
+
self.filepath.write_text(json.dumps(existing, indent=2))
|
|
124
|
+
|
|
125
|
+
await asyncio.to_thread(write)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Background handlers run after the value is committed — they can't veto or
|
|
129
|
+
change it, and failures are logged rather than raised.
|
|
130
|
+
|
|
131
|
+
### List fields (mutation tracking)
|
|
132
|
+
|
|
133
|
+
Policies on list-typed fields fire on **in-place mutations** — appends run
|
|
134
|
+
`on_append` per item, other mutations run `on_set` over the whole list:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
class NoEmptyStrings(Policy):
|
|
138
|
+
def on_append(self, event: AppendEvent, item) -> None:
|
|
139
|
+
if isinstance(item, str) and not item.strip():
|
|
140
|
+
raise ValueError("empty entries not allowed")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class NotesAgent(BaseAgent):
|
|
144
|
+
__system_message__ = "You keep notes."
|
|
145
|
+
|
|
146
|
+
notes: State[list] = spec.State(default_factory=list, policies=[NoEmptyStrings()])
|
|
147
|
+
|
|
148
|
+
# agent.notes.append("real note") -> stored
|
|
149
|
+
# agent.notes.append(" ") -> vetoed, list unchanged
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Message policy examples
|
|
155
|
+
|
|
156
|
+
Message policies attach via `__message_policies__` and receive the semantic
|
|
157
|
+
message types from `pyagentic.models.llm` — filter with `isinstance`.
|
|
158
|
+
|
|
159
|
+
### Redaction (transform on append)
|
|
160
|
+
|
|
161
|
+
Scrub secrets from tool output *before* it ever enters the context. The raw
|
|
162
|
+
history still holds the original for auditing.
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
import re
|
|
166
|
+
from pyagentic.models.llm import ToolResultMessage
|
|
167
|
+
|
|
168
|
+
class RedactSecretsPolicy(Policy):
|
|
169
|
+
"""Mask API keys / bearer tokens in tool results."""
|
|
170
|
+
|
|
171
|
+
PATTERN = re.compile(r"(sk-[A-Za-z0-9]{8,}|Bearer\s+\S+)")
|
|
172
|
+
|
|
173
|
+
def on_append(self, event: AppendEvent, item):
|
|
174
|
+
if isinstance(item, ToolResultMessage) and item.content:
|
|
175
|
+
redacted = self.PATTERN.sub("[REDACTED]", item.content)
|
|
176
|
+
if redacted != item.content:
|
|
177
|
+
return item.model_copy(update={"content": redacted})
|
|
178
|
+
return None
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Linked-agent response budget (targeted transform)
|
|
182
|
+
|
|
183
|
+
`AgentResultMessage` subclasses `ToolResultMessage`, so you can target linked
|
|
184
|
+
agents specifically and give them their own context budget:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from pyagentic.models.llm import AgentResultMessage
|
|
188
|
+
|
|
189
|
+
class AgentResultBudgetPolicy(Policy):
|
|
190
|
+
"""Keep any single sub-agent response under a size budget."""
|
|
191
|
+
|
|
192
|
+
def __init__(self, max_chars: int = 2000):
|
|
193
|
+
self.max_chars = max_chars
|
|
194
|
+
|
|
195
|
+
def on_append(self, event: AppendEvent, item):
|
|
196
|
+
if isinstance(item, AgentResultMessage) and len(item.content or "") > self.max_chars:
|
|
197
|
+
return item.model_copy(
|
|
198
|
+
update={"content": item.content[: self.max_chars] + "…[truncated]"}
|
|
199
|
+
)
|
|
200
|
+
return None
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Context metrics (observe on compile, change nothing)
|
|
204
|
+
|
|
205
|
+
`on_compile` sees the whole context plus the last inference's token usage —
|
|
206
|
+
return `None` and it's a pure observer:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
import logging
|
|
210
|
+
from pyagentic.models.llm import ToolResultMessage
|
|
211
|
+
|
|
212
|
+
logger = logging.getLogger(__name__)
|
|
213
|
+
|
|
214
|
+
class ContextMetricsPolicy(Policy):
|
|
215
|
+
"""Log context size before every inference; warn near a budget."""
|
|
216
|
+
|
|
217
|
+
def __init__(self, warn_at_tokens: int = 80_000):
|
|
218
|
+
self.warn_at_tokens = warn_at_tokens
|
|
219
|
+
|
|
220
|
+
async def on_compile(self, event: CompileEvent, items: list) -> None:
|
|
221
|
+
tool_chars = sum(
|
|
222
|
+
len(m.content or "") for m in items if isinstance(m, ToolResultMessage)
|
|
223
|
+
)
|
|
224
|
+
used = event.last_usage.input_tokens if event.last_usage else 0
|
|
225
|
+
logger.info(
|
|
226
|
+
f"context: {len(items)} messages, {tool_chars} tool chars, "
|
|
227
|
+
f"{used} input tokens last call"
|
|
228
|
+
)
|
|
229
|
+
if used > self.warn_at_tokens:
|
|
230
|
+
logger.warning(f"context nearing budget: {used}/{self.warn_at_tokens}")
|
|
231
|
+
return None
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Custom compile transforms
|
|
235
|
+
|
|
236
|
+
`on_compile` may also rewrite the list — the result is written back, so the
|
|
237
|
+
effect persists across turns. Two rules to respect:
|
|
238
|
+
|
|
239
|
+
1. **Never orphan a tool call/result pair.** Providers reject a
|
|
240
|
+
`ToolResultMessage` whose `ToolCallMessage` is missing (and vice versa).
|
|
241
|
+
Stub content instead of deleting messages, or move your cut past the pair —
|
|
242
|
+
see how the [built-in policies](built-in.md) handle this.
|
|
243
|
+
2. **Be idempotent.** Your policy runs before *every* inference; a second pass
|
|
244
|
+
over already-processed context should return `None`.
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
from pyagentic.models.llm import UserMessage
|
|
248
|
+
|
|
249
|
+
class KeepFirstUserTurnPolicy(Policy):
|
|
250
|
+
"""Always retain the original task statement when trimming."""
|
|
251
|
+
|
|
252
|
+
def __init__(self, max_messages: int = 40):
|
|
253
|
+
self.max_messages = max_messages
|
|
254
|
+
|
|
255
|
+
async def on_compile(self, event: CompileEvent, items: list) -> list | None:
|
|
256
|
+
if len(items) <= self.max_messages:
|
|
257
|
+
return None
|
|
258
|
+
first_user = next((m for m in items if isinstance(m, UserMessage)), None)
|
|
259
|
+
tail = items[-(self.max_messages - 1):]
|
|
260
|
+
if first_user is None or first_user in tail:
|
|
261
|
+
return tail
|
|
262
|
+
return [first_user] + tail
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Rules of the road
|
|
268
|
+
|
|
269
|
+
### Policies must be stateless
|
|
270
|
+
|
|
271
|
+
Policy instances are attached at **class-definition time** and shared across
|
|
272
|
+
all agent instances and forks. Configuration in `__init__` is fine; per-agent
|
|
273
|
+
mutable state on the policy is a bug (two agents would share it).
|
|
274
|
+
|
|
275
|
+
Derive anything per-agent from what the event gives you:
|
|
276
|
+
|
|
277
|
+
- the list contents themselves (e.g. check for an existing stub or
|
|
278
|
+
`CompactionSummaryMessage` marker instead of remembering "already ran")
|
|
279
|
+
- `event.state` for state fields, `event.last_usage` for token counts
|
|
280
|
+
|
|
281
|
+
### Ordering
|
|
282
|
+
|
|
283
|
+
Policies run in declaration order, each seeing the previous one's output —
|
|
284
|
+
validate first, transform second, side effects last:
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
score: State[int] = spec.State(
|
|
288
|
+
default=0,
|
|
289
|
+
policies=[
|
|
290
|
+
RangePolicy(0, 100), # 1. validate
|
|
291
|
+
# transformation would go here
|
|
292
|
+
JSONAuditPolicy("audit.json"), # 2. persist (background)
|
|
293
|
+
],
|
|
294
|
+
)
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The first exception in a sync pipeline aborts the operation; later handlers do
|
|
298
|
+
not run.
|
|
299
|
+
|
|
300
|
+
### Keep sync handlers fast
|
|
301
|
+
|
|
302
|
+
`on_get`/`on_set`/`on_append` block the operation. Anything that does I/O
|
|
303
|
+
belongs in a `background_*` handler, or in `on_compile` (which is async and
|
|
304
|
+
runs once per inference rather than once per access).
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Testing your policies
|
|
309
|
+
|
|
310
|
+
Events are plain dataclasses and handlers are plain methods — unit-test them
|
|
311
|
+
directly, no agent required:
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
import pytest
|
|
315
|
+
from pyagentic.policies._events import AppendEvent
|
|
316
|
+
from pyagentic.models.llm import ToolResultMessage
|
|
317
|
+
|
|
318
|
+
def test_redaction_masks_keys():
|
|
319
|
+
policy = RedactSecretsPolicy()
|
|
320
|
+
msg = ToolResultMessage(tool_call_id="c1", name="fetch", content="key=sk-abcdef123456")
|
|
321
|
+
|
|
322
|
+
result = policy.on_append(AppendEvent(name="messages", value=msg), msg)
|
|
323
|
+
|
|
324
|
+
assert "[REDACTED]" in result.content
|
|
325
|
+
assert "sk-" not in result.content
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
For end-to-end behavior, run an agent against the mock provider
|
|
329
|
+
(`model="_mock::test-model"`) and assert on `agent.state._context` (what the
|
|
330
|
+
LLM saw) versus `agent.state._messages` (what actually happened):
|
|
331
|
+
|
|
332
|
+
```python
|
|
333
|
+
@pytest.mark.asyncio
|
|
334
|
+
async def test_budget_policy_in_agent():
|
|
335
|
+
class _Agent(BaseAgent):
|
|
336
|
+
__system_message__ = "test"
|
|
337
|
+
__message_policies__ = [AgentResultBudgetPolicy(max_chars=100)]
|
|
338
|
+
|
|
339
|
+
agent = _Agent(model="_mock::test-model", api_key="k")
|
|
340
|
+
await agent.run("hello")
|
|
341
|
+
...
|
|
342
|
+
```
|