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.
Files changed (58) hide show
  1. agentforge_core/__init__.py +228 -0
  2. agentforge_core/_bm25.py +132 -0
  3. agentforge_core/config/__init__.py +62 -0
  4. agentforge_core/config/loader.py +239 -0
  5. agentforge_core/config/module_schemas.py +208 -0
  6. agentforge_core/config/schema.py +424 -0
  7. agentforge_core/contracts/__init__.py +52 -0
  8. agentforge_core/contracts/auth.py +33 -0
  9. agentforge_core/contracts/chat.py +118 -0
  10. agentforge_core/contracts/embedding.py +71 -0
  11. agentforge_core/contracts/evaluator.py +56 -0
  12. agentforge_core/contracts/finding.py +39 -0
  13. agentforge_core/contracts/graph_store.py +180 -0
  14. agentforge_core/contracts/guardrails.py +129 -0
  15. agentforge_core/contracts/llm.py +152 -0
  16. agentforge_core/contracts/memory.py +113 -0
  17. agentforge_core/contracts/migrator.py +120 -0
  18. agentforge_core/contracts/renderer.py +57 -0
  19. agentforge_core/contracts/reranker.py +91 -0
  20. agentforge_core/contracts/strategy.py +70 -0
  21. agentforge_core/contracts/task.py +73 -0
  22. agentforge_core/contracts/tool.py +71 -0
  23. agentforge_core/contracts/vector_store.py +151 -0
  24. agentforge_core/migrations/__init__.py +14 -0
  25. agentforge_core/migrations/discover.py +77 -0
  26. agentforge_core/migrations/template.py +34 -0
  27. agentforge_core/observability/__init__.py +18 -0
  28. agentforge_core/observability/tracing.py +37 -0
  29. agentforge_core/production/__init__.py +77 -0
  30. agentforge_core/production/budget.py +134 -0
  31. agentforge_core/production/exceptions.py +136 -0
  32. agentforge_core/production/fallback.py +321 -0
  33. agentforge_core/production/log_filter.py +49 -0
  34. agentforge_core/production/log_format.py +117 -0
  35. agentforge_core/production/run_context.py +108 -0
  36. agentforge_core/py.typed +0 -0
  37. agentforge_core/resolver/__init__.py +38 -0
  38. agentforge_core/resolver/discover.py +145 -0
  39. agentforge_core/resolver/resolve.py +168 -0
  40. agentforge_core/testing/__init__.py +45 -0
  41. agentforge_core/testing/conformance.py +1138 -0
  42. agentforge_core/values/__init__.py +103 -0
  43. agentforge_core/values/auth.py +20 -0
  44. agentforge_core/values/chat.py +131 -0
  45. agentforge_core/values/claim.py +30 -0
  46. agentforge_core/values/graph.py +136 -0
  47. agentforge_core/values/guardrails.py +49 -0
  48. agentforge_core/values/manifest.py +129 -0
  49. agentforge_core/values/messages.py +153 -0
  50. agentforge_core/values/module.py +40 -0
  51. agentforge_core/values/pipeline.py +43 -0
  52. agentforge_core/values/retrieval.py +53 -0
  53. agentforge_core/values/state.py +118 -0
  54. agentforge_core/values/vector.py +59 -0
  55. agentforge_core-0.2.1.dist-info/METADATA +66 -0
  56. agentforge_core-0.2.1.dist-info/RECORD +58 -0
  57. agentforge_core-0.2.1.dist-info/WHEEL +4 -0
  58. agentforge_core-0.2.1.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,208 @@
1
+ """Module-side config schema validation (feat-012 §4.4).
2
+
3
+ Per spec §4.4, each module ships its own Pydantic config schema; the
4
+ framework composes them at load time so `modules.memory.config:`,
5
+ `modules.evaluators[*].config:`, etc. get validated against the
6
+ right shape.
7
+
8
+ Module convention: a class registered with the resolver MAY declare
9
+ a class-level attribute:
10
+
11
+ class PostgresMemoryStore(MemoryStore):
12
+ config_schema: ClassVar[type[BaseModel] | None] = PostgresMemoryConfig
13
+
14
+ The validator walks the resolved config's `modules.*` blocks, looks
15
+ each entry's class up in the resolver, reads `cls.config_schema`,
16
+ and runs `schema.model_validate(entry.config)` if present. Modules
17
+ without a schema (the common case) accept any dict — the resolver
18
+ still confirms the class is registered (fail-at-startup, P11).
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import TYPE_CHECKING, Any
24
+
25
+ from pydantic import ValidationError
26
+
27
+ from agentforge_core.config.schema import (
28
+ AgentForgeConfig,
29
+ EvaluatorEntry,
30
+ ModuleEntry,
31
+ ObservabilityEntry,
32
+ PipelineTaskEntry,
33
+ RerankerEntry,
34
+ )
35
+ from agentforge_core.production.exceptions import ModuleError
36
+
37
+ if TYPE_CHECKING:
38
+ from agentforge_core.resolver import Resolver
39
+
40
+
41
+ def validate_module_configs(
42
+ cfg: AgentForgeConfig,
43
+ *,
44
+ resolver: Resolver | None = None,
45
+ strict: bool = True,
46
+ ) -> None:
47
+ """Validate each `modules.*` block against its module's schema.
48
+
49
+ Args:
50
+ cfg: A loaded `AgentForgeConfig` (post-`load_config`).
51
+ resolver: Resolver to look classes up in. Defaults to the
52
+ global resolver.
53
+ strict: When True (default), missing modules raise
54
+ `ModuleError`. When False, missing modules are skipped
55
+ (useful for `agentforge config validate` against a config
56
+ that references not-yet-installed packages).
57
+
58
+ Raises:
59
+ ModuleError: a referenced module isn't registered (strict
60
+ mode) or a config dict fails its module's schema.
61
+ """
62
+ # Late import — avoids a load-order cycle with the values /
63
+ # contracts modules that the resolver pulls in.
64
+ from agentforge_core.resolver import Resolver as _Resolver # noqa: PLC0415
65
+
66
+ r = resolver if resolver is not None else _Resolver.global_()
67
+
68
+ if cfg.modules.memory is not None:
69
+ _validate_one(r, "memory", cfg.modules.memory, strict=strict)
70
+ if cfg.modules.graph is not None:
71
+ _validate_one(r, "graph", cfg.modules.graph, strict=strict)
72
+ if cfg.modules.retriever is not None:
73
+ _validate_one(r, "retriever", cfg.modules.retriever, strict=strict)
74
+
75
+ for eval_entry in cfg.modules.evaluators:
76
+ _validate_named(r, "evaluators", eval_entry, strict=strict)
77
+ for obs_entry in cfg.modules.observability:
78
+ _validate_named(r, "hooks", obs_entry, strict=strict)
79
+ for proto_entry in cfg.modules.protocols:
80
+ _validate_named(r, "protocols", proto_entry, strict=strict)
81
+ if cfg.modules.pipeline is not None:
82
+ for task_entry in cfg.modules.pipeline.tasks:
83
+ _validate_named(r, "tasks", task_entry, strict=strict)
84
+ if cfg.modules.chat is not None:
85
+ if cfg.modules.chat.history is not None:
86
+ _validate_driver(
87
+ r,
88
+ "chat.history",
89
+ cfg.modules.chat.history.driver,
90
+ cfg.modules.chat.history.config,
91
+ strict=strict,
92
+ )
93
+ if cfg.modules.chat.truncation is not None:
94
+ _validate_driver(
95
+ r,
96
+ "chat.truncation",
97
+ cfg.modules.chat.truncation.strategy,
98
+ cfg.modules.chat.truncation.config,
99
+ strict=strict,
100
+ )
101
+
102
+ if cfg.retrieval is not None:
103
+ _validate_retrieval(r, cfg, strict=strict)
104
+
105
+
106
+ def _validate_one(
107
+ resolver: Resolver,
108
+ category: str,
109
+ entry: ModuleEntry,
110
+ *,
111
+ strict: bool,
112
+ ) -> None:
113
+ """Validate a `driver + config`-shaped module entry."""
114
+ try:
115
+ cls = resolver.resolve(category, entry.driver)
116
+ except ModuleError:
117
+ if strict:
118
+ raise
119
+ return
120
+ schema = _read_config_schema(cls)
121
+ if schema is None:
122
+ return
123
+ try:
124
+ schema.model_validate(entry.config)
125
+ except ValidationError as exc:
126
+ raise ModuleError(
127
+ f"modules.{category}.config failed validation for driver "
128
+ f"{entry.driver!r}: {exc.errors(include_url=False)}"
129
+ ) from exc
130
+
131
+
132
+ def _validate_named(
133
+ resolver: Resolver,
134
+ category: str,
135
+ entry: EvaluatorEntry | ObservabilityEntry | PipelineTaskEntry | RerankerEntry,
136
+ *,
137
+ strict: bool,
138
+ ) -> None:
139
+ """Validate a named-list entry (evaluators / observability /
140
+ protocols)."""
141
+ try:
142
+ cls = resolver.resolve(category, entry.name)
143
+ except ModuleError:
144
+ if strict:
145
+ raise
146
+ return
147
+ schema = _read_config_schema(cls)
148
+ if schema is None:
149
+ return
150
+ try:
151
+ schema.model_validate(entry.config)
152
+ except ValidationError as exc:
153
+ raise ModuleError(
154
+ f"modules.{category}[{entry.name!r}].config failed validation: "
155
+ f"{exc.errors(include_url=False)}"
156
+ ) from exc
157
+
158
+
159
+ def _validate_retrieval(
160
+ resolver: Resolver,
161
+ cfg: AgentForgeConfig,
162
+ *,
163
+ strict: bool,
164
+ ) -> None:
165
+ """Validate the top-level `retrieval:` block (feat-021 follow-up)."""
166
+ assert cfg.retrieval is not None
167
+ _validate_one(resolver, "vector_stores", cfg.retrieval.vector_store, strict=strict)
168
+ _validate_one(resolver, "embeddings", cfg.retrieval.embedder, strict=strict)
169
+ if cfg.retrieval.reranker is not None:
170
+ _validate_named(resolver, "rerankers", cfg.retrieval.reranker, strict=strict)
171
+
172
+
173
+ def _validate_driver(
174
+ resolver: Resolver,
175
+ category: str,
176
+ name: str,
177
+ config: dict[str, Any],
178
+ *,
179
+ strict: bool,
180
+ ) -> None:
181
+ """Validate a driver-by-name + config dict (feat-020 chat hook)."""
182
+ try:
183
+ cls = resolver.resolve(category, name)
184
+ except ModuleError:
185
+ if strict:
186
+ raise
187
+ return
188
+ schema = _read_config_schema(cls)
189
+ if schema is None:
190
+ return
191
+ try:
192
+ schema.model_validate(config)
193
+ except ValidationError as exc:
194
+ raise ModuleError(
195
+ f"modules.{category}.config failed validation for {name!r}: "
196
+ f"{exc.errors(include_url=False)}"
197
+ ) from exc
198
+
199
+
200
+ def _read_config_schema(cls: type) -> Any:
201
+ """Pull `cls.config_schema` if declared. Tolerant — modules without
202
+ one return `None` (any dict accepted)."""
203
+ schema = getattr(cls, "config_schema", None)
204
+ if schema is None:
205
+ return None
206
+ if not isinstance(schema, type):
207
+ return None
208
+ return schema
@@ -0,0 +1,424 @@
1
+ """Locked root models for `agentforge.yaml` (feat-012).
2
+
3
+ feat-001 shipped a minimal schema (`agent` + `logging`). feat-012
4
+ widens it to the full §4.2 surface: nested `budget`, `modules`
5
+ section with sub-shapes (memory, graph, retriever, evaluators,
6
+ observability, tools, protocols), `providers` named registry, and
7
+ `output` config. Adding fields is additive (ADR-0007 minor bump).
8
+
9
+ The runtime `Agent(budget_usd=, max_iterations=)` kwargs from
10
+ feat-001 remain the locked Public API; they continue to drive
11
+ `BudgetPolicy` internally. The YAML field shape (`budget.usd` etc.)
12
+ is the data-side representation.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from pathlib import Path
18
+ from typing import Any, Literal
19
+
20
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
21
+
22
+
23
+ class BudgetConfig(BaseModel):
24
+ """`agent.budget:` — nested budget shape per spec §4.1.
25
+
26
+ All caps optional; runtime defaults preserved when omitted. The
27
+ flat `agent.budget_usd: float` form from feat-001 is no longer
28
+ valid in YAML — `agentforge config validate` will report it as
29
+ an unknown field (set `agent.budget.usd:` instead).
30
+ """
31
+
32
+ model_config = ConfigDict(strict=True, extra="forbid")
33
+
34
+ usd: float = Field(default=1.0, ge=0.0)
35
+ max_tokens: int | None = Field(default=None, ge=1)
36
+ error_streak_limit: int | None = Field(default=None, ge=1)
37
+
38
+
39
+ class AgentConfig(BaseModel):
40
+ """`agent:` section — model, strategy, prompt, budget, etc."""
41
+
42
+ model_config = ConfigDict(strict=True, extra="forbid")
43
+
44
+ name: str | None = None
45
+ model: str | dict[str, Any] | None = None
46
+ strategy: str | dict[str, Any] | None = None
47
+ system_prompt: str | None = None
48
+ system_prompt_file: Path | None = None
49
+ tools: list[str | dict[str, Any]] = Field(default_factory=list)
50
+ budget: BudgetConfig = Field(default_factory=BudgetConfig)
51
+ max_iterations: int = Field(default=25, ge=1)
52
+ llm_options: dict[str, Any] = Field(default_factory=dict)
53
+
54
+ @field_validator("system_prompt_file", mode="before")
55
+ @classmethod
56
+ def _coerce_path(cls, value: Any) -> Any:
57
+ # Strict mode rejects strings for Path; YAML loads as `str`,
58
+ # so coerce here. None / Path stay as-is.
59
+ if isinstance(value, str):
60
+ return Path(value)
61
+ return value
62
+
63
+
64
+ class LoggingConfig(BaseModel):
65
+ """`logging:` — level, run-id filter toggle, format."""
66
+
67
+ model_config = ConfigDict(strict=True, extra="forbid")
68
+
69
+ level: str = "INFO"
70
+ run_id_filter: bool = True
71
+ format: str = "text" # "text" | "json"
72
+
73
+
74
+ class ModuleEntry(BaseModel):
75
+ """Generic `driver + config` shape used by the `modules.memory`,
76
+ `modules.graph`, `modules.retriever` sections."""
77
+
78
+ model_config = ConfigDict(strict=True, extra="forbid")
79
+
80
+ driver: str = Field(min_length=1)
81
+ config: dict[str, Any] = Field(default_factory=dict)
82
+
83
+
84
+ class MemoryModuleConfig(ModuleEntry):
85
+ """Alias of `ModuleEntry` for `modules.memory:` — distinct type so
86
+ schema docs can attach memory-specific guidance."""
87
+
88
+
89
+ class GraphModuleConfig(ModuleEntry):
90
+ """Alias of `ModuleEntry` for `modules.graph:`."""
91
+
92
+
93
+ class RetrieverModuleConfig(ModuleEntry):
94
+ """Alias of `ModuleEntry` for `modules.retriever:` (legacy).
95
+
96
+ .. deprecated:: 0.2
97
+ Use the top-level `retrieval:` block (see
98
+ :class:`RetrievalConfig`) instead. The legacy single-entry
99
+ form remains valid for v0.2 backward compatibility and may
100
+ be removed in v1.0.
101
+ """
102
+
103
+
104
+ class RerankerEntry(BaseModel):
105
+ """`retrieval.reranker:` — name + config for a `Reranker` impl.
106
+
107
+ Resolved against the `rerankers` entry-point category. Name
108
+ matches the registered entry-point name (e.g.
109
+ `sentence-transformers`).
110
+ """
111
+
112
+ model_config = ConfigDict(strict=True, extra="forbid")
113
+
114
+ name: str = Field(min_length=1)
115
+ config: dict[str, Any] = Field(default_factory=dict)
116
+
117
+
118
+ class GraphExpansionConfig(BaseModel):
119
+ """`retrieval.graph_expansion:` — wiring for GraphRAG hybrid
120
+ retrieval (feat-023).
121
+
122
+ The `store` field resolves against the `graph_stores`
123
+ entry-point category. When set on a `RetrievalConfig`, the
124
+ builder constructs a `GraphExpansion` value and forwards it
125
+ into the `Retriever` constructor so vector / hybrid hits get
126
+ augmented with N-hop graph neighbours.
127
+ """
128
+
129
+ model_config = ConfigDict(strict=True, extra="forbid")
130
+
131
+ store: ModuleEntry
132
+ max_hops: int = Field(default=2, ge=1)
133
+ edge_types: list[str] | None = None
134
+ """Edge-type filter. YAML lists deserialize to `list[str]`;
135
+ converted to a tuple by `build_retriever_from_config` before
136
+ constructing the `GraphExpansion` value."""
137
+ text_property: str = "text"
138
+ decay: float = Field(default=0.5, gt=0.0, le=1.0)
139
+
140
+
141
+ class RetrievalConfig(BaseModel):
142
+ """Top-level `retrieval:` block (feat-021 follow-up).
143
+
144
+ Models the full retrieval pipeline: a `VectorStore` + an
145
+ `EmbeddingClient` + an optional `Reranker`, plus retrieval-
146
+ time knobs (`top_k`, `over_fetch_factor`, `batch_size`).
147
+ Lives at the root of `agentforge.yaml`, not nested under
148
+ `modules:`.
149
+
150
+ The legacy `modules.retriever` single-entry form still works
151
+ for v0.2 backward compat; the new `retrieval:` block
152
+ supersedes it. Both should not be set simultaneously — the
153
+ builder picks `retrieval:` when present.
154
+ """
155
+
156
+ model_config = ConfigDict(strict=True, extra="forbid")
157
+
158
+ vector_store: ModuleEntry
159
+ embedder: ModuleEntry
160
+ reranker: RerankerEntry | None = None
161
+ top_k: int = Field(default=5, ge=1)
162
+ over_fetch_factor: int = Field(default=3, ge=1)
163
+ batch_size: int = Field(default=32, ge=1)
164
+ mode: Literal["vector", "hybrid"] = "vector"
165
+ """Retrieval mode (feat-022): ``"vector"`` for cosine-only or
166
+ ``"hybrid"`` for BM25 + cosine fused via RRF. Hybrid requires the
167
+ underlying ``VectorStore`` to declare the ``"hybrid_search"``
168
+ capability."""
169
+ rrf_k: int = Field(default=60, ge=1)
170
+ """RRF constant (Cormack 2009 default 60). Ignored when ``mode``
171
+ is ``"vector"``."""
172
+ graph_expansion: GraphExpansionConfig | None = None
173
+ """Optional graph-augmented retrieval (feat-023). When set the
174
+ builder resolves the graph store, constructs a
175
+ :class:`GraphExpansion`, and forwards it to ``Retriever`` so
176
+ vector / hybrid hits get expanded with N-hop graph neighbours.
177
+ Composes orthogonally with ``mode`` and ``reranker``."""
178
+
179
+
180
+ class EvaluatorEntry(BaseModel):
181
+ """An entry in `modules.evaluators:`. Two YAML shapes are valid:
182
+
183
+ - String form: `- faithfulness` (just the name).
184
+ - Mapping form: `- faithfulness: {cost_cap_usd: 0.05, ...}`.
185
+
186
+ We model the mapping form here; the loader normalises strings to
187
+ `EvaluatorEntry(name=..., config={})` before validation.
188
+ """
189
+
190
+ model_config = ConfigDict(strict=True, extra="forbid")
191
+
192
+ name: str = Field(min_length=1)
193
+ config: dict[str, Any] = Field(default_factory=dict)
194
+
195
+
196
+ class ObservabilityEntry(BaseModel):
197
+ """An entry in `modules.observability:` — same shape as evaluator
198
+ entries (name + config)."""
199
+
200
+ model_config = ConfigDict(strict=True, extra="forbid")
201
+
202
+ name: str = Field(min_length=1)
203
+ config: dict[str, Any] = Field(default_factory=dict)
204
+
205
+
206
+ class GuardrailPolicy(BaseModel):
207
+ """Framework-wide guardrail policy (feat-018).
208
+
209
+ Read from `agentforge.yaml` under the top-level
210
+ `guardrail_policy:` key. Defaults are conservative (per P6 —
211
+ loud defaults). Lives here rather than in `values/guardrails.py`
212
+ so the config-schema module doesn't have to reach into
213
+ `values` (avoids an import cycle through `values.state`).
214
+ The runtime `ValidationResult` remains in `values.guardrails`.
215
+ """
216
+
217
+ model_config = ConfigDict(strict=True, extra="forbid")
218
+
219
+ on_input_violation: Literal["block", "redact", "warn", "allow"] = "block"
220
+ on_output_violation: Literal["block", "redact", "warn", "allow"] = "redact"
221
+ on_tool_violation: Literal["block", "warn"] = "block"
222
+ audit_channel: str = "agentforge.audit"
223
+ fail_open: bool = False
224
+
225
+
226
+ class GuardrailEntry(BaseModel):
227
+ """One entry inside `modules.guardrails.{input,output,tool_gates}`.
228
+
229
+ Two YAML shapes are valid (mirrors `EvaluatorEntry`):
230
+
231
+ - String form: `- prompt_injection_basic` (just the name).
232
+ - Mapping form: `- presidio: {entities: ["EMAIL_ADDRESS"]}`.
233
+
234
+ Both normalise to `GuardrailEntry(name=..., config={})` before
235
+ validation.
236
+ """
237
+
238
+ model_config = ConfigDict(strict=True, extra="forbid")
239
+
240
+ name: str = Field(min_length=1)
241
+ config: dict[str, Any] = Field(default_factory=dict)
242
+
243
+
244
+ class ChatHistoryDriverConfig(BaseModel):
245
+ """`modules.chat.history:` — driver + config for a chat history
246
+ store (feat-020)."""
247
+
248
+ model_config = ConfigDict(strict=True, extra="forbid")
249
+
250
+ driver: str = Field(min_length=1)
251
+ config: dict[str, Any] = Field(default_factory=dict)
252
+
253
+
254
+ class ChatTruncationConfig(BaseModel):
255
+ """`modules.chat.truncation:` — strategy + config (feat-020)."""
256
+
257
+ model_config = ConfigDict(strict=True, extra="forbid")
258
+
259
+ strategy: str = Field(min_length=1)
260
+ config: dict[str, Any] = Field(default_factory=dict)
261
+
262
+
263
+ class ChatSessionConfig(BaseModel):
264
+ """`modules.chat.session:` — per-session policy knobs (feat-020)."""
265
+
266
+ model_config = ConfigDict(strict=True, extra="forbid")
267
+
268
+ per_turn_budget_usd: float | None = Field(default=None, ge=0.0)
269
+ per_session_budget_usd: float | None = Field(default=None, ge=0.0)
270
+ idempotency_window_s: float = Field(default=60.0, ge=0.0)
271
+ concurrency: Literal["queue", "reject", "replace"] = "queue"
272
+ safety_mode: Literal["buffer-then-stream", "sentence-window", "stream-then-redact"] = (
273
+ "buffer-then-stream"
274
+ )
275
+ """Output-guardrail policy on streamed assistant turns:
276
+
277
+ - ``"buffer-then-stream"`` (default) — agent runs to completion;
278
+ output validators see the full text once; the assembled response
279
+ is then sentence-segmented for the wire. Existing v0.2 behaviour.
280
+ - ``"sentence-window"`` (feat-020 v0.3) — for real per-token
281
+ streaming, buffer tokens until a sentence boundary, run
282
+ ``check_output`` over each completed sentence, emit the
283
+ validated sentence as the next ``text`` chunk. Trades a small
284
+ latency hit (visible chunks at sentence boundaries) for
285
+ streaming-aware safety.
286
+ - ``"stream-then-redact"`` (deferred) — currently an alias for
287
+ ``sentence-window``. A future v0.3+ pass may add inline regex
288
+ redaction without buffering.
289
+ """
290
+
291
+
292
+ class ChatConfig(BaseModel):
293
+ """`modules.chat:` — chat layer config (feat-020).
294
+
295
+ `history` may be ``None`` (defaults to in-memory). `truncation`
296
+ similarly defaults to `SlidingWindow(50)` when absent.
297
+ """
298
+
299
+ model_config = ConfigDict(strict=True, extra="forbid")
300
+
301
+ history: ChatHistoryDriverConfig | None = None
302
+ truncation: ChatTruncationConfig | None = None
303
+ session: ChatSessionConfig = Field(default_factory=ChatSessionConfig)
304
+
305
+
306
+ class PipelineTaskEntry(BaseModel):
307
+ """One entry inside `modules.pipeline.tasks` (feat-015).
308
+
309
+ Mirrors `EvaluatorEntry` / `GuardrailEntry`: a name (resolver
310
+ lookup under the `"tasks"` category) plus optional kwargs the
311
+ task class receives at construction.
312
+ """
313
+
314
+ model_config = ConfigDict(strict=True, extra="forbid")
315
+
316
+ name: str = Field(min_length=1)
317
+ config: dict[str, Any] = Field(default_factory=dict)
318
+
319
+
320
+ class PipelineConfig(BaseModel):
321
+ """`modules.pipeline:` — deterministic-task DAG (feat-015).
322
+
323
+ When ``enabled`` is True and ``tasks`` is non-empty, the runtime
324
+ resolves each entry against the global resolver's ``tasks``
325
+ category, builds a `Pipeline`, and wires it into the `Agent`.
326
+ """
327
+
328
+ model_config = ConfigDict(strict=True, extra="forbid")
329
+
330
+ enabled: bool = True
331
+ max_concurrent: int = Field(default=4, ge=1)
332
+ on_task_error: Literal["continue", "fail"] = "continue"
333
+ tasks: list[PipelineTaskEntry] = Field(default_factory=list)
334
+
335
+
336
+ class GuardrailsConfig(BaseModel):
337
+ """`modules.guardrails:` — input / output / tool-call validators.
338
+
339
+ `defaults: true` keeps the framework's built-in basic validators
340
+ (prompt_injection_basic, pii_redact_basic, capability_check)
341
+ installed alongside whatever's listed here. Disabling them is
342
+ explicit and emits a startup warning per P6 (loud defaults).
343
+ """
344
+
345
+ model_config = ConfigDict(strict=True, extra="forbid")
346
+
347
+ defaults: bool = True
348
+ input: list[GuardrailEntry] = Field(default_factory=list)
349
+ output: list[GuardrailEntry] = Field(default_factory=list)
350
+ tool_gates: list[GuardrailEntry] = Field(default_factory=list)
351
+
352
+
353
+ class ModulesConfig(BaseModel):
354
+ """`modules:` — every plug-and-play module the agent uses.
355
+
356
+ Each section accepts either a single driver-with-config or a list
357
+ of entries:
358
+
359
+ modules:
360
+ memory: {driver: postgres, config: {...}}
361
+ evaluators: [faithfulness, {geval: {rubric: "..."}}]
362
+ observability: [{name: otel, config: {endpoint: "..."}}]
363
+ guardrails:
364
+ input: [prompt_injection_basic]
365
+ output: [pii_redact_basic]
366
+ tool_gates: [capability_check]
367
+ """
368
+
369
+ model_config = ConfigDict(strict=True, extra="forbid")
370
+
371
+ memory: MemoryModuleConfig | None = None
372
+ graph: GraphModuleConfig | None = None
373
+ retriever: RetrieverModuleConfig | None = None
374
+ evaluators: list[EvaluatorEntry] = Field(default_factory=list)
375
+ observability: list[ObservabilityEntry] = Field(default_factory=list)
376
+ tools: list[str | dict[str, Any]] = Field(default_factory=list)
377
+ protocols: list[ObservabilityEntry] = Field(default_factory=list)
378
+ guardrails: GuardrailsConfig = Field(default_factory=GuardrailsConfig)
379
+ pipeline: PipelineConfig | None = None
380
+ chat: ChatConfig | None = None
381
+
382
+
383
+ class ProviderConfig(BaseModel):
384
+ """One entry in the `providers:` named registry.
385
+
386
+ `type` is the entry-point name (e.g. `"anthropic"`, `"bedrock"`).
387
+ `model` + extra kwargs are passed through to the provider's
388
+ constructor. `options` carries per-call LLM options.
389
+ """
390
+
391
+ model_config = ConfigDict(strict=True, extra="forbid")
392
+
393
+ type: str = Field(min_length=1)
394
+ model: str | None = None
395
+ options: dict[str, Any] = Field(default_factory=dict)
396
+ config: dict[str, Any] = Field(default_factory=dict)
397
+
398
+
399
+ class OutputConfig(BaseModel):
400
+ """`output:` — finding-variant defaults, renderer choice, thresholds."""
401
+
402
+ model_config = ConfigDict(strict=True, extra="forbid")
403
+
404
+ default_finding_variant: str = "simple"
405
+ default_renderer: str = "scorecard"
406
+ thresholds: dict[str, list[str]] = Field(default_factory=dict)
407
+
408
+
409
+ class AgentForgeConfig(BaseModel):
410
+ """Root model — `agentforge.yaml` shape.
411
+
412
+ Adding a field to this model (or any submodel) is a minor bump
413
+ under ADR-0007; removing or renaming requires a major bump.
414
+ """
415
+
416
+ model_config = ConfigDict(strict=True, extra="forbid")
417
+
418
+ agent: AgentConfig = Field(default_factory=AgentConfig)
419
+ modules: ModulesConfig = Field(default_factory=ModulesConfig)
420
+ retrieval: RetrievalConfig | None = None
421
+ providers: dict[str, ProviderConfig] = Field(default_factory=dict)
422
+ logging: LoggingConfig = Field(default_factory=LoggingConfig)
423
+ output: OutputConfig = Field(default_factory=OutputConfig)
424
+ guardrail_policy: GuardrailPolicy = Field(default_factory=GuardrailPolicy)
@@ -0,0 +1,52 @@
1
+ """Locked contracts — ABCs and the `Finding` Protocol.
2
+
3
+ Per ADR-0007, these are the framework's stable surface. Adding a method
4
+ to an ABC is a major version bump. Modules implement these; the runtime
5
+ consumes them by reference to the abstraction, never the implementation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from agentforge_core.contracts.auth import AuthPolicy
11
+ from agentforge_core.contracts.chat import ChatHistoryStore, HistoryTruncationStrategy
12
+ from agentforge_core.contracts.embedding import EmbeddingClient
13
+ from agentforge_core.contracts.evaluator import EvalResult, Evaluator
14
+ from agentforge_core.contracts.finding import Finding
15
+ from agentforge_core.contracts.graph_store import GraphStore
16
+ from agentforge_core.contracts.llm import LLMClient
17
+ from agentforge_core.contracts.memory import MemoryStore
18
+ from agentforge_core.contracts.migrator import (
19
+ Migration,
20
+ MigrationChecksumError,
21
+ MigrationStatus,
22
+ Migrator,
23
+ )
24
+ from agentforge_core.contracts.renderer import FindingRenderer
25
+ from agentforge_core.contracts.reranker import Reranker
26
+ from agentforge_core.contracts.strategy import ReasoningStrategy
27
+ from agentforge_core.contracts.task import Task
28
+ from agentforge_core.contracts.tool import Tool
29
+ from agentforge_core.contracts.vector_store import VectorStore
30
+
31
+ __all__ = [
32
+ "AuthPolicy",
33
+ "ChatHistoryStore",
34
+ "EmbeddingClient",
35
+ "EvalResult",
36
+ "Evaluator",
37
+ "Finding",
38
+ "FindingRenderer",
39
+ "GraphStore",
40
+ "HistoryTruncationStrategy",
41
+ "LLMClient",
42
+ "MemoryStore",
43
+ "Migration",
44
+ "MigrationChecksumError",
45
+ "MigrationStatus",
46
+ "Migrator",
47
+ "ReasoningStrategy",
48
+ "Reranker",
49
+ "Task",
50
+ "Tool",
51
+ "VectorStore",
52
+ ]