activegraph 1.0.0__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 (86) hide show
  1. activegraph/__init__.py +222 -0
  2. activegraph/__main__.py +5 -0
  3. activegraph/behaviors/__init__.py +1 -0
  4. activegraph/behaviors/base.py +153 -0
  5. activegraph/behaviors/decorators.py +218 -0
  6. activegraph/cli/__init__.py +17 -0
  7. activegraph/cli/main.py +793 -0
  8. activegraph/cli/quickstart.py +556 -0
  9. activegraph/core/__init__.py +1 -0
  10. activegraph/core/clock.py +46 -0
  11. activegraph/core/event.py +33 -0
  12. activegraph/core/graph.py +846 -0
  13. activegraph/core/ids.py +135 -0
  14. activegraph/core/patch.py +47 -0
  15. activegraph/core/view.py +59 -0
  16. activegraph/errors.py +345 -0
  17. activegraph/frame.py +15 -0
  18. activegraph/llm/__init__.py +51 -0
  19. activegraph/llm/anthropic.py +339 -0
  20. activegraph/llm/cache.py +180 -0
  21. activegraph/llm/errors.py +257 -0
  22. activegraph/llm/prompt.py +420 -0
  23. activegraph/llm/provider.py +66 -0
  24. activegraph/llm/recorded.py +270 -0
  25. activegraph/llm/types.py +130 -0
  26. activegraph/observability/__init__.py +61 -0
  27. activegraph/observability/logging.py +205 -0
  28. activegraph/observability/metrics.py +219 -0
  29. activegraph/observability/migration.py +404 -0
  30. activegraph/observability/prometheus.py +101 -0
  31. activegraph/observability/status.py +83 -0
  32. activegraph/packs/__init__.py +999 -0
  33. activegraph/packs/diligence/__init__.py +86 -0
  34. activegraph/packs/diligence/behaviors.py +447 -0
  35. activegraph/packs/diligence/fixtures/__init__.py +364 -0
  36. activegraph/packs/diligence/fixtures/companies.py +511 -0
  37. activegraph/packs/diligence/object_types.py +163 -0
  38. activegraph/packs/diligence/prompts/document_researcher.md +25 -0
  39. activegraph/packs/diligence/prompts/memo_synthesizer.md +35 -0
  40. activegraph/packs/diligence/prompts/question_generator.md +25 -0
  41. activegraph/packs/diligence/prompts/risk_identifier.md +26 -0
  42. activegraph/packs/diligence/settings.py +60 -0
  43. activegraph/packs/diligence/tools.py +124 -0
  44. activegraph/packs/loader.py +709 -0
  45. activegraph/packs/scaffold.py +317 -0
  46. activegraph/policy.py +20 -0
  47. activegraph/runtime/__init__.py +1 -0
  48. activegraph/runtime/behavior_graph.py +151 -0
  49. activegraph/runtime/budget.py +120 -0
  50. activegraph/runtime/config_errors.py +124 -0
  51. activegraph/runtime/diff.py +145 -0
  52. activegraph/runtime/errors.py +216 -0
  53. activegraph/runtime/exec_errors.py +232 -0
  54. activegraph/runtime/patterns.py +946 -0
  55. activegraph/runtime/queue.py +27 -0
  56. activegraph/runtime/registration_errors.py +291 -0
  57. activegraph/runtime/registry.py +111 -0
  58. activegraph/runtime/runtime.py +2441 -0
  59. activegraph/runtime/scheduler.py +206 -0
  60. activegraph/runtime/view_builder.py +65 -0
  61. activegraph/store/__init__.py +41 -0
  62. activegraph/store/base.py +66 -0
  63. activegraph/store/conformance.py +161 -0
  64. activegraph/store/errors.py +84 -0
  65. activegraph/store/memory.py +118 -0
  66. activegraph/store/postgres.py +609 -0
  67. activegraph/store/serde.py +202 -0
  68. activegraph/store/sqlite.py +446 -0
  69. activegraph/store/url.py +230 -0
  70. activegraph/tools/__init__.py +64 -0
  71. activegraph/tools/base.py +57 -0
  72. activegraph/tools/cache.py +157 -0
  73. activegraph/tools/context.py +48 -0
  74. activegraph/tools/decorators.py +70 -0
  75. activegraph/tools/errors.py +320 -0
  76. activegraph/tools/graph_query.py +94 -0
  77. activegraph/tools/recorded.py +205 -0
  78. activegraph/tools/web_fetch.py +80 -0
  79. activegraph/trace/__init__.py +1 -0
  80. activegraph/trace/causal.py +123 -0
  81. activegraph/trace/printer.py +495 -0
  82. activegraph-1.0.0.dist-info/METADATA +282 -0
  83. activegraph-1.0.0.dist-info/RECORD +86 -0
  84. activegraph-1.0.0.dist-info/WHEEL +5 -0
  85. activegraph-1.0.0.dist-info/entry_points.txt +5 -0
  86. activegraph-1.0.0.dist-info/top_level.txt +1 -0
@@ -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
+ }
@@ -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,345 @@
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.ai/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 primary `docs.activegraph.ai` domain (CONTRACT v1.0 #C6,
29
+ v1.0-rc3 amendment); the constant is the single swap point.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ from typing import Any, ClassVar
35
+
36
+
37
+ # CONTRACT v1.0 #C6 (v1.0-rc3 amendment): primary doc-site domain is
38
+ # docs.activegraph.ai. Until the user-owned Pages enablement and DNS
39
+ # land, the URL renders the same 404 the rc2 user-test surfaced; the
40
+ # v1.1 #9 deploy-verification gate fails until that lands, which is
41
+ # the correct signal. One-line swap point: update this constant and
42
+ # every error message URL and `DOCS_BASE_URL`-derived f-string follows.
43
+ DOCS_BASE_URL = "https://docs.activegraph.ai"
44
+
45
+
46
+ def _indent_continuation(text: str, indent: str = " ") -> str:
47
+ """Re-indent a multi-line block so every line after the first sits
48
+ under the same column as the first line. The format spec puts every
49
+ field's body on a continuation line indented by two spaces; multi-line
50
+ bodies (a how-to-fix with three steps, for instance) maintain that
51
+ indent on every line so the message reads as a single block."""
52
+ lines = text.split("\n")
53
+ if len(lines) == 1:
54
+ return lines[0]
55
+ return lines[0] + "\n" + "\n".join(indent + line if line else line for line in lines[1:])
56
+
57
+
58
+ class ActiveGraphError(Exception):
59
+ """Root of every framework error. CONTRACT v1.0 #4.
60
+
61
+ Subclasses construct by passing a one-line ``summary`` plus the three
62
+ structured fields (``what_failed``, ``why``, ``how_to_fix``) and any
63
+ error-specific context. ``__str__`` produces the locked format; the
64
+ structured fields stay accessible programmatically for tools that
65
+ want to render errors differently (a doc-site error catalog, a
66
+ machine-readable failure log, etc.).
67
+ """
68
+
69
+ # Each subclass sets its own slug. The base class slug exists so a
70
+ # bare `ActiveGraphError` (which should not be raised in practice;
71
+ # always reach for a concrete subclass) still produces a valid URL.
72
+ _doc_slug: ClassVar[str] = "active-graph-error"
73
+
74
+ def __init__(
75
+ self,
76
+ summary_or_message: str,
77
+ *,
78
+ what_failed: str | None = None,
79
+ why: str | None = None,
80
+ how_to_fix: str | None = None,
81
+ context: dict[str, Any] | None = None,
82
+ ) -> None:
83
+ """Two construction modes during the v1.0 transition:
84
+
85
+ - **Structured** (the v1.0 target): pass ``summary`` plus the three
86
+ named fields. ``__str__`` produces the locked format.
87
+ - **Legacy**: pass a single positional message. Used by error
88
+ leaves that have not yet migrated under the v1.0 PR series.
89
+ ``__str__`` returns the message verbatim — format-noncompliant
90
+ but valid Python, so existing raises keep working.
91
+
92
+ PR-A converts the ReplayError leaves (the reference category).
93
+ PR-B through PR-F convert the rest one PR at a time. The legacy
94
+ branch goes away once every leaf is migrated; until then this
95
+ gateway is the bridge.
96
+ """
97
+ self._summary = summary_or_message
98
+ self.what_failed = what_failed or ""
99
+ self.why = why or ""
100
+ self.how_to_fix = how_to_fix or ""
101
+ self.context: dict[str, Any] = dict(context) if context else {}
102
+ if self.is_structured():
103
+ super().__init__(self._format())
104
+ else:
105
+ super().__init__(summary_or_message)
106
+
107
+ def is_structured(self) -> bool:
108
+ """True when the three structured fields are populated. Used by
109
+ the format snapshot tests and the docs catalog to filter out
110
+ leaves that have not yet migrated to the v1.0 format."""
111
+ return bool(self.what_failed and self.why and self.how_to_fix)
112
+
113
+ @property
114
+ def doc_url(self) -> str:
115
+ return f"{DOCS_BASE_URL}/errors/{self._doc_slug}"
116
+
117
+ def _format(self) -> str:
118
+ return (
119
+ f"{type(self).__name__}: {self._summary}\n\n"
120
+ f"What failed:\n {_indent_continuation(self.what_failed)}\n\n"
121
+ f"Why:\n {_indent_continuation(self.why)}\n\n"
122
+ f"How to fix:\n {_indent_continuation(self.how_to_fix)}\n\n"
123
+ f"More:\n {self.doc_url}"
124
+ )
125
+
126
+ def __str__(self) -> str:
127
+ if self.is_structured():
128
+ return self._format()
129
+ return self._summary
130
+
131
+
132
+ # ---------- Category bases ----------------------------------------------
133
+ #
134
+ # The seven categories from CONTRACT v1.0 #4. Each is an abstract base —
135
+ # raise a concrete subclass, not a bare category. Until PR-B through PR-F
136
+ # land, some categories have no concrete leaves yet; the bases still
137
+ # exist so external code can `except RegistrationError:` today and have
138
+ # it cover those leaves once they migrate.
139
+
140
+
141
+ class ConfigurationError(ActiveGraphError):
142
+ """Runtime construction problems: invalid budget, malformed store URL,
143
+ missing required configuration. Fires before any work runs."""
144
+
145
+ _doc_slug = "configuration-error"
146
+
147
+
148
+ class RegistrationError(ActiveGraphError):
149
+ """Behavior, tool, or pack registration problems: conflicts at
150
+ registration time, version mismatches, missing providers, unknown
151
+ tools. Fires at registration / pack-load time."""
152
+
153
+ _doc_slug = "registration-error"
154
+
155
+
156
+ class ExecutionError(ActiveGraphError):
157
+ """Runtime execution problems: behavior failures, budget exhausted,
158
+ tool failures during a goal run. Named ExecutionError (not
159
+ RuntimeError) because Python already has builtin ``RuntimeError`` and
160
+ shadowing the builtin produces confusing stack traces."""
161
+
162
+ _doc_slug = "execution-error"
163
+
164
+
165
+ class ReplayError(ActiveGraphError):
166
+ """Replay and fork problems: cache hash mismatches, type-stream
167
+ divergence between recorded and re-run event logs. Fires only during
168
+ replay / fork; never during a fresh run."""
169
+
170
+ _doc_slug = "replay-error"
171
+
172
+
173
+ class StorageError(ActiveGraphError):
174
+ """Persistence problems: failed writes, malformed event payloads on
175
+ deserialize, schema version mismatches."""
176
+
177
+ _doc_slug = "storage-error"
178
+
179
+
180
+ class PatternError(ActiveGraphError):
181
+ """Pattern subscription problems: invalid Cypher syntax, unsupported
182
+ features, malformed WHERE clauses at registration time."""
183
+
184
+ _doc_slug = "pattern-error"
185
+
186
+
187
+ class PackError(ActiveGraphError):
188
+ """Pack-specific problems at runtime (not registration): schema
189
+ violations on add_object after pack load, pack-state inconsistencies.
190
+ Registration-time pack errors live under :class:`RegistrationError`
191
+ instead."""
192
+
193
+ _doc_slug = "pack-error"
194
+
195
+
196
+ class MissingOptionalDependency(RegistrationError, ImportError):
197
+ """A subsystem requires an optional Python package that isn't installed.
198
+
199
+ Used by the Postgres store, the Prometheus metrics backend, and the
200
+ Pack format (which requires Pydantic). Multi-inherits :class:`ImportError`
201
+ so user code that catches the builtin around optional-dep imports
202
+ continues to work. v1.0 PR-E.
203
+
204
+ Construct with the missing package name and the activegraph extras
205
+ name that bundles it; the structured message walks the user through
206
+ the install line.
207
+ """
208
+
209
+ _doc_slug = "missing-optional-dependency"
210
+
211
+ def __init__(
212
+ self,
213
+ *,
214
+ package: str,
215
+ feature: str,
216
+ extras: str | None = None,
217
+ ) -> None:
218
+ self.package = package
219
+ self.feature = feature
220
+ self.extras = extras
221
+ install_line = (
222
+ f"pip install 'activegraph[{extras}]'"
223
+ if extras
224
+ else f"pip install {package}"
225
+ )
226
+ ctx = {"package": package, "feature": feature}
227
+ if extras is not None:
228
+ ctx["extras"] = extras
229
+ RegistrationError.__init__(
230
+ self,
231
+ f"{feature} requires the {package!r} Python package",
232
+ what_failed=(
233
+ f"While initializing {feature}, the import of {package!r} "
234
+ f"failed because the package is not installed in this "
235
+ f"environment."
236
+ ),
237
+ why=(
238
+ "The framework keeps optional subsystems off the default "
239
+ "install path so a minimal install stays small. Each "
240
+ "optional subsystem declares its dependency explicitly; "
241
+ "missing it produces this error rather than failing later "
242
+ "with a confusing AttributeError or ImportError deep inside "
243
+ "the subsystem."
244
+ ),
245
+ how_to_fix=(
246
+ f"Install the optional dependency:\n"
247
+ f" {install_line}\n"
248
+ f"\n"
249
+ f"If you don't need {feature}, the bare `activegraph` "
250
+ f"install does not depend on {package!r} — the error only "
251
+ f"fires when the subsystem is actually used."
252
+ ),
253
+ context=ctx,
254
+ )
255
+
256
+
257
+ __all__ = [
258
+ "ActiveGraphError",
259
+ "ConfigurationError",
260
+ "RegistrationError",
261
+ "ExecutionError",
262
+ "ReplayError",
263
+ "StorageError",
264
+ "PatternError",
265
+ "PackError",
266
+ "MissingOptionalDependency",
267
+ "internal_bug_fields",
268
+ "GITHUB_NEW_ISSUE_URL",
269
+ "DOCS_BASE_URL",
270
+ ]
271
+
272
+
273
+ # ---------- v1.0 PR-G: shared internal-bug helper -----------------------
274
+ #
275
+ # Three sites in the framework raise exceptions for framework-bug
276
+ # conditions (the parser/evaluator sees something the framework itself
277
+ # produced incorrectly, not user input). Per PR-G consistency pass, all
278
+ # three use the same context-dict shape and the same recovery prose so
279
+ # GitHub Issues filed from these errors arrive with uniform metadata.
280
+
281
+
282
+ GITHUB_NEW_ISSUE_URL = "https://github.com/yoheinakajima/activegraph/issues/new"
283
+
284
+
285
+ def internal_bug_fields(
286
+ *,
287
+ summary: str,
288
+ what_happened: str,
289
+ why_invariant: str,
290
+ location: str,
291
+ extra_context: dict[str, Any] | None = None,
292
+ ) -> dict[str, Any]:
293
+ """Produce uniform structured fields for an internal-bug exception.
294
+
295
+ Used by the three framework-bug raise sites (two in
296
+ ``activegraph/runtime/patterns.py`` for the WHERE evaluator's
297
+ unknown-operator and unrecognized-AST-node cases; one in
298
+ ``activegraph/core/graph.py`` for the view-filter evaluator's
299
+ unknown-operator case). Returns the kwargs dict that an
300
+ :class:`ActiveGraphError` subclass's structured ``__init__``
301
+ consumes.
302
+
303
+ Uniform context dict shape:
304
+
305
+ - ``internal``: ``True``
306
+ - ``framework_version``: ``activegraph.__version__``
307
+ - ``internal_error_location``: module:function pointer for triage
308
+ - ``report_url``: the GitHub new-issue URL
309
+ - any per-site keys from ``extra_context``
310
+
311
+ Uniform recovery prose:
312
+
313
+ "This is a framework bug, not a problem with your code. Please
314
+ file an issue at <URL> with the framework version and the message
315
+ above."
316
+
317
+ PR-G normalization pass: the three pre-existing internal-bug
318
+ messages had drifted into three slightly different shapes; they
319
+ are unified here. Future internal-bug raises should call this
320
+ helper so the pattern stays uniform.
321
+ """
322
+ from activegraph import __version__ as _aw_version
323
+ ctx: dict[str, Any] = {
324
+ "internal": True,
325
+ "framework_version": _aw_version,
326
+ "internal_error_location": location,
327
+ "report_url": GITHUB_NEW_ISSUE_URL,
328
+ }
329
+ if extra_context:
330
+ ctx.update(extra_context)
331
+ return {
332
+ "summary": summary,
333
+ "what_failed": what_happened,
334
+ "why": why_invariant,
335
+ "how_to_fix": (
336
+ "This is a framework bug, not a problem with your code.\n"
337
+ "Please file an issue and include the framework version, the\n"
338
+ "internal error location, and the full message above:\n"
339
+ f" {GITHUB_NEW_ISSUE_URL}\n"
340
+ "\n"
341
+ f" framework version: activegraph {_aw_version}\n"
342
+ f" internal location: {location}"
343
+ ),
344
+ "context": ctx,
345
+ }
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
+ ]