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,103 @@
|
|
|
1
|
+
"""Locked value types — Pydantic v2 models the framework's contracts use.
|
|
2
|
+
|
|
3
|
+
Per ADR-0007, these shapes are part of the framework's stable surface;
|
|
4
|
+
adding a field requires a minor bump, removing or renaming a field
|
|
5
|
+
requires a major bump.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from agentforge_core.values.auth import Principal
|
|
11
|
+
from agentforge_core.values.chat import (
|
|
12
|
+
ChatChunk,
|
|
13
|
+
ChatChunkKind,
|
|
14
|
+
ChatResponse,
|
|
15
|
+
ChatRole,
|
|
16
|
+
ChatTurn,
|
|
17
|
+
SessionInfo,
|
|
18
|
+
StreamingChunkKind,
|
|
19
|
+
StreamingEvent,
|
|
20
|
+
)
|
|
21
|
+
from agentforge_core.values.claim import Claim
|
|
22
|
+
from agentforge_core.values.graph import (
|
|
23
|
+
GraphEdge,
|
|
24
|
+
GraphNode,
|
|
25
|
+
GraphPattern,
|
|
26
|
+
GraphSegment,
|
|
27
|
+
Path,
|
|
28
|
+
)
|
|
29
|
+
from agentforge_core.values.manifest import (
|
|
30
|
+
AppliedEnvVar,
|
|
31
|
+
AppliedManifest,
|
|
32
|
+
AppliedTemplate,
|
|
33
|
+
EnvVarEntry,
|
|
34
|
+
Manifest,
|
|
35
|
+
TemplateFile,
|
|
36
|
+
)
|
|
37
|
+
from agentforge_core.values.messages import (
|
|
38
|
+
EmbeddingResponse,
|
|
39
|
+
LLMResponse,
|
|
40
|
+
Message,
|
|
41
|
+
MessageRole,
|
|
42
|
+
StopReason,
|
|
43
|
+
StreamChunk,
|
|
44
|
+
StreamChunkKind,
|
|
45
|
+
TokenUsage,
|
|
46
|
+
ToolCall,
|
|
47
|
+
ToolSpec,
|
|
48
|
+
)
|
|
49
|
+
from agentforge_core.values.module import ModuleInfo
|
|
50
|
+
from agentforge_core.values.pipeline import PipelineResult
|
|
51
|
+
from agentforge_core.values.retrieval import GraphExpansion
|
|
52
|
+
from agentforge_core.values.state import (
|
|
53
|
+
AgentState,
|
|
54
|
+
FinishReason,
|
|
55
|
+
RunResult,
|
|
56
|
+
Step,
|
|
57
|
+
StepKind,
|
|
58
|
+
)
|
|
59
|
+
from agentforge_core.values.vector import VectorItem, VectorMatch
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"AgentState",
|
|
63
|
+
"AppliedEnvVar",
|
|
64
|
+
"AppliedManifest",
|
|
65
|
+
"AppliedTemplate",
|
|
66
|
+
"ChatChunk",
|
|
67
|
+
"ChatChunkKind",
|
|
68
|
+
"ChatResponse",
|
|
69
|
+
"ChatRole",
|
|
70
|
+
"ChatTurn",
|
|
71
|
+
"Claim",
|
|
72
|
+
"EmbeddingResponse",
|
|
73
|
+
"EnvVarEntry",
|
|
74
|
+
"FinishReason",
|
|
75
|
+
"GraphEdge",
|
|
76
|
+
"GraphExpansion",
|
|
77
|
+
"GraphNode",
|
|
78
|
+
"GraphPattern",
|
|
79
|
+
"GraphSegment",
|
|
80
|
+
"LLMResponse",
|
|
81
|
+
"Manifest",
|
|
82
|
+
"Message",
|
|
83
|
+
"MessageRole",
|
|
84
|
+
"ModuleInfo",
|
|
85
|
+
"Path",
|
|
86
|
+
"PipelineResult",
|
|
87
|
+
"Principal",
|
|
88
|
+
"RunResult",
|
|
89
|
+
"SessionInfo",
|
|
90
|
+
"Step",
|
|
91
|
+
"StepKind",
|
|
92
|
+
"StopReason",
|
|
93
|
+
"StreamChunk",
|
|
94
|
+
"StreamChunkKind",
|
|
95
|
+
"StreamingChunkKind",
|
|
96
|
+
"StreamingEvent",
|
|
97
|
+
"TemplateFile",
|
|
98
|
+
"TokenUsage",
|
|
99
|
+
"ToolCall",
|
|
100
|
+
"ToolSpec",
|
|
101
|
+
"VectorItem",
|
|
102
|
+
"VectorMatch",
|
|
103
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Auth value types (feat-014).
|
|
2
|
+
|
|
3
|
+
`Principal` is the identity returned by an `AuthPolicy` after a
|
|
4
|
+
successful authentication. Carries an opaque ``id`` (the token
|
|
5
|
+
itself by default; an opaque user id once a real identity
|
|
6
|
+
provider is wired) plus optional metadata for downstream use
|
|
7
|
+
(role tags, tenant id, etc.).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class Principal:
|
|
17
|
+
"""Identity returned by `AuthPolicy.authenticate(...)`."""
|
|
18
|
+
|
|
19
|
+
id: str
|
|
20
|
+
metadata: dict[str, str] = field(default_factory=dict)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Chat-agent value types (feat-020).
|
|
2
|
+
|
|
3
|
+
Frozen Pydantic models that ride the wire between `ChatSession`,
|
|
4
|
+
`ChatHistoryStore` drivers, and the chat-http server.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from typing import Any, Literal
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
13
|
+
|
|
14
|
+
from agentforge_core.values.messages import ToolCall
|
|
15
|
+
|
|
16
|
+
ChatRole = Literal["user", "assistant", "system", "tool"]
|
|
17
|
+
"""Closed enum of chat turn roles."""
|
|
18
|
+
|
|
19
|
+
StreamingChunkKind = Literal[
|
|
20
|
+
"text",
|
|
21
|
+
"thinking",
|
|
22
|
+
"step",
|
|
23
|
+
"tool_call",
|
|
24
|
+
"tool_result",
|
|
25
|
+
"done",
|
|
26
|
+
"error",
|
|
27
|
+
]
|
|
28
|
+
"""Closed enum of streaming chunk kinds shared across chat (token-level)
|
|
29
|
+
and A2A (step + token-level) wire formats. Adding a new kind requires a
|
|
30
|
+
minor version bump per ADR-0007. Receivers must ignore unknown kinds on
|
|
31
|
+
the wire — forward-compat for future additions.
|
|
32
|
+
|
|
33
|
+
The ``step`` kind is reserved for strategies that emit step-level
|
|
34
|
+
events alongside (or instead of) per-token text. Chat receivers
|
|
35
|
+
typically ignore it; A2A clients render it as a generic step boundary.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
ChatChunkKind = StreamingChunkKind
|
|
39
|
+
"""Backward-compatible alias retained for callers that imported the
|
|
40
|
+
chat-shaped name in feat-020 v0.1 / v0.2. Prefer ``StreamingChunkKind``
|
|
41
|
+
in new code."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ChatTurn(BaseModel):
|
|
45
|
+
"""One persisted message in a chat session.
|
|
46
|
+
|
|
47
|
+
Stored by `ChatHistoryStore` drivers, surfaced through
|
|
48
|
+
`ChatSession.history()`, and emitted on the chat-http wire.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
model_config = ConfigDict(frozen=True, strict=True)
|
|
52
|
+
|
|
53
|
+
id: str
|
|
54
|
+
session_id: str
|
|
55
|
+
role: ChatRole
|
|
56
|
+
content: str
|
|
57
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
58
|
+
run_id: str | None = None
|
|
59
|
+
"""Links assistant turns (and the tool turns produced inside them)
|
|
60
|
+
back to the AgentForge run that emitted them. None for user /
|
|
61
|
+
system turns."""
|
|
62
|
+
tool_calls: tuple[ToolCall, ...] = ()
|
|
63
|
+
tool_call_id: str | None = None
|
|
64
|
+
tokens_in: int = Field(default=0, ge=0)
|
|
65
|
+
tokens_out: int = Field(default=0, ge=0)
|
|
66
|
+
cost_usd: float = Field(default=0.0, ge=0.0)
|
|
67
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class SessionInfo(BaseModel):
|
|
71
|
+
"""Session-level metadata returned by `list_sessions` /
|
|
72
|
+
`delete_session` / `update_session_metadata`."""
|
|
73
|
+
|
|
74
|
+
model_config = ConfigDict(frozen=True, strict=True)
|
|
75
|
+
|
|
76
|
+
id: str
|
|
77
|
+
owner: str | None = None
|
|
78
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
79
|
+
last_active_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
80
|
+
turn_count: int = Field(default=0, ge=0)
|
|
81
|
+
total_cost_usd: float = Field(default=0.0, ge=0.0)
|
|
82
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ChatChunk(BaseModel):
|
|
86
|
+
"""One streaming chunk emitted by `ChatSession.stream()` /
|
|
87
|
+
`ChatServer` SSE+WS."""
|
|
88
|
+
|
|
89
|
+
model_config = ConfigDict(frozen=True, strict=True)
|
|
90
|
+
|
|
91
|
+
kind: ChatChunkKind
|
|
92
|
+
content: str | dict[str, Any] | None = None
|
|
93
|
+
cumulative_text: str | None = None
|
|
94
|
+
turn_id: str
|
|
95
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ChatResponse(BaseModel):
|
|
99
|
+
"""Aggregated response returned by `ChatSession.send()`."""
|
|
100
|
+
|
|
101
|
+
model_config = ConfigDict(frozen=True, strict=True)
|
|
102
|
+
|
|
103
|
+
content: str
|
|
104
|
+
turn_id: str
|
|
105
|
+
run_id: str
|
|
106
|
+
tool_calls: tuple[ToolCall, ...] = ()
|
|
107
|
+
tokens_in: int = Field(default=0, ge=0)
|
|
108
|
+
tokens_out: int = Field(default=0, ge=0)
|
|
109
|
+
cost_usd: float = Field(default=0.0, ge=0.0)
|
|
110
|
+
duration_ms: int = Field(default=0, ge=0)
|
|
111
|
+
finish_reason: str = "completed"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class StreamingEvent(BaseModel):
|
|
115
|
+
"""One event emitted by `ReasoningStrategy.stream()` (feat-020 v0.2).
|
|
116
|
+
|
|
117
|
+
Strategies that want per-token streaming override the default
|
|
118
|
+
`stream()` to yield these events as the LLM emits tokens / step
|
|
119
|
+
transitions. `ChatSession.stream()` forwards each event to a
|
|
120
|
+
`ChatChunk` on the wire (kinds map 1:1 with `ChatChunkKind`).
|
|
121
|
+
The default base-class `stream()` calls `run()` and yields a
|
|
122
|
+
single `done` event so existing concrete strategies keep
|
|
123
|
+
working unchanged.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
model_config = ConfigDict(frozen=True, strict=True)
|
|
127
|
+
|
|
128
|
+
kind: ChatChunkKind
|
|
129
|
+
content: str | dict[str, Any] | None = None
|
|
130
|
+
cumulative_text: str | None = None
|
|
131
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""`Claim` — a persisted unit of agent-produced knowledge.
|
|
2
|
+
|
|
3
|
+
Written through `MemoryStore`. The `(project, agent)` pair namespaces
|
|
4
|
+
claims; cross-agent and cross-project queries are explicit verbs (per
|
|
5
|
+
feat-005 design). `id` is a ULID for sortable monotonic ordering.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
14
|
+
from ulid import ULID
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Claim(BaseModel):
|
|
18
|
+
"""A persisted unit of agent-produced knowledge."""
|
|
19
|
+
|
|
20
|
+
model_config = ConfigDict(frozen=True, strict=True)
|
|
21
|
+
|
|
22
|
+
id: str = Field(default_factory=lambda: str(ULID()))
|
|
23
|
+
run_id: str
|
|
24
|
+
project: str
|
|
25
|
+
agent: str
|
|
26
|
+
category: str
|
|
27
|
+
payload: dict[str, Any]
|
|
28
|
+
supersedes: str | None = None
|
|
29
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
30
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Frozen value types for the `GraphStore` contract.
|
|
2
|
+
|
|
3
|
+
`GraphNode` and `GraphEdge` are what callers upsert; `GraphPattern`
|
|
4
|
+
(composed of `GraphSegment`) is the query DSL `match()` accepts; `Path`
|
|
5
|
+
is what `match()` and `traverse()` return. All immutable Pydantic
|
|
6
|
+
models — safe to pass across async boundaries without aliasing bugs.
|
|
7
|
+
|
|
8
|
+
Per ADR-0007 these shapes are part of the framework's locked surface.
|
|
9
|
+
Adding a field is a minor bump; removing or renaming requires a major
|
|
10
|
+
bump.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any, Literal
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GraphNode(BaseModel):
|
|
21
|
+
"""One node in a `GraphStore`.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
id: Caller-controlled identifier. Re-upserting an existing id
|
|
25
|
+
replaces the prior record (write-through, not append).
|
|
26
|
+
labels: Type tags. Empty tuple is allowed — nodes without
|
|
27
|
+
labels are still queryable by id and properties. Order is
|
|
28
|
+
insignificant; drivers may sort internally.
|
|
29
|
+
properties: Free-form key-value attributes. Used by
|
|
30
|
+
`GraphPattern.node_filters` for AND-style equality
|
|
31
|
+
filtering during `match()`.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
model_config = ConfigDict(frozen=True, strict=True)
|
|
35
|
+
|
|
36
|
+
id: str = Field(min_length=1)
|
|
37
|
+
labels: tuple[str, ...] = Field(default_factory=tuple)
|
|
38
|
+
properties: dict[str, Any] = Field(default_factory=dict)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class GraphEdge(BaseModel):
|
|
42
|
+
"""One directed edge in a `GraphStore`.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
src: Source node id. Drivers must reject edges referring to
|
|
46
|
+
absent nodes (raise `ValueError`); `add_node` first.
|
|
47
|
+
dst: Destination node id. Same constraint as `src`.
|
|
48
|
+
edge_type: Relationship type, e.g. `"CITES"`, `"AUTHORED_BY"`.
|
|
49
|
+
Required and non-empty (the field name `type` would shadow
|
|
50
|
+
the builtin, hence `edge_type`).
|
|
51
|
+
properties: Free-form key-value attributes on the edge itself
|
|
52
|
+
(weight, timestamp, etc.).
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
model_config = ConfigDict(frozen=True, strict=True)
|
|
56
|
+
|
|
57
|
+
src: str = Field(min_length=1)
|
|
58
|
+
dst: str = Field(min_length=1)
|
|
59
|
+
edge_type: str = Field(min_length=1)
|
|
60
|
+
properties: dict[str, Any] = Field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class GraphSegment(BaseModel):
|
|
64
|
+
"""One hop in a `GraphPattern`.
|
|
65
|
+
|
|
66
|
+
`None` for `src_label`, `edge_type`, or `dst_label` is a wildcard
|
|
67
|
+
at that position. `direction` controls how the edge is matched
|
|
68
|
+
against the underlying store — `"out"` is the default and matches
|
|
69
|
+
`src -> dst`; `"in"` matches `dst <- src` (i.e. reversed); `"any"`
|
|
70
|
+
matches either direction.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
model_config = ConfigDict(frozen=True, strict=True)
|
|
74
|
+
|
|
75
|
+
src_label: str | None = None
|
|
76
|
+
edge_type: str | None = None
|
|
77
|
+
dst_label: str | None = None
|
|
78
|
+
direction: Literal["out", "in", "any"] = "out"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class GraphPattern(BaseModel):
|
|
82
|
+
"""A chained pattern for `GraphStore.match`.
|
|
83
|
+
|
|
84
|
+
`segments` is the ordered chain of hops (length 1 = one edge).
|
|
85
|
+
`node_filters` is an optional sequence of equality filters indexed
|
|
86
|
+
by node-position: position 0 is the start node, position 1 is the
|
|
87
|
+
destination of segment 0, position 2 is the destination of segment
|
|
88
|
+
1, and so on. Length must be either 0 (no filters), or `len(segments) + 1`.
|
|
89
|
+
|
|
90
|
+
Example: matching `(:Doc {topic="ml"})-[:CITES]->(:Doc)` becomes
|
|
91
|
+
``GraphPattern(segments=(GraphSegment(src_label="Doc",
|
|
92
|
+
edge_type="CITES", dst_label="Doc"),),
|
|
93
|
+
node_filters=({"topic": "ml"}, {}))``.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
model_config = ConfigDict(frozen=True, strict=True)
|
|
97
|
+
|
|
98
|
+
segments: tuple[GraphSegment, ...] = Field(min_length=1)
|
|
99
|
+
node_filters: tuple[dict[str, Any], ...] = Field(default_factory=tuple)
|
|
100
|
+
|
|
101
|
+
@model_validator(mode="after")
|
|
102
|
+
def _filters_match_segments(self) -> GraphPattern:
|
|
103
|
+
if self.node_filters and len(self.node_filters) != len(self.segments) + 1:
|
|
104
|
+
msg = (
|
|
105
|
+
f"node_filters length {len(self.node_filters)} must equal "
|
|
106
|
+
f"len(segments) + 1 = {len(self.segments) + 1} (one filter "
|
|
107
|
+
f"per node position) or be empty"
|
|
108
|
+
)
|
|
109
|
+
raise ValueError(msg)
|
|
110
|
+
return self
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class Path(BaseModel):
|
|
114
|
+
"""An ordered chain of nodes connected by edges.
|
|
115
|
+
|
|
116
|
+
Returned by both `match()` and `traverse()`. Invariants enforced:
|
|
117
|
+
- at least one node
|
|
118
|
+
- `len(edges) == len(nodes) - 1`
|
|
119
|
+
- each edge `i` connects `nodes[i]` to `nodes[i+1]` (drivers
|
|
120
|
+
respect this; the value type doesn't re-check the topology
|
|
121
|
+
beyond length, since per-edge id checking would require
|
|
122
|
+
property-level equality which is wasteful)
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
model_config = ConfigDict(frozen=True, strict=True)
|
|
126
|
+
|
|
127
|
+
nodes: tuple[GraphNode, ...] = Field(min_length=1)
|
|
128
|
+
edges: tuple[GraphEdge, ...] = Field(default_factory=tuple)
|
|
129
|
+
|
|
130
|
+
@model_validator(mode="after")
|
|
131
|
+
def _edges_count_matches_nodes(self) -> Path:
|
|
132
|
+
expected = len(self.nodes) - 1
|
|
133
|
+
if len(self.edges) != expected:
|
|
134
|
+
msg = f"edges length {len(self.edges)} must equal len(nodes) - 1 = {expected}"
|
|
135
|
+
raise ValueError(msg)
|
|
136
|
+
return self
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Guardrail runtime value types (feat-018).
|
|
2
|
+
|
|
3
|
+
`ValidationResult` is the locked return shape every `InputValidator`,
|
|
4
|
+
`OutputValidator`, and `ToolCallGate` produces. The framework-wide
|
|
5
|
+
`GuardrailPolicy` lives in `agentforge_core.config.schema` instead
|
|
6
|
+
of here — co-locating it with the other config models avoids the
|
|
7
|
+
import cycle through `values.state`.
|
|
8
|
+
|
|
9
|
+
Per ADR-0009 this shape is frozen so it crosses process / module
|
|
10
|
+
boundaries safely.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ValidationResult(BaseModel):
|
|
21
|
+
"""Outcome of one validator's `validate(...)` / `authorize(...)` call."""
|
|
22
|
+
|
|
23
|
+
model_config = ConfigDict(frozen=True, strict=True)
|
|
24
|
+
|
|
25
|
+
passed: bool
|
|
26
|
+
score: float = Field(default=1.0, ge=0.0, le=1.0)
|
|
27
|
+
"""Confidence; 1.0 = definitely clean, 0.0 = definitely bad. Used
|
|
28
|
+
by redaction-vs-block policy thresholds and audit aggregation."""
|
|
29
|
+
|
|
30
|
+
violations: tuple[str, ...] = ()
|
|
31
|
+
"""Rule identifiers that fired (e.g. `("prompt_injection", "jailbreak")`).
|
|
32
|
+
Empty tuple when `passed=True`."""
|
|
33
|
+
|
|
34
|
+
redacted_content: str | None = None
|
|
35
|
+
"""If the validator can both flag AND redact, the post-redaction
|
|
36
|
+
string. Output validators with `policy.on_output_violation =
|
|
37
|
+
"redact"` use this; input validators usually only flag."""
|
|
38
|
+
|
|
39
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
40
|
+
"""Validator-specific extra detail — scores per entity, spans,
|
|
41
|
+
upstream raw payload. Never required, never relied on."""
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def ok(cls) -> ValidationResult:
|
|
45
|
+
"""Clean-pass result with no violations and full confidence."""
|
|
46
|
+
return cls(passed=True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = ["ValidationResult"]
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Manifest value types for `agentforge add/swap/remove module` (feat-010b).
|
|
2
|
+
|
|
3
|
+
Each Tier-3 module ships a `manifest.yaml` at the root of its
|
|
4
|
+
package describing the side-effects `agentforge add module X` should
|
|
5
|
+
apply to a consuming agent's repo:
|
|
6
|
+
|
|
7
|
+
# agentforge_memory_postgres/manifest.yaml
|
|
8
|
+
category: memory
|
|
9
|
+
name: postgres
|
|
10
|
+
env_vars:
|
|
11
|
+
- name: POSTGRES_DSN
|
|
12
|
+
description: "Connection string"
|
|
13
|
+
required: true
|
|
14
|
+
templates:
|
|
15
|
+
- source: db/migrations/0001_init.sql
|
|
16
|
+
destination: db/migrations/agentforge/0001_init.sql
|
|
17
|
+
config_block:
|
|
18
|
+
modules:
|
|
19
|
+
memory:
|
|
20
|
+
driver: postgres
|
|
21
|
+
config:
|
|
22
|
+
dsn: "${POSTGRES_DSN}"
|
|
23
|
+
next_steps:
|
|
24
|
+
- "Set POSTGRES_DSN in your .env file."
|
|
25
|
+
- "Run `agentforge db migrate` to apply the schema."
|
|
26
|
+
|
|
27
|
+
The applier serialises what it actually did into an `AppliedManifest`
|
|
28
|
+
state file at `.agentforge-state/manifests/<distribution>.yaml`, so
|
|
29
|
+
`agentforge remove module X` can reverse the application.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class EnvVarEntry(BaseModel):
|
|
40
|
+
"""One env-var the module needs. Appended to `.env.example` on apply."""
|
|
41
|
+
|
|
42
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
43
|
+
|
|
44
|
+
name: str = Field(min_length=1)
|
|
45
|
+
description: str = ""
|
|
46
|
+
required: bool = True
|
|
47
|
+
default: str | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TemplateFile(BaseModel):
|
|
51
|
+
"""A file the module ships that gets copied into the agent repo."""
|
|
52
|
+
|
|
53
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
54
|
+
|
|
55
|
+
source: str = Field(min_length=1)
|
|
56
|
+
"""Path inside the module package (relative to the package root)."""
|
|
57
|
+
|
|
58
|
+
destination: str = Field(min_length=1)
|
|
59
|
+
"""Path in the consuming repo (relative to cwd at `add` time)."""
|
|
60
|
+
|
|
61
|
+
overwrite: bool = False
|
|
62
|
+
"""Whether to overwrite an existing destination. Default False —
|
|
63
|
+
a pre-existing file aborts the apply with a clear error."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Manifest(BaseModel):
|
|
67
|
+
"""Parsed `manifest.yaml`. Source of truth for what `add` does."""
|
|
68
|
+
|
|
69
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
70
|
+
|
|
71
|
+
category: str = Field(min_length=1)
|
|
72
|
+
"""Module category (`memory`, `tools`, `providers`, etc.) —
|
|
73
|
+
must match the entry-point group suffix."""
|
|
74
|
+
|
|
75
|
+
name: str = Field(min_length=1)
|
|
76
|
+
"""Module name within the category (e.g. `postgres`)."""
|
|
77
|
+
|
|
78
|
+
env_vars: list[EnvVarEntry] = Field(default_factory=list)
|
|
79
|
+
templates: list[TemplateFile] = Field(default_factory=list)
|
|
80
|
+
config_block: dict[str, Any] = Field(default_factory=dict)
|
|
81
|
+
"""A nested-dict snippet to deep-merge into `agentforge.yaml`."""
|
|
82
|
+
|
|
83
|
+
next_steps: list[str] = Field(default_factory=list)
|
|
84
|
+
"""Free-form lines printed after a successful `add`."""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class AppliedEnvVar(BaseModel):
|
|
88
|
+
"""Record of an env var the applier appended to `.env.example`."""
|
|
89
|
+
|
|
90
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
91
|
+
|
|
92
|
+
name: str = Field(min_length=1)
|
|
93
|
+
line: str = Field(min_length=1)
|
|
94
|
+
"""The exact line written, so `remove` can match-and-strip it."""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class AppliedTemplate(BaseModel):
|
|
98
|
+
"""Record of a file the applier created."""
|
|
99
|
+
|
|
100
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
101
|
+
|
|
102
|
+
destination: str = Field(min_length=1)
|
|
103
|
+
"""Path relative to cwd; safe to `unlink` on `remove`."""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class AppliedManifest(BaseModel):
|
|
107
|
+
"""State file at `.agentforge-state/manifests/<distribution>.yaml`.
|
|
108
|
+
|
|
109
|
+
Records exactly what `agentforge add module X` wrote, so
|
|
110
|
+
`agentforge remove module X` can reverse it. Atomic at the
|
|
111
|
+
individual-step level (each list reflects what *did* land);
|
|
112
|
+
apply failures partway through still write the state for what
|
|
113
|
+
succeeded so `remove` can clean up.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
model_config = ConfigDict(strict=True, extra="forbid")
|
|
117
|
+
|
|
118
|
+
distribution: str = Field(min_length=1)
|
|
119
|
+
"""The `agentforge-X` distribution name."""
|
|
120
|
+
|
|
121
|
+
category: str = Field(min_length=1)
|
|
122
|
+
name: str = Field(min_length=1)
|
|
123
|
+
"""Mirrors `Manifest.category` / `Manifest.name` for diagnostics."""
|
|
124
|
+
|
|
125
|
+
env_vars: list[AppliedEnvVar] = Field(default_factory=list)
|
|
126
|
+
templates: list[AppliedTemplate] = Field(default_factory=list)
|
|
127
|
+
config_block_applied: bool = False
|
|
128
|
+
"""Whether the deep-merge into `agentforge.yaml` landed. Used by
|
|
129
|
+
`remove` to know whether to attempt the reverse merge."""
|