activegraph 1.0.0rc2__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.
- activegraph/__init__.py +222 -0
- activegraph/__main__.py +5 -0
- activegraph/behaviors/__init__.py +1 -0
- activegraph/behaviors/base.py +153 -0
- activegraph/behaviors/decorators.py +218 -0
- activegraph/cli/__init__.py +17 -0
- activegraph/cli/main.py +793 -0
- activegraph/cli/quickstart.py +556 -0
- activegraph/core/__init__.py +1 -0
- activegraph/core/clock.py +46 -0
- activegraph/core/event.py +33 -0
- activegraph/core/graph.py +846 -0
- activegraph/core/ids.py +135 -0
- activegraph/core/patch.py +47 -0
- activegraph/core/view.py +59 -0
- activegraph/errors.py +342 -0
- activegraph/frame.py +15 -0
- activegraph/llm/__init__.py +51 -0
- activegraph/llm/anthropic.py +339 -0
- activegraph/llm/cache.py +180 -0
- activegraph/llm/errors.py +257 -0
- activegraph/llm/prompt.py +420 -0
- activegraph/llm/provider.py +66 -0
- activegraph/llm/recorded.py +270 -0
- activegraph/llm/types.py +130 -0
- activegraph/observability/__init__.py +61 -0
- activegraph/observability/logging.py +205 -0
- activegraph/observability/metrics.py +219 -0
- activegraph/observability/migration.py +404 -0
- activegraph/observability/prometheus.py +101 -0
- activegraph/observability/status.py +83 -0
- activegraph/packs/__init__.py +999 -0
- activegraph/packs/diligence/__init__.py +86 -0
- activegraph/packs/diligence/behaviors.py +447 -0
- activegraph/packs/diligence/fixtures/__init__.py +364 -0
- activegraph/packs/diligence/fixtures/companies.py +511 -0
- activegraph/packs/diligence/object_types.py +163 -0
- activegraph/packs/diligence/settings.py +60 -0
- activegraph/packs/diligence/tools.py +124 -0
- activegraph/packs/loader.py +709 -0
- activegraph/packs/scaffold.py +317 -0
- activegraph/policy.py +20 -0
- activegraph/runtime/__init__.py +1 -0
- activegraph/runtime/behavior_graph.py +151 -0
- activegraph/runtime/budget.py +120 -0
- activegraph/runtime/config_errors.py +124 -0
- activegraph/runtime/diff.py +145 -0
- activegraph/runtime/errors.py +216 -0
- activegraph/runtime/exec_errors.py +232 -0
- activegraph/runtime/patterns.py +946 -0
- activegraph/runtime/queue.py +27 -0
- activegraph/runtime/registration_errors.py +291 -0
- activegraph/runtime/registry.py +111 -0
- activegraph/runtime/runtime.py +2441 -0
- activegraph/runtime/scheduler.py +206 -0
- activegraph/runtime/view_builder.py +65 -0
- activegraph/store/__init__.py +41 -0
- activegraph/store/base.py +66 -0
- activegraph/store/conformance.py +161 -0
- activegraph/store/errors.py +84 -0
- activegraph/store/memory.py +118 -0
- activegraph/store/postgres.py +609 -0
- activegraph/store/serde.py +202 -0
- activegraph/store/sqlite.py +446 -0
- activegraph/store/url.py +230 -0
- activegraph/tools/__init__.py +64 -0
- activegraph/tools/base.py +57 -0
- activegraph/tools/cache.py +157 -0
- activegraph/tools/context.py +48 -0
- activegraph/tools/decorators.py +70 -0
- activegraph/tools/errors.py +320 -0
- activegraph/tools/graph_query.py +94 -0
- activegraph/tools/recorded.py +205 -0
- activegraph/tools/web_fetch.py +80 -0
- activegraph/trace/__init__.py +1 -0
- activegraph/trace/causal.py +123 -0
- activegraph/trace/printer.py +495 -0
- activegraph-1.0.0rc2.dist-info/METADATA +228 -0
- activegraph-1.0.0rc2.dist-info/RECORD +82 -0
- activegraph-1.0.0rc2.dist-info/WHEEL +5 -0
- activegraph-1.0.0rc2.dist-info/entry_points.txt +5 -0
- activegraph-1.0.0rc2.dist-info/top_level.txt +1 -0
activegraph/core/ids.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""ID generation. CONTRACT #1 and CONTRACT v0.5 #6 (run ids are ULIDs).
|
|
2
|
+
|
|
3
|
+
All IDs flow through one generator so tests can swap in a deterministic one.
|
|
4
|
+
v0 uses short prefixed monotonic strings; v0.5 adds `run()` for ULID-shaped
|
|
5
|
+
run identifiers and `from_events()` to rebuild counters after a replay.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import time
|
|
13
|
+
from typing import Iterable
|
|
14
|
+
|
|
15
|
+
from activegraph.core.event import Event
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _ulid() -> str:
|
|
22
|
+
"""26-char Crockford base32 ULID. Time-prefixed, random suffix.
|
|
23
|
+
|
|
24
|
+
Not strictly monotonic within the same millisecond — good enough for
|
|
25
|
+
run identifiers (low write rate, no sort-criticality).
|
|
26
|
+
"""
|
|
27
|
+
ms = int(time.time() * 1000) & ((1 << 48) - 1)
|
|
28
|
+
rand = int.from_bytes(os.urandom(10), "big")
|
|
29
|
+
n = (ms << 80) | rand
|
|
30
|
+
out: list[str] = []
|
|
31
|
+
for _ in range(26):
|
|
32
|
+
out.append(_CROCKFORD[n & 0x1F])
|
|
33
|
+
n >>= 5
|
|
34
|
+
return "".join(reversed(out))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_OBJ_RE = re.compile(r"^(?P<type>[^#]+)#(?P<n>\d+)$")
|
|
38
|
+
_NUM_RE = re.compile(r"^[a-zA-Z]+_(?P<n>\d+)$")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class IDGen:
|
|
42
|
+
"""Per-graph monotonic ID generator. Not thread-safe (single-threaded loop)."""
|
|
43
|
+
|
|
44
|
+
def __init__(self) -> None:
|
|
45
|
+
# CONTRACT #1: objects use a global counter prefixed by type
|
|
46
|
+
# (matches the README trace: task#1, task#2, claim#3 — not claim#1).
|
|
47
|
+
self._object_counter = 0
|
|
48
|
+
self._event_counter = 0
|
|
49
|
+
self._relation_counter = 0
|
|
50
|
+
self._patch_counter = 0
|
|
51
|
+
self._frame_counter = 0
|
|
52
|
+
|
|
53
|
+
# ---- generators ----
|
|
54
|
+
|
|
55
|
+
def object(self, type_: str) -> str:
|
|
56
|
+
self._object_counter += 1
|
|
57
|
+
return f"{type_}#{self._object_counter}"
|
|
58
|
+
|
|
59
|
+
def event(self) -> str:
|
|
60
|
+
self._event_counter += 1
|
|
61
|
+
return f"evt_{self._event_counter:03d}"
|
|
62
|
+
|
|
63
|
+
def relation(self) -> str:
|
|
64
|
+
self._relation_counter += 1
|
|
65
|
+
return f"rel_{self._relation_counter:03d}"
|
|
66
|
+
|
|
67
|
+
def patch(self) -> str:
|
|
68
|
+
self._patch_counter += 1
|
|
69
|
+
return f"patch_{self._patch_counter:03d}"
|
|
70
|
+
|
|
71
|
+
def frame(self) -> str:
|
|
72
|
+
self._frame_counter += 1
|
|
73
|
+
return f"frame_{self._frame_counter:03d}"
|
|
74
|
+
|
|
75
|
+
def run(self) -> str:
|
|
76
|
+
# ULID per CONTRACT v0.5 #6. Not counter-based: runs live in storage
|
|
77
|
+
# and are looked up by id, so collisions across files are the risk.
|
|
78
|
+
return _ulid()
|
|
79
|
+
|
|
80
|
+
# ---- replay reconstruction (CONTRACT v0.5 #14, #15) ----
|
|
81
|
+
|
|
82
|
+
def reseed_from_events(self, events: Iterable[Event]) -> None:
|
|
83
|
+
"""Set counters past the highest id seen in `events`.
|
|
84
|
+
|
|
85
|
+
Used after replay so subsequent `object()/event()/...` continue
|
|
86
|
+
monotonically from where the loaded log ended. Forks call this too,
|
|
87
|
+
which is why two forks at the same point produce IDs that diverge
|
|
88
|
+
identically (decision #12 — fine because the IDs live in different
|
|
89
|
+
runs).
|
|
90
|
+
"""
|
|
91
|
+
max_obj = 0
|
|
92
|
+
max_evt = 0
|
|
93
|
+
max_rel = 0
|
|
94
|
+
max_patch = 0
|
|
95
|
+
max_frame = 0
|
|
96
|
+
for e in events:
|
|
97
|
+
n = _suffix_num(e.id)
|
|
98
|
+
if n is not None:
|
|
99
|
+
max_evt = max(max_evt, n)
|
|
100
|
+
if e.frame_id:
|
|
101
|
+
fn = _suffix_num(e.frame_id)
|
|
102
|
+
if fn is not None:
|
|
103
|
+
max_frame = max(max_frame, fn)
|
|
104
|
+
p = e.payload or {}
|
|
105
|
+
if e.type == "object.created":
|
|
106
|
+
obj_id = (p.get("object") or {}).get("id", "")
|
|
107
|
+
m = _OBJ_RE.match(obj_id)
|
|
108
|
+
if m:
|
|
109
|
+
max_obj = max(max_obj, int(m.group("n")))
|
|
110
|
+
elif e.type == "relation.created":
|
|
111
|
+
rel_id = (p.get("relation") or {}).get("id", "")
|
|
112
|
+
rn = _suffix_num(rel_id)
|
|
113
|
+
if rn is not None:
|
|
114
|
+
max_rel = max(max_rel, rn)
|
|
115
|
+
elif e.type in ("patch.proposed", "patch.applied"):
|
|
116
|
+
patch_id = (p.get("patch") or {}).get("id", "")
|
|
117
|
+
pn = _suffix_num(patch_id)
|
|
118
|
+
if pn is not None:
|
|
119
|
+
max_patch = max(max_patch, pn)
|
|
120
|
+
elif e.type == "patch.rejected":
|
|
121
|
+
pn = _suffix_num(p.get("patch_id", "") or "")
|
|
122
|
+
if pn is not None:
|
|
123
|
+
max_patch = max(max_patch, pn)
|
|
124
|
+
self._object_counter = max(self._object_counter, max_obj)
|
|
125
|
+
self._event_counter = max(self._event_counter, max_evt)
|
|
126
|
+
self._relation_counter = max(self._relation_counter, max_rel)
|
|
127
|
+
self._patch_counter = max(self._patch_counter, max_patch)
|
|
128
|
+
self._frame_counter = max(self._frame_counter, max_frame)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _suffix_num(s: str) -> int | None:
|
|
132
|
+
m = _NUM_RE.match(s or "")
|
|
133
|
+
if not m:
|
|
134
|
+
return None
|
|
135
|
+
return int(m.group("n"))
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Patch primitives. CONTRACT #4 (versioning) and #12 (single-target atomic).
|
|
2
|
+
|
|
3
|
+
A Patch is a proposed mutation. Lifecycle:
|
|
4
|
+
proposed -> applied
|
|
5
|
+
proposed -> rejected
|
|
6
|
+
`patch_object` is the auto-apply shortcut: builds a patch, version-checks,
|
|
7
|
+
emits patch.applied (or patch.rejected) directly. `propose_patch` emits
|
|
8
|
+
patch.proposed and waits for explicit approval.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
PATCH_OPS = {"create", "update", "replace", "remove"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Patch:
|
|
22
|
+
id: str
|
|
23
|
+
target: str
|
|
24
|
+
op: str
|
|
25
|
+
value: dict[str, Any]
|
|
26
|
+
expected_version: int
|
|
27
|
+
proposed_by: str
|
|
28
|
+
rationale: Optional[str] = None
|
|
29
|
+
evidence: list[str] = field(default_factory=list)
|
|
30
|
+
status: str = "proposed" # proposed | applied | rejected
|
|
31
|
+
rejection_reason: Optional[str] = None
|
|
32
|
+
provenance: dict[str, Any] = field(default_factory=dict)
|
|
33
|
+
|
|
34
|
+
def to_dict(self) -> dict[str, Any]:
|
|
35
|
+
return {
|
|
36
|
+
"id": self.id,
|
|
37
|
+
"target": self.target,
|
|
38
|
+
"op": self.op,
|
|
39
|
+
"value": self.value,
|
|
40
|
+
"expected_version": self.expected_version,
|
|
41
|
+
"proposed_by": self.proposed_by,
|
|
42
|
+
"rationale": self.rationale,
|
|
43
|
+
"evidence": list(self.evidence),
|
|
44
|
+
"status": self.status,
|
|
45
|
+
"rejection_reason": self.rejection_reason,
|
|
46
|
+
"provenance": dict(self.provenance),
|
|
47
|
+
}
|
activegraph/core/view.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Read-only scoped slice of the graph passed to behaviors as ctx.view.
|
|
2
|
+
|
|
3
|
+
CONTRACT #11: behaviors do not build their own views via graph.query —
|
|
4
|
+
they declare what they want via decorator metadata and the runtime
|
|
5
|
+
constructs a View before invocation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from activegraph.core.event import Event
|
|
13
|
+
from activegraph.core.graph import Object, Relation, evaluate_where
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class View:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
objects: list[Object],
|
|
20
|
+
relations: list[Relation],
|
|
21
|
+
events: list[Event],
|
|
22
|
+
) -> None:
|
|
23
|
+
self._objects = objects
|
|
24
|
+
self._relations = relations
|
|
25
|
+
self._events = events
|
|
26
|
+
|
|
27
|
+
def objects(
|
|
28
|
+
self,
|
|
29
|
+
type: Optional[str] = None,
|
|
30
|
+
where: Optional[dict[str, Any]] = None,
|
|
31
|
+
) -> list[Object]:
|
|
32
|
+
out = self._objects
|
|
33
|
+
if type is not None:
|
|
34
|
+
out = [o for o in out if o.type == type]
|
|
35
|
+
if where:
|
|
36
|
+
out = [o for o in out if evaluate_where(where, _object_root(o))]
|
|
37
|
+
return list(out)
|
|
38
|
+
|
|
39
|
+
def relations(self, type: Optional[str] = None) -> list[Relation]:
|
|
40
|
+
out = self._relations
|
|
41
|
+
if type is not None:
|
|
42
|
+
out = [r for r in out if r.type == type]
|
|
43
|
+
return list(out)
|
|
44
|
+
|
|
45
|
+
def events(self, type: Optional[str] = None) -> list[Event]:
|
|
46
|
+
out = self._events
|
|
47
|
+
if type is not None:
|
|
48
|
+
out = [e for e in out if e.type == type]
|
|
49
|
+
return list(out)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _object_root(o: Object) -> dict[str, Any]:
|
|
53
|
+
return {
|
|
54
|
+
"id": o.id,
|
|
55
|
+
"type": o.type,
|
|
56
|
+
"data": o.data,
|
|
57
|
+
"version": o.version,
|
|
58
|
+
"provenance": o.provenance,
|
|
59
|
+
}
|
activegraph/errors.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""ActiveGraphError hierarchy. CONTRACT v1.0 #3, #4 — the error format and
|
|
2
|
+
the class tree are public contract.
|
|
3
|
+
|
|
4
|
+
Every framework error inherits from `ActiveGraphError` and renders in the
|
|
5
|
+
locked format:
|
|
6
|
+
|
|
7
|
+
<ErrorClass>: <one-line summary>
|
|
8
|
+
|
|
9
|
+
What failed:
|
|
10
|
+
<specific thing that went wrong, with names>
|
|
11
|
+
|
|
12
|
+
Why:
|
|
13
|
+
<root cause, not the symptom>
|
|
14
|
+
|
|
15
|
+
How to fix:
|
|
16
|
+
<concrete action>
|
|
17
|
+
|
|
18
|
+
More:
|
|
19
|
+
https://docs.activegraph.dev/errors/<slug>
|
|
20
|
+
|
|
21
|
+
The seven category bases are stable. Concrete leaves are migrated under
|
|
22
|
+
their categories one PR at a time (CONTRACT v1.0 #C1 — the rewrite ships
|
|
23
|
+
as a PR series, not one PR). PR-A lands the foundation plus ReplayError
|
|
24
|
+
as the reference category; subsequent PRs migrate the other categories
|
|
25
|
+
without changing the bases.
|
|
26
|
+
|
|
27
|
+
`_doc_slug` on each class is the URL slug for the error's doc page. The
|
|
28
|
+
base URL is the github.io fallback (CONTRACT v1.0 #C6); once DNS for
|
|
29
|
+
`docs.activegraph.dev` is live, the constant is swapped in one place.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from typing import Any, ClassVar
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# CONTRACT v1.0 #C6: until docs.activegraph.dev DNS is live, error URLs
|
|
38
|
+
# point at the github.io fallback. The cutover is a one-line edit here
|
|
39
|
+
# plus a README update; the URLs in every error message follow.
|
|
40
|
+
DOCS_BASE_URL = "https://yoheinakajima.github.io/activegraph"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _indent_continuation(text: str, indent: str = " ") -> str:
|
|
44
|
+
"""Re-indent a multi-line block so every line after the first sits
|
|
45
|
+
under the same column as the first line. The format spec puts every
|
|
46
|
+
field's body on a continuation line indented by two spaces; multi-line
|
|
47
|
+
bodies (a how-to-fix with three steps, for instance) maintain that
|
|
48
|
+
indent on every line so the message reads as a single block."""
|
|
49
|
+
lines = text.split("\n")
|
|
50
|
+
if len(lines) == 1:
|
|
51
|
+
return lines[0]
|
|
52
|
+
return lines[0] + "\n" + "\n".join(indent + line if line else line for line in lines[1:])
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ActiveGraphError(Exception):
|
|
56
|
+
"""Root of every framework error. CONTRACT v1.0 #4.
|
|
57
|
+
|
|
58
|
+
Subclasses construct by passing a one-line ``summary`` plus the three
|
|
59
|
+
structured fields (``what_failed``, ``why``, ``how_to_fix``) and any
|
|
60
|
+
error-specific context. ``__str__`` produces the locked format; the
|
|
61
|
+
structured fields stay accessible programmatically for tools that
|
|
62
|
+
want to render errors differently (a doc-site error catalog, a
|
|
63
|
+
machine-readable failure log, etc.).
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
# Each subclass sets its own slug. The base class slug exists so a
|
|
67
|
+
# bare `ActiveGraphError` (which should not be raised in practice;
|
|
68
|
+
# always reach for a concrete subclass) still produces a valid URL.
|
|
69
|
+
_doc_slug: ClassVar[str] = "active-graph-error"
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
summary_or_message: str,
|
|
74
|
+
*,
|
|
75
|
+
what_failed: str | None = None,
|
|
76
|
+
why: str | None = None,
|
|
77
|
+
how_to_fix: str | None = None,
|
|
78
|
+
context: dict[str, Any] | None = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Two construction modes during the v1.0 transition:
|
|
81
|
+
|
|
82
|
+
- **Structured** (the v1.0 target): pass ``summary`` plus the three
|
|
83
|
+
named fields. ``__str__`` produces the locked format.
|
|
84
|
+
- **Legacy**: pass a single positional message. Used by error
|
|
85
|
+
leaves that have not yet migrated under the v1.0 PR series.
|
|
86
|
+
``__str__`` returns the message verbatim — format-noncompliant
|
|
87
|
+
but valid Python, so existing raises keep working.
|
|
88
|
+
|
|
89
|
+
PR-A converts the ReplayError leaves (the reference category).
|
|
90
|
+
PR-B through PR-F convert the rest one PR at a time. The legacy
|
|
91
|
+
branch goes away once every leaf is migrated; until then this
|
|
92
|
+
gateway is the bridge.
|
|
93
|
+
"""
|
|
94
|
+
self._summary = summary_or_message
|
|
95
|
+
self.what_failed = what_failed or ""
|
|
96
|
+
self.why = why or ""
|
|
97
|
+
self.how_to_fix = how_to_fix or ""
|
|
98
|
+
self.context: dict[str, Any] = dict(context) if context else {}
|
|
99
|
+
if self.is_structured():
|
|
100
|
+
super().__init__(self._format())
|
|
101
|
+
else:
|
|
102
|
+
super().__init__(summary_or_message)
|
|
103
|
+
|
|
104
|
+
def is_structured(self) -> bool:
|
|
105
|
+
"""True when the three structured fields are populated. Used by
|
|
106
|
+
the format snapshot tests and the docs catalog to filter out
|
|
107
|
+
leaves that have not yet migrated to the v1.0 format."""
|
|
108
|
+
return bool(self.what_failed and self.why and self.how_to_fix)
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def doc_url(self) -> str:
|
|
112
|
+
return f"{DOCS_BASE_URL}/errors/{self._doc_slug}"
|
|
113
|
+
|
|
114
|
+
def _format(self) -> str:
|
|
115
|
+
return (
|
|
116
|
+
f"{type(self).__name__}: {self._summary}\n\n"
|
|
117
|
+
f"What failed:\n {_indent_continuation(self.what_failed)}\n\n"
|
|
118
|
+
f"Why:\n {_indent_continuation(self.why)}\n\n"
|
|
119
|
+
f"How to fix:\n {_indent_continuation(self.how_to_fix)}\n\n"
|
|
120
|
+
f"More:\n {self.doc_url}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def __str__(self) -> str:
|
|
124
|
+
if self.is_structured():
|
|
125
|
+
return self._format()
|
|
126
|
+
return self._summary
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ---------- Category bases ----------------------------------------------
|
|
130
|
+
#
|
|
131
|
+
# The seven categories from CONTRACT v1.0 #4. Each is an abstract base —
|
|
132
|
+
# raise a concrete subclass, not a bare category. Until PR-B through PR-F
|
|
133
|
+
# land, some categories have no concrete leaves yet; the bases still
|
|
134
|
+
# exist so external code can `except RegistrationError:` today and have
|
|
135
|
+
# it cover those leaves once they migrate.
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ConfigurationError(ActiveGraphError):
|
|
139
|
+
"""Runtime construction problems: invalid budget, malformed store URL,
|
|
140
|
+
missing required configuration. Fires before any work runs."""
|
|
141
|
+
|
|
142
|
+
_doc_slug = "configuration-error"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class RegistrationError(ActiveGraphError):
|
|
146
|
+
"""Behavior, tool, or pack registration problems: conflicts at
|
|
147
|
+
registration time, version mismatches, missing providers, unknown
|
|
148
|
+
tools. Fires at registration / pack-load time."""
|
|
149
|
+
|
|
150
|
+
_doc_slug = "registration-error"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class ExecutionError(ActiveGraphError):
|
|
154
|
+
"""Runtime execution problems: behavior failures, budget exhausted,
|
|
155
|
+
tool failures during a goal run. Named ExecutionError (not
|
|
156
|
+
RuntimeError) because Python already has builtin ``RuntimeError`` and
|
|
157
|
+
shadowing the builtin produces confusing stack traces."""
|
|
158
|
+
|
|
159
|
+
_doc_slug = "execution-error"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class ReplayError(ActiveGraphError):
|
|
163
|
+
"""Replay and fork problems: cache hash mismatches, type-stream
|
|
164
|
+
divergence between recorded and re-run event logs. Fires only during
|
|
165
|
+
replay / fork; never during a fresh run."""
|
|
166
|
+
|
|
167
|
+
_doc_slug = "replay-error"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class StorageError(ActiveGraphError):
|
|
171
|
+
"""Persistence problems: failed writes, malformed event payloads on
|
|
172
|
+
deserialize, schema version mismatches."""
|
|
173
|
+
|
|
174
|
+
_doc_slug = "storage-error"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class PatternError(ActiveGraphError):
|
|
178
|
+
"""Pattern subscription problems: invalid Cypher syntax, unsupported
|
|
179
|
+
features, malformed WHERE clauses at registration time."""
|
|
180
|
+
|
|
181
|
+
_doc_slug = "pattern-error"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class PackError(ActiveGraphError):
|
|
185
|
+
"""Pack-specific problems at runtime (not registration): schema
|
|
186
|
+
violations on add_object after pack load, pack-state inconsistencies.
|
|
187
|
+
Registration-time pack errors live under :class:`RegistrationError`
|
|
188
|
+
instead."""
|
|
189
|
+
|
|
190
|
+
_doc_slug = "pack-error"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class MissingOptionalDependency(RegistrationError, ImportError):
|
|
194
|
+
"""A subsystem requires an optional Python package that isn't installed.
|
|
195
|
+
|
|
196
|
+
Used by the Postgres store, the Prometheus metrics backend, and the
|
|
197
|
+
Pack format (which requires Pydantic). Multi-inherits :class:`ImportError`
|
|
198
|
+
so user code that catches the builtin around optional-dep imports
|
|
199
|
+
continues to work. v1.0 PR-E.
|
|
200
|
+
|
|
201
|
+
Construct with the missing package name and the activegraph extras
|
|
202
|
+
name that bundles it; the structured message walks the user through
|
|
203
|
+
the install line.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
_doc_slug = "missing-optional-dependency"
|
|
207
|
+
|
|
208
|
+
def __init__(
|
|
209
|
+
self,
|
|
210
|
+
*,
|
|
211
|
+
package: str,
|
|
212
|
+
feature: str,
|
|
213
|
+
extras: str | None = None,
|
|
214
|
+
) -> None:
|
|
215
|
+
self.package = package
|
|
216
|
+
self.feature = feature
|
|
217
|
+
self.extras = extras
|
|
218
|
+
install_line = (
|
|
219
|
+
f"pip install 'activegraph[{extras}]'"
|
|
220
|
+
if extras
|
|
221
|
+
else f"pip install {package}"
|
|
222
|
+
)
|
|
223
|
+
ctx = {"package": package, "feature": feature}
|
|
224
|
+
if extras is not None:
|
|
225
|
+
ctx["extras"] = extras
|
|
226
|
+
RegistrationError.__init__(
|
|
227
|
+
self,
|
|
228
|
+
f"{feature} requires the {package!r} Python package",
|
|
229
|
+
what_failed=(
|
|
230
|
+
f"While initializing {feature}, the import of {package!r} "
|
|
231
|
+
f"failed because the package is not installed in this "
|
|
232
|
+
f"environment."
|
|
233
|
+
),
|
|
234
|
+
why=(
|
|
235
|
+
"The framework keeps optional subsystems off the default "
|
|
236
|
+
"install path so a minimal install stays small. Each "
|
|
237
|
+
"optional subsystem declares its dependency explicitly; "
|
|
238
|
+
"missing it produces this error rather than failing later "
|
|
239
|
+
"with a confusing AttributeError or ImportError deep inside "
|
|
240
|
+
"the subsystem."
|
|
241
|
+
),
|
|
242
|
+
how_to_fix=(
|
|
243
|
+
f"Install the optional dependency:\n"
|
|
244
|
+
f" {install_line}\n"
|
|
245
|
+
f"\n"
|
|
246
|
+
f"If you don't need {feature}, the bare `activegraph` "
|
|
247
|
+
f"install does not depend on {package!r} — the error only "
|
|
248
|
+
f"fires when the subsystem is actually used."
|
|
249
|
+
),
|
|
250
|
+
context=ctx,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
__all__ = [
|
|
255
|
+
"ActiveGraphError",
|
|
256
|
+
"ConfigurationError",
|
|
257
|
+
"RegistrationError",
|
|
258
|
+
"ExecutionError",
|
|
259
|
+
"ReplayError",
|
|
260
|
+
"StorageError",
|
|
261
|
+
"PatternError",
|
|
262
|
+
"PackError",
|
|
263
|
+
"MissingOptionalDependency",
|
|
264
|
+
"internal_bug_fields",
|
|
265
|
+
"GITHUB_NEW_ISSUE_URL",
|
|
266
|
+
"DOCS_BASE_URL",
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ---------- v1.0 PR-G: shared internal-bug helper -----------------------
|
|
271
|
+
#
|
|
272
|
+
# Three sites in the framework raise exceptions for framework-bug
|
|
273
|
+
# conditions (the parser/evaluator sees something the framework itself
|
|
274
|
+
# produced incorrectly, not user input). Per PR-G consistency pass, all
|
|
275
|
+
# three use the same context-dict shape and the same recovery prose so
|
|
276
|
+
# GitHub Issues filed from these errors arrive with uniform metadata.
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
GITHUB_NEW_ISSUE_URL = "https://github.com/yoheinakajima/activegraph/issues/new"
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def internal_bug_fields(
|
|
283
|
+
*,
|
|
284
|
+
summary: str,
|
|
285
|
+
what_happened: str,
|
|
286
|
+
why_invariant: str,
|
|
287
|
+
location: str,
|
|
288
|
+
extra_context: dict[str, Any] | None = None,
|
|
289
|
+
) -> dict[str, Any]:
|
|
290
|
+
"""Produce uniform structured fields for an internal-bug exception.
|
|
291
|
+
|
|
292
|
+
Used by the three framework-bug raise sites (two in
|
|
293
|
+
``activegraph/runtime/patterns.py`` for the WHERE evaluator's
|
|
294
|
+
unknown-operator and unrecognized-AST-node cases; one in
|
|
295
|
+
``activegraph/core/graph.py`` for the view-filter evaluator's
|
|
296
|
+
unknown-operator case). Returns the kwargs dict that an
|
|
297
|
+
:class:`ActiveGraphError` subclass's structured ``__init__``
|
|
298
|
+
consumes.
|
|
299
|
+
|
|
300
|
+
Uniform context dict shape:
|
|
301
|
+
|
|
302
|
+
- ``internal``: ``True``
|
|
303
|
+
- ``framework_version``: ``activegraph.__version__``
|
|
304
|
+
- ``internal_error_location``: module:function pointer for triage
|
|
305
|
+
- ``report_url``: the GitHub new-issue URL
|
|
306
|
+
- any per-site keys from ``extra_context``
|
|
307
|
+
|
|
308
|
+
Uniform recovery prose:
|
|
309
|
+
|
|
310
|
+
"This is a framework bug, not a problem with your code. Please
|
|
311
|
+
file an issue at <URL> with the framework version and the message
|
|
312
|
+
above."
|
|
313
|
+
|
|
314
|
+
PR-G normalization pass: the three pre-existing internal-bug
|
|
315
|
+
messages had drifted into three slightly different shapes; they
|
|
316
|
+
are unified here. Future internal-bug raises should call this
|
|
317
|
+
helper so the pattern stays uniform.
|
|
318
|
+
"""
|
|
319
|
+
from activegraph import __version__ as _aw_version
|
|
320
|
+
ctx: dict[str, Any] = {
|
|
321
|
+
"internal": True,
|
|
322
|
+
"framework_version": _aw_version,
|
|
323
|
+
"internal_error_location": location,
|
|
324
|
+
"report_url": GITHUB_NEW_ISSUE_URL,
|
|
325
|
+
}
|
|
326
|
+
if extra_context:
|
|
327
|
+
ctx.update(extra_context)
|
|
328
|
+
return {
|
|
329
|
+
"summary": summary,
|
|
330
|
+
"what_failed": what_happened,
|
|
331
|
+
"why": why_invariant,
|
|
332
|
+
"how_to_fix": (
|
|
333
|
+
"This is a framework bug, not a problem with your code.\n"
|
|
334
|
+
"Please file an issue and include the framework version, the\n"
|
|
335
|
+
"internal error location, and the full message above:\n"
|
|
336
|
+
f" {GITHUB_NEW_ISSUE_URL}\n"
|
|
337
|
+
"\n"
|
|
338
|
+
f" framework version: activegraph {_aw_version}\n"
|
|
339
|
+
f" internal location: {location}"
|
|
340
|
+
),
|
|
341
|
+
"context": ctx,
|
|
342
|
+
}
|
activegraph/frame.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Mission context for a run."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Frame:
|
|
11
|
+
goal: str
|
|
12
|
+
id: Optional[str] = None
|
|
13
|
+
constraints: list[str] = field(default_factory=list)
|
|
14
|
+
success_criteria: list[str] = field(default_factory=list)
|
|
15
|
+
permissions: list[str] = field(default_factory=list)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""LLM behaviors subpackage. CONTRACT v0.6.
|
|
2
|
+
|
|
3
|
+
Public surface:
|
|
4
|
+
|
|
5
|
+
LLMProvider — Protocol every provider implements
|
|
6
|
+
LLMMessage — single role-tagged message
|
|
7
|
+
LLMResponse — what `complete()` returns
|
|
8
|
+
AnthropicProvider — reference implementation
|
|
9
|
+
RecordedLLMProvider — fixture-backed provider for tests
|
|
10
|
+
RecordingLLMProvider — wraps another provider, persists responses
|
|
11
|
+
as fixtures (for first-time test seed)
|
|
12
|
+
LLMCache — content-keyed replay cache
|
|
13
|
+
AssembledPrompt — the deterministic prompt + params blob
|
|
14
|
+
returned by `LLMBehavior.build_prompt`
|
|
15
|
+
MissingProviderError — raised when an @llm_behavior is registered
|
|
16
|
+
without a runtime provider
|
|
17
|
+
LLMBehaviorError — structured failure carrier from @llm_behavior
|
|
18
|
+
wrappers; runtime turns it into
|
|
19
|
+
`behavior.failed` with a `reason`
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from activegraph.llm.anthropic import AnthropicProvider
|
|
23
|
+
from activegraph.llm.cache import LLMCache
|
|
24
|
+
from activegraph.llm.errors import LLMBehaviorError, MissingProviderError
|
|
25
|
+
from activegraph.llm.prompt import (
|
|
26
|
+
AssembledPrompt,
|
|
27
|
+
assemble_prompt,
|
|
28
|
+
schema_to_json,
|
|
29
|
+
serialize_view,
|
|
30
|
+
)
|
|
31
|
+
from activegraph.llm.provider import LLMProvider
|
|
32
|
+
from activegraph.llm.recorded import RecordedLLMProvider, RecordingLLMProvider
|
|
33
|
+
from activegraph.llm.types import LLMMessage, LLMResponse, ToolCall
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"AnthropicProvider",
|
|
38
|
+
"AssembledPrompt",
|
|
39
|
+
"LLMBehaviorError",
|
|
40
|
+
"LLMCache",
|
|
41
|
+
"LLMMessage",
|
|
42
|
+
"LLMProvider",
|
|
43
|
+
"LLMResponse",
|
|
44
|
+
"MissingProviderError",
|
|
45
|
+
"RecordedLLMProvider",
|
|
46
|
+
"RecordingLLMProvider",
|
|
47
|
+
"ToolCall",
|
|
48
|
+
"assemble_prompt",
|
|
49
|
+
"schema_to_json",
|
|
50
|
+
"serialize_view",
|
|
51
|
+
]
|