agentforge-core 0.2.3__tar.gz → 0.3.0__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.3.0}/.gitignore +10 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/PKG-INFO +1 -1
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/pyproject.toml +1 -1
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/__init__.py +4 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/config/__init__.py +6 -0
- agentforge_core-0.3.0/src/agentforge_core/config/app_sections.py +124 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/config/loader.py +70 -8
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/config/schema.py +91 -10
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/contracts/__init__.py +2 -1
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/contracts/chat.py +24 -1
- agentforge_core-0.3.0/src/agentforge_core/contracts/protocol_bridge.py +50 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/contracts/tool.py +35 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/production/__init__.py +2 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/production/exceptions.py +14 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/resolver/discover.py +8 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/testing/conformance.py +26 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/values/messages.py +11 -10
- agentforge_core-0.3.0/tests/integration/test_app_sections_real_discovery.py +170 -0
- agentforge_core-0.3.0/tests/integration/test_config_imports_cli.py +86 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_chat_conformance.py +3 -1
- agentforge_core-0.3.0/tests/unit/test_config_app_passthrough.py +154 -0
- agentforge_core-0.3.0/tests/unit/test_config_app_sections.py +169 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_config_feat012.py +7 -10
- agentforge_core-0.3.0/tests/unit/test_config_imports.py +216 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_config_module_schemas.py +55 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_contracts_tool.py +49 -1
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_messages.py +11 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_resolver_discovery.py +37 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/LICENSE +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/README.md +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/_bm25.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/config/module_schemas.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/contracts/auth.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/contracts/embedding.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/contracts/evaluator.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/contracts/finding.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/contracts/graph_store.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/contracts/guardrails.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/contracts/llm.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/contracts/memory.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/contracts/migrator.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/contracts/renderer.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/contracts/reranker.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/contracts/strategy.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/contracts/task.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/contracts/vector_store.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/migrations/__init__.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/migrations/discover.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/migrations/template.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/observability/__init__.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/observability/tracing.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/production/budget.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/production/fallback.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/production/log_filter.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/production/log_format.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/production/run_context.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/py.typed +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/resolver/__init__.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/resolver/resolve.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/testing/__init__.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/values/__init__.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/values/auth.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/values/chat.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/values/claim.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/values/graph.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/values/guardrails.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/values/manifest.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/values/module.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/values/pipeline.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/values/retrieval.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/values/state.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/src/agentforge_core/values/vector.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/conftest.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/.gitkeep +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_auth_values.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_bm25.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_budget.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_capability_extensions.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_chat_values.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_claim.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_config_chat.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_config_pipeline.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_config_retrieval.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_contracts_auth.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_contracts_chat.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_contracts_evaluator.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_contracts_finding.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_contracts_llm.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_contracts_memory.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_contracts_strategy.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_contracts_task.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_embedding_client.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_exceptions.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_fallback_chain.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_graph_store_contract.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_guardrails_config.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_guardrails_contracts.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_log_filter.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_log_format.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_migrations.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_pipeline_values.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_provider_errors.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_reranker_contract.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_resolver.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_run_context.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_state.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_strategy_conformance.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_strategy_stream_default.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_task_conformance.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_values_graph.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_values_vector.py +0 -0
- {agentforge_core-0.2.3 → agentforge_core-0.3.0}/tests/unit/test_vector_store_contract.py +0 -0
|
@@ -47,3 +47,13 @@ Thumbs.db
|
|
|
47
47
|
# Project-local
|
|
48
48
|
*.local
|
|
49
49
|
.agentforge-state/.session-cache
|
|
50
|
+
|
|
51
|
+
# AI-assistant working state — local session tracking, not part of
|
|
52
|
+
# the published project. The process docs under .claude/ (standards,
|
|
53
|
+
# checklists, CLAUDE.md) are intentionally kept tracked; only the
|
|
54
|
+
# churny per-session state files are ignored.
|
|
55
|
+
.claude/state/
|
|
56
|
+
|
|
57
|
+
# Launch / go-to-market drafts — local-only marketing material,
|
|
58
|
+
# never published to the repo.
|
|
59
|
+
launch/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentforge-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
]
|
|
@@ -20,6 +20,10 @@ bump; removing or renaming requires a major bump.
|
|
|
20
20
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
|
+
from agentforge_core.config.app_sections import (
|
|
24
|
+
discover_app_sections,
|
|
25
|
+
validate_app_config,
|
|
26
|
+
)
|
|
23
27
|
from agentforge_core.config.loader import load_config, parse_overrides
|
|
24
28
|
from agentforge_core.config.module_schemas import validate_module_configs
|
|
25
29
|
from agentforge_core.config.schema import (
|
|
@@ -56,7 +60,9 @@ __all__ = [
|
|
|
56
60
|
"RerankerEntry",
|
|
57
61
|
"RetrievalConfig",
|
|
58
62
|
"RetrieverModuleConfig",
|
|
63
|
+
"discover_app_sections",
|
|
59
64
|
"load_config",
|
|
60
65
|
"parse_overrides",
|
|
66
|
+
"validate_app_config",
|
|
61
67
|
"validate_module_configs",
|
|
62
68
|
]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Registered app-config section validation (feat-026 Phase 2).
|
|
2
|
+
|
|
3
|
+
Phase 1 (enh-002) added the reserved ``app:`` namespace and the
|
|
4
|
+
``app_as`` accessor: the framework stored the subtree but validated
|
|
5
|
+
nothing inside it — a derived agent owned validation entirely. Phase 2
|
|
6
|
+
closes the parity gap with *module* config. A derived agent or plugin
|
|
7
|
+
registers a Pydantic schema per ``app.<section>`` through a new
|
|
8
|
+
entry-point group, exactly mirroring how modules register their classes
|
|
9
|
+
(ADR-0004)::
|
|
10
|
+
|
|
11
|
+
[project.entry-points."agentforge.config_sections"]
|
|
12
|
+
graph = "agentforge_graph.config:GraphConfig"
|
|
13
|
+
|
|
14
|
+
``agentforge config validate`` then validates each registered section
|
|
15
|
+
the same way :func:`validate_module_configs` validates ``modules.*``:
|
|
16
|
+
|
|
17
|
+
- A section present in ``app:`` **and** registered → validated strictly
|
|
18
|
+
against its schema; a typo or bad value fails the command.
|
|
19
|
+
- A section present in ``app:`` but **not** registered → left untouched
|
|
20
|
+
(free-form, like an undocumented ``[tool.x]`` in ``pyproject.toml``).
|
|
21
|
+
- A registered section **absent** from ``app:`` → nothing to validate.
|
|
22
|
+
- A section whose package isn't installed → simply never discovered, so
|
|
23
|
+
validation degrades gracefully with no special-casing (the lenient
|
|
24
|
+
behaviour ``validate_module_configs`` gets from ``strict=False``).
|
|
25
|
+
|
|
26
|
+
This keeps the single ``app:`` boundary (feat-026 §8): registration maps
|
|
27
|
+
names *under* ``app:`` to schemas; it never opens new top-level keys.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import logging
|
|
33
|
+
from importlib.metadata import entry_points
|
|
34
|
+
from typing import TYPE_CHECKING
|
|
35
|
+
|
|
36
|
+
from pydantic import BaseModel, ValidationError
|
|
37
|
+
|
|
38
|
+
from agentforge_core.production.exceptions import ModuleError
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from collections.abc import Mapping
|
|
42
|
+
|
|
43
|
+
from agentforge_core.config.schema import AgentForgeConfig
|
|
44
|
+
|
|
45
|
+
_log = logging.getLogger("agentforge.config")
|
|
46
|
+
|
|
47
|
+
#: Entry-point group mapping an ``app.<section>`` name to a Pydantic
|
|
48
|
+
#: schema. Parallel to the ``agentforge.<category>`` groups the resolver
|
|
49
|
+
#: scans, but consumed here rather than by the runtime resolver (the
|
|
50
|
+
#: resolver skips this group — see ``resolver.discover``).
|
|
51
|
+
SECTIONS_GROUP = "agentforge.config_sections"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def discover_app_sections() -> dict[str, type[BaseModel]]:
|
|
55
|
+
"""Scan the ``agentforge.config_sections`` entry-point group.
|
|
56
|
+
|
|
57
|
+
Returns a mapping of section name → Pydantic model class. Entries
|
|
58
|
+
that fail to import, or whose target is not a ``BaseModel`` subclass,
|
|
59
|
+
are skipped with a warning. On a duplicate name across distributions
|
|
60
|
+
the first registration wins (matches the resolver's §8 conflict
|
|
61
|
+
rule).
|
|
62
|
+
"""
|
|
63
|
+
sections: dict[str, type[BaseModel]] = {}
|
|
64
|
+
for ep in entry_points(group=SECTIONS_GROUP):
|
|
65
|
+
if ep.name in sections:
|
|
66
|
+
_log.warning(
|
|
67
|
+
"duplicate app-config section %r ignored (first registration wins)",
|
|
68
|
+
ep.name,
|
|
69
|
+
)
|
|
70
|
+
continue
|
|
71
|
+
try:
|
|
72
|
+
model = ep.load()
|
|
73
|
+
except Exception as exc:
|
|
74
|
+
_log.warning(
|
|
75
|
+
"skipping app-config section %r: load failed (%s: %s)",
|
|
76
|
+
ep.name,
|
|
77
|
+
type(exc).__name__,
|
|
78
|
+
exc,
|
|
79
|
+
)
|
|
80
|
+
continue
|
|
81
|
+
if not (isinstance(model, type) and issubclass(model, BaseModel)):
|
|
82
|
+
_log.warning(
|
|
83
|
+
"skipping app-config section %r: %r is not a pydantic BaseModel subclass",
|
|
84
|
+
ep.name,
|
|
85
|
+
model,
|
|
86
|
+
)
|
|
87
|
+
continue
|
|
88
|
+
sections[ep.name] = model
|
|
89
|
+
return sections
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def validate_app_config(
|
|
93
|
+
cfg: AgentForgeConfig,
|
|
94
|
+
*,
|
|
95
|
+
sections: Mapping[str, type[BaseModel]] | None = None,
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Validate each registered ``app.<section>`` against its schema.
|
|
98
|
+
|
|
99
|
+
Mirrors :func:`validate_module_configs`. Only sections that are both
|
|
100
|
+
present in ``cfg.app`` and registered via an entry point are checked;
|
|
101
|
+
unregistered sections are left untouched (free-form). A registered
|
|
102
|
+
section that fails its schema always raises :class:`ModuleError` —
|
|
103
|
+
validation failures are fatal, the same as for module config.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
cfg: a loaded :class:`AgentForgeConfig` (post-``load_config``).
|
|
107
|
+
sections: pre-discovered section map. Defaults to
|
|
108
|
+
:func:`discover_app_sections`; injectable so tests (and
|
|
109
|
+
future callers with a cached registry) can supply their own.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
ModuleError: a registered ``app.<section>`` subtree fails its
|
|
113
|
+
schema.
|
|
114
|
+
"""
|
|
115
|
+
registry = sections if sections is not None else discover_app_sections()
|
|
116
|
+
for name, model in registry.items():
|
|
117
|
+
if name not in cfg.app:
|
|
118
|
+
continue
|
|
119
|
+
try:
|
|
120
|
+
model.model_validate(cfg.app[name])
|
|
121
|
+
except ValidationError as exc:
|
|
122
|
+
raise ModuleError(
|
|
123
|
+
f"app.{name} failed validation: {exc.errors(include_url=False)}"
|
|
124
|
+
) from exc
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
Resolution order (last wins) per spec §4.3:
|
|
4
4
|
|
|
5
5
|
1. Defaults from each Pydantic model.
|
|
6
|
-
2.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
2. Files pulled in via an `imports:` directive (feat-026 Phase 3),
|
|
7
|
+
lower precedence than the file that imports them.
|
|
8
|
+
3. agentforge.yaml on disk (if present).
|
|
9
|
+
4. agentforge.<env>.yaml (if AGENTFORGE_ENV set).
|
|
10
|
+
5. Env-var interpolation inside YAML values.
|
|
11
|
+
6. CLI / loader-API `--override agent.budget.usd=10` arguments.
|
|
12
|
+
7. Constructor kwargs to Agent (handled in `agentforge.agent`).
|
|
11
13
|
|
|
12
14
|
Env-var interpolation syntax (feat-001):
|
|
13
15
|
- `${VAR}` — required; raises at load if missing.
|
|
@@ -155,6 +157,65 @@ def _read_yaml(path: Path) -> dict[str, Any]:
|
|
|
155
157
|
return raw
|
|
156
158
|
|
|
157
159
|
|
|
160
|
+
#: Reserved top-level loader directive (feat-026 Phase 3): a list of
|
|
161
|
+
#: paths to other config files to pull in. Consumed by the loader
|
|
162
|
+
#: (popped before validation), so it never reaches `AgentForgeConfig`.
|
|
163
|
+
_IMPORTS_KEY = "imports"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _load_file_with_imports(path: Path, *, _visiting: tuple[Path, ...] = ()) -> dict[str, Any]:
|
|
167
|
+
"""Read a config file and resolve its ``imports:`` directive (feat-026
|
|
168
|
+
Phase 3 — pluggable config *sources*).
|
|
169
|
+
|
|
170
|
+
``imports:`` is a top-level list of paths to other config files,
|
|
171
|
+
resolved **relative to this file's directory** (absolute paths are
|
|
172
|
+
honoured as-is). Import path strings get ``${ENV}`` interpolation so
|
|
173
|
+
``imports: ["${EXTRA_CONFIG}"]`` works.
|
|
174
|
+
|
|
175
|
+
Precedence follows Spring Boot's ``spring.config.import``: an imported
|
|
176
|
+
file is **lower precedence than the file that imports it** — you
|
|
177
|
+
import shared defaults and override them locally. Within one
|
|
178
|
+
``imports:`` list, later entries beat earlier ones. Imports compose
|
|
179
|
+
transitively (an imported file may itself import); cycles raise
|
|
180
|
+
``ModuleError``.
|
|
181
|
+
|
|
182
|
+
The directive is consumed here — the returned mapping never contains
|
|
183
|
+
an ``imports`` key, so it doesn't collide with the root model's
|
|
184
|
+
``extra="forbid"`` and ``config show --resolved`` shows the merged
|
|
185
|
+
result, not the directive.
|
|
186
|
+
"""
|
|
187
|
+
resolved = path.resolve()
|
|
188
|
+
if resolved in _visiting:
|
|
189
|
+
chain = " -> ".join(str(p) for p in (*_visiting, resolved))
|
|
190
|
+
raise ModuleError(f"Circular config import detected: {chain}")
|
|
191
|
+
|
|
192
|
+
raw = _read_yaml(path)
|
|
193
|
+
imports = raw.pop(_IMPORTS_KEY, None)
|
|
194
|
+
if imports is None:
|
|
195
|
+
return raw
|
|
196
|
+
if not isinstance(imports, list):
|
|
197
|
+
raise ModuleError(
|
|
198
|
+
f"`imports:` in {path} must be a list of file paths; got {type(imports).__name__}."
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
merged: dict[str, Any] = {}
|
|
202
|
+
for entry in imports:
|
|
203
|
+
if not isinstance(entry, str):
|
|
204
|
+
raise ModuleError(f"`imports:` entries in {path} must be strings; got {entry!r}.")
|
|
205
|
+
import_path = Path(_interp(entry))
|
|
206
|
+
if not import_path.is_absolute():
|
|
207
|
+
import_path = path.parent / import_path
|
|
208
|
+
if not import_path.exists():
|
|
209
|
+
raise ModuleError(
|
|
210
|
+
f"Imported config file not found: {import_path} (imported by {path})."
|
|
211
|
+
)
|
|
212
|
+
imported = _load_file_with_imports(import_path, _visiting=(*_visiting, resolved))
|
|
213
|
+
merged = _deep_merge(merged, imported)
|
|
214
|
+
|
|
215
|
+
# The importing file's own keys win over everything it imports.
|
|
216
|
+
return _deep_merge(merged, raw)
|
|
217
|
+
|
|
218
|
+
|
|
158
219
|
def _env_overlay_path(base: Path, env: str) -> Path:
|
|
159
220
|
"""Compute the overlay path next to `base`: foo.yaml → foo.<env>.yaml."""
|
|
160
221
|
return base.with_suffix(f".{env}{base.suffix}")
|
|
@@ -191,14 +252,15 @@ def load_config(
|
|
|
191
252
|
if resolved_path is None or not resolved_path.exists():
|
|
192
253
|
merged: dict[str, Any] = {}
|
|
193
254
|
else:
|
|
194
|
-
merged =
|
|
255
|
+
merged = _load_file_with_imports(resolved_path)
|
|
195
256
|
# Layered env file overlays the base. Missing overlay is fine
|
|
196
|
-
# (env-without-file is just "use base").
|
|
257
|
+
# (env-without-file is just "use base"). The overlay is itself
|
|
258
|
+
# import-aware, so an `agentforge.<env>.yaml` may pull in files too.
|
|
197
259
|
resolved_env = env if env is not None else os.environ.get("AGENTFORGE_ENV")
|
|
198
260
|
if resolved_env:
|
|
199
261
|
overlay_path = _env_overlay_path(resolved_path, resolved_env)
|
|
200
262
|
if overlay_path.exists():
|
|
201
|
-
merged = _deep_merge(merged,
|
|
263
|
+
merged = _deep_merge(merged, _load_file_with_imports(overlay_path))
|
|
202
264
|
|
|
203
265
|
interpolated = _walk(merged)
|
|
204
266
|
if overrides:
|
|
@@ -15,9 +15,39 @@ is the data-side representation.
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
from pathlib import Path
|
|
18
|
-
from typing import Any, Literal
|
|
18
|
+
from typing import Any, Literal, TypeVar
|
|
19
19
|
|
|
20
|
-
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
20
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
21
|
+
|
|
22
|
+
_AppModelT = TypeVar("_AppModelT", bound=BaseModel)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _normalise_named_entry(value: Any) -> Any:
|
|
26
|
+
"""Normalise the terse YAML sugar for `name + config` entries into the
|
|
27
|
+
canonical mapping before strict validation (bug-019).
|
|
28
|
+
|
|
29
|
+
Three shapes are accepted; the first two are sugar:
|
|
30
|
+
|
|
31
|
+
- String form ``faithfulness`` → ``{"name": "faithfulness"}``
|
|
32
|
+
- Single-key mapping ``{geval: {rubric: "..."}}`` →
|
|
33
|
+
``{"name": "geval", "config": {"rubric": "..."}}``
|
|
34
|
+
- Canonical mapping ``{name: x, config: {...}}`` → returned unchanged.
|
|
35
|
+
|
|
36
|
+
Anything else is returned untouched so the model's own validation
|
|
37
|
+
raises a clear error. Used by `EvaluatorEntry` and `GuardrailEntry`,
|
|
38
|
+
so every list that holds them (`modules.evaluators` and guardrails'
|
|
39
|
+
`input` / `output` / `tool_gates`) accepts all three forms.
|
|
40
|
+
"""
|
|
41
|
+
if isinstance(value, str):
|
|
42
|
+
return {"name": value}
|
|
43
|
+
if isinstance(value, dict) and "name" not in value and len(value) == 1:
|
|
44
|
+
((key, cfg),) = value.items()
|
|
45
|
+
if isinstance(key, str):
|
|
46
|
+
entry: dict[str, Any] = {"name": key}
|
|
47
|
+
if cfg is not None:
|
|
48
|
+
entry["config"] = cfg
|
|
49
|
+
return entry
|
|
50
|
+
return value
|
|
21
51
|
|
|
22
52
|
|
|
23
53
|
class BudgetConfig(BaseModel):
|
|
@@ -178,13 +208,14 @@ class RetrievalConfig(BaseModel):
|
|
|
178
208
|
|
|
179
209
|
|
|
180
210
|
class EvaluatorEntry(BaseModel):
|
|
181
|
-
"""An entry in `modules.evaluators:`.
|
|
211
|
+
"""An entry in `modules.evaluators:`. Three YAML shapes are valid:
|
|
182
212
|
|
|
183
213
|
- String form: `- faithfulness` (just the name).
|
|
184
|
-
-
|
|
214
|
+
- Single-key mapping: `- faithfulness: {cost_cap_usd: 0.05, ...}`.
|
|
215
|
+
- Canonical mapping: `- {name: faithfulness, config: {...}}`.
|
|
185
216
|
|
|
186
|
-
|
|
187
|
-
`EvaluatorEntry(name=..., config={})` before validation.
|
|
217
|
+
The first two are sugar; a `mode="before"` validator normalises them
|
|
218
|
+
to `EvaluatorEntry(name=..., config={...})` before strict validation.
|
|
188
219
|
"""
|
|
189
220
|
|
|
190
221
|
model_config = ConfigDict(strict=True, extra="forbid")
|
|
@@ -192,6 +223,11 @@ class EvaluatorEntry(BaseModel):
|
|
|
192
223
|
name: str = Field(min_length=1)
|
|
193
224
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
194
225
|
|
|
226
|
+
@model_validator(mode="before")
|
|
227
|
+
@classmethod
|
|
228
|
+
def _coerce_sugar(cls, value: Any) -> Any:
|
|
229
|
+
return _normalise_named_entry(value)
|
|
230
|
+
|
|
195
231
|
|
|
196
232
|
class ObservabilityEntry(BaseModel):
|
|
197
233
|
"""An entry in `modules.observability:` — same shape as evaluator
|
|
@@ -226,13 +262,14 @@ class GuardrailPolicy(BaseModel):
|
|
|
226
262
|
class GuardrailEntry(BaseModel):
|
|
227
263
|
"""One entry inside `modules.guardrails.{input,output,tool_gates}`.
|
|
228
264
|
|
|
229
|
-
|
|
265
|
+
Three YAML shapes are valid (mirrors `EvaluatorEntry`):
|
|
230
266
|
|
|
231
267
|
- String form: `- prompt_injection_basic` (just the name).
|
|
232
|
-
-
|
|
268
|
+
- Single-key mapping: `- presidio: {entities: ["EMAIL_ADDRESS"]}`.
|
|
269
|
+
- Canonical mapping: `- {name: presidio, config: {...}}`.
|
|
233
270
|
|
|
234
|
-
|
|
235
|
-
validation.
|
|
271
|
+
The first two are sugar; a `mode="before"` validator normalises them
|
|
272
|
+
to `GuardrailEntry(name=..., config={...})` before strict validation.
|
|
236
273
|
"""
|
|
237
274
|
|
|
238
275
|
model_config = ConfigDict(strict=True, extra="forbid")
|
|
@@ -240,6 +277,11 @@ class GuardrailEntry(BaseModel):
|
|
|
240
277
|
name: str = Field(min_length=1)
|
|
241
278
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
242
279
|
|
|
280
|
+
@model_validator(mode="before")
|
|
281
|
+
@classmethod
|
|
282
|
+
def _coerce_sugar(cls, value: Any) -> Any:
|
|
283
|
+
return _normalise_named_entry(value)
|
|
284
|
+
|
|
243
285
|
|
|
244
286
|
class ChatHistoryDriverConfig(BaseModel):
|
|
245
287
|
"""`modules.chat.history:` — driver + config for a chat history
|
|
@@ -269,6 +311,14 @@ class ChatSessionConfig(BaseModel):
|
|
|
269
311
|
per_session_budget_usd: float | None = Field(default=None, ge=0.0)
|
|
270
312
|
idempotency_window_s: float = Field(default=60.0, ge=0.0)
|
|
271
313
|
concurrency: Literal["queue", "reject", "replace"] = "queue"
|
|
314
|
+
persist_steps: bool = True
|
|
315
|
+
"""When True (default), intermediate `act` / `observe` agent steps
|
|
316
|
+
are persisted to `ChatHistoryStore` as `role="assistant"` (with
|
|
317
|
+
`tool_calls`) and `role="tool"` (with `tool_call_id`) turns
|
|
318
|
+
respectively, in addition to the final assistant turn. Tool-using
|
|
319
|
+
chat agents need this on for the next turn's prompt to reflect
|
|
320
|
+
what tools ran. Opt out by setting to False when an external
|
|
321
|
+
consumer reconstructs history from another source (bug-010)."""
|
|
272
322
|
safety_mode: Literal["buffer-then-stream", "sentence-window", "stream-then-redact"] = (
|
|
273
323
|
"buffer-then-stream"
|
|
274
324
|
)
|
|
@@ -422,3 +472,34 @@ class AgentForgeConfig(BaseModel):
|
|
|
422
472
|
logging: LoggingConfig = Field(default_factory=LoggingConfig)
|
|
423
473
|
output: OutputConfig = Field(default_factory=OutputConfig)
|
|
424
474
|
guardrail_policy: GuardrailPolicy = Field(default_factory=GuardrailPolicy)
|
|
475
|
+
app: dict[str, Any] = Field(default_factory=dict)
|
|
476
|
+
"""Reserved namespace for **application** config (enh-002, feat-026
|
|
477
|
+
Phase 1). The framework accepts this subtree but does not interpret
|
|
478
|
+
it: a consuming agent puts its own config here and validates it with
|
|
479
|
+
its own Pydantic model via :meth:`app_as`. Every other top-level key
|
|
480
|
+
stays strict (`extra="forbid"`), so framework-key typos are still
|
|
481
|
+
caught. Values inside `app:` get `${ENV}` interpolation, env-file
|
|
482
|
+
layering, dotted-path overrides, and `config show --resolved` for
|
|
483
|
+
free — they ride the same loader passes as framework keys. The
|
|
484
|
+
framework performs no *registered-schema* validation inside `app:`
|
|
485
|
+
in Phase 1; that arrives in feat-026 Phase 2."""
|
|
486
|
+
|
|
487
|
+
def app_as(self, model: type[_AppModelT], key: str | None = None) -> _AppModelT:
|
|
488
|
+
"""Validate and return an application-config subtree.
|
|
489
|
+
|
|
490
|
+
`key=None` validates the whole `app:` mapping; otherwise the
|
|
491
|
+
`app[key]` subtree (missing key → empty mapping, so the caller's
|
|
492
|
+
model supplies its own defaults). The caller's model owns its own
|
|
493
|
+
strictness, so app-key typos surface here — strictness is
|
|
494
|
+
delegated into `app:`, not lost.
|
|
495
|
+
|
|
496
|
+
Example::
|
|
497
|
+
|
|
498
|
+
class GraphConfig(BaseModel):
|
|
499
|
+
store: StoreConfig
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
graph_cfg = cfg.app_as(GraphConfig, "graph")
|
|
503
|
+
"""
|
|
504
|
+
raw = self.app if key is None else self.app.get(key, {})
|
|
505
|
+
return model.model_validate(raw)
|
|
@@ -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.3.0}/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.
|