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,222 @@
1
+ """Active Graph Runtime. Public API surface.
2
+
3
+ The graph is the world. Behaviors are physics. The trace is the proof.
4
+ """
5
+
6
+ from activegraph.behaviors.base import Behavior, LLMBehavior, RelationBehavior
7
+ from activegraph.behaviors.decorators import (
8
+ behavior,
9
+ clear_registry,
10
+ get_registry,
11
+ llm_behavior,
12
+ relation_behavior,
13
+ )
14
+ from activegraph.core.clock import Clock, FrozenClock, TickingClock
15
+ from activegraph.core.event import Event
16
+ from activegraph.core.graph import Graph, Object, Relation
17
+ from activegraph.core.ids import IDGen
18
+ from activegraph.core.patch import Patch
19
+ from activegraph.core.view import View
20
+ from activegraph.errors import (
21
+ ActiveGraphError,
22
+ ConfigurationError,
23
+ ExecutionError,
24
+ MissingOptionalDependency,
25
+ PackError,
26
+ PatternError,
27
+ RegistrationError,
28
+ ReplayError,
29
+ StorageError,
30
+ )
31
+ from activegraph.runtime.registration_errors import (
32
+ AmbiguousBehaviorError,
33
+ AmbiguousToolError,
34
+ BehaviorNotFoundError,
35
+ InvalidToolRegistration,
36
+ ToolNotFoundError,
37
+ )
38
+ from activegraph.runtime.scheduler import InvalidActivateAfter
39
+ from activegraph.frame import Frame
40
+ from activegraph.llm.errors import LLMBehaviorError, MissingProviderError
41
+ from activegraph.policy import Policy
42
+ from activegraph.runtime.budget import Budget
43
+ from activegraph.runtime.diff import Diff, DivergentObject, DivergentRelation
44
+ from activegraph.runtime.config_errors import (
45
+ IncompatibleRuntimeState,
46
+ InvalidArgumentType,
47
+ InvalidRuntimeConfiguration,
48
+ )
49
+ from activegraph.runtime.errors import ReplayDivergenceError
50
+ from activegraph.runtime.exec_errors import (
51
+ ApprovalNotFoundError,
52
+ InternalEvaluatorError,
53
+ InvalidPatchLifecycleState,
54
+ RuntimeContextRequiredError,
55
+ )
56
+ from activegraph.runtime.patterns import UnsupportedPatternError
57
+ from activegraph.runtime.runtime import Runtime
58
+ from activegraph.store import (
59
+ CorruptedEventPayloadError,
60
+ DuplicateEventError,
61
+ EventNotFoundError,
62
+ EventStore,
63
+ InMemoryEventStore,
64
+ InvalidStoreURL,
65
+ NonSerializableEventError,
66
+ RunRecord,
67
+ SQLiteEventStore,
68
+ SchemaVersionMismatch,
69
+ open_store,
70
+ parse_store_url,
71
+ )
72
+ # v0.7 public surface for tools
73
+ from activegraph.tools import (
74
+ MissingToolError,
75
+ Tool,
76
+ ToolContext,
77
+ ToolError,
78
+ UnknownToolError,
79
+ clear_tool_registry,
80
+ get_tool_registry,
81
+ tool,
82
+ )
83
+ # v0.8 observability surface
84
+ from activegraph.observability import (
85
+ Metrics,
86
+ MigrationReport,
87
+ MigrationRunReport,
88
+ NoOpMetrics,
89
+ PrometheusMetrics,
90
+ RuntimeStatus,
91
+ configure_logging,
92
+ migrate,
93
+ )
94
+ # v0.9 packs surface (top-level: the API for *using* packs from user code).
95
+ # Pack-aware decorators live under `activegraph.packs` and are intentionally
96
+ # NOT re-exported here — pack authors must import them from `activegraph.packs`
97
+ # so the import path makes the boundary explicit. CONTRACT v0.9 #3.
98
+ from activegraph.packs import (
99
+ DiscoveredPack,
100
+ EmptySettings,
101
+ ObjectType,
102
+ Pack,
103
+ PackConflictError,
104
+ PackError,
105
+ PackNotFoundError,
106
+ PackPolicy,
107
+ PackPrompt,
108
+ PackPromptLoadError,
109
+ PackSchemaViolation,
110
+ PackSettingsMissingError,
111
+ PackValidationError,
112
+ PackVersionConflictError,
113
+ PendingApproval,
114
+ RelationType,
115
+ clear_discovery_cache,
116
+ discover,
117
+ load_by_name,
118
+ load_prompts_from_dir,
119
+ )
120
+
121
+ __all__ = [
122
+ "ActiveGraphError",
123
+ "AmbiguousBehaviorError",
124
+ "AmbiguousToolError",
125
+ "ApprovalNotFoundError",
126
+ "Behavior",
127
+ "BehaviorNotFoundError",
128
+ "Budget",
129
+ "Clock",
130
+ "ConfigurationError",
131
+ "CorruptedEventPayloadError",
132
+ "Diff",
133
+ "DiscoveredPack",
134
+ "DivergentObject",
135
+ "DivergentRelation",
136
+ "DuplicateEventError",
137
+ "EmptySettings",
138
+ "Event",
139
+ "EventNotFoundError",
140
+ "EventStore",
141
+ "ExecutionError",
142
+ "Frame",
143
+ "FrozenClock",
144
+ "Graph",
145
+ "IDGen",
146
+ "InMemoryEventStore",
147
+ "IncompatibleRuntimeState",
148
+ "InternalEvaluatorError",
149
+ "InvalidActivateAfter",
150
+ "InvalidArgumentType",
151
+ "InvalidPatchLifecycleState",
152
+ "InvalidRuntimeConfiguration",
153
+ "InvalidStoreURL",
154
+ "InvalidToolRegistration",
155
+ "LLMBehavior",
156
+ "LLMBehaviorError",
157
+ "Metrics",
158
+ "MigrationReport",
159
+ "MigrationRunReport",
160
+ "MissingOptionalDependency",
161
+ "MissingProviderError",
162
+ "MissingToolError",
163
+ "NoOpMetrics",
164
+ "NonSerializableEventError",
165
+ "Object",
166
+ "ObjectType",
167
+ "Pack",
168
+ "PackConflictError",
169
+ "PackError",
170
+ "PackNotFoundError",
171
+ "PackPolicy",
172
+ "PackPromptLoadError",
173
+ "PackPrompt",
174
+ "PackSchemaViolation",
175
+ "PackSettingsMissingError",
176
+ "PackValidationError",
177
+ "PackVersionConflictError",
178
+ "PatternError",
179
+ "Patch",
180
+ "PendingApproval",
181
+ "Policy",
182
+ "PrometheusMetrics",
183
+ "Relation",
184
+ "RelationBehavior",
185
+ "RegistrationError",
186
+ "RelationType",
187
+ "ReplayDivergenceError",
188
+ "ReplayError",
189
+ "RunRecord",
190
+ "Runtime",
191
+ "RuntimeContextRequiredError",
192
+ "RuntimeStatus",
193
+ "SQLiteEventStore",
194
+ "SchemaVersionMismatch",
195
+ "StorageError",
196
+ "TickingClock",
197
+ "Tool",
198
+ "ToolContext",
199
+ "ToolError",
200
+ "ToolNotFoundError",
201
+ "UnknownToolError",
202
+ "UnsupportedPatternError",
203
+ "View",
204
+ "behavior",
205
+ "clear_discovery_cache",
206
+ "clear_registry",
207
+ "clear_tool_registry",
208
+ "configure_logging",
209
+ "discover",
210
+ "get_registry",
211
+ "get_tool_registry",
212
+ "llm_behavior",
213
+ "load_by_name",
214
+ "load_prompts_from_dir",
215
+ "migrate",
216
+ "open_store",
217
+ "parse_store_url",
218
+ "relation_behavior",
219
+ "tool",
220
+ ]
221
+
222
+ __version__ = "1.0.0rc2"
@@ -0,0 +1,5 @@
1
+ """``python -m activegraph`` → CLI entry point."""
2
+
3
+ from activegraph.cli.main import main
4
+
5
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ """Behavior decorators and base classes. Imports core only (CONTRACT #14)."""
@@ -0,0 +1,153 @@
1
+ """Behavior + RelationBehavior + LLMBehavior. Plain holders for metadata
2
+ plus the callable.
3
+
4
+ A Behavior is data, not magic. The decorator wraps a function in one of
5
+ these; class-based behaviors subclass directly. The runtime introspects
6
+ the metadata to match events and build views.
7
+
8
+ CONTRACT v0.6 #2: an LLMBehavior is structurally a Behavior whose
9
+ invocation lifecycle is owned by the runtime — the developer-supplied
10
+ function is the 4-arg `handler` (event, graph, ctx, llm_output), NOT
11
+ the 3-arg `fn`. Per CONTRACT v0.6 #20, `LLMBehavior.build_prompt` is
12
+ public so a developer can inspect the exact bytes that would be sent
13
+ to the model without making a call.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass, field
19
+ from typing import TYPE_CHECKING, Any, Callable, Optional
20
+
21
+ if TYPE_CHECKING:
22
+ from activegraph.core.event import Event
23
+ from activegraph.core.graph import Graph
24
+ from activegraph.frame import Frame
25
+ from activegraph.llm.prompt import AssembledPrompt
26
+
27
+
28
+ def _llm_behavior_fn_placeholder(event, graph, ctx) -> None: # pragma: no cover
29
+ raise RuntimeError(
30
+ "LLMBehavior.fn invoked directly. The runtime owns LLM behavior "
31
+ "invocation via _invoke_llm; calling .run() bypasses prompt "
32
+ "assembly, the provider, and the LLM event log. This is a bug."
33
+ )
34
+
35
+
36
+ @dataclass
37
+ class Behavior:
38
+ name: str
39
+ fn: Callable[..., None]
40
+ on: list[str] = field(default_factory=list)
41
+ where: Optional[dict[str, Any]] = None
42
+ view_spec: Optional[dict[str, Any]] = None
43
+ creates: list[str] = field(default_factory=list)
44
+ budget: Optional[dict[str, Any]] = None
45
+ priority: int = 0 # reserved; v0 ties resolved by registration order
46
+ # CONTRACT v0.7 #8 / #9 / #11. A Cypher subset pattern string.
47
+ # Compiled at registration time. None = event-type-only matching
48
+ # (CONTRACT #10).
49
+ pattern: Optional[str] = None
50
+ pattern_matcher: Any = None # PatternMatcher; set by Registry at startup
51
+ # CONTRACT v0.7 #13. Event-count delay. None = fire immediately.
52
+ activate_after: Optional[int] = None
53
+
54
+ def run(self, event, graph, ctx) -> None:
55
+ self.fn(event, graph, ctx)
56
+
57
+
58
+ @dataclass
59
+ class RelationBehavior:
60
+ name: str
61
+ fn: Callable[..., None]
62
+ relation_type: str
63
+ on: list[str] = field(default_factory=list)
64
+ where: Optional[dict[str, Any]] = None
65
+ view_spec: Optional[dict[str, Any]] = None
66
+ creates: list[str] = field(default_factory=list)
67
+ budget: Optional[dict[str, Any]] = None
68
+ priority: int = 0
69
+ # Pattern subscriptions also work on relation behaviors. The match
70
+ # fires once per (event, relation) pair when the pattern matches.
71
+ pattern: Optional[str] = None
72
+ pattern_matcher: Any = None
73
+ activate_after: Optional[int] = None
74
+
75
+ def run(self, relation, event, graph, ctx) -> None:
76
+ self.fn(relation, event, graph, ctx)
77
+
78
+
79
+ @dataclass
80
+ class LLMBehavior(Behavior):
81
+ """A behavior whose body is an LLM call.
82
+
83
+ The runtime owns prompt assembly, cache lookup, the provider call,
84
+ event emission, and structured-output parsing. The developer's
85
+ `handler` is invoked only after the LLM has responded (or a cached
86
+ response was found) and the output was successfully parsed.
87
+
88
+ CONTRACT v0.7: `tools=` declares a list of `Tool` objects (or
89
+ strings naming globally-registered tools) the LLM can call during
90
+ the turn loop. The runtime orchestrates the loop; the handler
91
+ receives only the final structured output, never raw tool calls.
92
+ """
93
+
94
+ handler: Optional[Callable[..., None]] = None
95
+ description: str = ""
96
+ model: str = "claude-sonnet-4-5"
97
+ output_schema: Optional[type] = None
98
+ deterministic: bool = False
99
+ max_tokens: int = 4096
100
+ temperature: float = 0.7
101
+ top_p: float = 1.0
102
+ timeout_seconds: float = 60.0
103
+ prompt_template: Optional[str] = None
104
+ # v0.7
105
+ tools: list = field(default_factory=list)
106
+ max_tool_turns: int = 6
107
+
108
+ def build_prompt(
109
+ self,
110
+ event: "Event",
111
+ graph: "Graph",
112
+ *,
113
+ frame: Optional["Frame"] = None,
114
+ ) -> "AssembledPrompt":
115
+ """Assemble the prompt that would be sent for this event.
116
+
117
+ CONTRACT v0.6 #20 — public so a developer can inspect prompts
118
+ without making an API call. Reproducible (pure over inputs);
119
+ cheap (no I/O).
120
+ """
121
+
122
+ from activegraph.llm.prompt import assemble_prompt
123
+ from activegraph.runtime.view_builder import (
124
+ DEFAULT_RECENT_EVENTS,
125
+ _resolve_event_path,
126
+ build_view,
127
+ )
128
+
129
+ view = build_view(self, event, graph)
130
+ around_id: Optional[str] = None
131
+ depth: Optional[int] = None
132
+ if self.view_spec:
133
+ ar_expr = self.view_spec.get("around")
134
+ if ar_expr:
135
+ around_id = _resolve_event_path(ar_expr, event)
136
+ depth = self.view_spec.get("depth")
137
+ return assemble_prompt(
138
+ behavior_name=self.name,
139
+ description=self.description,
140
+ model=self.model,
141
+ output_schema=self.output_schema,
142
+ creates=self.creates,
143
+ view=view,
144
+ event=event,
145
+ frame=frame,
146
+ around=around_id,
147
+ depth=depth,
148
+ max_tokens=self.max_tokens,
149
+ temperature=self.temperature,
150
+ top_p=self.top_p,
151
+ deterministic=self.deterministic,
152
+ prompt_template=self.prompt_template,
153
+ )
@@ -0,0 +1,218 @@
1
+ """@behavior, @relation_behavior, and @llm_behavior decorators
2
+ plus the global registry.
3
+
4
+ CONTRACT #6: regular behavior signature is (event, graph, ctx) -> None.
5
+ CONTRACT v0.6 #2: @llm_behavior handler signature is
6
+ (event, graph, ctx, llm_output) -> None — the 4th arg is bound by the
7
+ decorator's wrapper, not by the runtime.
8
+
9
+ The global registry exists so the README quickstart works without an
10
+ explicit `behaviors=[...]` list. `Runtime(graph)` reads the global
11
+ registry by default; passing `behaviors=[...]` overrides it. Tests
12
+ that need isolation can call `clear_registry()`.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Any, Callable, Optional, Union
18
+
19
+ from activegraph.behaviors.base import (
20
+ Behavior,
21
+ LLMBehavior,
22
+ RelationBehavior,
23
+ _llm_behavior_fn_placeholder,
24
+ )
25
+
26
+
27
+ _REGISTRY: list[Union[Behavior, RelationBehavior]] = []
28
+
29
+
30
+ def clear_registry() -> None:
31
+ _REGISTRY.clear()
32
+
33
+
34
+ def get_registry() -> list[Union[Behavior, RelationBehavior]]:
35
+ return list(_REGISTRY)
36
+
37
+
38
+ def behavior(
39
+ name: Optional[str] = None,
40
+ on: Optional[list[str]] = None,
41
+ where: Optional[dict[str, Any]] = None,
42
+ view: Optional[dict[str, Any]] = None,
43
+ creates: Optional[list[str]] = None,
44
+ budget: Optional[dict[str, Any]] = None,
45
+ priority: int = 0,
46
+ *,
47
+ pattern: Optional[str] = None,
48
+ activate_after: Any = None,
49
+ ) -> Callable[[Callable], Behavior]:
50
+ """Decorate a function as an event-driven behavior.
51
+
52
+ v0.7 additions (both keyword-only):
53
+ - `pattern=`: a Cypher subset pattern string. When set, the
54
+ behavior fires only when the pattern matches the post-event
55
+ graph state. Combined with `on=` both conditions must hold
56
+ (CONTRACT v0.7 #11). Matches are exposed as `ctx.matches`.
57
+ - `activate_after=`: int event count or "N events". Delays
58
+ invocation by N events; re-checks `where=` at fire time
59
+ (CONTRACT v0.7 #13).
60
+ """
61
+
62
+ from activegraph.runtime.patterns import parse as _parse_pattern
63
+ from activegraph.runtime.scheduler import parse_activate_after as _parse_aa
64
+
65
+ compiled_matcher = None
66
+ if pattern is not None:
67
+ compiled_matcher = _parse_pattern(pattern).compile()
68
+ delay_n: Optional[int] = None
69
+ if activate_after is not None:
70
+ delay_n = _parse_aa(activate_after)
71
+
72
+ def wrap(fn: Callable) -> Behavior:
73
+ b = Behavior(
74
+ name=name or fn.__name__,
75
+ fn=fn,
76
+ on=list(on or []),
77
+ where=dict(where) if where else None,
78
+ view_spec=dict(view) if view else None,
79
+ creates=list(creates or []),
80
+ budget=dict(budget) if budget else None,
81
+ priority=priority,
82
+ pattern=pattern,
83
+ pattern_matcher=compiled_matcher,
84
+ activate_after=delay_n,
85
+ )
86
+ _REGISTRY.append(b)
87
+ return b
88
+
89
+ return wrap
90
+
91
+
92
+ def llm_behavior(
93
+ *,
94
+ name: Optional[str] = None,
95
+ on: Optional[list[str]] = None,
96
+ where: Optional[dict[str, Any]] = None,
97
+ description: str = "",
98
+ model: str = "claude-sonnet-4-5",
99
+ output_schema: Optional[type] = None,
100
+ view: Optional[dict[str, Any]] = None,
101
+ creates: Optional[list[str]] = None,
102
+ budget: Optional[dict[str, Any]] = None,
103
+ deterministic: bool = False,
104
+ max_tokens: int = 4096,
105
+ temperature: float = 0.7,
106
+ top_p: float = 1.0,
107
+ timeout_seconds: float = 60.0,
108
+ prompt_template: Optional[str] = None,
109
+ priority: int = 0,
110
+ pattern: Optional[str] = None,
111
+ activate_after: Any = None,
112
+ tools: Optional[list] = None,
113
+ max_tool_turns: int = 6,
114
+ ) -> Callable[[Callable], LLMBehavior]:
115
+ """Decorate a function as an LLM-driven behavior.
116
+
117
+ The decorated function's signature is
118
+ `(event, graph, ctx, llm_output) -> None`. The runtime assembles
119
+ the prompt, calls the provider, parses the structured output, and
120
+ only then invokes the handler with the parsed result. Failures
121
+ flow as `behavior.failed` events with a `reason` from
122
+ CONTRACT v0.6 #11 (`llm.parse_error`, `llm.schema_violation`,
123
+ `llm.network_error`, `llm.rate_limited`, `budget.cost_exhausted`).
124
+
125
+ Keyword-only on purpose — `@llm_behavior` carries enough
126
+ parameters that positional binding would be a footgun.
127
+ """
128
+
129
+ from activegraph.runtime.patterns import parse as _parse_pattern
130
+ from activegraph.runtime.scheduler import parse_activate_after as _parse_aa
131
+
132
+ compiled_matcher = None
133
+ if pattern is not None:
134
+ compiled_matcher = _parse_pattern(pattern).compile()
135
+ delay_n: Optional[int] = None
136
+ if activate_after is not None:
137
+ delay_n = _parse_aa(activate_after)
138
+
139
+ def wrap(fn: Callable) -> LLMBehavior:
140
+ b = LLMBehavior(
141
+ name=name or fn.__name__,
142
+ fn=_llm_behavior_fn_placeholder,
143
+ on=list(on or []),
144
+ where=dict(where) if where else None,
145
+ view_spec=dict(view) if view else None,
146
+ creates=list(creates or []),
147
+ budget=dict(budget) if budget else None,
148
+ priority=priority,
149
+ handler=fn,
150
+ description=description,
151
+ model=model,
152
+ output_schema=output_schema,
153
+ deterministic=deterministic,
154
+ max_tokens=max_tokens,
155
+ temperature=temperature,
156
+ top_p=top_p,
157
+ timeout_seconds=timeout_seconds,
158
+ prompt_template=prompt_template,
159
+ pattern=pattern,
160
+ pattern_matcher=compiled_matcher,
161
+ activate_after=delay_n,
162
+ tools=list(tools) if tools else [],
163
+ max_tool_turns=max_tool_turns,
164
+ )
165
+ _REGISTRY.append(b)
166
+ return b
167
+
168
+ return wrap
169
+
170
+
171
+ def relation_behavior(
172
+ relation_type: str,
173
+ on: Optional[list[str]] = None,
174
+ name: Optional[str] = None,
175
+ where: Optional[dict[str, Any]] = None,
176
+ view: Optional[dict[str, Any]] = None,
177
+ creates: Optional[list[str]] = None,
178
+ budget: Optional[dict[str, Any]] = None,
179
+ priority: int = 0,
180
+ *,
181
+ pattern: Optional[str] = None,
182
+ activate_after: Any = None,
183
+ ) -> Callable[[Callable], RelationBehavior]:
184
+ """Decorate a function as a relation behavior — fires once per matching edge.
185
+
186
+ v0.7: also accepts `pattern=` and `activate_after=` per CONTRACT
187
+ v0.7 #8 / #11 / #13.
188
+ """
189
+
190
+ from activegraph.runtime.patterns import parse as _parse_pattern
191
+ from activegraph.runtime.scheduler import parse_activate_after as _parse_aa
192
+
193
+ compiled_matcher = None
194
+ if pattern is not None:
195
+ compiled_matcher = _parse_pattern(pattern).compile()
196
+ delay_n: Optional[int] = None
197
+ if activate_after is not None:
198
+ delay_n = _parse_aa(activate_after)
199
+
200
+ def wrap(fn: Callable) -> RelationBehavior:
201
+ rb = RelationBehavior(
202
+ name=name or fn.__name__,
203
+ fn=fn,
204
+ relation_type=relation_type,
205
+ on=list(on or []),
206
+ where=dict(where) if where else None,
207
+ view_spec=dict(view) if view else None,
208
+ creates=list(creates or []),
209
+ budget=dict(budget) if budget else None,
210
+ priority=priority,
211
+ pattern=pattern,
212
+ pattern_matcher=compiled_matcher,
213
+ activate_after=delay_n,
214
+ )
215
+ _REGISTRY.append(rb)
216
+ return rb
217
+
218
+ return wrap
@@ -0,0 +1,17 @@
1
+ """activegraph CLI. CONTRACT v0.8 #12.
2
+
3
+ A thin wrapper around library APIs. No business logic in the CLI — every
4
+ subcommand calls into the library and formats the result. Programmatic
5
+ users do exactly the same things the CLI does.
6
+
7
+ Entry point is wired via ``[project.scripts]`` in pyproject:
8
+
9
+ activegraph = "activegraph.cli.main:main"
10
+
11
+ So ``activegraph inspect ...`` runs ``activegraph.cli.main:main`` with
12
+ the rest of argv.
13
+ """
14
+
15
+ from activegraph.cli.main import main, EXIT_CODES
16
+
17
+ __all__ = ["main", "EXIT_CODES"]