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.
- {pyagentic_core-2.12.3.dev1/pyagentic_core.egg-info → pyagentic_core-2.13.0a1}/PKG-INFO +1 -1
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/policies.md +170 -3
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_agent/_agent.py +40 -16
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_agent/_agent_state.py +154 -14
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_metaclasses.py +12 -1
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/llm/__init__.py +0 -2
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/llm/_anthropic.py +50 -64
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/llm/_gemini.py +40 -71
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/llm/_mock.py +21 -49
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/llm/_openai.py +37 -41
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/llm/_provider.py +1 -28
- pyagentic_core-2.13.0a1/pyagentic/models/llm.py +196 -0
- pyagentic_core-2.13.0a1/pyagentic/policies/__init__.py +19 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/policies/_events.py +33 -0
- pyagentic_core-2.13.0a1/pyagentic/policies/_list.py +173 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/policies/_policy.py +25 -6
- pyagentic_core-2.13.0a1/pyagentic/policies/messages.py +212 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1/pyagentic_core.egg-info}/PKG-INFO +1 -1
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic_core.egg-info/SOURCES.txt +10 -1
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic_core.egg-info/scm_file_list.json +10 -1
- pyagentic_core-2.13.0a1/pyagentic_core.egg-info/scm_version.json +8 -0
- pyagentic_core-2.13.0a1/tests/_base/test_agent_forking.py +139 -0
- pyagentic_core-2.13.0a1/tests/_base/test_agent_message_policies.py +187 -0
- pyagentic_core-2.13.0a1/tests/_base/test_policy_list.py +130 -0
- pyagentic_core-2.13.0a1/tests/llm/test_message_conversion.py +114 -0
- pyagentic_core-2.13.0a1/tests/models/test_messages.py +100 -0
- pyagentic_core-2.13.0a1/tests/policies/__init__.py +0 -0
- pyagentic_core-2.13.0a1/tests/policies/test_message_policies.py +227 -0
- pyagentic_core-2.13.0a1/tests/tracing/__init__.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/uv.lock +14 -14
- pyagentic_core-2.12.3.dev1/pyagentic/llm/_openaiv1.py +0 -221
- pyagentic_core-2.12.3.dev1/pyagentic/models/llm.py +0 -92
- pyagentic_core-2.12.3.dev1/pyagentic/policies/__init__.py +0 -3
- pyagentic_core-2.12.3.dev1/pyagentic_core.egg-info/scm_version.json +0 -8
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/.github/workflows/docs.yml +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/.github/workflows/release.yml +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/.github/workflows/testing.yml +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/.gitignore +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/CHANGELOG.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/LICENSE +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/README.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/agent-linking.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/api/creating-an-app.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/api/deploying.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/api/index.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/api/jobs.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/api/running.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/diagrams/declaration.svg +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/diagrams/instantiation.svg +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/diagrams/runtime.svg +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/diagrams/source/README.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/diagrams/source/declaration.d2 +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/diagrams/source/instantiation.d2 +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/diagrams/source/runtime.d2 +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/execution-modes.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/getting-started.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/images/langfuse.png +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/index.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/inheritance.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/observability.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/phases.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/reference/architecture.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/reference/modules.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/responses.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/states.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/structured-output.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/docs/tools.md +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/mkdocs.yml +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/__init__.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/__init__.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_agent/__init__.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_agent/_agent_linking.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_depends.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_exceptions.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_info.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_mcp.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_ref.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_spec.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_state.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_tool.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_validation.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_utils/_typing.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_utils/_warnings.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_version_scheme.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/__init__.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/_app.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/_build.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/_config.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/_docker.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/_mcp_server.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/_models.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/_sessions.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/jobs/__init__.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/jobs/_models.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/jobs/_orchestrator.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/jobs/_routes.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/jobs/backends/__init__.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/jobs/backends/_base.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/jobs/backends/_in_process.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/jobs/store/__init__.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/jobs/store/_base.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/api/jobs/store/_sqlite.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/logging.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/models/response.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/models/tracing.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/tracing/__init__.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/tracing/_basic.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/tracing/_langfuse.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/tracing/_tracer.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/updates.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic_core.egg-info/dependency_links.txt +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic_core.egg-info/requires.txt +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic_core.egg-info/top_level.txt +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyproject.toml +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/setup.cfg +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/setup.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/__init__.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/_base/__init__.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/_base/test_agent.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/_base/test_agent_inheritance.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/_base/test_agent_linking.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/_base/test_agent_provider.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/_base/test_params.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/_base/test_phases.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/_base/test_state.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/_base/test_tool.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/_base/test_validator.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/__init__.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/jobs/__init__.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/jobs/test_orchestrator.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/jobs/test_routes.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/jobs/test_store_sqlite.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/test_app.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/test_build_agent.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/test_config.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/test_construct_model.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/test_dependencies_integration.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/test_docker.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/test_mcp_server.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/test_metaclass_models.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/test_router.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/api/test_sessions.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/conftest.py +0 -0
- {pyagentic_core-2.12.3.dev1/tests/tracing → pyagentic_core-2.13.0a1/tests/llm}/__init__.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/models/test_responses.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/tracing/test_basic_tracer.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/tracing/test_models.py +0 -0
- {pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/tests/tracing/test_tracer.py +0 -0
|
@@ -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
|
-
* **
|
|
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
|
|
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.
|
|
488
|
-
|
|
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
|
|
516
|
-
self.state.
|
|
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.
|
|
551
|
-
|
|
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.
|
|
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.
|
|
626
|
-
|
|
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
|
|
656
|
-
self.state.
|
|
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.
|
|
680
|
-
|
|
681
|
-
|
|
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.
|
|
834
|
+
self.state.add_message(AssistantMessage(content=response.text))
|
|
811
835
|
break
|
|
812
836
|
|
|
813
837
|
tasks = []
|
{pyagentic_core-2.12.3.dev1 → pyagentic_core-2.13.0a1}/pyagentic/_base/_agent/_agent_state.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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]:
|
|
422
|
+
list[Message]: Raw message history with system message prepended
|
|
294
423
|
"""
|
|
295
424
|
messages = self._messages.copy()
|
|
296
|
-
messages.insert(0,
|
|
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, **
|
|
454
|
+
content = self._input_template.render(phase=self.phase, **data)
|
|
315
455
|
else:
|
|
316
|
-
content = self._input_template.render(**
|
|
456
|
+
content = self._input_template.render(**data)
|
|
317
457
|
else:
|
|
318
458
|
content = message
|
|
319
|
-
self.
|
|
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
|