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,27 @@
1
+ """Single in-process FIFO queue. CONTRACT #10: no priority, no async."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import deque
6
+ from typing import Optional
7
+
8
+ from activegraph.core.event import Event
9
+
10
+
11
+ class EventQueue:
12
+ def __init__(self) -> None:
13
+ self._q: deque[Event] = deque()
14
+
15
+ def push(self, event: Event) -> None:
16
+ self._q.append(event)
17
+
18
+ def pop(self) -> Optional[Event]:
19
+ if not self._q:
20
+ return None
21
+ return self._q.popleft()
22
+
23
+ def __len__(self) -> int:
24
+ return len(self._q)
25
+
26
+ def __bool__(self) -> bool:
27
+ return bool(self._q)
@@ -0,0 +1,291 @@
1
+ """Runtime-side registration error leaves. v1.0 PR-E.
2
+
3
+ These leaves fire at registration time (or at registration-adjacent
4
+ lookup time) and are grouped here rather than scattered across
5
+ runtime.py / scheduler.py / behaviors/. Topic-module raise sites
6
+ import from here; consolidation makes the category audit trivial.
7
+
8
+ Pack registration errors live in :mod:`activegraph.packs.__init__`
9
+ (alongside the other Pack* classes for back-compat). LLM-side
10
+ :class:`MissingProviderError` and tool-side :class:`MissingToolError`
11
+ stay in their topic modules and re-parent in place.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Any, Optional
17
+
18
+ from activegraph.errors import RegistrationError
19
+
20
+
21
+ class BehaviorNotFoundError(RegistrationError, LookupError):
22
+ """``runtime.get_behavior(name)`` could not resolve the name to a
23
+ registered behavior.
24
+
25
+ Multi-inherits :class:`LookupError` so user code that catches the
26
+ builtin around behavior lookups continues to work.
27
+ """
28
+
29
+ _doc_slug = "behavior-not-found-error"
30
+
31
+ def __init__(
32
+ self,
33
+ name: str,
34
+ *,
35
+ registered: Optional[tuple[str, ...]] = None,
36
+ pack_state: bool = False,
37
+ ) -> None:
38
+ self.name = name
39
+ self.registered = registered or ()
40
+ ctx: dict[str, Any] = {"name": name, "pack_state": pack_state}
41
+ if self.registered:
42
+ ctx["registered"] = list(self.registered)
43
+ sample = ""
44
+ if self.registered:
45
+ preview = ", ".join(repr(n) for n in list(self.registered)[:6])
46
+ extra = f" (+{len(self.registered) - 6} more)" if len(self.registered) > 6 else ""
47
+ sample = f"\n registered: {preview}{extra}"
48
+ RegistrationError.__init__(
49
+ self,
50
+ f"no behavior named {name!r} is loaded",
51
+ what_failed=(
52
+ f"runtime.get_behavior({name!r}) could not resolve the name "
53
+ f"to a registered behavior.{sample}"
54
+ ),
55
+ why=(
56
+ "Behaviors are addressable by their declared name. The "
57
+ "lookup is strict — the runtime refuses to fall back to a "
58
+ "fuzzy match or a no-op because a wrong-behavior dispatch "
59
+ "would silently corrupt the audit trail. Behaviors live "
60
+ "either in the global registry (decorated with `@behavior` "
61
+ "or `@llm_behavior` at module load) or in a loaded pack."
62
+ ),
63
+ how_to_fix=(
64
+ "Check the spelling against the registered behaviors above. "
65
+ "If the behavior comes from a pack, confirm the pack is "
66
+ "loaded:\n"
67
+ " rt.load_pack(my_pack)\n"
68
+ "If the behavior is defined in user code, confirm the "
69
+ "decorator ran (the module is imported) before the runtime "
70
+ "is constructed.\n"
71
+ "\n"
72
+ "For canonical lookups across packs, use the fully-qualified "
73
+ "form `'pack_name.behavior_name'`."
74
+ ),
75
+ context=ctx,
76
+ )
77
+
78
+
79
+ class AmbiguousBehaviorError(RegistrationError, ValueError):
80
+ """A short behavior name resolves to more than one loaded pack.
81
+
82
+ Fires only when both packs declare a behavior under the same short
83
+ name. The user is asked to disambiguate by using the canonical
84
+ `pack_name.behavior_name` form. CONTRACT v0.9 #8.
85
+ """
86
+
87
+ _doc_slug = "ambiguous-behavior-error"
88
+
89
+ def __init__(
90
+ self,
91
+ name: str,
92
+ *,
93
+ packs: Optional[tuple[str, ...]] = None,
94
+ ) -> None:
95
+ self.name = name
96
+ self.packs = packs or ()
97
+ ctx: dict[str, Any] = {"name": name}
98
+ if self.packs:
99
+ ctx["packs"] = list(self.packs)
100
+ pack_list = (
101
+ ", ".join(f"{p!r}" for p in self.packs)
102
+ if self.packs
103
+ else "(multiple packs)"
104
+ )
105
+ example = (
106
+ f"{self.packs[0]}.{name}" if self.packs else f"pack_name.{name}"
107
+ )
108
+ RegistrationError.__init__(
109
+ self,
110
+ f"behavior name {name!r} is ambiguous across loaded packs",
111
+ what_failed=(
112
+ f"The short behavior name {name!r} resolves to behaviors in "
113
+ f"more than one loaded pack: {pack_list}. The runtime cannot "
114
+ f"pick one without an explicit choice."
115
+ ),
116
+ why=(
117
+ "Pack-prefixed names are canonical; short names are a "
118
+ "convenience for the common single-pack case (CONTRACT "
119
+ "v0.9 #8). When multiple packs declare the same short "
120
+ "name, the convenience would have to either pick one "
121
+ "silently (which would change behavior on pack-load order) "
122
+ "or pick neither — both produce surprises. The runtime "
123
+ "refuses the lookup and asks for the canonical name."
124
+ ),
125
+ how_to_fix=(
126
+ f"Use the canonical form:\n"
127
+ f" rt.get_behavior({example!r})\n"
128
+ f"\n"
129
+ f"If you wanted both packs' behaviors to fire together "
130
+ f"under the same trigger, they are registered separately "
131
+ f"in the runtime — both will fire on a matching event "
132
+ f"regardless of which one your lookup names. The lookup "
133
+ f"is for explicit by-name access, not for trigger "
134
+ f"dispatch."
135
+ ),
136
+ context=ctx,
137
+ )
138
+
139
+
140
+ class ToolNotFoundError(RegistrationError, LookupError):
141
+ """``runtime.get_tool(name)`` could not resolve the name to a
142
+ registered tool.
143
+
144
+ Symmetric with :class:`BehaviorNotFoundError`. Multi-inherits
145
+ :class:`LookupError` for back-compat.
146
+ """
147
+
148
+ _doc_slug = "tool-not-found-error"
149
+
150
+ def __init__(
151
+ self,
152
+ name: str,
153
+ *,
154
+ registered: Optional[tuple[str, ...]] = None,
155
+ ) -> None:
156
+ self.name = name
157
+ self.registered = registered or ()
158
+ ctx: dict[str, Any] = {"name": name}
159
+ if self.registered:
160
+ ctx["registered"] = list(self.registered)
161
+ sample = ""
162
+ if self.registered:
163
+ preview = ", ".join(repr(n) for n in list(self.registered)[:6])
164
+ extra = f" (+{len(self.registered) - 6} more)" if len(self.registered) > 6 else ""
165
+ sample = f"\n registered: {preview}{extra}"
166
+ RegistrationError.__init__(
167
+ self,
168
+ f"no tool named {name!r} is loaded",
169
+ what_failed=(
170
+ f"runtime.get_tool({name!r}) could not resolve the name to a "
171
+ f"registered tool.{sample}"
172
+ ),
173
+ why=(
174
+ "Tools are addressable by their declared name. The runtime "
175
+ "refuses to fall back to a fuzzy match because the tool's "
176
+ "input/output schema is part of the contract — invoking the "
177
+ "wrong tool with the right name would silently produce "
178
+ "wrong-shape data."
179
+ ),
180
+ how_to_fix=(
181
+ "Check the spelling against the registered tools above. If "
182
+ "the tool comes from a pack, confirm the pack is loaded. If "
183
+ "the tool is in user code, confirm the @tool decorator ran "
184
+ "(module imported) before the runtime is constructed.\n"
185
+ "\n"
186
+ "For canonical lookups across packs, use "
187
+ "`'pack_name.tool_name'`."
188
+ ),
189
+ context=ctx,
190
+ )
191
+
192
+
193
+ class AmbiguousToolError(RegistrationError, ValueError):
194
+ """A short tool name resolves to more than one loaded pack.
195
+
196
+ Symmetric with :class:`AmbiguousBehaviorError`. CONTRACT v0.9 #9.
197
+ """
198
+
199
+ _doc_slug = "ambiguous-tool-error"
200
+
201
+ def __init__(
202
+ self,
203
+ name: str,
204
+ *,
205
+ packs: Optional[tuple[str, ...]] = None,
206
+ ) -> None:
207
+ self.name = name
208
+ self.packs = packs or ()
209
+ ctx: dict[str, Any] = {"name": name}
210
+ if self.packs:
211
+ ctx["packs"] = list(self.packs)
212
+ pack_list = (
213
+ ", ".join(f"{p!r}" for p in self.packs)
214
+ if self.packs
215
+ else "(multiple packs)"
216
+ )
217
+ example = (
218
+ f"{self.packs[0]}.{name}" if self.packs else f"pack_name.{name}"
219
+ )
220
+ RegistrationError.__init__(
221
+ self,
222
+ f"tool name {name!r} is ambiguous across loaded packs",
223
+ what_failed=(
224
+ f"The short tool name {name!r} resolves to tools in more "
225
+ f"than one loaded pack: {pack_list}. The runtime cannot pick "
226
+ f"one without an explicit choice."
227
+ ),
228
+ why=(
229
+ "Same rule as behavior names (CONTRACT v0.9 #9): "
230
+ "pack-prefixed names are canonical, short names are a "
231
+ "convenience for the single-pack case. Multi-pack "
232
+ "ambiguity is refused because picking one would silently "
233
+ "swap which tool's schema validates the LLM's call."
234
+ ),
235
+ how_to_fix=(
236
+ f"Use the canonical form:\n"
237
+ f" rt.get_tool({example!r})\n"
238
+ f"\n"
239
+ f"If you want the @llm_behavior to be able to choose "
240
+ f"either pack's version, list both canonical names in "
241
+ f"its `tools=[...]` argument."
242
+ ),
243
+ context=ctx,
244
+ )
245
+
246
+
247
+ class InvalidToolRegistration(RegistrationError, TypeError):
248
+ """A value passed to ``Runtime(tools=[...])`` is not a Tool instance.
249
+
250
+ Common cause: the developer passed a bare function instead of one
251
+ decorated with ``@tool``. Multi-inherits :class:`TypeError` for
252
+ back-compat.
253
+ """
254
+
255
+ _doc_slug = "invalid-tool-registration"
256
+
257
+ def __init__(self, value: Any) -> None:
258
+ self.value = value
259
+ type_name = type(value).__name__
260
+ repr_short = repr(value)
261
+ if len(repr_short) > 80:
262
+ repr_short = repr_short[:77] + "..."
263
+ RegistrationError.__init__(
264
+ self,
265
+ f"tool registration value is not a Tool instance (got {type_name})",
266
+ what_failed=(
267
+ f"Runtime(tools=[...]) was given a value that isn't a Tool "
268
+ f"instance:\n value: {repr_short}\n type: {type_name}"
269
+ ),
270
+ why=(
271
+ "The Tool wrapper carries the tool's declared name, input "
272
+ "schema, output schema, timeout, and deterministic flag. "
273
+ "Registering a bare function would skip those declarations "
274
+ "and the runtime could not validate calls into the tool — "
275
+ "schema-violating calls would reach the body and produce "
276
+ "wrong-shape data. The check fails fast at construction."
277
+ ),
278
+ how_to_fix=(
279
+ "Decorate the function with @tool, and pass the decorated "
280
+ "object:\n"
281
+ " @tool(name='my_tool', input_schema=..., output_schema=...)\n"
282
+ " def my_tool(args, ctx): ...\n"
283
+ "\n"
284
+ " rt = Runtime(graph, tools=[my_tool])\n"
285
+ "\n"
286
+ "If the function was already decorated, confirm you're "
287
+ "passing the decorator's return value (the wrapped Tool), "
288
+ "not the original function."
289
+ ),
290
+ context={"type": type_name, "repr": repr_short},
291
+ )
@@ -0,0 +1,111 @@
1
+ """Match events to behaviors. CONTRACT #10: registration order for ties.
2
+
3
+ v0.7 (CONTRACT v0.7 #11): a behavior with both `on=[...]` and
4
+ `pattern=...` requires BOTH conditions — the event type matches AND
5
+ the pattern matches against the post-event graph state. A behavior
6
+ with only `pattern=...` (empty `on`) matches on every non-lifecycle
7
+ event.
8
+
9
+ `match()` returns `(behavior, relations, matches)` triples. The
10
+ `matches` list is the pattern matcher's bindings (empty for
11
+ behaviors without `pattern=`). The runtime forwards `matches` as
12
+ `ctx.matches`.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Any, Iterable, Union
18
+
19
+ from activegraph.behaviors.base import Behavior, RelationBehavior
20
+ from activegraph.core.event import Event
21
+ from activegraph.core.graph import Graph, Relation, evaluate_where
22
+
23
+
24
+ BehaviorLike = Union[Behavior, RelationBehavior]
25
+
26
+
27
+ class Registry:
28
+ def __init__(self, behaviors: Iterable[BehaviorLike]) -> None:
29
+ self._behaviors: list[BehaviorLike] = list(behaviors)
30
+
31
+ def all(self) -> list[BehaviorLike]:
32
+ return list(self._behaviors)
33
+
34
+ def index_of(self, behavior: BehaviorLike) -> int:
35
+ for i, b in enumerate(self._behaviors):
36
+ if b is behavior:
37
+ return i
38
+ return -1
39
+
40
+ def match(
41
+ self, event: Event, graph: Graph
42
+ ) -> list[tuple[BehaviorLike, list[Relation], list[Any]]]:
43
+ """Return (behavior, matching_relations, pattern_matches) triples
44
+ in registration order. Pattern matches are empty for behaviors
45
+ without `pattern=`.
46
+ """
47
+ out: list[tuple[BehaviorLike, list[Relation], list[Any]]] = []
48
+ for b in self._behaviors:
49
+ # Event-type filter: required only when `on=` is non-empty.
50
+ # Pattern-only behaviors (empty `on`) skip this gate.
51
+ if b.on and event.type not in b.on:
52
+ continue
53
+ # Suppress lifecycle events for pattern-only behaviors so a
54
+ # pattern doesn't fire on behavior.started, etc.
55
+ if not b.on and _is_lifecycle(event):
56
+ continue
57
+ pattern_matches: list[Any] = []
58
+ if b.pattern_matcher is not None:
59
+ pattern_matches = b.pattern_matcher.matches(event, graph)
60
+ if not pattern_matches:
61
+ continue
62
+ if isinstance(b, RelationBehavior):
63
+ rels = _matching_relations(b, event, graph)
64
+ if rels:
65
+ out.append((b, rels, pattern_matches))
66
+ else:
67
+ if b.where and not evaluate_where(b.where, event.payload):
68
+ continue
69
+ out.append((b, [], pattern_matches))
70
+ return out
71
+
72
+
73
+ def _is_lifecycle(event: Event) -> bool:
74
+ return (
75
+ event.type.startswith("behavior.")
76
+ or event.type.startswith("relation_behavior.")
77
+ or event.type.startswith("runtime.")
78
+ or event.type.startswith("llm.")
79
+ or event.type.startswith("tool.")
80
+ )
81
+
82
+
83
+ def _matching_relations(
84
+ rb: RelationBehavior, event: Event, graph: Graph
85
+ ) -> list[Relation]:
86
+ candidates = [r for r in graph.all_relations() if r.type == rb.relation_type]
87
+ referenced = _collect_string_values(event.payload)
88
+ out: list[Relation] = []
89
+ for r in candidates:
90
+ if r.source in referenced or r.target in referenced:
91
+ if rb.where and not evaluate_where(rb.where, event.payload):
92
+ continue
93
+ out.append(r)
94
+ return out
95
+
96
+
97
+ def _collect_string_values(obj) -> set[str]:
98
+ out: set[str] = set()
99
+ _walk(obj, out)
100
+ return out
101
+
102
+
103
+ def _walk(obj, out: set[str]) -> None:
104
+ if isinstance(obj, str):
105
+ out.add(obj)
106
+ elif isinstance(obj, dict):
107
+ for v in obj.values():
108
+ _walk(v, out)
109
+ elif isinstance(obj, list):
110
+ for v in obj:
111
+ _walk(v, out)