crewcontext 0.1.0__tar.gz

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 (31) hide show
  1. crewcontext-0.1.0/LICENSE +21 -0
  2. crewcontext-0.1.0/PKG-INFO +135 -0
  3. crewcontext-0.1.0/README.md +102 -0
  4. crewcontext-0.1.0/crewcontext/__init__.py +40 -0
  5. crewcontext-0.1.0/crewcontext/cli.py +39 -0
  6. crewcontext-0.1.0/crewcontext/context.py +281 -0
  7. crewcontext-0.1.0/crewcontext/demos/__init__.py +1 -0
  8. crewcontext-0.1.0/crewcontext/demos/vendor_discrepancy.py +205 -0
  9. crewcontext-0.1.0/crewcontext/models.py +134 -0
  10. crewcontext-0.1.0/crewcontext/projection/__init__.py +5 -0
  11. crewcontext-0.1.0/crewcontext/projection/neo4j.py +228 -0
  12. crewcontext-0.1.0/crewcontext/projection/projector.py +130 -0
  13. crewcontext-0.1.0/crewcontext/router.py +237 -0
  14. crewcontext-0.1.0/crewcontext/store/__init__.py +5 -0
  15. crewcontext-0.1.0/crewcontext/store/base.py +86 -0
  16. crewcontext-0.1.0/crewcontext/store/postgres.py +356 -0
  17. crewcontext-0.1.0/crewcontext/utils.py +8 -0
  18. crewcontext-0.1.0/crewcontext.egg-info/PKG-INFO +135 -0
  19. crewcontext-0.1.0/crewcontext.egg-info/SOURCES.txt +29 -0
  20. crewcontext-0.1.0/crewcontext.egg-info/dependency_links.txt +1 -0
  21. crewcontext-0.1.0/crewcontext.egg-info/entry_points.txt +2 -0
  22. crewcontext-0.1.0/crewcontext.egg-info/requires.txt +9 -0
  23. crewcontext-0.1.0/crewcontext.egg-info/top_level.txt +4 -0
  24. crewcontext-0.1.0/pyproject.toml +55 -0
  25. crewcontext-0.1.0/setup.cfg +4 -0
  26. crewcontext-0.1.0/tests/__init__.py +1 -0
  27. crewcontext-0.1.0/tests/conftest.py +29 -0
  28. crewcontext-0.1.0/tests/test_models.py +102 -0
  29. crewcontext-0.1.0/tests/test_postgres_store.py +176 -0
  30. crewcontext-0.1.0/tests/test_router.py +165 -0
  31. crewcontext-0.1.0/tests/test_temporal_asof.py +6 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CrewContext
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: crewcontext
3
+ Version: 0.1.0
4
+ Summary: Auditable shared memory for AI agent systems
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/crewcontext/crewcontext
7
+ Project-URL: Documentation, https://github.com/crewcontext/crewcontext/tree/main/docs
8
+ Project-URL: Repository, https://github.com/crewcontext/crewcontext
9
+ Project-URL: Changelog, https://github.com/crewcontext/crewcontext/blob/main/CHANGELOG.md
10
+ Project-URL: Issues, https://github.com/crewcontext/crewcontext/issues
11
+ Keywords: agents,multi-agent,context,event-sourcing,audit,orchestration
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Topic :: System :: Distributed Computing
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: psycopg[binary]>=3.1
25
+ Requires-Dist: psycopg_pool>=3.1
26
+ Requires-Dist: neo4j>=5.0
27
+ Requires-Dist: python-dotenv>=1.0
28
+ Requires-Dist: click>=8.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7.0; extra == "dev"
31
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ <p align="center">
35
+ <img width="128" height="128" alt="crewcontext_logo_128" src="https://github.com/user-attachments/assets/db17d668-adca-401d-ab4e-901c05f60af4" />
36
+ </p>
37
+ <p align="center">
38
+ <h1 align="center">CrewContext</h1>
39
+ <p align="center"><strong>Auditable shared memory for AI agent systems.</strong></p>
40
+ </p>
41
+
42
+
43
+ ## The Problem
44
+
45
+ Multi-agent AI systems break at handoffs. Agent 1 processes an invoice. Agent 2 validates it. Agent 3 reconciles discrepancies. But Agent 3 has no idea what Agent 1 found. Context is lost. Decisions are invisible. Nothing is auditable.
46
+
47
+ Existing agent frameworks give you orchestration — **but not memory**. Chat history is personal. RAG is read-only. Neither gives you a shared, structured, temporal record of what happened, who did it, and why.
48
+
49
+ In regulated industries — finance, insurance, compliance — this isn't just inconvenient. It's a liability.
50
+
51
+ ## What CrewContext Does
52
+
53
+ CrewContext is a **context coordination layer** that sits underneath your agent framework and provides:
54
+
55
+ - **Shared event store** — Every agent action is recorded. Nothing is lost at handoffs.
56
+ - **Causal DAG** — Every event tracks what caused it. You can answer "why did this happen?" by walking the chain backwards.
57
+ - **Temporal queries** — Reconstruct the exact state of any entity at any point in time. "What did we know at 2pm yesterday?"
58
+ - **Versioned entities** — Business objects (invoices, customers, claims) are snapshotted at each stage, never overwritten.
59
+ - **Policy router** — Deterministic, auditable routing rules with composable conditions. No black boxes.
60
+ - **Provenance tracking** — Every event records which agent, what scope, and when. Built for auditors.
61
+
62
+ ## Architecture
63
+
64
+ ```
65
+ ┌──────────────────────────────────┐
66
+ │ ProcessContext API │
67
+ │ emit · query · timeline · causal│
68
+ └──────────┬───────────────────────┘
69
+
70
+ ┌────────────────┼────────────────┐
71
+ │ │ │
72
+ ┌─────────▼──────┐ ┌─────▼──────┐ ┌──────▼──────┐
73
+ │ PostgreSQL │ │ Neo4j │ │ Policy │
74
+ │ Event Store │ │ Graph │ │ Router │
75
+ │ │ │ │ │ │
76
+ │ Append-only │ │ Lineage │ │ Rules │
77
+ │ Temporal │ │ Causal │ │ Pub/Sub │
78
+ │ Causal links │ │ DAG │ │ Routing │
79
+ │ Versioned │ │ Typed │ │ decisions │
80
+ │ entities │ │ relations │ │ │
81
+ └────────────────┘ └────────────┘ └─────────────┘
82
+ (truth) (optional) (in-process)
83
+ ```
84
+
85
+ **PostgreSQL** is the source of truth — append-only event log, versioned entity snapshots, causal link table. **Neo4j** is an optional projection for graph queries and lineage visualization. **Policy Router** evaluates events against composable rules in-process.
86
+
87
+ ## Who It's For
88
+
89
+ CrewContext is for teams building multi-agent systems where **trust, auditability, and context preservation** matter:
90
+
91
+ - **Financial operations** — Payment processing, reconciliation, dispute resolution
92
+ - **KYC/AML compliance** — Auditable decision trails for regulators
93
+ - **Insurance claims** — Multi-stage pipelines where context loss means money lost
94
+ - **Supply chain** — Order-to-delivery orchestration across multiple agents
95
+ - **Any regulated workflow** where "the AI decided" isn't a good enough answer
96
+
97
+ ## Framework-Agnostic
98
+
99
+ CrewContext is not a replacement for your agent framework. It's the **memory layer underneath it**. It works with:
100
+
101
+ - [CrewAI](https://github.com/joaomdmoura/crewAI)
102
+ - [LangGraph](https://github.com/langchain-ai/langgraph)
103
+ - [AutoGen](https://github.com/microsoft/autogen)
104
+ - [OpenAI Agents SDK](https://github.com/openai/openai-agents-python)
105
+ - Custom agent systems
106
+
107
+ ## Getting Started
108
+
109
+ ```bash
110
+ pip install crewcontext
111
+ docker compose up -d
112
+ crewcontext init-db
113
+ crewcontext demo vendor-discrepancy
114
+ ```
115
+
116
+ Full API documentation and examples are available in the [docs](docs/) directory.
117
+
118
+ ## Configuration
119
+
120
+ | Environment Variable | Default | Description |
121
+ |---------------------|---------|-------------|
122
+ | `CREWCONTEXT_DB_URL` | `postgresql://crew:crew@localhost:5432/crewcontext` | PostgreSQL connection |
123
+ | `CREWCONTEXT_NEO4J_URI` | `bolt://localhost:7687` | Neo4j Bolt endpoint |
124
+ | `CREWCONTEXT_NEO4J_USER` | `neo4j` | Neo4j username |
125
+ | `CREWCONTEXT_NEO4J_PASSWORD` | `crewcontext123` | Neo4j password |
126
+
127
+ Neo4j is optional. Pass `enable_neo4j=False` for Postgres-only mode.
128
+
129
+ ## Contributing
130
+
131
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
132
+
133
+ ## License
134
+
135
+ [MIT](LICENSE)
@@ -0,0 +1,102 @@
1
+ <p align="center">
2
+ <img width="128" height="128" alt="crewcontext_logo_128" src="https://github.com/user-attachments/assets/db17d668-adca-401d-ab4e-901c05f60af4" />
3
+ </p>
4
+ <p align="center">
5
+ <h1 align="center">CrewContext</h1>
6
+ <p align="center"><strong>Auditable shared memory for AI agent systems.</strong></p>
7
+ </p>
8
+
9
+
10
+ ## The Problem
11
+
12
+ Multi-agent AI systems break at handoffs. Agent 1 processes an invoice. Agent 2 validates it. Agent 3 reconciles discrepancies. But Agent 3 has no idea what Agent 1 found. Context is lost. Decisions are invisible. Nothing is auditable.
13
+
14
+ Existing agent frameworks give you orchestration — **but not memory**. Chat history is personal. RAG is read-only. Neither gives you a shared, structured, temporal record of what happened, who did it, and why.
15
+
16
+ In regulated industries — finance, insurance, compliance — this isn't just inconvenient. It's a liability.
17
+
18
+ ## What CrewContext Does
19
+
20
+ CrewContext is a **context coordination layer** that sits underneath your agent framework and provides:
21
+
22
+ - **Shared event store** — Every agent action is recorded. Nothing is lost at handoffs.
23
+ - **Causal DAG** — Every event tracks what caused it. You can answer "why did this happen?" by walking the chain backwards.
24
+ - **Temporal queries** — Reconstruct the exact state of any entity at any point in time. "What did we know at 2pm yesterday?"
25
+ - **Versioned entities** — Business objects (invoices, customers, claims) are snapshotted at each stage, never overwritten.
26
+ - **Policy router** — Deterministic, auditable routing rules with composable conditions. No black boxes.
27
+ - **Provenance tracking** — Every event records which agent, what scope, and when. Built for auditors.
28
+
29
+ ## Architecture
30
+
31
+ ```
32
+ ┌──────────────────────────────────┐
33
+ │ ProcessContext API │
34
+ │ emit · query · timeline · causal│
35
+ └──────────┬───────────────────────┘
36
+
37
+ ┌────────────────┼────────────────┐
38
+ │ │ │
39
+ ┌─────────▼──────┐ ┌─────▼──────┐ ┌──────▼──────┐
40
+ │ PostgreSQL │ │ Neo4j │ │ Policy │
41
+ │ Event Store │ │ Graph │ │ Router │
42
+ │ │ │ │ │ │
43
+ │ Append-only │ │ Lineage │ │ Rules │
44
+ │ Temporal │ │ Causal │ │ Pub/Sub │
45
+ │ Causal links │ │ DAG │ │ Routing │
46
+ │ Versioned │ │ Typed │ │ decisions │
47
+ │ entities │ │ relations │ │ │
48
+ └────────────────┘ └────────────┘ └─────────────┘
49
+ (truth) (optional) (in-process)
50
+ ```
51
+
52
+ **PostgreSQL** is the source of truth — append-only event log, versioned entity snapshots, causal link table. **Neo4j** is an optional projection for graph queries and lineage visualization. **Policy Router** evaluates events against composable rules in-process.
53
+
54
+ ## Who It's For
55
+
56
+ CrewContext is for teams building multi-agent systems where **trust, auditability, and context preservation** matter:
57
+
58
+ - **Financial operations** — Payment processing, reconciliation, dispute resolution
59
+ - **KYC/AML compliance** — Auditable decision trails for regulators
60
+ - **Insurance claims** — Multi-stage pipelines where context loss means money lost
61
+ - **Supply chain** — Order-to-delivery orchestration across multiple agents
62
+ - **Any regulated workflow** where "the AI decided" isn't a good enough answer
63
+
64
+ ## Framework-Agnostic
65
+
66
+ CrewContext is not a replacement for your agent framework. It's the **memory layer underneath it**. It works with:
67
+
68
+ - [CrewAI](https://github.com/joaomdmoura/crewAI)
69
+ - [LangGraph](https://github.com/langchain-ai/langgraph)
70
+ - [AutoGen](https://github.com/microsoft/autogen)
71
+ - [OpenAI Agents SDK](https://github.com/openai/openai-agents-python)
72
+ - Custom agent systems
73
+
74
+ ## Getting Started
75
+
76
+ ```bash
77
+ pip install crewcontext
78
+ docker compose up -d
79
+ crewcontext init-db
80
+ crewcontext demo vendor-discrepancy
81
+ ```
82
+
83
+ Full API documentation and examples are available in the [docs](docs/) directory.
84
+
85
+ ## Configuration
86
+
87
+ | Environment Variable | Default | Description |
88
+ |---------------------|---------|-------------|
89
+ | `CREWCONTEXT_DB_URL` | `postgresql://crew:crew@localhost:5432/crewcontext` | PostgreSQL connection |
90
+ | `CREWCONTEXT_NEO4J_URI` | `bolt://localhost:7687` | Neo4j Bolt endpoint |
91
+ | `CREWCONTEXT_NEO4J_USER` | `neo4j` | Neo4j username |
92
+ | `CREWCONTEXT_NEO4J_PASSWORD` | `crewcontext123` | Neo4j password |
93
+
94
+ Neo4j is optional. Pass `enable_neo4j=False` for Postgres-only mode.
95
+
96
+ ## Contributing
97
+
98
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
99
+
100
+ ## License
101
+
102
+ [MIT](LICENSE)
@@ -0,0 +1,40 @@
1
+ """CrewContext — Context coordination layer for multi-agent workflows.
2
+
3
+ Usage:
4
+ from crewcontext import ProcessContext, Entity, Event, Relation
5
+
6
+ with ProcessContext(process_id="p1", agent_id="agent-1") as ctx:
7
+ event = ctx.emit("invoice.received", {"amount": 5000}, entity_id="inv-1")
8
+ """
9
+ from .context import ProcessContext
10
+ from .models import Entity, Event, Relation, RoutingDecision, generate_id
11
+ from .router import (
12
+ PolicyRouter,
13
+ all_of,
14
+ any_of,
15
+ none_of,
16
+ data_field_eq,
17
+ data_field_gt,
18
+ data_field_ne,
19
+ data_fields_differ,
20
+ event_type_is,
21
+ )
22
+
23
+ __version__ = "0.1.0"
24
+ __all__ = [
25
+ "ProcessContext",
26
+ "Entity",
27
+ "Event",
28
+ "Relation",
29
+ "RoutingDecision",
30
+ "generate_id",
31
+ "PolicyRouter",
32
+ "all_of",
33
+ "any_of",
34
+ "none_of",
35
+ "data_field_eq",
36
+ "data_field_gt",
37
+ "data_field_ne",
38
+ "data_fields_differ",
39
+ "event_type_is",
40
+ ]
@@ -0,0 +1,39 @@
1
+ """CLI for CrewContext."""
2
+ import click
3
+ from .utils import load_env
4
+
5
+
6
+ @click.group()
7
+ @click.version_option(version="0.1.0")
8
+ def main():
9
+ """CrewContext - Context coordination for multi-agent workflows."""
10
+ load_env()
11
+
12
+
13
+ @main.group()
14
+ def demo():
15
+ """Run demo scenarios."""
16
+ pass
17
+
18
+
19
+ @demo.command("vendor-discrepancy")
20
+ def vendor_discrepancy():
21
+ """Run the vendor discrepancy demo."""
22
+ from .demos.vendor_discrepancy import run_demo
23
+ run_demo()
24
+
25
+
26
+ @main.command("init-db")
27
+ @click.option("--db-url", envvar="CREWCONTEXT_DB_URL", default=None)
28
+ def init_db(db_url):
29
+ """Initialise the database schema."""
30
+ from .store.postgres import PostgresStore
31
+ store = PostgresStore(db_url)
32
+ store.connect()
33
+ store.init_schema()
34
+ store.close()
35
+ click.echo("Database schema initialised.")
36
+
37
+
38
+ if __name__ == "__main__":
39
+ main()
@@ -0,0 +1,281 @@
1
+ """ProcessContext — the main API agents interact with.
2
+
3
+ This is the public interface. Agents emit events, query history,
4
+ build entity snapshots, and trace causal chains through this class.
5
+
6
+ Usage:
7
+ with ProcessContext(process_id="proc-1", agent_id="agent-a") as ctx:
8
+ e1 = ctx.emit("invoice.received", {"amount": 5000}, entity_id="inv-1")
9
+ e2 = ctx.emit("invoice.validated", {"ok": True}, entity_id="inv-1", caused_by=[e1])
10
+ history = ctx.timeline("inv-1")
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from datetime import datetime
16
+ from typing import Any, Callable, Dict, List, Optional, Sequence
17
+
18
+ from .models import Entity, Event, Relation, RoutingDecision, generate_id
19
+ from .projection.projector import Neo4jProjector
20
+ from .router import PolicyRouter
21
+ from .store.postgres import PostgresStore
22
+
23
+ log = logging.getLogger(__name__)
24
+
25
+
26
+ class ProcessContext:
27
+ """Scoped, temporal, causal context for a business process.
28
+
29
+ Features:
30
+ - **emit()**: Record events with optional causal parents.
31
+ - **query()**: Retrieve events with temporal/scope/type filtering.
32
+ - **timeline()**: Ordered event history for an entity.
33
+ - **snapshot()**: Save/retrieve versioned entity state.
34
+ - **causal_chain()**: Walk the causal DAG.
35
+ - **subscribe()**: React to event types in real-time.
36
+ - **batch_emit()**: Atomic multi-event writes.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ process_id: str,
42
+ agent_id: str,
43
+ scope: str = "default",
44
+ db_url: Optional[str] = None,
45
+ enable_neo4j: bool = True,
46
+ ):
47
+ self.process_id = process_id
48
+ self.agent_id = agent_id
49
+ self.scope = scope
50
+
51
+ self._store = PostgresStore(db_url)
52
+ self._projector = Neo4jProjector() if enable_neo4j else None
53
+ self._router = PolicyRouter()
54
+ self._connected = False
55
+
56
+ # -- context manager ----------------------------------------------------
57
+
58
+ def __enter__(self) -> ProcessContext:
59
+ self.connect()
60
+ return self
61
+
62
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
63
+ self.close()
64
+
65
+ def connect(self) -> None:
66
+ self._store.connect()
67
+ self._store.init_schema()
68
+ self._connected = True
69
+ if self._projector:
70
+ self._projector.connect() # best-effort, logs on failure
71
+ log.info(
72
+ "ProcessContext ready: process=%s agent=%s scope=%s",
73
+ self.process_id, self.agent_id, self.scope,
74
+ )
75
+
76
+ def close(self) -> None:
77
+ self._store.close()
78
+ if self._projector:
79
+ self._projector.close()
80
+ self._connected = False
81
+
82
+ # -- router access ------------------------------------------------------
83
+
84
+ @property
85
+ def router(self) -> PolicyRouter:
86
+ return self._router
87
+
88
+ # -- emit events --------------------------------------------------------
89
+
90
+ def emit(
91
+ self,
92
+ event_type: str,
93
+ data: Dict[str, Any],
94
+ entity_id: Optional[str] = None,
95
+ relation_id: Optional[str] = None,
96
+ metadata: Optional[Dict[str, Any]] = None,
97
+ caused_by: Optional[Sequence[Event]] = None,
98
+ ) -> Event:
99
+ """Emit an event into the process context.
100
+
101
+ Args:
102
+ event_type: Dotted event name (e.g. "invoice.received").
103
+ data: Event payload.
104
+ entity_id: Optional entity this event affects.
105
+ relation_id: Optional relation this event affects.
106
+ metadata: Arbitrary metadata.
107
+ caused_by: Parent events that caused this one (builds the DAG).
108
+
109
+ Returns:
110
+ The persisted Event object.
111
+ """
112
+ parent_ids = tuple(e.id for e in caused_by) if caused_by else ()
113
+
114
+ event = Event(
115
+ id=generate_id(),
116
+ type=event_type,
117
+ process_id=self.process_id,
118
+ data=data,
119
+ agent_id=self.agent_id,
120
+ entity_id=entity_id,
121
+ relation_id=relation_id,
122
+ scope=self.scope,
123
+ metadata=metadata or {},
124
+ parent_ids=parent_ids,
125
+ )
126
+
127
+ # 1. Persist to source of truth
128
+ self._store.save_event(event)
129
+
130
+ # 2. Project to graph (best-effort)
131
+ if self._projector:
132
+ self._projector.project_event(event)
133
+
134
+ # 3. Notify subscribers
135
+ self._router.notify_subscribers(event)
136
+
137
+ # 4. Evaluate routing rules
138
+ decision = self._router.evaluate(event)
139
+ if decision:
140
+ self._persist_routing_decision(decision, parent_event=event)
141
+
142
+ log.debug("Emitted: %s (type=%s, entity=%s)", event.id[:8], event_type, entity_id)
143
+ return event
144
+
145
+ def batch_emit(
146
+ self, events_spec: List[Dict[str, Any]]
147
+ ) -> List[Event]:
148
+ """Atomically emit multiple events in a single transaction.
149
+
150
+ Each spec is a dict with keys: event_type, data, and optionally
151
+ entity_id, relation_id, metadata.
152
+ """
153
+ events = []
154
+ for spec in events_spec:
155
+ event = Event(
156
+ id=generate_id(),
157
+ type=spec["event_type"],
158
+ process_id=self.process_id,
159
+ data=spec["data"],
160
+ agent_id=self.agent_id,
161
+ entity_id=spec.get("entity_id"),
162
+ relation_id=spec.get("relation_id"),
163
+ scope=self.scope,
164
+ metadata=spec.get("metadata", {}),
165
+ )
166
+ events.append(event)
167
+
168
+ self._store.save_events(events)
169
+
170
+ # Best-effort graph projection
171
+ if self._projector:
172
+ for ev in events:
173
+ self._projector.project_event(ev)
174
+
175
+ return events
176
+
177
+ def _persist_routing_decision(
178
+ self, decision: RoutingDecision, parent_event: Event
179
+ ) -> None:
180
+ """Save a routing decision as a child event (no re-evaluation)."""
181
+ decision_event = Event(
182
+ id=generate_id(),
183
+ type="routing.decision",
184
+ process_id=self.process_id,
185
+ data=decision.to_dict(),
186
+ agent_id="system.router",
187
+ entity_id=parent_event.entity_id,
188
+ scope=self.scope,
189
+ parent_ids=(parent_event.id,),
190
+ )
191
+ self._store.save_event(decision_event)
192
+ if self._projector:
193
+ self._projector.project_event(decision_event)
194
+
195
+ # -- query events -------------------------------------------------------
196
+
197
+ def query(
198
+ self,
199
+ entity_id: Optional[str] = None,
200
+ event_type: Optional[str] = None,
201
+ scope: Optional[str] = None,
202
+ as_of: Optional[datetime] = None,
203
+ limit: int = 1000,
204
+ offset: int = 0,
205
+ ) -> List[Dict[str, Any]]:
206
+ """Query events in this process with optional filters."""
207
+ return self._store.query_events(
208
+ self.process_id,
209
+ entity_id=entity_id,
210
+ event_type=event_type,
211
+ scope=scope or self.scope,
212
+ as_of=as_of,
213
+ limit=limit,
214
+ offset=offset,
215
+ )
216
+
217
+ def timeline(self, entity_id: str, as_of: Optional[datetime] = None) -> List[Dict[str, Any]]:
218
+ """Get the full ordered event history for an entity."""
219
+ return self._store.query_events(
220
+ self.process_id,
221
+ entity_id=entity_id,
222
+ as_of=as_of,
223
+ )
224
+
225
+ # -- entity snapshots ---------------------------------------------------
226
+
227
+ def save_entity(self, entity: Entity) -> None:
228
+ """Save a versioned entity snapshot."""
229
+ self._store.save_entity(entity)
230
+ if self._projector:
231
+ self._projector.project_entity(entity)
232
+
233
+ def get_entity(
234
+ self, entity_id: str, as_of: Optional[datetime] = None
235
+ ) -> Optional[Dict[str, Any]]:
236
+ """Retrieve the latest entity state (or state at a point in time)."""
237
+ return self._store.get_entity(entity_id, as_of=as_of)
238
+
239
+ # -- relations ----------------------------------------------------------
240
+
241
+ def save_relation(self, relation: Relation) -> None:
242
+ """Persist a typed relation between entities."""
243
+ self._store.save_relation(relation)
244
+ if self._projector:
245
+ self._projector.project_relation(relation)
246
+
247
+ # -- causal DAG ---------------------------------------------------------
248
+
249
+ def causal_parents(self, event_id: str) -> List[str]:
250
+ """Get the events that caused this event."""
251
+ return self._store.get_causal_parents(event_id)
252
+
253
+ def causal_children(self, event_id: str) -> List[str]:
254
+ """Get the events caused by this event."""
255
+ return self._store.get_causal_children(event_id)
256
+
257
+ def causal_chain(self, event_id: str, max_depth: int = 10) -> List[Dict[str, Any]]:
258
+ """Walk the full causal DAG via Neo4j (if available)."""
259
+ if self._projector:
260
+ return self._projector.get_causal_chain(event_id, max_depth=max_depth)
261
+ return []
262
+
263
+ # -- graph queries ------------------------------------------------------
264
+
265
+ def lineage(self, entity_id: str, max_depth: int = 20) -> List[Dict[str, Any]]:
266
+ """Get Neo4j lineage for an entity (if available)."""
267
+ if self._projector:
268
+ return self._projector.get_lineage(entity_id, max_depth=max_depth)
269
+ return []
270
+
271
+ def cypher(self, query: str, params: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
272
+ """Execute a raw Cypher query against Neo4j."""
273
+ if self._projector:
274
+ return self._projector.run_cypher(query, params)
275
+ return []
276
+
277
+ # -- pub/sub convenience ------------------------------------------------
278
+
279
+ def subscribe(self, event_type: str, callback: Callable[[Event], None]) -> None:
280
+ """Subscribe to events of a given type."""
281
+ self._router.subscribe(event_type, callback)
@@ -0,0 +1 @@
1
+ """Demo scenarios."""