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.
Files changed (150) hide show
  1. {pyagentic_core-2.13.0a1/pyagentic_core.egg-info → pyagentic_core-2.14.1.dev1}/PKG-INFO +1 -1
  2. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/index.md +3 -3
  3. pyagentic_core-2.14.1.dev1/docs/policies/built-in.md +211 -0
  4. pyagentic_core-2.14.1.dev1/docs/policies/custom.md +342 -0
  5. pyagentic_core-2.14.1.dev1/docs/policies/index.md +246 -0
  6. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/states.md +5 -5
  7. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/mkdocs.yml +4 -1
  8. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1/pyagentic_core.egg-info}/PKG-INFO +1 -1
  9. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic_core.egg-info/SOURCES.txt +3 -1
  10. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic_core.egg-info/scm_file_list.json +3 -1
  11. pyagentic_core-2.14.1.dev1/pyagentic_core.egg-info/scm_version.json +8 -0
  12. pyagentic_core-2.13.0a1/docs/policies.md +0 -601
  13. pyagentic_core-2.13.0a1/pyagentic_core.egg-info/scm_version.json +0 -8
  14. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  15. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  16. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/.github/workflows/docs.yml +0 -0
  17. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/.github/workflows/release.yml +0 -0
  18. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/.github/workflows/testing.yml +0 -0
  19. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/.gitignore +0 -0
  20. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/CHANGELOG.md +0 -0
  21. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/LICENSE +0 -0
  22. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/README.md +0 -0
  23. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/agent-linking.md +0 -0
  24. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/api/creating-an-app.md +0 -0
  25. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/api/deploying.md +0 -0
  26. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/api/index.md +0 -0
  27. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/api/jobs.md +0 -0
  28. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/api/running.md +0 -0
  29. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/diagrams/declaration.svg +0 -0
  30. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/diagrams/instantiation.svg +0 -0
  31. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/diagrams/runtime.svg +0 -0
  32. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/diagrams/source/README.md +0 -0
  33. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/diagrams/source/declaration.d2 +0 -0
  34. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/diagrams/source/instantiation.d2 +0 -0
  35. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/diagrams/source/runtime.d2 +0 -0
  36. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/execution-modes.md +0 -0
  37. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/getting-started.md +0 -0
  38. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/images/langfuse.png +0 -0
  39. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/inheritance.md +0 -0
  40. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/observability.md +0 -0
  41. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/phases.md +0 -0
  42. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/reference/architecture.md +0 -0
  43. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/reference/modules.md +0 -0
  44. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/responses.md +0 -0
  45. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/structured-output.md +0 -0
  46. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/docs/tools.md +0 -0
  47. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/__init__.py +0 -0
  48. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/__init__.py +0 -0
  49. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_agent/__init__.py +0 -0
  50. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_agent/_agent.py +0 -0
  51. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_agent/_agent_linking.py +0 -0
  52. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_agent/_agent_state.py +0 -0
  53. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_depends.py +0 -0
  54. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_exceptions.py +0 -0
  55. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_info.py +0 -0
  56. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_mcp.py +0 -0
  57. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_metaclasses.py +0 -0
  58. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_ref.py +0 -0
  59. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_spec.py +0 -0
  60. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_state.py +0 -0
  61. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_tool.py +0 -0
  62. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_base/_validation.py +0 -0
  63. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_utils/_typing.py +0 -0
  64. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_utils/_warnings.py +0 -0
  65. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/_version_scheme.py +0 -0
  66. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/__init__.py +0 -0
  67. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/_app.py +0 -0
  68. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/_build.py +0 -0
  69. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/_config.py +0 -0
  70. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/_docker.py +0 -0
  71. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/_mcp_server.py +0 -0
  72. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/_models.py +0 -0
  73. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/_sessions.py +0 -0
  74. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/jobs/__init__.py +0 -0
  75. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/jobs/_models.py +0 -0
  76. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/jobs/_orchestrator.py +0 -0
  77. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/jobs/_routes.py +0 -0
  78. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/jobs/backends/__init__.py +0 -0
  79. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/jobs/backends/_base.py +0 -0
  80. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/jobs/backends/_in_process.py +0 -0
  81. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/jobs/store/__init__.py +0 -0
  82. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/jobs/store/_base.py +0 -0
  83. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/api/jobs/store/_sqlite.py +0 -0
  84. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/llm/__init__.py +0 -0
  85. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/llm/_anthropic.py +0 -0
  86. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/llm/_gemini.py +0 -0
  87. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/llm/_mock.py +0 -0
  88. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/llm/_openai.py +0 -0
  89. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/llm/_provider.py +0 -0
  90. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/logging.py +0 -0
  91. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/models/llm.py +0 -0
  92. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/models/response.py +0 -0
  93. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/models/tracing.py +0 -0
  94. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/policies/__init__.py +0 -0
  95. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/policies/_events.py +0 -0
  96. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/policies/_list.py +0 -0
  97. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/policies/_policy.py +0 -0
  98. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/policies/messages.py +0 -0
  99. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/tracing/__init__.py +0 -0
  100. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/tracing/_basic.py +0 -0
  101. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/tracing/_langfuse.py +0 -0
  102. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/tracing/_tracer.py +0 -0
  103. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic/updates.py +0 -0
  104. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic_core.egg-info/dependency_links.txt +0 -0
  105. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic_core.egg-info/requires.txt +0 -0
  106. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyagentic_core.egg-info/top_level.txt +0 -0
  107. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/pyproject.toml +0 -0
  108. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/setup.cfg +0 -0
  109. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/setup.py +0 -0
  110. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/__init__.py +0 -0
  111. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/__init__.py +0 -0
  112. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_agent.py +0 -0
  113. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_agent_forking.py +0 -0
  114. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_agent_inheritance.py +0 -0
  115. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_agent_linking.py +0 -0
  116. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_agent_message_policies.py +0 -0
  117. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_agent_provider.py +0 -0
  118. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_params.py +0 -0
  119. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_phases.py +0 -0
  120. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_policy_list.py +0 -0
  121. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_state.py +0 -0
  122. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_tool.py +0 -0
  123. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/_base/test_validator.py +0 -0
  124. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/__init__.py +0 -0
  125. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/jobs/__init__.py +0 -0
  126. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/jobs/test_orchestrator.py +0 -0
  127. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/jobs/test_routes.py +0 -0
  128. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/jobs/test_store_sqlite.py +0 -0
  129. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/test_app.py +0 -0
  130. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/test_build_agent.py +0 -0
  131. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/test_config.py +0 -0
  132. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/test_construct_model.py +0 -0
  133. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/test_dependencies_integration.py +0 -0
  134. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/test_docker.py +0 -0
  135. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/test_mcp_server.py +0 -0
  136. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/test_metaclass_models.py +0 -0
  137. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/test_router.py +0 -0
  138. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/api/test_sessions.py +0 -0
  139. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/conftest.py +0 -0
  140. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/llm/__init__.py +0 -0
  141. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/llm/test_message_conversion.py +0 -0
  142. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/models/test_messages.py +0 -0
  143. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/models/test_responses.py +0 -0
  144. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/policies/__init__.py +0 -0
  145. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/policies/test_message_policies.py +0 -0
  146. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/tracing/__init__.py +0 -0
  147. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/tracing/test_basic_tracer.py +0 -0
  148. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/tracing/test_models.py +0 -0
  149. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/tests/tracing/test_tracer.py +0 -0
  150. {pyagentic_core-2.13.0a1 → pyagentic_core-2.14.1.dev1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyagentic-core
3
- Version: 2.13.0a1
3
+ Version: 2.14.1.dev1
4
4
  Summary: Build LLM Agents in a Pythonic way
5
5
  Author-email: Ryan Mikulec <rmikulec.dev@gmail.com>
6
6
  License: MIT
@@ -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, history tracking, persistence, and custom behaviors.
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
+ ```