agentforge-core 0.2.1__py3-none-any.whl
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/__init__.py +228 -0
- agentforge_core/_bm25.py +132 -0
- agentforge_core/config/__init__.py +62 -0
- agentforge_core/config/loader.py +239 -0
- agentforge_core/config/module_schemas.py +208 -0
- agentforge_core/config/schema.py +424 -0
- agentforge_core/contracts/__init__.py +52 -0
- agentforge_core/contracts/auth.py +33 -0
- agentforge_core/contracts/chat.py +118 -0
- agentforge_core/contracts/embedding.py +71 -0
- agentforge_core/contracts/evaluator.py +56 -0
- agentforge_core/contracts/finding.py +39 -0
- agentforge_core/contracts/graph_store.py +180 -0
- agentforge_core/contracts/guardrails.py +129 -0
- agentforge_core/contracts/llm.py +152 -0
- agentforge_core/contracts/memory.py +113 -0
- agentforge_core/contracts/migrator.py +120 -0
- agentforge_core/contracts/renderer.py +57 -0
- agentforge_core/contracts/reranker.py +91 -0
- agentforge_core/contracts/strategy.py +70 -0
- agentforge_core/contracts/task.py +73 -0
- agentforge_core/contracts/tool.py +71 -0
- agentforge_core/contracts/vector_store.py +151 -0
- agentforge_core/migrations/__init__.py +14 -0
- agentforge_core/migrations/discover.py +77 -0
- agentforge_core/migrations/template.py +34 -0
- agentforge_core/observability/__init__.py +18 -0
- agentforge_core/observability/tracing.py +37 -0
- agentforge_core/production/__init__.py +77 -0
- agentforge_core/production/budget.py +134 -0
- agentforge_core/production/exceptions.py +136 -0
- agentforge_core/production/fallback.py +321 -0
- agentforge_core/production/log_filter.py +49 -0
- agentforge_core/production/log_format.py +117 -0
- agentforge_core/production/run_context.py +108 -0
- agentforge_core/py.typed +0 -0
- agentforge_core/resolver/__init__.py +38 -0
- agentforge_core/resolver/discover.py +145 -0
- agentforge_core/resolver/resolve.py +168 -0
- agentforge_core/testing/__init__.py +45 -0
- agentforge_core/testing/conformance.py +1138 -0
- agentforge_core/values/__init__.py +103 -0
- agentforge_core/values/auth.py +20 -0
- agentforge_core/values/chat.py +131 -0
- agentforge_core/values/claim.py +30 -0
- agentforge_core/values/graph.py +136 -0
- agentforge_core/values/guardrails.py +49 -0
- agentforge_core/values/manifest.py +129 -0
- agentforge_core/values/messages.py +153 -0
- agentforge_core/values/module.py +40 -0
- agentforge_core/values/pipeline.py +43 -0
- agentforge_core/values/retrieval.py +53 -0
- agentforge_core/values/state.py +118 -0
- agentforge_core/values/vector.py +59 -0
- agentforge_core-0.2.1.dist-info/METADATA +66 -0
- agentforge_core-0.2.1.dist-info/RECORD +58 -0
- agentforge_core-0.2.1.dist-info/WHEEL +4 -0
- agentforge_core-0.2.1.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""`RunContext` and the `current_run()` ContextVar.
|
|
2
|
+
|
|
3
|
+
Per ADR-0010, every agent run carries a `run_id` propagated through
|
|
4
|
+
async tasks via `contextvars.ContextVar`. Every log line, every span,
|
|
5
|
+
every tool call, every claim record carries this id — there is no path
|
|
6
|
+
to a log line without one.
|
|
7
|
+
|
|
8
|
+
Generation: ULID (sortable, monotonic, 26 chars). Imported from
|
|
9
|
+
`python-ulid`.
|
|
10
|
+
|
|
11
|
+
Idempotency keys are derived from the run's `idempotency_seed` plus
|
|
12
|
+
caller-supplied parts. Same parts within the same run produce the same
|
|
13
|
+
key; different parts produce different keys; different runs produce
|
|
14
|
+
different keys for the same parts.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import hashlib
|
|
20
|
+
from contextvars import ContextVar, Token
|
|
21
|
+
from datetime import UTC, datetime
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
25
|
+
from ulid import ULID
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RunContext(BaseModel):
|
|
29
|
+
"""Per-run correlation primitive bound to the current async task."""
|
|
30
|
+
|
|
31
|
+
model_config = ConfigDict(strict=True, validate_assignment=True)
|
|
32
|
+
|
|
33
|
+
run_id: str
|
|
34
|
+
parent_run_id: str | None = None
|
|
35
|
+
started_at: datetime
|
|
36
|
+
idempotency_seed: str
|
|
37
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
def idempotency_key_for(self, *parts: object) -> str:
|
|
40
|
+
"""Stable key derived from `(idempotency_seed, *parts)`.
|
|
41
|
+
|
|
42
|
+
Tools that mutate external state read this to safely retry within
|
|
43
|
+
a single run. Same `parts` → same key; different `parts` →
|
|
44
|
+
different key; different run → different key.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
64-hex-character SHA-256 digest.
|
|
48
|
+
"""
|
|
49
|
+
joined = self.idempotency_seed
|
|
50
|
+
for part in parts:
|
|
51
|
+
joined += "|" + str(part)
|
|
52
|
+
return hashlib.sha256(joined.encode("utf-8")).hexdigest()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_current_run: ContextVar[RunContext | None] = ContextVar("agentforge_current_run", default=None)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def current_run() -> RunContext:
|
|
59
|
+
"""Return the live `RunContext` bound to this async task.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
RuntimeError: no run is active. `current_run()` is only callable
|
|
63
|
+
from within an `Agent.run()` invocation.
|
|
64
|
+
"""
|
|
65
|
+
ctx = _current_run.get()
|
|
66
|
+
if ctx is None:
|
|
67
|
+
raise RuntimeError(
|
|
68
|
+
"No active RunContext. current_run() can only be called inside "
|
|
69
|
+
"Agent.run() (or a tool / hook invoked from one)."
|
|
70
|
+
)
|
|
71
|
+
return ctx
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def new_run(
|
|
75
|
+
*,
|
|
76
|
+
parent_run_id: str | None = None,
|
|
77
|
+
task: str | None = None,
|
|
78
|
+
) -> RunContext:
|
|
79
|
+
"""Create a new `RunContext` with a fresh ULID `run_id`.
|
|
80
|
+
|
|
81
|
+
Does NOT bind it to the ContextVar — call `bind_run` for that.
|
|
82
|
+
"""
|
|
83
|
+
run_id = str(ULID())
|
|
84
|
+
seed_input = f"{run_id}|{task or ''}"
|
|
85
|
+
seed = hashlib.sha256(seed_input.encode("utf-8")).hexdigest()[:32]
|
|
86
|
+
return RunContext(
|
|
87
|
+
run_id=run_id,
|
|
88
|
+
parent_run_id=parent_run_id,
|
|
89
|
+
started_at=datetime.now(UTC),
|
|
90
|
+
idempotency_seed=seed,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def bind_run(ctx: RunContext) -> Token[RunContext | None]:
|
|
95
|
+
"""Bind a `RunContext` to the current async scope.
|
|
96
|
+
|
|
97
|
+
Returns the `Token` that `reset_run` consumes. Always pair with
|
|
98
|
+
`reset_run` in a try/finally to avoid leaking context across runs.
|
|
99
|
+
"""
|
|
100
|
+
return _current_run.set(ctx)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def reset_run(token: Token[RunContext | None]) -> None:
|
|
104
|
+
"""Reset the ContextVar to its prior value.
|
|
105
|
+
|
|
106
|
+
Pair with `bind_run`; safe to call exactly once per token.
|
|
107
|
+
"""
|
|
108
|
+
_current_run.reset(token)
|
agentforge_core/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Module resolver — maps name → registered class.
|
|
2
|
+
|
|
3
|
+
Per ADR-0004, modules normally register via Python entry points
|
|
4
|
+
(loaded by feat-010). For feat-001 we ship the in-process registry
|
|
5
|
+
that the entry-point loader will populate. Anyone can register a
|
|
6
|
+
class manually with `@register("strategies", "my-loop")` for an
|
|
7
|
+
in-repo or test-only module.
|
|
8
|
+
|
|
9
|
+
A model identifier string `"<provider>:<model_id>"` is parsed by
|
|
10
|
+
`parse_model_string`; the leading provider is looked up as an entry
|
|
11
|
+
in `agentforge.providers`, the trailing piece is treated as the
|
|
12
|
+
model id and passed through to the provider constructor (per
|
|
13
|
+
feat-003).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from agentforge_core.resolver.discover import (
|
|
19
|
+
discover_entry_points,
|
|
20
|
+
reset_discovery,
|
|
21
|
+
)
|
|
22
|
+
from agentforge_core.resolver.resolve import (
|
|
23
|
+
Resolver,
|
|
24
|
+
parse_model_string,
|
|
25
|
+
register,
|
|
26
|
+
register_embedding_provider,
|
|
27
|
+
register_provider,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"Resolver",
|
|
32
|
+
"discover_entry_points",
|
|
33
|
+
"parse_model_string",
|
|
34
|
+
"register",
|
|
35
|
+
"register_embedding_provider",
|
|
36
|
+
"register_provider",
|
|
37
|
+
"reset_discovery",
|
|
38
|
+
]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Entry-point discovery for feat-010.
|
|
2
|
+
|
|
3
|
+
Scans `importlib.metadata.entry_points()` for groups starting with
|
|
4
|
+
`agentforge.` and registers each entry against the global resolver.
|
|
5
|
+
A group named `agentforge.<category>` (e.g. `agentforge.providers`,
|
|
6
|
+
`agentforge.memory`) maps to the resolver's `<category>` slot.
|
|
7
|
+
|
|
8
|
+
The scan runs lazily — `Resolver.resolve` calls `ensure_discovered()`
|
|
9
|
+
on first use. Tests that want to start with a clean slate can call
|
|
10
|
+
`Resolver.global_().clear()` (also resets the discovery cache).
|
|
11
|
+
|
|
12
|
+
Conflict handling: if two distributions register under the same
|
|
13
|
+
`(category, name)` pair, the first one to register wins (entry-point
|
|
14
|
+
iteration order is the source). The resolver's own `register`
|
|
15
|
+
method raises `ModuleError` when the second registration arrives at
|
|
16
|
+
a different class — we catch that, log, and let the first win. This
|
|
17
|
+
matches the spec's §8 "Conflict resolution" entry while keeping the
|
|
18
|
+
runtime predictable.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
from importlib.metadata import entry_points
|
|
25
|
+
from typing import TYPE_CHECKING
|
|
26
|
+
|
|
27
|
+
from agentforge_core.production.exceptions import ModuleError
|
|
28
|
+
from agentforge_core.values.module import ModuleInfo
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from agentforge_core.resolver.resolve import Resolver
|
|
32
|
+
|
|
33
|
+
_log = logging.getLogger("agentforge.resolver")
|
|
34
|
+
|
|
35
|
+
_GROUP_PREFIX = "agentforge."
|
|
36
|
+
|
|
37
|
+
# Module-level state held on a mutable list to avoid `global` (PLW0603).
|
|
38
|
+
_discovered: list[bool] = [False]
|
|
39
|
+
# Cache of resolved ModuleInfo per registered entry — keyed by
|
|
40
|
+
# `(category, name)`. Populated alongside the registry by
|
|
41
|
+
# `discover_entry_points`.
|
|
42
|
+
_module_info_cache: dict[tuple[str, str], ModuleInfo] = {}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def ensure_discovered(resolver: Resolver) -> None:
|
|
46
|
+
"""Lazy hook — runs `discover_entry_points` once per resolver lifetime.
|
|
47
|
+
|
|
48
|
+
Resolver methods that need entry-point-registered modules call
|
|
49
|
+
this on entry. Safe to call repeatedly; only the first call does
|
|
50
|
+
work.
|
|
51
|
+
"""
|
|
52
|
+
if _discovered[0]:
|
|
53
|
+
return
|
|
54
|
+
discover_entry_points(resolver)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def discover_entry_points(resolver: Resolver, *, force: bool = False) -> int:
|
|
58
|
+
"""Scan `agentforge.*` entry points and register them on `resolver`.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
resolver: Target resolver — typically `Resolver.global_()`.
|
|
62
|
+
force: When `True`, re-scan even if discovery has already run
|
|
63
|
+
(used by tests and by `Resolver.clear()`).
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Number of entries registered (excluding conflicts skipped).
|
|
67
|
+
"""
|
|
68
|
+
if _discovered[0] and not force:
|
|
69
|
+
return 0
|
|
70
|
+
registered = 0
|
|
71
|
+
eps = entry_points()
|
|
72
|
+
# `entry_points()` returns an `EntryPoints` selectable view on
|
|
73
|
+
# 3.10+; iterate by group prefix.
|
|
74
|
+
for ep in eps:
|
|
75
|
+
if not ep.group.startswith(_GROUP_PREFIX):
|
|
76
|
+
continue
|
|
77
|
+
category = ep.group[len(_GROUP_PREFIX) :]
|
|
78
|
+
try:
|
|
79
|
+
cls = ep.load()
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
_log.warning(
|
|
82
|
+
"skipping entry point %s.%s: load failed (%s: %s)",
|
|
83
|
+
ep.group,
|
|
84
|
+
ep.name,
|
|
85
|
+
type(exc).__name__,
|
|
86
|
+
exc,
|
|
87
|
+
)
|
|
88
|
+
continue
|
|
89
|
+
if not isinstance(cls, type):
|
|
90
|
+
_log.warning(
|
|
91
|
+
"skipping entry point %s.%s: target %r is not a class",
|
|
92
|
+
ep.group,
|
|
93
|
+
ep.name,
|
|
94
|
+
cls,
|
|
95
|
+
)
|
|
96
|
+
continue
|
|
97
|
+
try:
|
|
98
|
+
resolver.register(category, ep.name, cls)
|
|
99
|
+
except ModuleError as exc:
|
|
100
|
+
# Another distribution already registered the same key —
|
|
101
|
+
# first wins per spec §8. Log and move on.
|
|
102
|
+
_log.warning(
|
|
103
|
+
"entry-point conflict %s.%s ignored: %s",
|
|
104
|
+
ep.group,
|
|
105
|
+
ep.name,
|
|
106
|
+
exc,
|
|
107
|
+
)
|
|
108
|
+
continue
|
|
109
|
+
# Carry the source distribution metadata for `list_installed`.
|
|
110
|
+
dist = ep.dist
|
|
111
|
+
info = ModuleInfo(
|
|
112
|
+
category=category,
|
|
113
|
+
name=ep.name,
|
|
114
|
+
package=dist.name if dist is not None else None,
|
|
115
|
+
version=dist.version if dist is not None else None,
|
|
116
|
+
cls_qualname=f"{cls.__module__}.{cls.__qualname__}",
|
|
117
|
+
)
|
|
118
|
+
_module_info_cache[(category, ep.name)] = info
|
|
119
|
+
registered += 1
|
|
120
|
+
_discovered[0] = True
|
|
121
|
+
return registered
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def reset_discovery() -> None:
|
|
125
|
+
"""Clear the discovery cache. Called from `Resolver.clear()` and
|
|
126
|
+
by tests that want a clean slate."""
|
|
127
|
+
_discovered[0] = False
|
|
128
|
+
_module_info_cache.clear()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def module_info_for(category: str, name: str, cls: type) -> ModuleInfo:
|
|
132
|
+
"""Return cached `ModuleInfo` if the entry was discovered via
|
|
133
|
+
entry points; otherwise synthesise one from `cls.__module__` /
|
|
134
|
+
`__qualname__` (for `@register`-registered classes).
|
|
135
|
+
"""
|
|
136
|
+
cached = _module_info_cache.get((category, name))
|
|
137
|
+
if cached is not None:
|
|
138
|
+
return cached
|
|
139
|
+
return ModuleInfo(
|
|
140
|
+
category=category,
|
|
141
|
+
name=name,
|
|
142
|
+
package=None,
|
|
143
|
+
version=None,
|
|
144
|
+
cls_qualname=f"{cls.__module__}.{cls.__qualname__}",
|
|
145
|
+
)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""In-process module registry + model-string parsing.
|
|
2
|
+
|
|
3
|
+
feat-010 will extend `Resolver` to scan `importlib.metadata` entry
|
|
4
|
+
points; until then, anything in this registry was put there by an
|
|
5
|
+
explicit `register(...)` call. The two-phase design lets us write
|
|
6
|
+
integration tests in feat-001 without entry-point machinery.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
13
|
+
|
|
14
|
+
from agentforge_core.production.exceptions import ModuleError
|
|
15
|
+
from agentforge_core.resolver.discover import (
|
|
16
|
+
ensure_discovered,
|
|
17
|
+
module_info_for,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from agentforge_core.values.module import ModuleInfo
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T", bound=type)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Resolver:
|
|
27
|
+
"""Maps `(category, name) -> registered class`.
|
|
28
|
+
|
|
29
|
+
A single global instance is shared by `register()` and `Agent`.
|
|
30
|
+
Tests reset it via `Resolver.global_().clear()`.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
self._registry: dict[tuple[str, str], type] = {}
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def global_(cls) -> Resolver:
|
|
38
|
+
global _GLOBAL_RESOLVER # noqa: PLW0603 — singleton intentional
|
|
39
|
+
if _GLOBAL_RESOLVER is None:
|
|
40
|
+
_GLOBAL_RESOLVER = cls()
|
|
41
|
+
return _GLOBAL_RESOLVER
|
|
42
|
+
|
|
43
|
+
def register(self, category: str, name: str, cls: type) -> None:
|
|
44
|
+
key = (category, name)
|
|
45
|
+
if key in self._registry and self._registry[key] is not cls:
|
|
46
|
+
raise ModuleError(
|
|
47
|
+
f"Cannot register {cls.__name__} as {category}:{name}; "
|
|
48
|
+
f"already registered to {self._registry[key].__name__}."
|
|
49
|
+
)
|
|
50
|
+
self._registry[key] = cls
|
|
51
|
+
|
|
52
|
+
def resolve(self, category: str, name: str) -> type:
|
|
53
|
+
# Lazy entry-point discovery (feat-010): the first call to
|
|
54
|
+
# `resolve()` triggers a one-shot scan of installed
|
|
55
|
+
# `agentforge.*` entry points so packages-by-pip-install are
|
|
56
|
+
# available without explicit imports.
|
|
57
|
+
ensure_discovered(self)
|
|
58
|
+
key = (category, name)
|
|
59
|
+
if key not in self._registry:
|
|
60
|
+
available = sorted(n for c, n in self._registry if c == category)
|
|
61
|
+
raise ModuleError(
|
|
62
|
+
f"No module registered for {category}:{name!r}. "
|
|
63
|
+
f"Registered {category}: {available or '(none)'}. "
|
|
64
|
+
f"Install the relevant agentforge-* package or register "
|
|
65
|
+
f"a custom class with @register('{category}', '{name}')."
|
|
66
|
+
)
|
|
67
|
+
return self._registry[key]
|
|
68
|
+
|
|
69
|
+
def list_(self, category: str | None = None) -> list[tuple[str, str, type]]:
|
|
70
|
+
ensure_discovered(self)
|
|
71
|
+
items = [(c, n, cls) for (c, n), cls in self._registry.items()]
|
|
72
|
+
if category is not None:
|
|
73
|
+
items = [item for item in items if item[0] == category]
|
|
74
|
+
return sorted(items, key=lambda item: (item[0], item[1]))
|
|
75
|
+
|
|
76
|
+
def list_installed(self, category: str | None = None) -> list[ModuleInfo]:
|
|
77
|
+
"""Return `ModuleInfo` for every registered module (feat-010).
|
|
78
|
+
|
|
79
|
+
Triggers entry-point discovery first so the returned list
|
|
80
|
+
reflects every `agentforge-*` package pip-installed in the
|
|
81
|
+
active environment plus anything registered manually via
|
|
82
|
+
`@register`.
|
|
83
|
+
"""
|
|
84
|
+
ensure_discovered(self)
|
|
85
|
+
items: list[ModuleInfo] = []
|
|
86
|
+
for (cat, name), cls in self._registry.items():
|
|
87
|
+
if category is not None and cat != category:
|
|
88
|
+
continue
|
|
89
|
+
items.append(module_info_for(cat, name, cls))
|
|
90
|
+
return sorted(items, key=lambda m: (m.category, m.name))
|
|
91
|
+
|
|
92
|
+
def clear(self) -> None:
|
|
93
|
+
"""Empty the in-process registry.
|
|
94
|
+
|
|
95
|
+
Does NOT reset the entry-point discovery cache — call
|
|
96
|
+
`reset_discovery()` separately if you need a fresh scan on
|
|
97
|
+
the next `resolve()`. Typical tests want a clean slate
|
|
98
|
+
without re-triggering discovery; tests that install fake
|
|
99
|
+
entry points opt in to the rediscovery explicitly.
|
|
100
|
+
"""
|
|
101
|
+
self._registry.clear()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
_GLOBAL_RESOLVER: Resolver | None = None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def register(category: str, name: str) -> Callable[[T], T]:
|
|
108
|
+
"""Decorator: register a class under `(category, name)` in the global resolver.
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
@register("strategies", "my-loop")
|
|
112
|
+
class MyLoop(ReasoningStrategy):
|
|
113
|
+
...
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def decorator(cls: T) -> T:
|
|
117
|
+
Resolver.global_().register(category, name, cls)
|
|
118
|
+
return cls
|
|
119
|
+
|
|
120
|
+
return decorator
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def register_provider(name: str) -> Callable[[T], T]:
|
|
124
|
+
"""Decorator: register an `LLMClient` subclass as a model provider.
|
|
125
|
+
|
|
126
|
+
Convenience wrapper around `register("providers", name)` — used by
|
|
127
|
+
every concrete provider package (`agentforge-bedrock`,
|
|
128
|
+
`agentforge-anthropic`, ...). The provider name corresponds to the
|
|
129
|
+
leading token in a model string: `register_provider("bedrock")`
|
|
130
|
+
enables `Agent(model="bedrock:...")`.
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
@register_provider("bedrock")
|
|
134
|
+
class BedrockClient(LLMClient):
|
|
135
|
+
def __init__(self, *, model_id: str, region: str = "us-east-1") -> None:
|
|
136
|
+
...
|
|
137
|
+
"""
|
|
138
|
+
return register("providers", name)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def register_embedding_provider(name: str) -> Callable[[T], T]:
|
|
142
|
+
"""Decorator: register an `EmbeddingClient` subclass as an embedding provider.
|
|
143
|
+
|
|
144
|
+
Mirrors `register_provider` but under the `"embeddings"` category
|
|
145
|
+
so chat models and embedding models can share a provider name
|
|
146
|
+
without colliding (e.g. `bedrock:anthropic.claude-...` for chat,
|
|
147
|
+
`embeddings:bedrock:amazon.titan-embed-text-v2:0` for embeddings).
|
|
148
|
+
"""
|
|
149
|
+
return register("embeddings", name)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def parse_model_string(model_str: str) -> tuple[str, str]:
|
|
153
|
+
"""Parse `"<provider>:<model_id>"` into `(provider, model_id)`.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
ModuleError: if the string does not contain a `:`.
|
|
157
|
+
"""
|
|
158
|
+
if ":" not in model_str:
|
|
159
|
+
raise ModuleError(
|
|
160
|
+
f"Invalid model string {model_str!r}: expected '<provider>:<model_id>' "
|
|
161
|
+
f"(for example, 'anthropic:claude-sonnet-4.7')."
|
|
162
|
+
)
|
|
163
|
+
provider, _, model_id = model_str.partition(":")
|
|
164
|
+
if not provider or not model_id:
|
|
165
|
+
raise ModuleError(
|
|
166
|
+
f"Invalid model string {model_str!r}: provider and model_id must both be non-empty."
|
|
167
|
+
)
|
|
168
|
+
return provider, model_id
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Testing utilities — conformance suites that every driver must pass.
|
|
2
|
+
|
|
3
|
+
Per ADR-0007 and feat-016, every ABC in `agentforge-core` ships a
|
|
4
|
+
shared conformance suite. External module packages (e.g.
|
|
5
|
+
`agentforge-memory-postgres`) import these helpers to verify their
|
|
6
|
+
drivers against the locked contract.
|
|
7
|
+
|
|
8
|
+
The suites live in core (rather than the runtime package) so module
|
|
9
|
+
authors only need `agentforge-core` to test conformance — they don't
|
|
10
|
+
have to depend on the full runtime.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from agentforge_core.testing.conformance import (
|
|
16
|
+
run_chat_history_conformance,
|
|
17
|
+
run_embedding_conformance,
|
|
18
|
+
run_graph_conformance,
|
|
19
|
+
run_hybrid_search_conformance,
|
|
20
|
+
run_input_validator_conformance,
|
|
21
|
+
run_memory_conformance,
|
|
22
|
+
run_output_validator_conformance,
|
|
23
|
+
run_reranker_conformance,
|
|
24
|
+
run_strategy_conformance,
|
|
25
|
+
run_task_conformance,
|
|
26
|
+
run_tool_gate_conformance,
|
|
27
|
+
run_truncation_conformance,
|
|
28
|
+
run_vector_conformance,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"run_chat_history_conformance",
|
|
33
|
+
"run_embedding_conformance",
|
|
34
|
+
"run_graph_conformance",
|
|
35
|
+
"run_hybrid_search_conformance",
|
|
36
|
+
"run_input_validator_conformance",
|
|
37
|
+
"run_memory_conformance",
|
|
38
|
+
"run_output_validator_conformance",
|
|
39
|
+
"run_reranker_conformance",
|
|
40
|
+
"run_strategy_conformance",
|
|
41
|
+
"run_task_conformance",
|
|
42
|
+
"run_tool_gate_conformance",
|
|
43
|
+
"run_truncation_conformance",
|
|
44
|
+
"run_vector_conformance",
|
|
45
|
+
]
|