agentforge-core 0.2.3__tar.gz → 0.2.4__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.
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/PKG-INFO +1 -1
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/pyproject.toml +1 -1
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/__init__.py +4 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/config/schema.py +57 -9
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/contracts/__init__.py +2 -1
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/contracts/chat.py +24 -1
- agentforge_core-0.2.4/src/agentforge_core/contracts/protocol_bridge.py +50 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/contracts/tool.py +35 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/production/__init__.py +2 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/production/exceptions.py +14 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/testing/conformance.py +26 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/values/messages.py +11 -10
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_chat_conformance.py +3 -1
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_config_feat012.py +7 -10
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_config_module_schemas.py +55 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_contracts_tool.py +49 -1
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_messages.py +11 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/.gitignore +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/LICENSE +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/README.md +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/_bm25.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/config/__init__.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/config/loader.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/config/module_schemas.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/contracts/auth.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/contracts/embedding.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/contracts/evaluator.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/contracts/finding.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/contracts/graph_store.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/contracts/guardrails.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/contracts/llm.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/contracts/memory.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/contracts/migrator.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/contracts/renderer.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/contracts/reranker.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/contracts/strategy.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/contracts/task.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/contracts/vector_store.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/migrations/__init__.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/migrations/discover.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/migrations/template.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/observability/__init__.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/observability/tracing.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/production/budget.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/production/fallback.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/production/log_filter.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/production/log_format.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/production/run_context.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/py.typed +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/resolver/__init__.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/resolver/discover.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/resolver/resolve.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/testing/__init__.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/values/__init__.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/values/auth.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/values/chat.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/values/claim.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/values/graph.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/values/guardrails.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/values/manifest.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/values/module.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/values/pipeline.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/values/retrieval.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/values/state.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/values/vector.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/conftest.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/.gitkeep +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_auth_values.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_bm25.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_budget.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_capability_extensions.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_chat_values.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_claim.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_config_chat.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_config_pipeline.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_config_retrieval.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_contracts_auth.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_contracts_chat.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_contracts_evaluator.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_contracts_finding.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_contracts_llm.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_contracts_memory.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_contracts_strategy.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_contracts_task.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_embedding_client.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_exceptions.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_fallback_chain.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_graph_store_contract.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_guardrails_config.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_guardrails_contracts.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_log_filter.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_log_format.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_migrations.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_pipeline_values.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_provider_errors.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_reranker_contract.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_resolver.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_resolver_discovery.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_run_context.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_state.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_strategy_conformance.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_strategy_stream_default.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_task_conformance.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_values_graph.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_values_vector.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.2.4}/tests/unit/test_vector_store_contract.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentforge-core
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: AgentForge core — stable contracts (ABCs, value types) for the agentic framework
|
|
5
5
|
Project-URL: Homepage, https://github.com/Scaffoldic/agentforge-py
|
|
6
6
|
Project-URL: Repository, https://github.com/Scaffoldic/agentforge-py
|
|
@@ -47,6 +47,7 @@ from agentforge_core.contracts import (
|
|
|
47
47
|
Reranker,
|
|
48
48
|
Tool,
|
|
49
49
|
VectorStore,
|
|
50
|
+
validate_tool_name,
|
|
50
51
|
)
|
|
51
52
|
from agentforge_core.migrations import discover_migrations
|
|
52
53
|
from agentforge_core.observability import SCOPE_NAME as OBSERVABILITY_SCOPE_NAME
|
|
@@ -67,6 +68,7 @@ from agentforge_core.production import (
|
|
|
67
68
|
RunIdFilter,
|
|
68
69
|
ServiceError,
|
|
69
70
|
TimeoutError,
|
|
71
|
+
ToolNameInvalidError,
|
|
70
72
|
bind_run,
|
|
71
73
|
current_run,
|
|
72
74
|
install_json_formatter,
|
|
@@ -202,6 +204,7 @@ __all__ = [
|
|
|
202
204
|
"TokenUsage",
|
|
203
205
|
"Tool",
|
|
204
206
|
"ToolCall",
|
|
207
|
+
"ToolNameInvalidError",
|
|
205
208
|
"ToolSpec",
|
|
206
209
|
"VectorItem",
|
|
207
210
|
"VectorMatch",
|
|
@@ -225,4 +228,5 @@ __all__ = [
|
|
|
225
228
|
"reset_run",
|
|
226
229
|
"uninstall_json_formatter",
|
|
227
230
|
"uninstall_run_id_filter",
|
|
231
|
+
"validate_tool_name",
|
|
228
232
|
]
|
|
@@ -17,7 +17,35 @@ from __future__ import annotations
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import Any, Literal
|
|
19
19
|
|
|
20
|
-
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
20
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _normalise_named_entry(value: Any) -> Any:
|
|
24
|
+
"""Normalise the terse YAML sugar for `name + config` entries into the
|
|
25
|
+
canonical mapping before strict validation (bug-019).
|
|
26
|
+
|
|
27
|
+
Three shapes are accepted; the first two are sugar:
|
|
28
|
+
|
|
29
|
+
- String form ``faithfulness`` → ``{"name": "faithfulness"}``
|
|
30
|
+
- Single-key mapping ``{geval: {rubric: "..."}}`` →
|
|
31
|
+
``{"name": "geval", "config": {"rubric": "..."}}``
|
|
32
|
+
- Canonical mapping ``{name: x, config: {...}}`` → returned unchanged.
|
|
33
|
+
|
|
34
|
+
Anything else is returned untouched so the model's own validation
|
|
35
|
+
raises a clear error. Used by `EvaluatorEntry` and `GuardrailEntry`,
|
|
36
|
+
so every list that holds them (`modules.evaluators` and guardrails'
|
|
37
|
+
`input` / `output` / `tool_gates`) accepts all three forms.
|
|
38
|
+
"""
|
|
39
|
+
if isinstance(value, str):
|
|
40
|
+
return {"name": value}
|
|
41
|
+
if isinstance(value, dict) and "name" not in value and len(value) == 1:
|
|
42
|
+
((key, cfg),) = value.items()
|
|
43
|
+
if isinstance(key, str):
|
|
44
|
+
entry: dict[str, Any] = {"name": key}
|
|
45
|
+
if cfg is not None:
|
|
46
|
+
entry["config"] = cfg
|
|
47
|
+
return entry
|
|
48
|
+
return value
|
|
21
49
|
|
|
22
50
|
|
|
23
51
|
class BudgetConfig(BaseModel):
|
|
@@ -178,13 +206,14 @@ class RetrievalConfig(BaseModel):
|
|
|
178
206
|
|
|
179
207
|
|
|
180
208
|
class EvaluatorEntry(BaseModel):
|
|
181
|
-
"""An entry in `modules.evaluators:`.
|
|
209
|
+
"""An entry in `modules.evaluators:`. Three YAML shapes are valid:
|
|
182
210
|
|
|
183
211
|
- String form: `- faithfulness` (just the name).
|
|
184
|
-
-
|
|
212
|
+
- Single-key mapping: `- faithfulness: {cost_cap_usd: 0.05, ...}`.
|
|
213
|
+
- Canonical mapping: `- {name: faithfulness, config: {...}}`.
|
|
185
214
|
|
|
186
|
-
|
|
187
|
-
`EvaluatorEntry(name=..., config={})` before validation.
|
|
215
|
+
The first two are sugar; a `mode="before"` validator normalises them
|
|
216
|
+
to `EvaluatorEntry(name=..., config={...})` before strict validation.
|
|
188
217
|
"""
|
|
189
218
|
|
|
190
219
|
model_config = ConfigDict(strict=True, extra="forbid")
|
|
@@ -192,6 +221,11 @@ class EvaluatorEntry(BaseModel):
|
|
|
192
221
|
name: str = Field(min_length=1)
|
|
193
222
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
194
223
|
|
|
224
|
+
@model_validator(mode="before")
|
|
225
|
+
@classmethod
|
|
226
|
+
def _coerce_sugar(cls, value: Any) -> Any:
|
|
227
|
+
return _normalise_named_entry(value)
|
|
228
|
+
|
|
195
229
|
|
|
196
230
|
class ObservabilityEntry(BaseModel):
|
|
197
231
|
"""An entry in `modules.observability:` — same shape as evaluator
|
|
@@ -226,13 +260,14 @@ class GuardrailPolicy(BaseModel):
|
|
|
226
260
|
class GuardrailEntry(BaseModel):
|
|
227
261
|
"""One entry inside `modules.guardrails.{input,output,tool_gates}`.
|
|
228
262
|
|
|
229
|
-
|
|
263
|
+
Three YAML shapes are valid (mirrors `EvaluatorEntry`):
|
|
230
264
|
|
|
231
265
|
- String form: `- prompt_injection_basic` (just the name).
|
|
232
|
-
-
|
|
266
|
+
- Single-key mapping: `- presidio: {entities: ["EMAIL_ADDRESS"]}`.
|
|
267
|
+
- Canonical mapping: `- {name: presidio, config: {...}}`.
|
|
233
268
|
|
|
234
|
-
|
|
235
|
-
validation.
|
|
269
|
+
The first two are sugar; a `mode="before"` validator normalises them
|
|
270
|
+
to `GuardrailEntry(name=..., config={...})` before strict validation.
|
|
236
271
|
"""
|
|
237
272
|
|
|
238
273
|
model_config = ConfigDict(strict=True, extra="forbid")
|
|
@@ -240,6 +275,11 @@ class GuardrailEntry(BaseModel):
|
|
|
240
275
|
name: str = Field(min_length=1)
|
|
241
276
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
242
277
|
|
|
278
|
+
@model_validator(mode="before")
|
|
279
|
+
@classmethod
|
|
280
|
+
def _coerce_sugar(cls, value: Any) -> Any:
|
|
281
|
+
return _normalise_named_entry(value)
|
|
282
|
+
|
|
243
283
|
|
|
244
284
|
class ChatHistoryDriverConfig(BaseModel):
|
|
245
285
|
"""`modules.chat.history:` — driver + config for a chat history
|
|
@@ -269,6 +309,14 @@ class ChatSessionConfig(BaseModel):
|
|
|
269
309
|
per_session_budget_usd: float | None = Field(default=None, ge=0.0)
|
|
270
310
|
idempotency_window_s: float = Field(default=60.0, ge=0.0)
|
|
271
311
|
concurrency: Literal["queue", "reject", "replace"] = "queue"
|
|
312
|
+
persist_steps: bool = True
|
|
313
|
+
"""When True (default), intermediate `act` / `observe` agent steps
|
|
314
|
+
are persisted to `ChatHistoryStore` as `role="assistant"` (with
|
|
315
|
+
`tool_calls`) and `role="tool"` (with `tool_call_id`) turns
|
|
316
|
+
respectively, in addition to the final assistant turn. Tool-using
|
|
317
|
+
chat agents need this on for the next turn's prompt to reflect
|
|
318
|
+
what tools ran. Opt out by setting to False when an external
|
|
319
|
+
consumer reconstructs history from another source (bug-010)."""
|
|
272
320
|
safety_mode: Literal["buffer-then-stream", "sentence-window", "stream-then-redact"] = (
|
|
273
321
|
"buffer-then-stream"
|
|
274
322
|
)
|
|
@@ -25,7 +25,7 @@ from agentforge_core.contracts.renderer import FindingRenderer
|
|
|
25
25
|
from agentforge_core.contracts.reranker import Reranker
|
|
26
26
|
from agentforge_core.contracts.strategy import ReasoningStrategy
|
|
27
27
|
from agentforge_core.contracts.task import Task
|
|
28
|
-
from agentforge_core.contracts.tool import Tool
|
|
28
|
+
from agentforge_core.contracts.tool import Tool, validate_tool_name
|
|
29
29
|
from agentforge_core.contracts.vector_store import VectorStore
|
|
30
30
|
|
|
31
31
|
__all__ = [
|
|
@@ -49,4 +49,5 @@ __all__ = [
|
|
|
49
49
|
"Task",
|
|
50
50
|
"Tool",
|
|
51
51
|
"VectorStore",
|
|
52
|
+
"validate_tool_name",
|
|
52
53
|
]
|
|
@@ -71,9 +71,32 @@ class ChatHistoryStore(ABC):
|
|
|
71
71
|
"""Merge ``metadata`` into the session's metadata dict.
|
|
72
72
|
|
|
73
73
|
Implementations may overwrite top-level keys; nested merging
|
|
74
|
-
is the caller's responsibility.
|
|
74
|
+
is the caller's responsibility. The session need **not** already
|
|
75
|
+
exist: if it doesn't, the driver creates it (upsert), so callers
|
|
76
|
+
can record metadata before the first turn is appended (bug-018).
|
|
75
77
|
"""
|
|
76
78
|
|
|
79
|
+
async def create_session(
|
|
80
|
+
self,
|
|
81
|
+
session_id: str,
|
|
82
|
+
*,
|
|
83
|
+
owner: str | None = None,
|
|
84
|
+
metadata: Mapping[str, Any] | None = None,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Ensure a session row exists before any turn is appended.
|
|
87
|
+
|
|
88
|
+
Concrete (non-abstract) so it is additive to this locked ABC
|
|
89
|
+
(ADR-0007): existing and third-party drivers inherit it without
|
|
90
|
+
change. The default records the initial ``owner`` / ``metadata``
|
|
91
|
+
via :meth:`update_session_metadata`, which every shipped driver
|
|
92
|
+
upserts. Idempotent — calling it for an existing session merges
|
|
93
|
+
the given metadata. Drivers may override with a direct insert.
|
|
94
|
+
"""
|
|
95
|
+
merged: dict[str, Any] = dict(metadata or {})
|
|
96
|
+
if owner is not None:
|
|
97
|
+
merged["owner"] = owner
|
|
98
|
+
await self.update_session_metadata(session_id, merged)
|
|
99
|
+
|
|
77
100
|
@abstractmethod
|
|
78
101
|
async def expire_before(self, cutoff: datetime) -> int:
|
|
79
102
|
"""TTL sweep: delete every session whose ``last_active_at <
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""`ProtocolBridge` — the runtime contract for `modules.protocols`
|
|
2
|
+
handlers (feat-013).
|
|
3
|
+
|
|
4
|
+
A protocol handler turns a `modules.protocols[*]` config block into a
|
|
5
|
+
set of `Tool`s the agent can call, and owns the lifecycle of whatever
|
|
6
|
+
connections back that (subprocesses, sockets, sessions). The runtime
|
|
7
|
+
(`build_agent_from_config`) resolves each entry's name under the
|
|
8
|
+
``protocols`` resolver category, builds the handler from its config,
|
|
9
|
+
`start()`s it, merges `tools` into the `Agent`, and `close()`s it on
|
|
10
|
+
`Agent.close()`.
|
|
11
|
+
|
|
12
|
+
The contract is a `@runtime_checkable` `Protocol` rather than an ABC so
|
|
13
|
+
handlers in optional packages (`agentforge-mcp`'s `MCPBridge`, a future
|
|
14
|
+
`agentforge-a2a` bridge) satisfy it structurally — `agentforge` /
|
|
15
|
+
`agentforge-core` never import the handler packages.
|
|
16
|
+
|
|
17
|
+
Expected construction shape (duck-typed, not part of the Protocol since
|
|
18
|
+
classmethods don't express cleanly here): a classmethod
|
|
19
|
+
``from_config(config: dict) -> ProtocolBridge`` that is **pure data** —
|
|
20
|
+
it must not open transports or drive an event loop (do that in
|
|
21
|
+
``start()``), so it is safe to call from within a running loop.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Protocol, runtime_checkable
|
|
27
|
+
|
|
28
|
+
from agentforge_core.contracts.tool import Tool
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@runtime_checkable
|
|
32
|
+
class ProtocolBridge(Protocol):
|
|
33
|
+
"""Lifecycle + tool-source contract for a protocols handler."""
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def tools(self) -> list[Tool]:
|
|
37
|
+
"""The tools this bridge contributes, populated by `start()`."""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
async def start(self) -> None:
|
|
41
|
+
"""Open connections and populate `tools`. Safe to call inside a
|
|
42
|
+
running event loop."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
async def close(self) -> None:
|
|
46
|
+
"""Tear down every connection this bridge opened."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = ["ProtocolBridge"]
|
|
@@ -9,13 +9,48 @@ hood.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import inspect
|
|
12
|
+
import re
|
|
12
13
|
from abc import ABC, abstractmethod
|
|
13
14
|
from typing import Any, ClassVar
|
|
14
15
|
|
|
15
16
|
from pydantic import BaseModel
|
|
16
17
|
|
|
18
|
+
from agentforge_core.production.exceptions import ToolNameInvalidError
|
|
17
19
|
from agentforge_core.values.messages import ToolSpec
|
|
18
20
|
|
|
21
|
+
# The tool-name charset every major provider enforces: Bedrock Converse,
|
|
22
|
+
# OpenAI function calling, and Anthropic tool use all validate names
|
|
23
|
+
# against `^[a-zA-Z0-9_-]{1,64}$`. A name legal here is portable across
|
|
24
|
+
# all of them.
|
|
25
|
+
_TOOL_NAME_RE = re.compile(r"[a-zA-Z0-9_-]{1,64}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def validate_tool_name(name: str) -> None:
|
|
29
|
+
"""Raise `ToolNameInvalidError` if `name` isn't portable across providers.
|
|
30
|
+
|
|
31
|
+
Providers call this at request-build time so an illegal name (e.g. the
|
|
32
|
+
dotted `kb.search`) surfaces as a local, actionable error *before* the
|
|
33
|
+
request leaves the process — instead of a cryptic remote validation
|
|
34
|
+
failure on the first LLM call.
|
|
35
|
+
|
|
36
|
+
Core itself does **not** auto-invoke this: `ToolSpec` stays a neutral
|
|
37
|
+
representation, and each provider opts into the policy it enforces. The
|
|
38
|
+
charset happens to be identical across today's providers, but it is a
|
|
39
|
+
per-provider wire constraint, not a property of the tool definition.
|
|
40
|
+
"""
|
|
41
|
+
if not _TOOL_NAME_RE.fullmatch(name):
|
|
42
|
+
raise ToolNameInvalidError(
|
|
43
|
+
f"tool name {name!r} is not portable: it must match [a-zA-Z0-9_-] "
|
|
44
|
+
f"and be 1-64 characters (the charset Bedrock, OpenAI, and "
|
|
45
|
+
f"Anthropic all enforce). Try {_suggest_tool_name(name)!r}."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _suggest_tool_name(name: str) -> str:
|
|
50
|
+
"""Best-effort legal rewrite for the error message (`kb.search` → `kb_search`)."""
|
|
51
|
+
cleaned = re.sub(r"[^a-zA-Z0-9_-]", "_", name)[:64]
|
|
52
|
+
return cleaned or "tool"
|
|
53
|
+
|
|
19
54
|
|
|
20
55
|
class Tool(ABC):
|
|
21
56
|
"""A typed callable the agent can invoke.
|
|
@@ -31,6 +31,7 @@ from agentforge_core.production.exceptions import (
|
|
|
31
31
|
RateLimitError,
|
|
32
32
|
ServiceError,
|
|
33
33
|
TimeoutError,
|
|
34
|
+
ToolNameInvalidError,
|
|
34
35
|
)
|
|
35
36
|
from agentforge_core.production.log_filter import (
|
|
36
37
|
RunIdFilter,
|
|
@@ -66,6 +67,7 @@ __all__ = [
|
|
|
66
67
|
"RunIdFilter",
|
|
67
68
|
"ServiceError",
|
|
68
69
|
"TimeoutError",
|
|
70
|
+
"ToolNameInvalidError",
|
|
69
71
|
"bind_run",
|
|
70
72
|
"current_run",
|
|
71
73
|
"install_json_formatter",
|
{agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/production/exceptions.py
RENAMED
|
@@ -101,6 +101,20 @@ class TimeoutError(ProviderError):
|
|
|
101
101
|
"""
|
|
102
102
|
|
|
103
103
|
|
|
104
|
+
class ToolNameInvalidError(ProviderError):
|
|
105
|
+
"""A tool name violates the portable tool-name charset.
|
|
106
|
+
|
|
107
|
+
Every major provider (Bedrock Converse, OpenAI, Anthropic) validates
|
|
108
|
+
tool names against ``^[a-zA-Z0-9_-]{1,64}$``. Provider drivers call
|
|
109
|
+
`agentforge_core.contracts.tool.validate_tool_name` at request-build
|
|
110
|
+
time and raise this *before* the request leaves the process, turning a
|
|
111
|
+
cryptic remote validation failure on the first LLM call into a local,
|
|
112
|
+
actionable error. Subclasses `ProviderError` so the same handler that
|
|
113
|
+
catches other provider failures catches this too. Not retryable — the
|
|
114
|
+
fix is to rename the tool.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
|
|
104
118
|
class CapabilityNotSupported(AgentForgeError):
|
|
105
119
|
"""Raised when an optional capability is invoked on a driver that
|
|
106
120
|
does not declare it.
|
|
@@ -815,6 +815,29 @@ async def run_task_conformance(
|
|
|
815
815
|
# ----------------------------------------------------------------------
|
|
816
816
|
|
|
817
817
|
|
|
818
|
+
async def _assert_create_before_first_turn(store: ChatHistoryStore) -> None:
|
|
819
|
+
"""A session must be creatable / metadata-settable before its first
|
|
820
|
+
turn is appended (bug-018), and listable as soon as it is."""
|
|
821
|
+
from uuid import uuid4 # noqa: PLC0415
|
|
822
|
+
|
|
823
|
+
fresh = f"conf-{uuid4().hex[:8]}"
|
|
824
|
+
await store.create_session(fresh, owner="carol")
|
|
825
|
+
listed = await store.list_sessions()
|
|
826
|
+
assert fresh in {s.id for s in listed}, (
|
|
827
|
+
"create_session() must make a session listable before any turn"
|
|
828
|
+
)
|
|
829
|
+
assert any(s.id == fresh and s.owner == "carol" for s in listed), (
|
|
830
|
+
"create_session(owner=...) must persist the owner"
|
|
831
|
+
)
|
|
832
|
+
fresh2 = f"conf-{uuid4().hex[:8]}"
|
|
833
|
+
await store.update_session_metadata(fresh2, {"owner": "dave"})
|
|
834
|
+
assert fresh2 in {s.id for s in await store.list_sessions()}, (
|
|
835
|
+
"update_session_metadata() on an unknown session must upsert it, not raise"
|
|
836
|
+
)
|
|
837
|
+
await store.delete_session(fresh)
|
|
838
|
+
await store.delete_session(fresh2)
|
|
839
|
+
|
|
840
|
+
|
|
818
841
|
async def run_chat_history_conformance(store: ChatHistoryStore) -> None:
|
|
819
842
|
"""Validate that a `ChatHistoryStore` honours the locked contract.
|
|
820
843
|
|
|
@@ -906,6 +929,9 @@ async def run_chat_history_conformance(store: ChatHistoryStore) -> None:
|
|
|
906
929
|
removed_b = await store.expire_before(far_future)
|
|
907
930
|
assert isinstance(removed_b, int), "expire_before must return an int"
|
|
908
931
|
|
|
932
|
+
# 11. create_session / metadata before any turn (bug-018).
|
|
933
|
+
await _assert_create_before_first_turn(store)
|
|
934
|
+
|
|
909
935
|
# 11. capabilities is a set.
|
|
910
936
|
caps = store.capabilities()
|
|
911
937
|
assert isinstance(caps, set)
|
|
@@ -24,6 +24,16 @@ StopReason = Literal["end_turn", "tool_use", "max_tokens", "stop_sequence", "oth
|
|
|
24
24
|
"""Provider-normalised reason the LLM stopped emitting tokens."""
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
class ToolCall(BaseModel):
|
|
28
|
+
"""A tool invocation emitted by the LLM."""
|
|
29
|
+
|
|
30
|
+
model_config = ConfigDict(frozen=True, strict=True)
|
|
31
|
+
|
|
32
|
+
id: str
|
|
33
|
+
name: str
|
|
34
|
+
arguments: dict[str, Any] = Field(default_factory=dict)
|
|
35
|
+
|
|
36
|
+
|
|
27
37
|
class Message(BaseModel):
|
|
28
38
|
"""One turn in the chat-completion exchange."""
|
|
29
39
|
|
|
@@ -33,16 +43,7 @@ class Message(BaseModel):
|
|
|
33
43
|
content: str
|
|
34
44
|
name: str | None = None
|
|
35
45
|
tool_call_id: str | None = None
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class ToolCall(BaseModel):
|
|
39
|
-
"""A tool invocation emitted by the LLM."""
|
|
40
|
-
|
|
41
|
-
model_config = ConfigDict(frozen=True, strict=True)
|
|
42
|
-
|
|
43
|
-
id: str
|
|
44
|
-
name: str
|
|
45
|
-
arguments: dict[str, Any] = Field(default_factory=dict)
|
|
46
|
+
tool_calls: tuple[ToolCall, ...] = ()
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
class ToolSpec(BaseModel):
|
|
@@ -70,7 +70,9 @@ class _DictHistory(ChatHistoryStore):
|
|
|
70
70
|
limit: int = 100,
|
|
71
71
|
before: datetime | None = None,
|
|
72
72
|
) -> list[SessionInfo]:
|
|
73
|
-
|
|
73
|
+
# Include metadata-only sessions (created before their first
|
|
74
|
+
# turn — bug-018), not just sessions that have turns.
|
|
75
|
+
ids = {t.session_id for t in self._turns} | set(self._meta)
|
|
74
76
|
out: list[SessionInfo] = []
|
|
75
77
|
for sid in ids:
|
|
76
78
|
o = self._owners.get(sid)
|
|
@@ -14,7 +14,6 @@ from agentforge_core.config import (
|
|
|
14
14
|
parse_overrides,
|
|
15
15
|
)
|
|
16
16
|
from agentforge_core.production.exceptions import ModuleError
|
|
17
|
-
from pydantic import ValidationError
|
|
18
17
|
|
|
19
18
|
# --- widened schema ----------------------------------------------
|
|
20
19
|
|
|
@@ -269,16 +268,14 @@ def test_explicit_env_arg_beats_env_var(tmp_path: Path, monkeypatch: pytest.Monk
|
|
|
269
268
|
assert cfg.agent.budget.usd == pytest.approx(50.0)
|
|
270
269
|
|
|
271
270
|
|
|
272
|
-
# --- evaluator string-shorthand (
|
|
271
|
+
# --- evaluator string-shorthand (normalised since bug-019) -----
|
|
273
272
|
|
|
274
273
|
|
|
275
|
-
def
|
|
276
|
-
"""
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
Implementation status.
|
|
280
|
-
"""
|
|
274
|
+
def test_evaluator_string_shorthand_loads(tmp_path: Path) -> None:
|
|
275
|
+
"""Spec §4.1's `evaluators: - faithfulness` (bare string) now loads
|
|
276
|
+
end-to-end: the entry's `mode="before"` validator normalises the
|
|
277
|
+
string to `{name: faithfulness, config: {}}` (bug-019)."""
|
|
281
278
|
yaml_path = tmp_path / "agentforge.yaml"
|
|
282
279
|
yaml_path.write_text("modules:\n evaluators:\n - faithfulness\n")
|
|
283
|
-
|
|
284
|
-
|
|
280
|
+
cfg = load_config(yaml_path)
|
|
281
|
+
assert [(e.name, e.config) for e in cfg.modules.evaluators] == [("faithfulness", {})]
|
|
@@ -172,6 +172,61 @@ def test_evaluator_entry_invalid_config_raises():
|
|
|
172
172
|
validate_module_configs(cfg)
|
|
173
173
|
|
|
174
174
|
|
|
175
|
+
# --- bug-019: terse string / single-key-mapping sugar normalises ----
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_evaluators_accept_all_three_sugar_forms() -> None:
|
|
179
|
+
"""String, single-key-mapping, and canonical forms all parse from raw
|
|
180
|
+
YAML-shaped dicts (bug-019)."""
|
|
181
|
+
modules = ModulesConfig.model_validate(
|
|
182
|
+
{
|
|
183
|
+
"evaluators": [
|
|
184
|
+
"faithfulness", # string sugar
|
|
185
|
+
{"geval": {"rubric": "be nice"}}, # single-key mapping sugar
|
|
186
|
+
{"name": "correctness", "config": {"ground_truth_field": "ref"}}, # canonical
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
assert [(e.name, e.config) for e in modules.evaluators] == [
|
|
191
|
+
("faithfulness", {}),
|
|
192
|
+
("geval", {"rubric": "be nice"}),
|
|
193
|
+
("correctness", {"ground_truth_field": "ref"}),
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@pytest.mark.parametrize("gate", ["input", "output", "tool_gates"])
|
|
198
|
+
def test_guardrail_gates_accept_all_three_sugar_forms(gate: str) -> None:
|
|
199
|
+
modules = ModulesConfig.model_validate(
|
|
200
|
+
{
|
|
201
|
+
"guardrails": {
|
|
202
|
+
gate: [
|
|
203
|
+
"prompt_injection_basic", # string sugar
|
|
204
|
+
{"presidio": {"entities": ["EMAIL_ADDRESS"]}}, # single-key mapping sugar
|
|
205
|
+
{"name": "capability_check", "config": {}}, # canonical
|
|
206
|
+
]
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
entries = getattr(modules.guardrails, gate)
|
|
211
|
+
assert [(e.name, e.config) for e in entries] == [
|
|
212
|
+
("prompt_injection_basic", {}),
|
|
213
|
+
("presidio", {"entities": ["EMAIL_ADDRESS"]}),
|
|
214
|
+
("capability_check", {}),
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_string_sugar_rejects_empty_name() -> None:
|
|
219
|
+
"""An empty string still fails (name has min_length=1)."""
|
|
220
|
+
with pytest.raises(ValueError, match="evaluators"):
|
|
221
|
+
ModulesConfig.model_validate({"evaluators": [""]})
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_canonical_form_still_forbids_extra_keys() -> None:
|
|
225
|
+
"""Normalisation must not loosen strict/extra=forbid for canonical dicts."""
|
|
226
|
+
with pytest.raises(ValueError, match="evaluators"):
|
|
227
|
+
ModulesConfig.model_validate({"evaluators": [{"name": "x", "config": {}, "bogus": 1}]})
|
|
228
|
+
|
|
229
|
+
|
|
175
230
|
# --- non-class schema attribute (defensive) --------------------
|
|
176
231
|
|
|
177
232
|
|
|
@@ -6,7 +6,8 @@ from abc import abstractmethod
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
|
-
from agentforge_core.contracts.tool import Tool
|
|
9
|
+
from agentforge_core.contracts.tool import Tool, validate_tool_name
|
|
10
|
+
from agentforge_core.production.exceptions import ProviderError, ToolNameInvalidError
|
|
10
11
|
from pydantic import BaseModel
|
|
11
12
|
|
|
12
13
|
|
|
@@ -92,3 +93,50 @@ def test_inherited_attributes_satisfy_the_check() -> None:
|
|
|
92
93
|
return "child"
|
|
93
94
|
|
|
94
95
|
assert _Child.name == "base"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---- validate_tool_name (bug-017) ----
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@pytest.mark.parametrize(
|
|
102
|
+
"name",
|
|
103
|
+
[
|
|
104
|
+
"search",
|
|
105
|
+
"kb_search",
|
|
106
|
+
"fs__read_file",
|
|
107
|
+
"tool-1",
|
|
108
|
+
"A",
|
|
109
|
+
"a" * 64,
|
|
110
|
+
"Mixed_Case-123",
|
|
111
|
+
],
|
|
112
|
+
)
|
|
113
|
+
def test_validate_tool_name_accepts_portable_names(name: str) -> None:
|
|
114
|
+
validate_tool_name(name) # does not raise
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@pytest.mark.parametrize(
|
|
118
|
+
"name",
|
|
119
|
+
[
|
|
120
|
+
"kb.search", # dot — the bug-012 / bug-017 case
|
|
121
|
+
"ns:tool", # colon
|
|
122
|
+
"a b", # space
|
|
123
|
+
"tool/name", # slash
|
|
124
|
+
"café", # non-ascii
|
|
125
|
+
"", # empty
|
|
126
|
+
"a" * 65, # too long
|
|
127
|
+
],
|
|
128
|
+
)
|
|
129
|
+
def test_validate_tool_name_rejects_illegal_names(name: str) -> None:
|
|
130
|
+
with pytest.raises(ToolNameInvalidError):
|
|
131
|
+
validate_tool_name(name)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_validate_tool_name_error_is_a_provider_error() -> None:
|
|
135
|
+
"""Subclasses ProviderError so existing provider-failure handlers catch it."""
|
|
136
|
+
with pytest.raises(ProviderError):
|
|
137
|
+
validate_tool_name("kb.search")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_validate_tool_name_message_includes_actionable_suggestion() -> None:
|
|
141
|
+
with pytest.raises(ToolNameInvalidError, match="kb_search"):
|
|
142
|
+
validate_tool_name("kb.search")
|
|
@@ -39,6 +39,17 @@ def test_message_accepts_each_valid_role(role: str) -> None:
|
|
|
39
39
|
Message(role=role, content="ok") # type: ignore[arg-type]
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
def test_message_default_tool_calls_is_empty_tuple() -> None:
|
|
43
|
+
m = Message(role="assistant", content="hi")
|
|
44
|
+
assert m.tool_calls == ()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_message_carries_tool_calls() -> None:
|
|
48
|
+
tc = ToolCall(id="t-1", name="search", arguments={"q": "hi"})
|
|
49
|
+
m = Message(role="assistant", content="", tool_calls=(tc,))
|
|
50
|
+
assert m.tool_calls == (tc,)
|
|
51
|
+
|
|
52
|
+
|
|
42
53
|
# ---- ToolCall ----
|
|
43
54
|
|
|
44
55
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/config/module_schemas.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/contracts/graph_store.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/contracts/vector_store.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/observability/__init__.py
RENAMED
|
File without changes
|
{agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/observability/tracing.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/production/log_filter.py
RENAMED
|
File without changes
|
{agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/production/log_format.py
RENAMED
|
File without changes
|
{agentforge_core-0.2.3 → agentforge_core-0.2.4}/src/agentforge_core/production/run_context.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|