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,124 @@
1
+ """Runtime-side configuration error leaves. v1.0 PR-F.
2
+
3
+ Configuration errors fire when the caller constructs or calls the
4
+ runtime with arguments that violate a static contract — wrong types,
5
+ conflicting kwargs, out-of-range values, operations that require a
6
+ specific backend that isn't attached, state that's already set and
7
+ can't be re-set. The errors are caller-actionable: the developer
8
+ either fixes the call or restructures their setup.
9
+
10
+ Three classes here cover the audit:
11
+
12
+ - :class:`InvalidRuntimeConfiguration` — most ValueError shapes
13
+ (conflicting args, missing required args, out-of-range values).
14
+ Multi-inherits :class:`ValueError` for back-compat.
15
+ - :class:`InvalidArgumentType` — wrong-type values passed to
16
+ constructors. Multi-inherits :class:`TypeError`.
17
+ - :class:`IncompatibleRuntimeState` — operations that require a
18
+ specific runtime state (e.g., fork requires a SQLite-backed
19
+ runtime; graph already has a store attached). Multi-inherits
20
+ :class:`RuntimeError`.
21
+
22
+ See CONTRACT v1.0 PR-F for the audit table mapping each migrated
23
+ raise site to its class.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from typing import Any, Optional
29
+
30
+ from activegraph.errors import ConfigurationError
31
+
32
+
33
+ class InvalidRuntimeConfiguration(ConfigurationError, ValueError):
34
+ """Caller-provided configuration is invalid (conflicting arguments,
35
+ missing required argument, out-of-range value).
36
+
37
+ Multi-inherits :class:`ValueError` so user code that catches the
38
+ builtin around runtime construction or method calls keeps working.
39
+
40
+ Construct with a one-line ``summary`` plus the three structured
41
+ fields. The recovery prose is per-call-site, not table-driven —
42
+ each configuration mistake has a different fix.
43
+ """
44
+
45
+ _doc_slug = "invalid-runtime-configuration"
46
+
47
+ def __init__(
48
+ self,
49
+ summary: str,
50
+ *,
51
+ what_failed: str,
52
+ why: str,
53
+ how_to_fix: str,
54
+ context: Optional[dict[str, Any]] = None,
55
+ ) -> None:
56
+ ConfigurationError.__init__(
57
+ self,
58
+ summary,
59
+ what_failed=what_failed,
60
+ why=why,
61
+ how_to_fix=how_to_fix,
62
+ context=context,
63
+ )
64
+
65
+
66
+ class InvalidArgumentType(ConfigurationError, TypeError):
67
+ """A value passed to a constructor or method has the wrong type.
68
+
69
+ Multi-inherits :class:`TypeError`. Used when the framework's
70
+ contract is type-based (e.g., :class:`PostgresEventStore` accepts
71
+ a URL string, a psycopg.Connection, or a psycopg_pool.ConnectionPool —
72
+ anything else is refused at construction).
73
+ """
74
+
75
+ _doc_slug = "invalid-argument-type"
76
+
77
+ def __init__(
78
+ self,
79
+ summary: str,
80
+ *,
81
+ what_failed: str,
82
+ why: str,
83
+ how_to_fix: str,
84
+ context: Optional[dict[str, Any]] = None,
85
+ ) -> None:
86
+ ConfigurationError.__init__(
87
+ self,
88
+ summary,
89
+ what_failed=what_failed,
90
+ why=why,
91
+ how_to_fix=how_to_fix,
92
+ context=context,
93
+ )
94
+
95
+
96
+ class IncompatibleRuntimeState(ConfigurationError, RuntimeError):
97
+ """An operation requires a runtime state that isn't satisfied —
98
+ either a state that must be set but isn't, or a state that mustn't
99
+ be set but is.
100
+
101
+ Examples: ``runtime.fork()`` requires a SQLite-backed runtime;
102
+ ``graph.attach_store()`` requires no existing store. Multi-inherits
103
+ :class:`RuntimeError` for back-compat.
104
+ """
105
+
106
+ _doc_slug = "incompatible-runtime-state"
107
+
108
+ def __init__(
109
+ self,
110
+ summary: str,
111
+ *,
112
+ what_failed: str,
113
+ why: str,
114
+ how_to_fix: str,
115
+ context: Optional[dict[str, Any]] = None,
116
+ ) -> None:
117
+ ConfigurationError.__init__(
118
+ self,
119
+ summary,
120
+ what_failed=what_failed,
121
+ why=why,
122
+ how_to_fix=how_to_fix,
123
+ context=context,
124
+ )
@@ -0,0 +1,145 @@
1
+ """Structural diff between two runs (typically parent vs fork).
2
+
3
+ CONTRACT v0.5 #10: diff is structural only — divergent objects, divergent
4
+ relations, and which event ranges belong to each side. Semantic comparison
5
+ (e.g., "do these two claims express the same idea?") is a behavior's job,
6
+ not the runtime's.
7
+
8
+ Diff is computed by walking both graphs' final state and event logs. We
9
+ ignore lifecycle events (`behavior.*`, `runtime.*`) when computing the
10
+ event partition — they're scaffolding, not history.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import copy
16
+ from dataclasses import dataclass, field
17
+ from typing import Any, Optional
18
+
19
+ from activegraph.core.event import Event
20
+ from activegraph.core.graph import Graph, Object, Relation
21
+
22
+
23
+ _LIFECYCLE_PREFIXES = ("behavior.", "relation_behavior.", "runtime.")
24
+
25
+
26
+ def _is_lifecycle(e: Event) -> bool:
27
+ return any(e.type.startswith(p) for p in _LIFECYCLE_PREFIXES)
28
+
29
+
30
+ @dataclass
31
+ class DivergentObject:
32
+ id: str
33
+ in_parent: Optional[dict[str, Any]] # to_dict snapshot or None
34
+ in_fork: Optional[dict[str, Any]]
35
+
36
+ def summary(self) -> str:
37
+ if self.in_parent is None:
38
+ return f"{self.id} only in fork"
39
+ if self.in_fork is None:
40
+ return f"{self.id} only in parent"
41
+ return f"{self.id} differs (parent v{self.in_parent.get('version')} ↔ fork v{self.in_fork.get('version')})"
42
+
43
+
44
+ @dataclass
45
+ class DivergentRelation:
46
+ id: str
47
+ in_parent: Optional[dict[str, Any]]
48
+ in_fork: Optional[dict[str, Any]]
49
+
50
+ def summary(self) -> str:
51
+ if self.in_parent is None:
52
+ r = self.in_fork
53
+ return f"{self.id} only in fork ({r['source']} --{r['type']}--> {r['target']})"
54
+ if self.in_fork is None:
55
+ r = self.in_parent
56
+ return f"{self.id} only in parent ({r['source']} --{r['type']}--> {r['target']})"
57
+ return f"{self.id} differs"
58
+
59
+
60
+ @dataclass
61
+ class Diff:
62
+ parent_run_id: str
63
+ fork_run_id: str
64
+ shared_events: list[Event] = field(default_factory=list)
65
+ parent_only_events: list[Event] = field(default_factory=list)
66
+ fork_only_events: list[Event] = field(default_factory=list)
67
+ divergent_objects: list[DivergentObject] = field(default_factory=list)
68
+ divergent_relations: list[DivergentRelation] = field(default_factory=list)
69
+
70
+ @property
71
+ def is_identical(self) -> bool:
72
+ return (
73
+ not self.parent_only_events
74
+ and not self.fork_only_events
75
+ and not self.divergent_objects
76
+ and not self.divergent_relations
77
+ )
78
+
79
+
80
+ def compute_diff(parent: Graph, fork: Graph, parent_run_id: str, fork_run_id: str) -> Diff:
81
+ parent_events = [e for e in parent.events if not _is_lifecycle(e)]
82
+ fork_events = [e for e in fork.events if not _is_lifecycle(e)]
83
+
84
+ # Shared prefix: events that match by id, type AND payload. Same id with
85
+ # different content means the fork already diverged (logical ids are
86
+ # scoped to run_id — CONTRACT #12 — so collisions after the fork point
87
+ # are expected and must not be flattened into "shared".)
88
+ shared: list[Event] = []
89
+ i = 0
90
+ while i < len(parent_events) and i < len(fork_events):
91
+ a, b = parent_events[i], fork_events[i]
92
+ if a.id == b.id and a.type == b.type and a.payload == b.payload:
93
+ shared.append(a)
94
+ i += 1
95
+ else:
96
+ break
97
+ parent_only = parent_events[i:]
98
+ fork_only = fork_events[i:]
99
+
100
+ # Object divergence.
101
+ obj_diffs: list[DivergentObject] = []
102
+ parent_objs = {o.id: o for o in parent.all_objects()}
103
+ fork_objs = {o.id: o for o in fork.all_objects()}
104
+ for oid in sorted(set(parent_objs) | set(fork_objs)):
105
+ po = parent_objs.get(oid)
106
+ fo = fork_objs.get(oid)
107
+ pd = _normalize_object(po) if po else None
108
+ fd = _normalize_object(fo) if fo else None
109
+ if pd != fd:
110
+ obj_diffs.append(DivergentObject(id=oid, in_parent=pd, in_fork=fd))
111
+
112
+ # Relation divergence.
113
+ rel_diffs: list[DivergentRelation] = []
114
+ parent_rels = {r.id: r for r in parent.all_relations()}
115
+ fork_rels = {r.id: r for r in fork.all_relations()}
116
+ for rid in sorted(set(parent_rels) | set(fork_rels)):
117
+ pr = parent_rels.get(rid)
118
+ fr = fork_rels.get(rid)
119
+ pd = _normalize_relation(pr) if pr else None
120
+ fd = _normalize_relation(fr) if fr else None
121
+ if pd != fd:
122
+ rel_diffs.append(DivergentRelation(id=rid, in_parent=pd, in_fork=fd))
123
+
124
+ return Diff(
125
+ parent_run_id=parent_run_id,
126
+ fork_run_id=fork_run_id,
127
+ shared_events=shared,
128
+ parent_only_events=parent_only,
129
+ fork_only_events=fork_only,
130
+ divergent_objects=obj_diffs,
131
+ divergent_relations=rel_diffs,
132
+ )
133
+
134
+
135
+ def _normalize_object(o: Object) -> dict[str, Any]:
136
+ """Strip provenance (timestamps, run_id) so equality is structural."""
137
+ d = o.to_dict()
138
+ d.pop("provenance", None)
139
+ return d
140
+
141
+
142
+ def _normalize_relation(r: Relation) -> dict[str, Any]:
143
+ d = r.to_dict()
144
+ d.pop("provenance", None)
145
+ return d
@@ -0,0 +1,216 @@
1
+ """Replay errors. CONTRACT v0.5 #7 (replay strictness), CONTRACT v1.0 #C1
2
+ (error format) — migrated to the ActiveGraphError hierarchy in v1.0 PR-A.
3
+
4
+ `ReplayDivergenceError` is the canonical replay error and the reference
5
+ error class for the v1.0 message rewrite series. Its message obeys the
6
+ locked format (CONTRACT v1.0 #3); three distinct call sites in the
7
+ runtime produce three distinct messages, discriminated by the inputs
8
+ to ``__init__`` and snapshot-tested individually:
9
+
10
+ 1. **prompt_hash_mismatch** — the LLM-cache prompt hash for a recorded
11
+ ``llm.requested`` event no longer matches the live re-run hash.
12
+ Something changed in the behavior code, prompt template, or a
13
+ tool's input arguments.
14
+ 2. **type_mismatch** — an event at the same stream position has a
15
+ different ``type`` than recorded. The behavior graph produced a
16
+ different shape of work.
17
+ 3. **length_mismatch** — the recorded stream and the live re-run
18
+ produced different numbers of events. A behavior was added, removed,
19
+ or short-circuits differently.
20
+
21
+ The signature ``ReplayDivergenceError(event_id=..., expected=..., actual=...)``
22
+ is preserved from v0.5 so existing call sites and tests stay valid; the
23
+ discriminator is inferred from the inputs.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from typing import Any
29
+
30
+ from activegraph.errors import ReplayError
31
+
32
+
33
+ _NO_RECORDED_EVENT_SENTINEL = "<no recorded event>"
34
+
35
+
36
+ class ReplayDivergenceError(ReplayError):
37
+ """Raised when a replay (``replay_strict=True``) or a fork produces an
38
+ event stream that does not match the recorded log.
39
+
40
+ ``event_id`` pins the first divergence point so an operator can jump
41
+ directly to it. ``expected`` and ``actual`` describe what was
42
+ recorded vs. what the live re-run produced; one is ``None`` when the
43
+ re-run finished early or produced an extra event with no recorded
44
+ counterpart.
45
+ """
46
+
47
+ _doc_slug = "replay-divergence-error"
48
+
49
+ def __init__(
50
+ self,
51
+ *,
52
+ event_id: str,
53
+ expected: str,
54
+ actual: str | None,
55
+ ) -> None:
56
+ self.event_id = event_id
57
+ self.expected = expected
58
+ self.actual = actual
59
+ kind, summary, what_failed, why, how_to_fix = _build_message(
60
+ event_id=event_id, expected=expected, actual=actual
61
+ )
62
+ self.kind = kind
63
+ context: dict[str, Any] = {
64
+ "event_id": event_id,
65
+ "kind": kind,
66
+ "expected": expected,
67
+ "actual": actual,
68
+ }
69
+ super().__init__(
70
+ summary,
71
+ what_failed=what_failed,
72
+ why=why,
73
+ how_to_fix=how_to_fix,
74
+ context=context,
75
+ )
76
+
77
+
78
+ def _build_message(
79
+ *, event_id: str, expected: str, actual: str | None
80
+ ) -> tuple[str, str, str, str, str]:
81
+ """Return ``(kind, summary, what_failed, why, how_to_fix)`` for the
82
+ three replay-divergence shapes. The discriminator is the input shape:
83
+
84
+ - ``expected`` starts with ``prompt_hash=`` -> prompt_hash_mismatch
85
+ - ``expected`` is ``"<no recorded event>"`` or ``actual is None`` ->
86
+ length_mismatch
87
+ - otherwise -> type_mismatch
88
+ """
89
+ if isinstance(expected, str) and expected.startswith("prompt_hash="):
90
+ return _prompt_hash_message(event_id, expected, actual)
91
+ if expected == _NO_RECORDED_EVENT_SENTINEL or actual is None:
92
+ return _length_message(event_id, expected, actual)
93
+ return _type_message(event_id, expected, actual)
94
+
95
+
96
+ def _prompt_hash_message(
97
+ event_id: str, expected: str, actual: str | None,
98
+ ) -> tuple[str, str, str, str, str]:
99
+ actual_str = actual if actual is not None else "<no live response>"
100
+ return (
101
+ "prompt_hash_mismatch",
102
+ f"replay diverged at {event_id}: LLM prompt hash mismatch",
103
+ (
104
+ f"Event {event_id} (an `llm.requested` event in the recorded log) had a "
105
+ f"different prompt hash during this replay than the parent run recorded:\n"
106
+ f" recorded: {expected}\n"
107
+ f" live: {actual_str}"
108
+ ),
109
+ (
110
+ "The replay cache keys on the full prompt hash, so any change to an LLM "
111
+ "behavior's code, a prompt template, a system message, or a tool's input "
112
+ "arguments produces a mismatch. The framework refuses to silently substitute "
113
+ "a stale cached response under a new prompt — that would break the audit "
114
+ "trail the cache is designed to preserve."
115
+ ),
116
+ (
117
+ f"If the change was intentional (you edited a behavior or a prompt template),\n"
118
+ f"re-record the cache from the divergence point:\n"
119
+ f" activegraph fork <parent-run> --at-event {event_id} --record\n"
120
+ f"\n"
121
+ f"If the change was unintentional, diff your code against the recorded run's\n"
122
+ f"pack version and revert the change:\n"
123
+ f" activegraph inspect <parent-run> --pack-version\n"
124
+ f"\n"
125
+ f"To see the full recorded prompt for this event:\n"
126
+ f" activegraph inspect <parent-run> --event {event_id}"
127
+ ),
128
+ )
129
+
130
+
131
+ def _type_message(
132
+ event_id: str, expected: str, actual: str | None,
133
+ ) -> tuple[str, str, str, str, str]:
134
+ actual_str = actual if actual is not None else "<no live event>"
135
+ return (
136
+ "type_mismatch",
137
+ f"replay diverged at {event_id}: event type mismatch",
138
+ (
139
+ f"At the stream position pinned to event {event_id}, the live re-run "
140
+ f"produced a different event type than recorded:\n"
141
+ f" recorded: {expected!r}\n"
142
+ f" live: {actual_str!r}"
143
+ ),
144
+ (
145
+ "Strict replay compares the type stream of non-lifecycle events between the "
146
+ "recorded log and the live re-run. A type mismatch means the behavior graph "
147
+ "took a different branch — usually because a behavior's `where` filter, a "
148
+ "pattern subscription, or a conditional `graph.emit` changed since the "
149
+ "recorded run."
150
+ ),
151
+ (
152
+ f"Identify the behavior that produced event {event_id} in the recorded log:\n"
153
+ f" activegraph inspect <parent-run> --event {event_id}\n"
154
+ f"\n"
155
+ f"Diff that behavior against your current source. If the change was\n"
156
+ f"intentional, re-run without `replay_strict=True` (or fork with --record\n"
157
+ f"from the divergence point). If unintentional, revert the behavior."
158
+ ),
159
+ )
160
+
161
+
162
+ def _length_message(
163
+ event_id: str, expected: str, actual: str | None,
164
+ ) -> tuple[str, str, str, str, str]:
165
+ if actual is None:
166
+ return (
167
+ "length_mismatch",
168
+ f"replay diverged at {event_id}: live re-run finished early",
169
+ (
170
+ f"The recorded log contained event {event_id} (type {expected!r}) at this "
171
+ f"position, but the live re-run terminated before producing it.\n"
172
+ f" recorded: {expected!r}\n"
173
+ f" live: <no event produced>"
174
+ ),
175
+ (
176
+ "Strict replay requires the live re-run to produce the same number and "
177
+ "shape of non-lifecycle events as the recording. A short live re-run means "
178
+ "a behavior that fired in the recorded run no longer fires, or short-"
179
+ "circuits earlier — usually because a pattern subscription, a `where` "
180
+ "filter, or a guard condition was tightened since the recording."
181
+ ),
182
+ (
183
+ f"Identify the behavior that produced {event_id} in the recorded log:\n"
184
+ f" activegraph inspect <parent-run> --event {event_id}\n"
185
+ f"\n"
186
+ f"Compare that behavior's current trigger conditions against the recorded\n"
187
+ f"run's. If the change was intentional, fork with --record from the\n"
188
+ f"divergence point to refresh the recording. If unintentional, revert."
189
+ ),
190
+ )
191
+ return (
192
+ "length_mismatch",
193
+ f"replay diverged at {event_id}: live re-run produced an unrecorded event",
194
+ (
195
+ f"At the position pinned to event {event_id}, the live re-run produced an "
196
+ f"event of type {actual!r}, but the recorded log had no event here.\n"
197
+ f" recorded: <no event recorded>\n"
198
+ f" live: {actual!r}"
199
+ ),
200
+ (
201
+ "Strict replay requires the live re-run's event stream to match the "
202
+ "recording position-for-position. An extra live event means a behavior "
203
+ "fires now that did not fire in the recorded run — usually because a new "
204
+ "behavior was added, or a pattern subscription was loosened."
205
+ ),
206
+ (
207
+ f"List the behaviors currently registered and compare against the recorded\n"
208
+ f"pack version:\n"
209
+ f" activegraph inspect <parent-run> --behaviors\n"
210
+ f"\n"
211
+ f"If the new behavior is intentional, re-record from this position:\n"
212
+ f" activegraph fork <parent-run> --at-event {event_id} --record\n"
213
+ f"\n"
214
+ f"If the behavior shouldn't fire here, tighten its trigger conditions."
215
+ ),
216
+ )