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.
Files changed (82) 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 +342 -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/settings.py +60 -0
  39. activegraph/packs/diligence/tools.py +124 -0
  40. activegraph/packs/loader.py +709 -0
  41. activegraph/packs/scaffold.py +317 -0
  42. activegraph/policy.py +20 -0
  43. activegraph/runtime/__init__.py +1 -0
  44. activegraph/runtime/behavior_graph.py +151 -0
  45. activegraph/runtime/budget.py +120 -0
  46. activegraph/runtime/config_errors.py +124 -0
  47. activegraph/runtime/diff.py +145 -0
  48. activegraph/runtime/errors.py +216 -0
  49. activegraph/runtime/exec_errors.py +232 -0
  50. activegraph/runtime/patterns.py +946 -0
  51. activegraph/runtime/queue.py +27 -0
  52. activegraph/runtime/registration_errors.py +291 -0
  53. activegraph/runtime/registry.py +111 -0
  54. activegraph/runtime/runtime.py +2441 -0
  55. activegraph/runtime/scheduler.py +206 -0
  56. activegraph/runtime/view_builder.py +65 -0
  57. activegraph/store/__init__.py +41 -0
  58. activegraph/store/base.py +66 -0
  59. activegraph/store/conformance.py +161 -0
  60. activegraph/store/errors.py +84 -0
  61. activegraph/store/memory.py +118 -0
  62. activegraph/store/postgres.py +609 -0
  63. activegraph/store/serde.py +202 -0
  64. activegraph/store/sqlite.py +446 -0
  65. activegraph/store/url.py +230 -0
  66. activegraph/tools/__init__.py +64 -0
  67. activegraph/tools/base.py +57 -0
  68. activegraph/tools/cache.py +157 -0
  69. activegraph/tools/context.py +48 -0
  70. activegraph/tools/decorators.py +70 -0
  71. activegraph/tools/errors.py +320 -0
  72. activegraph/tools/graph_query.py +94 -0
  73. activegraph/tools/recorded.py +205 -0
  74. activegraph/tools/web_fetch.py +80 -0
  75. activegraph/trace/__init__.py +1 -0
  76. activegraph/trace/causal.py +123 -0
  77. activegraph/trace/printer.py +495 -0
  78. activegraph-1.0.0rc2.dist-info/METADATA +228 -0
  79. activegraph-1.0.0rc2.dist-info/RECORD +82 -0
  80. activegraph-1.0.0rc2.dist-info/WHEEL +5 -0
  81. activegraph-1.0.0rc2.dist-info/entry_points.txt +5 -0
  82. activegraph-1.0.0rc2.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,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
+ ]