pyagentic-core 2.12.3.dev1__tar.gz → 2.13.0a1__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.12.3.dev1/pyagentic_core.egg-info → pyagentic_core-2.13.0a1}/PKG-INFO +1 -1
  2. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/policies.md +170 -3
  3. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_agent/_agent.py +40 -16
  4. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_agent/_agent_state.py +154 -14
  5. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_metaclasses.py +12 -1
  6. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/llm/__init__.py +0 -2
  7. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/llm/_anthropic.py +50 -64
  8. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/llm/_gemini.py +40 -71
  9. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/llm/_mock.py +21 -49
  10. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/llm/_openai.py +37 -41
  11. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/llm/_provider.py +1 -28
  12. pyagentic_core-2.13.0a1/pyagentic/models/llm.py +196 -0
  13. pyagentic_core-2.13.0a1/pyagentic/policies/__init__.py +19 -0
  14. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/policies/_events.py +33 -0
  15. pyagentic_core-2.13.0a1/pyagentic/policies/_list.py +173 -0
  16. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/policies/_policy.py +25 -6
  17. pyagentic_core-2.13.0a1/pyagentic/policies/messages.py +212 -0
  18. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1/pyagentic_core.egg-info}/PKG-INFO +1 -1
  19. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic_core.egg-info/SOURCES.txt +10 -1
  20. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic_core.egg-info/scm_file_list.json +10 -1
  21. pyagentic_core-2.13.0a1/pyagentic_core.egg-info/scm_version.json +8 -0
  22. pyagentic_core-2.13.0a1/tests/_base/test_agent_forking.py +139 -0
  23. pyagentic_core-2.13.0a1/tests/_base/test_agent_message_policies.py +187 -0
  24. pyagentic_core-2.13.0a1/tests/_base/test_policy_list.py +130 -0
  25. pyagentic_core-2.13.0a1/tests/llm/test_message_conversion.py +114 -0
  26. pyagentic_core-2.13.0a1/tests/models/test_messages.py +100 -0
  27. pyagentic_core-2.13.0a1/tests/policies/__init__.py +0 -0
  28. pyagentic_core-2.13.0a1/tests/policies/test_message_policies.py +227 -0
  29. pyagentic_core-2.13.0a1/tests/tracing/__init__.py +0 -0
  30. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/uv.lock +14 -14
  31. pyagentic_core-2.12.3.dev1/pyagentic/llm/_openaiv1.py +0 -221
  32. pyagentic_core-2.12.3.dev1/pyagentic/models/llm.py +0 -92
  33. pyagentic_core-2.12.3.dev1/pyagentic/policies/__init__.py +0 -3
  34. pyagentic_core-2.12.3.dev1/pyagentic_core.egg-info/scm_version.json +0 -8
  35. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  36. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  37. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/.github/workflows/docs.yml +0 -0
  38. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/.github/workflows/release.yml +0 -0
  39. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/.github/workflows/testing.yml +0 -0
  40. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/.gitignore +0 -0
  41. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/CHANGELOG.md +0 -0
  42. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/LICENSE +0 -0
  43. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/README.md +0 -0
  44. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/agent-linking.md +0 -0
  45. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/api/creating-an-app.md +0 -0
  46. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/api/deploying.md +0 -0
  47. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/api/index.md +0 -0
  48. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/api/jobs.md +0 -0
  49. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/api/running.md +0 -0
  50. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/diagrams/declaration.svg +0 -0
  51. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/diagrams/instantiation.svg +0 -0
  52. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/diagrams/runtime.svg +0 -0
  53. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/diagrams/source/README.md +0 -0
  54. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/diagrams/source/declaration.d2 +0 -0
  55. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/diagrams/source/instantiation.d2 +0 -0
  56. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/diagrams/source/runtime.d2 +0 -0
  57. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/execution-modes.md +0 -0
  58. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/getting-started.md +0 -0
  59. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/images/langfuse.png +0 -0
  60. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/index.md +0 -0
  61. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/inheritance.md +0 -0
  62. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/observability.md +0 -0
  63. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/phases.md +0 -0
  64. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/reference/architecture.md +0 -0
  65. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/reference/modules.md +0 -0
  66. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/responses.md +0 -0
  67. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/states.md +0 -0
  68. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/structured-output.md +0 -0
  69. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/tools.md +0 -0
  70. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/mkdocs.yml +0 -0
  71. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/__init__.py +0 -0
  72. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/__init__.py +0 -0
  73. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_agent/__init__.py +0 -0
  74. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_agent/_agent_linking.py +0 -0
  75. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_depends.py +0 -0
  76. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_exceptions.py +0 -0
  77. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_info.py +0 -0
  78. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_mcp.py +0 -0
  79. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_ref.py +0 -0
  80. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_spec.py +0 -0
  81. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_state.py +0 -0
  82. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_tool.py +0 -0
  83. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_validation.py +0 -0
  84. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_utils/_typing.py +0 -0
  85. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_utils/_warnings.py +0 -0
  86. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_version_scheme.py +0 -0
  87. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/__init__.py +0 -0
  88. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/_app.py +0 -0
  89. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/_build.py +0 -0
  90. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/_config.py +0 -0
  91. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/_docker.py +0 -0
  92. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/_mcp_server.py +0 -0
  93. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/_models.py +0 -0
  94. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/_sessions.py +0 -0
  95. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/jobs/__init__.py +0 -0
  96. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/jobs/_models.py +0 -0
  97. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/jobs/_orchestrator.py +0 -0
  98. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/jobs/_routes.py +0 -0
  99. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/jobs/backends/__init__.py +0 -0
  100. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/jobs/backends/_base.py +0 -0
  101. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/jobs/backends/_in_process.py +0 -0
  102. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/jobs/store/__init__.py +0 -0
  103. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/jobs/store/_base.py +0 -0
  104. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/jobs/store/_sqlite.py +0 -0
  105. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/logging.py +0 -0
  106. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/models/response.py +0 -0
  107. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/models/tracing.py +0 -0
  108. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/tracing/__init__.py +0 -0
  109. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/tracing/_basic.py +0 -0
  110. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/tracing/_langfuse.py +0 -0
  111. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/tracing/_tracer.py +0 -0
  112. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/updates.py +0 -0
  113. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic_core.egg-info/dependency_links.txt +0 -0
  114. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic_core.egg-info/requires.txt +0 -0
  115. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic_core.egg-info/top_level.txt +0 -0
  116. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyproject.toml +0 -0
  117. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/setup.cfg +0 -0
  118. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/setup.py +0 -0
  119. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/__init__.py +0 -0
  120. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/_base/__init__.py +0 -0
  121. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/_base/test_agent.py +0 -0
  122. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/_base/test_agent_inheritance.py +0 -0
  123. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/_base/test_agent_linking.py +0 -0
  124. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/_base/test_agent_provider.py +0 -0
  125. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/_base/test_params.py +0 -0
  126. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/_base/test_phases.py +0 -0
  127. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/_base/test_state.py +0 -0
  128. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/_base/test_tool.py +0 -0
  129. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/_base/test_validator.py +0 -0
  130. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/__init__.py +0 -0
  131. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/jobs/__init__.py +0 -0
  132. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/jobs/test_orchestrator.py +0 -0
  133. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/jobs/test_routes.py +0 -0
  134. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/jobs/test_store_sqlite.py +0 -0
  135. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/test_app.py +0 -0
  136. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/test_build_agent.py +0 -0
  137. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/test_config.py +0 -0
  138. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/test_construct_model.py +0 -0
  139. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/test_dependencies_integration.py +0 -0
  140. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/test_docker.py +0 -0
  141. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/test_mcp_server.py +0 -0
  142. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/test_metaclass_models.py +0 -0
  143. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/test_router.py +0 -0
  144. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/test_sessions.py +0 -0
  145. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/conftest.py +0 -0
  146. {pyagentic_core-2.12.3.dev1/tests/tracing → pyagentic_core-2.13.0a1/tests/llm}/__init__.py +0 -0
  147. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/models/test_responses.py +0 -0
  148. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/tracing/test_basic_tracer.py +0 -0
  149. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/tracing/test_models.py +0 -0
  150. {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/tracing/test_tracer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyagentic-core
3
- Version: 2.12.3.dev1
3
+ Version: 2.13.0a1
4
4
  Summary: Build LLM Agents in a Pythonic way
5
5
  Author-email: Ryan Mikulec <rmikulec.dev@gmail.com>
6
6
  License: MIT
@@ -154,21 +154,58 @@ class SetEvent:
154
154
  timestamp: datetime # When the change occurred
155
155
  ```
156
156
 
157
+ **`AppendEvent`** — triggered when an item is appended to a list-valued state field or the message context
158
+
159
+ ```python
160
+ @dataclass
161
+ class AppendEvent:
162
+ name: str # Field name (or "messages" for the message context)
163
+ value: Any # The item being appended
164
+ timestamp: datetime # When the append occurred
165
+ ```
166
+
167
+ **`CompileEvent`** — triggered right before each LLM inference, over a whole list
168
+
169
+ ```python
170
+ @dataclass
171
+ class CompileEvent:
172
+ name: str # Field name (or "messages")
173
+ value: list # The list being compiled
174
+ provider: LLMProvider # The provider about to be called (e.g. for summarization)
175
+ last_usage: UsageInfo | None # Token usage from the previous inference
176
+ system_message: str | None # Rendered system prompt (read-only)
177
+ state: _AgentState # The owning state
178
+ timestamp: datetime
179
+ ```
180
+
157
181
  ### Handler Semantics
158
182
 
159
- * **Synchronous (`on_get`, `on_set`)**
183
+ * **Synchronous (`on_get`, `on_set`, `on_append`)**
160
184
 
161
185
  * Run **before** the value is returned/stored and **block** the operation.
162
186
  * May **transform** the value by returning a replacement.
163
- * May **validate** by raising an exception, which aborts the operation.
187
+ * May **validate** by raising an exception, which aborts the operation
188
+ (for `on_append`, a raise skips inserting the item).
189
+ * `on_set` also fires when a policied list field is mutated in place
190
+ (item assignment, deletion, `remove`, `pop`, `clear`).
164
191
  * **Ordering:** policies run in the order they’re declared; the first exception aborts, and later handlers do not run.
165
192
 
166
- * **Background (`background_get`, `background_set`)**
193
+ * **Async compile (`on_compile`)**
194
+
195
+ * Runs right **before every LLM inference**, receiving the whole list.
196
+ * May return a transformed list; the result is **written back**, so effects
197
+ like eviction and compaction persist instead of re-running every turn.
198
+ * Async by design — a compile policy can call the LLM itself
199
+ (see `CompactionPolicy`).
200
+
201
+ * **Background (`background_get`, `background_set`, `background_append`)**
167
202
 
168
203
  * Run **after** sync handlers complete and **must not** mutate stored state.
169
204
  * Intended for **side effects only**: logging, metrics, notifications, persistence.
170
205
  * Failures should be handled internally (e.g., retries/backoff); they cannot prevent an already-completed operation.
171
206
 
207
+ Implement only the handlers you need — missing handlers are skipped.
208
+
172
209
  ### Execution Flow
173
210
 
174
211
  ```mermaid
@@ -427,6 +464,136 @@ class GameAgent(BaseAgent):
427
464
 
428
465
  ---
429
466
 
467
+ ## Message Policies & Context Management
468
+
469
+ The same `Policy` protocol also manages the agent's **message context** — the
470
+ conversation history sent to the LLM. This is how you keep context small and
471
+ prevent context explosion on long-running agents.
472
+
473
+ ### Attaching message policies
474
+
475
+ Declare `__message_policies__` on the agent class:
476
+
477
+ ```python
478
+ from pyagentic.policies import (
479
+ ToolOutputClipPolicy,
480
+ ToolEvictionPolicy,
481
+ CompactionPolicy,
482
+ )
483
+
484
+ class ResearchAgent(BaseAgent):
485
+ __system_message__ = "You research topics using tools."
486
+ __message_policies__ = [
487
+ ToolOutputClipPolicy(max_chars=8000), # clip huge tool outputs on entry
488
+ ToolEvictionPolicy(keep_last_n=5), # stub stale tool results
489
+ CompactionPolicy(max_input_tokens=80_000), # summarize when context grows
490
+ ]
491
+ ```
492
+
493
+ `__message_policies__` is inherited by subclasses (and composable via
494
+ `AgentExtension` mixins). Policies fire:
495
+
496
+ * `on_append` / `background_append` — for each message entering the context
497
+ (user turns, assistant replies, tool calls/results)
498
+ * `on_compile` — over the whole context, right before every LLM inference
499
+
500
+ ### Dual history: raw log vs. working context
501
+
502
+ The state keeps **two** message lists:
503
+
504
+ * `state._messages` — the **raw log**: every message, append-only, never touched
505
+ by policies. Use `state.raw_messages` (system message included) for
506
+ debugging and auditing.
507
+ * `state._context` — the **working context** providers actually send. Appends run
508
+ through `on_append` (clipping, vetoes), and `on_compile` rewrites it in place
509
+ (eviction stubs, compaction summaries). Use `state.messages` for the compiled
510
+ view with the system message prepended.
511
+
512
+ Because compile results are written back, `CompactionPolicy` summarizes once per
513
+ threshold crossing — not on every turn — and the context keeps stable prefixes
514
+ for provider prompt caching.
515
+
516
+ ### Filtering by message type
517
+
518
+ Context messages are semantic types from `pyagentic.models.llm`, so policies
519
+ filter with `isinstance`:
520
+
521
+ | Type | Meaning |
522
+ |---|---|
523
+ | `UserMessage` / `AssistantMessage` | Conversation turns |
524
+ | `ToolCallMessage` | LLM requested a tool (`id`, `name`, `arguments`) |
525
+ | `ToolResultMessage` | Tool output (`tool_call_id`, `name`, content) |
526
+ | `AgentCallMessage` / `AgentResultMessage` | Linked-agent calls — subclasses of the tool types, so generic tool policies match them too |
527
+ | `CompactionSummaryMessage` | Summary emitted by `CompactionPolicy` |
528
+
529
+ ```python
530
+ class AgentResultBudgetPolicy(Policy):
531
+ """Clip linked-agent responses only (a per-link context budget)."""
532
+
533
+ def on_append(self, event, item):
534
+ if isinstance(item, AgentResultMessage) and len(item.content or "") > 2000:
535
+ return item.model_copy(update={"content": item.content[:2000] + "…"})
536
+ return None
537
+ ```
538
+
539
+ ### Prewritten policies
540
+
541
+ All in `pyagentic.policies`:
542
+
543
+ * **`ToolOutputClipPolicy(max_chars, suffix)`** — clips oversized tool results at
544
+ append time, so a huge output never occupies context.
545
+ * **`ToolEvictionPolicy(keep_last_n, stub, include_agent_results)`** — replaces
546
+ the content of all but the last N tool results with a stub. The message and
547
+ its `tool_call_id` survive (providers reject orphaned call/result pairs).
548
+ * **`SlidingWindowPolicy(max_messages)`** — bounds the context to the most
549
+ recent N messages, never splitting a tool call from its result.
550
+ * **`CompactionPolicy(max_input_tokens, keep_recent, summary_prompt)`** —
551
+ when the previous inference's input tokens cross the threshold, summarizes
552
+ older history into a single `CompactionSummaryMessage` via an LLM call.
553
+
554
+ An observability policy is just an `on_compile` that records metrics and
555
+ returns `None`:
556
+
557
+ ```python
558
+ class ContextMetricsPolicy(Policy):
559
+ async def on_compile(self, event, items):
560
+ tool_chars = sum(
561
+ len(m.content or "") for m in items if isinstance(m, ToolResultMessage)
562
+ )
563
+ logger.info(f"context: {len(items)} messages, {tool_chars} tool chars")
564
+ return None
565
+ ```
566
+
567
+ ### List-valued state fields
568
+
569
+ Policies on list-typed state fields now fire on **in-place mutations**, not just
570
+ assignment. Appends run `on_append`; other mutations (`[i] = x`, `remove`,
571
+ `pop`, `clear`) run `on_set` over the whole list:
572
+
573
+ ```python
574
+ class NotesAgent(BaseAgent):
575
+ __system_message__ = "You keep notes."
576
+
577
+ notes: State[list] = spec.State(
578
+ default_factory=list,
579
+ policies=[TrimAndLowerAppendPolicy()],
580
+ )
581
+
582
+ # agent.notes.append(" HELLO ") -> stored as "hello", policy enforced
583
+ ```
584
+
585
+ `on_compile` also runs for policied list fields before each inference, so a
586
+ policy can e.g. cap a scratchpad list the same way it caps messages.
587
+
588
+ ### Statelessness rule
589
+
590
+ Policy instances are attached at class-definition time and **shared across all
591
+ agent instances and forks**. Keep them config-only: derive anything per-agent
592
+ from the event or the list contents (e.g. `CompactionSummaryMessage` markers),
593
+ never from attributes mutated on the policy itself.
594
+
595
+ ---
596
+
430
597
  ## Next Steps
431
598
 
432
599
  * Learn about **State Management** to see how policies integrate with state lifecycles.
@@ -31,7 +31,15 @@ if TYPE_CHECKING:
31
31
  from pyagentic._base._mcp import _MCPDefinition
32
32
 
33
33
  from pyagentic.models.response import ToolResponse, AgentResponse, ErrorResponse
34
- from pyagentic.models.llm import Message, ToolCall, LLMResponse
34
+ from pyagentic.models.llm import (
35
+ AgentCallMessage,
36
+ AgentResultMessage,
37
+ AssistantMessage,
38
+ LLMResponse,
39
+ ToolCall,
40
+ ToolCallMessage,
41
+ ToolResultMessage,
42
+ )
35
43
  from pyagentic.models.tracing import SpanKind
36
44
 
37
45
  from pyagentic.updates import AiUpdate, Status, EmitUpdate, ToolUpdate
@@ -171,6 +179,7 @@ class BaseAgent(metaclass=AgentMeta):
171
179
  __description__: ClassVar[str] # Optional: description for linked agents
172
180
  __input_template__: ClassVar[str] = None # Optional: template for user input
173
181
  __response_format__: ClassVar[Type[BaseModel]] = None # Optional: structured output format
182
+ __message_policies__: ClassVar[list] = None # Optional: policies on the message context
174
183
  phases: ClassVar[list[tuple[str, str, Callable]]] = None
175
184
 
176
185
  # Generated Class Attributes (built by metaclass)
@@ -469,12 +478,17 @@ class BaseAgent(metaclass=AgentMeta):
469
478
  )
470
479
 
471
480
  try:
481
+ # Run attached policies (eviction, compaction, ...) over the message
482
+ # context and policied list state fields before the provider reads them
483
+ await self.state.compile_context(self.provider)
484
+
472
485
  response = await self.provider.generate(
473
486
  state=self.state,
474
487
  tool_defs=tool_defs,
475
488
  response_format=self.__response_format__,
476
489
  **kwargs,
477
490
  )
491
+ self.state._last_usage = response.usage
478
492
  usage_details = response.usage.model_dump() if response.usage else {}
479
493
  self.tracer.set_attributes(
480
494
  usage_details=usage_details, model=self.provider._model
@@ -484,8 +498,8 @@ class BaseAgent(metaclass=AgentMeta):
484
498
  # Handle inference errors gracefully
485
499
  logger.exception(e)
486
500
  # Add error message to conversation history
487
- self.state._messages.append(
488
- Message(role="assistant", content="Failed to generate a response")
501
+ self.state.add_message(
502
+ AssistantMessage(content="Failed to generate a response")
489
503
  )
490
504
  response = LLMResponse(
491
505
  text=f"The LLM failed to generate a response: {e}", tool_calls=[]
@@ -512,8 +526,10 @@ class BaseAgent(metaclass=AgentMeta):
512
526
  )
513
527
  logger.info(f"Calling {tool_call.name} with kwargs: {tool_call.arguments}")
514
528
 
515
- # Add tool call message to conversation history
516
- self.state._messages.append(self.provider.to_tool_call_message(tool_call))
529
+ # Add agent call message to conversation history
530
+ self.state.add_message(
531
+ AgentCallMessage(id=tool_call.id, name=tool_call.name, arguments=tool_call.arguments)
532
+ )
517
533
 
518
534
  # By default each call runs on a fresh, isolated fork of the linked agent
519
535
  # so concurrent/repeated calls neither race nor pollute one another. A
@@ -547,8 +563,10 @@ class BaseAgent(metaclass=AgentMeta):
547
563
  if issubclass(result.__class__, BaseModel)
548
564
  else str(result)
549
565
  )
550
- self.state._messages.append(
551
- self.provider.to_tool_call_result_message(result=stringified_result, id_=tool_call.id)
566
+ self.state.add_message(
567
+ AgentResultMessage(
568
+ tool_call_id=tool_call.id, name=tool_call.name, content=stringified_result
569
+ )
552
570
  )
553
571
  if self.phases:
554
572
  self.state._update_state_machine(phases=self.phases)
@@ -575,7 +593,9 @@ class BaseAgent(metaclass=AgentMeta):
575
593
  return await self._process_mcp_tool_call(tool_call, call_depth)
576
594
 
577
595
  # Add tool call message to conversation history
578
- self.state._messages.append(self.provider.to_tool_call_message(tool_call))
596
+ self.state.add_message(
597
+ ToolCallMessage(id=tool_call.id, name=tool_call.name, arguments=tool_call.arguments)
598
+ )
579
599
 
580
600
  # Look up the tool definition and bound method
581
601
  try:
@@ -622,8 +642,8 @@ class BaseAgent(metaclass=AgentMeta):
622
642
  if issubclass(result.__class__, BaseModel)
623
643
  else str(result)
624
644
  )
625
- self.state._messages.append(
626
- self.provider.to_tool_call_result_message(result=message, id_=tool_call.id)
645
+ self.state.add_message(
646
+ ToolResultMessage(tool_call_id=tool_call.id, name=tool_call.name, content=message)
627
647
  )
628
648
 
629
649
  if self.phases:
@@ -652,8 +672,10 @@ class BaseAgent(metaclass=AgentMeta):
652
672
  Returns:
653
673
  ToolResponse: The response from the MCP tool execution.
654
674
  """
655
- # Add tool call to conversation history (provider-specific format)
656
- self.state._messages.append(self.provider.to_tool_call_message(tool_call))
675
+ # Add tool call to conversation history
676
+ self.state.add_message(
677
+ ToolCallMessage(id=tool_call.id, name=tool_call.name, arguments=tool_call.arguments)
678
+ )
657
679
 
658
680
  client, original_name = self._mcp_tool_routing[tool_call.name]
659
681
  kwargs = json.loads(tool_call.arguments)
@@ -676,9 +698,11 @@ class BaseAgent(metaclass=AgentMeta):
676
698
  error = f"MCP tool `{tool_call.name}` failed: {e}. Please kindly state to the user that it failed, provide state, and ask if they want to try again." # noqa E501
677
699
 
678
700
  # Add result (or error) to conversation history
679
- self.state._messages.append(
680
- self.provider.to_tool_call_result_message(
681
- result=error if error is not None else result, id_=tool_call.id
701
+ self.state.add_message(
702
+ ToolResultMessage(
703
+ tool_call_id=tool_call.id,
704
+ name=tool_call.name,
705
+ content=error if error is not None else result,
682
706
  )
683
707
  )
684
708
 
@@ -807,7 +831,7 @@ class BaseAgent(metaclass=AgentMeta):
807
831
  # If the model produced final text without tool calls, we're done
808
832
  if not response.tool_calls:
809
833
  final_ai_output = response.parsed if response.parsed else response.text
810
- self.state._messages.append(Message(role="assistant", content=response.text))
834
+ self.state.add_message(AssistantMessage(content=response.text))
811
835
  break
812
836
 
813
837
  tasks = []
@@ -9,9 +9,10 @@ from transitions import Machine
9
9
  from pyagentic._base._exceptions import InvalidStateRefNotFoundInState
10
10
  from pyagentic._base._state import _StateDefinition
11
11
  from pyagentic.policies._policy import Policy
12
- from pyagentic.policies._events import Event, EventKind, GetEvent, SetEvent
12
+ from pyagentic.policies._events import Event, EventKind, GetEvent, SetEvent, CompileEvent
13
+ from pyagentic.policies._list import PolicyList
13
14
 
14
- from pyagentic.models.llm import Message
15
+ from pyagentic.models.llm import Message, SystemMessage, UserMessage, UsageInfo
15
16
 
16
17
 
17
18
  class _AgentState(BaseModel):
@@ -25,13 +26,18 @@ class _AgentState(BaseModel):
25
26
  ("background", EventKind.GET): "background_get",
26
27
  ("on", EventKind.SET): "on_set",
27
28
  ("background", EventKind.SET): "background_set",
29
+ ("on", EventKind.APPEND): "on_append",
30
+ ("background", EventKind.APPEND): "background_append",
28
31
  }
29
- __policies__: ClassVar[dict[str, list[Policy]]]
32
+ __policies__: ClassVar[dict[str, list[Policy]]] = {}
30
33
 
31
34
  instructions: str
32
35
  input_template: Optional[str] = "{{ user_message }}"
36
+
33
37
  _machine: Machine = PrivateAttr(default=None)
34
38
  _messages: list[Message] = PrivateAttr(default_factory=list)
39
+ _context: list[Message] = PrivateAttr(default_factory=list)
40
+ _last_usage: Optional[UsageInfo] = PrivateAttr(default=None)
35
41
  _instructions_template: Template = PrivateAttr(default_factory=lambda: Template(source=""))
36
42
  _input_template: Template = PrivateAttr(
37
43
  default_factory=lambda: Template(source="{{ user_message }}")
@@ -72,6 +78,20 @@ class _AgentState(BaseModel):
72
78
  self._input_template = Template(source=self.input_template)
73
79
 
74
80
  self._state_lock = threading.Lock()
81
+
82
+ # Bind the message context to the "messages" policy key so appends and
83
+ # compiles route through any attached message policies
84
+ self._context = PolicyList(self._context, state=self, name="messages")
85
+
86
+ # Wrap list-valued state fields that have policies attached, so in-place
87
+ # mutations (append, etc.) trigger the policy pipeline
88
+ for field_name, policies in self.__policies__.items():
89
+ if field_name == "messages" or not policies:
90
+ continue
91
+ value = getattr(self, field_name, None)
92
+ if isinstance(value, list) and not isinstance(value, PolicyList):
93
+ setattr(self, field_name, PolicyList(value, state=self, name=field_name))
94
+
75
95
  return super().model_post_init(state)
76
96
 
77
97
  def get_policies(self, state_name: str) -> list[Policy]:
@@ -84,7 +104,7 @@ class _AgentState(BaseModel):
84
104
  Returns:
85
105
  list[Policy]: List of policies for the field, or empty list if none
86
106
  """
87
- return self.__policies__.get(state_name, [])
107
+ return self.__policies__.get(state_name) or []
88
108
 
89
109
  def _run_policies(self, event: Event, policy_type: Literal["on", "background"]) -> Any:
90
110
  """
@@ -114,7 +134,9 @@ class _AgentState(BaseModel):
114
134
  if not handler_name:
115
135
  raise ValueError(f"No handler for ({policy_type}, {event.kind})")
116
136
 
117
- handler = getattr(policy, handler_name)
137
+ handler = getattr(policy, handler_name, None)
138
+ if handler is None:
139
+ continue
118
140
 
119
141
  if policy_type != "on":
120
142
  raise RuntimeError("_run_policies is only for synchronous 'on' policies")
@@ -154,7 +176,9 @@ class _AgentState(BaseModel):
154
176
  if not handler_name:
155
177
  raise ValueError(f"No handler for ({policy_type}, {event.kind})")
156
178
 
157
- handler = getattr(policy, handler_name)
179
+ handler = getattr(policy, handler_name, None)
180
+ if handler is None:
181
+ continue
158
182
 
159
183
  try:
160
184
  maybe_new_value = await handler(event, value)
@@ -188,9 +212,19 @@ class _AgentState(BaseModel):
188
212
 
189
213
  event = GetEvent(name=name, value=stored_value)
190
214
  transformed_value = self._run_policies(event, "on")
191
- asyncio.create_task(self._dispatch_policies(event, "background"))
215
+ self._schedule_background(event)
192
216
  return transformed_value
193
217
 
218
+ def _schedule_background(self, event: Event):
219
+ """Schedule background policies on the running loop; skip in sync contexts."""
220
+ if not self.get_policies(event.name):
221
+ return
222
+ try:
223
+ loop = asyncio.get_running_loop()
224
+ except RuntimeError:
225
+ return
226
+ loop.create_task(self._dispatch_policies(event, "background"))
227
+
194
228
  def set(self, name: str, value: Any):
195
229
  """
196
230
  Sets a value via SET policies (transform, validate, store).
@@ -210,9 +244,87 @@ class _AgentState(BaseModel):
210
244
  event = SetEvent(name=name, previous=previous, value=value)
211
245
  final_value = self._run_policies(event, "on")
212
246
 
247
+ # Keep policied list fields observable so in-place mutations keep
248
+ # routing through the policy pipeline
249
+ if (
250
+ isinstance(final_value, list)
251
+ and not isinstance(final_value, PolicyList)
252
+ and self.get_policies(name)
253
+ ):
254
+ final_value = PolicyList(final_value, state=self, name=name)
255
+
213
256
  with self._state_lock:
214
257
  setattr(self, name, final_value)
215
- asyncio.create_task(self._dispatch_policies(event, "background"))
258
+ self._schedule_background(event)
259
+
260
+ async def _run_compile(self, event: CompileEvent, items: list) -> list:
261
+ """
262
+ Run the async on_compile pipeline for one list. Each policy may return a
263
+ transformed list (or None for no change); exceptions are logged and skipped.
264
+ """
265
+ value = items
266
+ for policy in self.get_policies(event.name):
267
+ handler = getattr(policy, "on_compile", None)
268
+ if handler is None:
269
+ continue
270
+ try:
271
+ new_value = await handler(event, value)
272
+ if new_value is not None:
273
+ value = new_value
274
+ except Exception as e:
275
+ print(f"[PolicyError] {policy.__class__.__name__}.on_compile failed: {e}")
276
+ return value
277
+
278
+ async def compile_context(self, provider) -> list[Message]:
279
+ """
280
+ Compiles the message context (and any policied list-valued state fields)
281
+ by running attached policies' on_compile handlers. Called right before
282
+ each LLM inference; the transformed lists are written back, so policies
283
+ like eviction and compaction persist their effect.
284
+
285
+ Args:
286
+ provider: The LLM provider about to be called, exposed to policies
287
+ via CompileEvent (e.g. for summarization calls).
288
+
289
+ Returns:
290
+ list[Message]: The compiled message context providers should send.
291
+ """
292
+ # Compile policied list-valued state fields first so the system message
293
+ # (rendered from state) reflects their final contents
294
+ for field_name, policies in self.__policies__.items():
295
+ if field_name == "messages" or not policies:
296
+ continue
297
+ value = getattr(self, field_name, None)
298
+ if not isinstance(value, list):
299
+ continue
300
+ event = CompileEvent(
301
+ name=field_name,
302
+ value=list(value),
303
+ provider=provider,
304
+ last_usage=self._last_usage,
305
+ state=self,
306
+ )
307
+ result = await self._run_compile(event, list(value))
308
+ with self._state_lock:
309
+ if isinstance(value, PolicyList):
310
+ value._set_contents(result)
311
+ else:
312
+ setattr(self, field_name, result)
313
+
314
+ if self.get_policies("messages"):
315
+ event = CompileEvent(
316
+ name="messages",
317
+ value=list(self._context),
318
+ provider=provider,
319
+ last_usage=self._last_usage,
320
+ system_message=self.system_message,
321
+ state=self,
322
+ )
323
+ result = await self._run_compile(event, list(self._context))
324
+ with self._state_lock:
325
+ self._context._set_contents(result)
326
+
327
+ return self._context
216
328
 
217
329
  @classmethod
218
330
  def make_state_model(
@@ -287,15 +399,43 @@ class _AgentState(BaseModel):
287
399
  @property
288
400
  def messages(self) -> list[Message]:
289
401
  """
290
- Returns a list of OpenAI-ready messages with the most up-to-date system message.
402
+ Returns the compiled message context with the most up-to-date system message.
403
+
404
+ This is the policy-shaped view providers consume; see `raw_messages` for the
405
+ untouched history.
406
+
407
+ Returns:
408
+ list[Message]: Compiled message context with system message prepended
409
+ """
410
+ messages = self._context.copy()
411
+ messages.insert(0, SystemMessage(content=self.system_message))
412
+ return messages
413
+
414
+ @property
415
+ def raw_messages(self) -> list[Message]:
416
+ """
417
+ Returns the raw, append-only message history with the most up-to-date
418
+ system message. Policies never modify this list; use it for debugging
419
+ and auditing what actually happened.
291
420
 
292
421
  Returns:
293
- list[Message]: Complete message list with system message prepended
422
+ list[Message]: Raw message history with system message prepended
294
423
  """
295
424
  messages = self._messages.copy()
296
- messages.insert(0, Message(role="system", content=self.system_message))
425
+ messages.insert(0, SystemMessage(content=self.system_message))
297
426
  return messages
298
427
 
428
+ def add_message(self, message: Message) -> None:
429
+ """
430
+ Adds a message to both histories: the raw append-only log (`_messages`)
431
+ and the working context (`_context`) that providers consume.
432
+
433
+ Args:
434
+ message (Message): The message to record.
435
+ """
436
+ self._messages.append(message)
437
+ self._context.append(message)
438
+
299
439
  def add_user_message(self, message: str):
300
440
  """
301
441
  Adds a user message to the message list. If an `input_template` is given then
@@ -311,9 +451,9 @@ class _AgentState(BaseModel):
311
451
  data = self.model_dump()
312
452
  data["user_message"] = message
313
453
  if self.phase:
314
- content = self._input_template.render(phase=self.phase, **self.model_dump())
454
+ content = self._input_template.render(phase=self.phase, **data)
315
455
  else:
316
- content = self._input_template.render(**self.model_dump())
456
+ content = self._input_template.render(**data)
317
457
  else:
318
458
  content = message
319
- self._messages.append(Message(role="user", content=content))
459
+ self.add_message(UserMessage(content=content))
@@ -756,13 +756,24 @@ class AgentMeta(type):
756
756
  ]
757
757
 
758
758
  # Create a Pydantic model for the agent's state
759
+ if "messages" in cls.__state_defs__:
760
+ raise ValueError(
761
+ f"Agent '{cls.__name__}' declares a state field named 'messages', which is "
762
+ "reserved for the message context (it collides with the `messages` property "
763
+ "and the message-policy registry). Rename the field."
764
+ )
759
765
  StateClass = _AgentState.make_state_model(
760
766
  name=cls.__name__, state_definitions=cls.__state_defs__
761
767
  )
762
- # Attach policies to the state class for runtime policy enforcement
768
+ # Attach policies to the state class for runtime policy enforcement.
769
+ # Message policies (from __message_policies__) register under the
770
+ # reserved "messages" key, alongside per-field state policies.
763
771
  StateClass.__policies__ = {
764
772
  name: def_.info.policies for name, def_ in cls.__state_defs__.items()
765
773
  }
774
+ StateClass.__policies__["messages"] = tuple(
775
+ (inherited_namespace | namespace).get("__message_policies__") or ()
776
+ )
766
777
 
767
778
  # Create the final agent response model
768
779
  ResponseModel = AgentResponse.from_agent_class(
@@ -8,7 +8,6 @@ Anthropic, and mock providers for testing purposes.
8
8
  from enum import Enum
9
9
 
10
10
  from pyagentic.llm._openai import OpenAIProvider
11
- from pyagentic.llm._openaiv1 import OpenAIV1Provider
12
11
  from pyagentic.llm._anthropic import AnthropicProvider
13
12
  from pyagentic.llm._gemini import GeminiProvider
14
13
  from pyagentic.llm._mock import _MockProvider
@@ -26,7 +25,6 @@ class LLMProviders(Enum):
26
25
  """
27
26
 
28
27
  OPENAI = OpenAIProvider
29
- OPENAIV1 = OpenAIV1Provider
30
28
  ANTHROPIC = AnthropicProvider
31
29
  GEMINI = GeminiProvider
32
30
  _MOCK = _MockProvider