synapse-memory-engine 1.3.1__tar.gz → 1.4.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 (82) hide show
  1. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/PKG-INFO +65 -1
  2. synapse_memory_engine-1.4.0/README.md +116 -0
  3. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/pyproject.toml +1 -1
  4. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/setup.py +1 -1
  5. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/__init__.py +29 -1
  6. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/api/fastapi_server.py +200 -6
  7. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/client.py +51 -1
  8. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/auth.py +3 -0
  9. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/knapsack_packer.py +13 -3
  10. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/migrations.py +130 -1
  11. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/pgvector_db.py +55 -0
  12. synapse_memory_engine-1.4.0/synapse_memory/core/run_trace_store.py +547 -0
  13. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/sqlite_db.py +1 -0
  14. synapse_memory_engine-1.4.0/synapse_memory/runtime.py +540 -0
  15. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory_engine.egg-info/PKG-INFO +65 -1
  16. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory_engine.egg-info/SOURCES.txt +11 -0
  17. synapse_memory_engine-1.4.0/tests/test_agent_run_api.py +160 -0
  18. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_db_role_provisioning.py +43 -0
  19. synapse_memory_engine-1.4.0/tests/test_developer_relevance_scoring.py +108 -0
  20. synapse_memory_engine-1.4.0/tests/test_developer_review_packet.py +57 -0
  21. synapse_memory_engine-1.4.0/tests/test_first_run_http_e2e.py +154 -0
  22. synapse_memory_engine-1.4.0/tests/test_knapsack_score_precision.py +55 -0
  23. synapse_memory_engine-1.4.0/tests/test_longmemeval_benchmark.py +142 -0
  24. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_migrations.py +19 -0
  25. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_pgvector_integration.py +74 -0
  26. synapse_memory_engine-1.4.0/tests/test_retrieval_baseline_benchmark.py +247 -0
  27. synapse_memory_engine-1.4.0/tests/test_run_trace_store.py +202 -0
  28. synapse_memory_engine-1.4.0/tests/test_runtime.py +280 -0
  29. synapse_memory_engine-1.3.1/README.md +0 -52
  30. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/setup.cfg +0 -0
  31. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/cli/synapse_cli.py +0 -0
  32. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/__init__.py +0 -0
  33. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/decay_engine.py +0 -0
  34. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/deduplicator.py +0 -0
  35. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/embedder.py +0 -0
  36. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/embedding_provider.py +0 -0
  37. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/encryption.py +0 -0
  38. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/hybrid_search.py +0 -0
  39. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/memory_manager.py +0 -0
  40. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/observability.py +0 -0
  41. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/pruner.py +0 -0
  42. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/core/rate_limiter.py +0 -0
  43. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/examples/langchain_integration.py +0 -0
  44. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/examples/llamaindex_integration.py +0 -0
  45. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/integrations/__init__.py +0 -0
  46. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/integrations/langchain.py +0 -0
  47. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/integrations/llamaindex.py +0 -0
  48. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/main.py +0 -0
  49. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory/py.typed +0 -0
  50. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory_engine.egg-info/dependency_links.txt +0 -0
  51. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory_engine.egg-info/entry_points.txt +0 -0
  52. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory_engine.egg-info/requires.txt +0 -0
  53. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/synapse_memory_engine.egg-info/top_level.txt +0 -0
  54. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_auth_multitenancy.py +0 -0
  55. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_backup_restore_drill.py +0 -0
  56. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_client_retry_policy.py +0 -0
  57. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_decay.py +0 -0
  58. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_deduplicator.py +0 -0
  59. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_deterministic_embedding.py +0 -0
  60. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_encryption.py +0 -0
  61. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_f01_auth_escalation.py +0 -0
  62. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_f02_f07_probes.py +0 -0
  63. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_f05_f06_data_at_rest.py +0 -0
  64. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_f10_f15_hardening.py +0 -0
  65. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_f17_provider_dimension.py +0 -0
  66. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_f18_pgvector_3072.py +0 -0
  67. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_helm_chart.py +0 -0
  68. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_integration.py +0 -0
  69. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_knapsack.py +0 -0
  70. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_langchain.py +0 -0
  71. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_llamaindex.py +0 -0
  72. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_load_resilience.py +0 -0
  73. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_max_record_limit.py +0 -0
  74. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_p0_safety_regressions.py +0 -0
  75. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_p1_contract_regressions.py +0 -0
  76. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_pgvector.py +0 -0
  77. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_phase_cd.py +0 -0
  78. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_pruner.py +0 -0
  79. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_rate_limit.py +0 -0
  80. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_required_auth_gateway.py +0 -0
  81. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_retrieval_eval.py +0 -0
  82. {synapse_memory_engine-1.3.1 → synapse_memory_engine-1.4.0}/tests/test_synapse.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: synapse-memory-engine
3
- Version: 1.3.1
3
+ Version: 1.4.0
4
4
  Summary: Production-grade active RAG & cognitive memory engine with multi-tenancy, pgvector HNSW indexing, semantic deduplication, decay-based pruning, and token-aware context packing.
5
5
  Home-page: https://github.com/721189/Synapse-Memory
6
6
  Author: Synapse Cognitive Systems
@@ -59,6 +59,8 @@ Python distribution for the Synapse Memory engine. The distribution name is `syn
59
59
  - **Semantic Deduplication**: Dual lexical Jaccard and dense cosine similarity detection to prevent node fragmentation.
60
60
  - **Hybrid Retrieval**: BM25 sparse lexical ranking fused with dense vector embeddings via Reciprocal Rank Fusion (RRF).
61
61
  - **Framework Integrations**: True drop-in adapters for LangChain (`BaseMemory`) and LlamaIndex (`BaseMemory`).
62
+ - **Agent reliability runtime**: Opt-in run budgets, repeated failed-call blocking, versioned event serialization, and persistent tenant-scoped run traces.
63
+ - **Opt-in agent run guard**: Bounded steps/tool calls, cooperative deadlines, repeated-failure blocking, and privacy-minimized in-memory events.
62
64
 
63
65
  ## Installation
64
66
 
@@ -87,6 +89,68 @@ with SynapseClient(
87
89
 
88
90
  The client automatically retries idempotent `GET`/`HEAD`/`OPTIONS` requests and honours `Retry-After`. It does not automatically replay `POST` requests (including `/query`, which updates access metadata) after an ambiguous failure; callers should reconcile the operation before manually retrying, or provide their own idempotency mechanism.
89
91
 
92
+ ## Opt-in Python run guard
93
+
94
+ The runtime only guards tool calls explicitly routed through `SynapseRun`; it does not intercept arbitrary framework calls. Events stay in memory until the application exports or persists them. Event records contain tool names and keyed argument fingerprints, not raw arguments, prompts, tool results, or exception messages.
95
+
96
+ ```python
97
+ from synapse_memory import RunPolicy, SynapseRun
98
+
99
+ policy = RunPolicy(
100
+ max_steps=12,
101
+ max_tool_calls=8,
102
+ timeout_seconds=30,
103
+ max_repeated_failures=2,
104
+ )
105
+
106
+ with SynapseRun(policy=policy) as run:
107
+ customer = run.call_tool(
108
+ "lookup_customer",
109
+ {"customer_id": "cust_123"},
110
+ lookup_customer, # callable accepts arguments as keyword arguments
111
+ )
112
+ run.record_step() # account for non-tool work
113
+ print(customer)
114
+
115
+ # Inspect metadata-only events after the context exits.
116
+ for event in run.events:
117
+ print(event.event_type.value, event.reason, event.tool_fingerprint)
118
+ ```
119
+
120
+ Limits are checked before admitting each step/tool call. The deadline is cooperative: a synchronous tool already in progress cannot be forcibly interrupted. A run guard is not a sandbox, does not authorize tools, does not intercept unwrapped calls, or enforce provider token/cost budgets. Repeated-call blocking applies after the configured number of consecutive failures for the same tool and equivalent JSON arguments; a successful call resets that failure count. No automatic retries are performed.
121
+
122
+ ## Persistent run traces and retrieval provenance
123
+
124
+ Trace persistence is an explicit opt-in operation. The event contract is versioned (schema_version=1) and records only run lifecycle metadata, tool name/status, a per-run keyed argument fingerprint, typed failure reasons, and retrieval evidence. It does not record prompts, raw tool arguments, tool results, memory content, or the retrieval query. Retrieval evidence must be supplied by the application from its own retrieval/packing pipeline; Synapse does not silently instrument arbitrary framework calls or automatically attach every /query response to a run.
125
+
126
+ To persist a completed SynapseRun, use a key with agent-runs:read and agent-runs:write scopes, plus agent-runs:delete only when deletion is needed:
127
+
128
+ ~~~python
129
+ from synapse_memory import RunPolicy, SynapseClient, SynapseRun
130
+
131
+ with SynapseRun(policy=RunPolicy(max_steps=12, max_tool_calls=8)) as run:
132
+ # The app can query Synapse or another retriever here.
133
+ candidates = [
134
+ {"id": "memory_01", "relevance_score": 0.91, "token_cost": 18},
135
+ {"id": "memory_02", "relevance_score": 0.43, "token_cost": 16},
136
+ ]
137
+ packed_ids = ["memory_01"]
138
+ run.record_memory_retrieval(candidates, packed_memory_ids=packed_ids)
139
+ answer = run.call_tool("lookup_customer", {"customer_id": "cust_123"}, lookup_customer)
140
+
141
+ with SynapseClient(
142
+ "https://your-synapse-api",
143
+ api_key="syn_live_your_key",
144
+ tenant_id="your-tenant",
145
+ ) as client:
146
+ receipt = client.upload_agent_run(run)
147
+ print(receipt["events_inserted"], receipt["events_deduplicated"])
148
+ ~~~
149
+
150
+ The client uploads the run's ordered events after the run ends. Stable event IDs make retrying a partially completed upload idempotent. Event records are tenant-bound by the authenticated API key; a caller-supplied tenant field in an event is rejected. Available endpoints are documented in the API contract (../../docs/api-reference.md). Each tenant can list and delete only its own trace records; event pages are bounded and ordered by sequence number.
151
+
152
+ This first persistence layer is not yet a live streaming sink, a full observability dashboard, an outcome verifier, or a retention scheduler. Deployments must apply schema migration v6 before starting a PostgreSQL least-privilege runtime role. Use the API's deletion endpoint or a separately configured retention job to remove stored runs.
153
+
90
154
  ## Testing
91
155
 
92
156
  From the repository root:
@@ -0,0 +1,116 @@
1
+ # Synapse Memory Python SDK
2
+
3
+ Python distribution for the Synapse Memory engine. The distribution name is `synapse-memory-engine`; the import package is `synapse_memory`.
4
+
5
+ ## Features
6
+
7
+ - **Encrypted SQLite Storage**: Fernet AES-128-CBC with HMAC-SHA256 authenticated encryption at rest.
8
+ - **Dynamic Programming Knapsack Packing**: Exact 0/1 Knapsack optimizer for packing high-relevance memories into strict token envelopes.
9
+ - **Ebbinghaus Decay Engine**: Continuous temporal decay with positive/negative reinforcement factors.
10
+ - **Semantic Deduplication**: Dual lexical Jaccard and dense cosine similarity detection to prevent node fragmentation.
11
+ - **Hybrid Retrieval**: BM25 sparse lexical ranking fused with dense vector embeddings via Reciprocal Rank Fusion (RRF).
12
+ - **Framework Integrations**: True drop-in adapters for LangChain (`BaseMemory`) and LlamaIndex (`BaseMemory`).
13
+ - **Agent reliability runtime**: Opt-in run budgets, repeated failed-call blocking, versioned event serialization, and persistent tenant-scoped run traces.
14
+ - **Opt-in agent run guard**: Bounded steps/tool calls, cooperative deadlines, repeated-failure blocking, and privacy-minimized in-memory events.
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install synapse-memory-engine # from PyPI
20
+ # or, from a checkout of this repository:
21
+ pip install -e .
22
+ ```
23
+
24
+ ## REST client quick start
25
+
26
+ Use `SynapseClient` when connecting an application to a running Synapse API:
27
+
28
+ ```python
29
+ from synapse_memory import SynapseClient, SynapseError
30
+
31
+ with SynapseClient(
32
+ "http://127.0.0.1:8008",
33
+ api_key="syn_live_your_key",
34
+ tenant_id="your-tenant",
35
+ ) as client:
36
+ created = client.create_memory("Customer selected the annual plan.", category="fact")
37
+ result = client.retrieve("Which plan did the customer select?")
38
+ print(created.id, result.fused_context)
39
+ ```
40
+
41
+ The client automatically retries idempotent `GET`/`HEAD`/`OPTIONS` requests and honours `Retry-After`. It does not automatically replay `POST` requests (including `/query`, which updates access metadata) after an ambiguous failure; callers should reconcile the operation before manually retrying, or provide their own idempotency mechanism.
42
+
43
+ ## Opt-in Python run guard
44
+
45
+ The runtime only guards tool calls explicitly routed through `SynapseRun`; it does not intercept arbitrary framework calls. Events stay in memory until the application exports or persists them. Event records contain tool names and keyed argument fingerprints, not raw arguments, prompts, tool results, or exception messages.
46
+
47
+ ```python
48
+ from synapse_memory import RunPolicy, SynapseRun
49
+
50
+ policy = RunPolicy(
51
+ max_steps=12,
52
+ max_tool_calls=8,
53
+ timeout_seconds=30,
54
+ max_repeated_failures=2,
55
+ )
56
+
57
+ with SynapseRun(policy=policy) as run:
58
+ customer = run.call_tool(
59
+ "lookup_customer",
60
+ {"customer_id": "cust_123"},
61
+ lookup_customer, # callable accepts arguments as keyword arguments
62
+ )
63
+ run.record_step() # account for non-tool work
64
+ print(customer)
65
+
66
+ # Inspect metadata-only events after the context exits.
67
+ for event in run.events:
68
+ print(event.event_type.value, event.reason, event.tool_fingerprint)
69
+ ```
70
+
71
+ Limits are checked before admitting each step/tool call. The deadline is cooperative: a synchronous tool already in progress cannot be forcibly interrupted. A run guard is not a sandbox, does not authorize tools, does not intercept unwrapped calls, or enforce provider token/cost budgets. Repeated-call blocking applies after the configured number of consecutive failures for the same tool and equivalent JSON arguments; a successful call resets that failure count. No automatic retries are performed.
72
+
73
+ ## Persistent run traces and retrieval provenance
74
+
75
+ Trace persistence is an explicit opt-in operation. The event contract is versioned (schema_version=1) and records only run lifecycle metadata, tool name/status, a per-run keyed argument fingerprint, typed failure reasons, and retrieval evidence. It does not record prompts, raw tool arguments, tool results, memory content, or the retrieval query. Retrieval evidence must be supplied by the application from its own retrieval/packing pipeline; Synapse does not silently instrument arbitrary framework calls or automatically attach every /query response to a run.
76
+
77
+ To persist a completed SynapseRun, use a key with agent-runs:read and agent-runs:write scopes, plus agent-runs:delete only when deletion is needed:
78
+
79
+ ~~~python
80
+ from synapse_memory import RunPolicy, SynapseClient, SynapseRun
81
+
82
+ with SynapseRun(policy=RunPolicy(max_steps=12, max_tool_calls=8)) as run:
83
+ # The app can query Synapse or another retriever here.
84
+ candidates = [
85
+ {"id": "memory_01", "relevance_score": 0.91, "token_cost": 18},
86
+ {"id": "memory_02", "relevance_score": 0.43, "token_cost": 16},
87
+ ]
88
+ packed_ids = ["memory_01"]
89
+ run.record_memory_retrieval(candidates, packed_memory_ids=packed_ids)
90
+ answer = run.call_tool("lookup_customer", {"customer_id": "cust_123"}, lookup_customer)
91
+
92
+ with SynapseClient(
93
+ "https://your-synapse-api",
94
+ api_key="syn_live_your_key",
95
+ tenant_id="your-tenant",
96
+ ) as client:
97
+ receipt = client.upload_agent_run(run)
98
+ print(receipt["events_inserted"], receipt["events_deduplicated"])
99
+ ~~~
100
+
101
+ The client uploads the run's ordered events after the run ends. Stable event IDs make retrying a partially completed upload idempotent. Event records are tenant-bound by the authenticated API key; a caller-supplied tenant field in an event is rejected. Available endpoints are documented in the API contract (../../docs/api-reference.md). Each tenant can list and delete only its own trace records; event pages are bounded and ordered by sequence number.
102
+
103
+ This first persistence layer is not yet a live streaming sink, a full observability dashboard, an outcome verifier, or a retention scheduler. Deployments must apply schema migration v6 before starting a PostgreSQL least-privilege runtime role. Use the API's deletion endpoint or a separately configured retention job to remove stored runs.
104
+
105
+ ## Testing
106
+
107
+ From the repository root:
108
+
109
+ ```bash
110
+ pip install -e "sdk/python[pgvector,langchain,llamaindex,dev]"
111
+ PYTHONPATH=sdk/python pytest sdk/python/tests -v
112
+ ```
113
+
114
+ Database-gated tests require a disposable PostgreSQL/pgvector database and explicitly documented environment flags. Passing the offline suite alone is not production verification.
115
+
116
+ **Licence:** proprietary. Installation does not grant unrestricted usage or deployment rights; obtain written permission under [`../../LICENSE`](../../LICENSE) before using this package outside the permissions granted by the copyright holder.
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
  # owned by an unrelated third-party project. Import package stays `synapse_memory`.
8
8
  # See docs/publishing.md.
9
9
  name = "synapse-memory-engine"
10
- version = "1.3.1"
10
+ version = "1.4.0"
11
11
  authors = [{name = "Synapse Cognitive Systems", email = "singhshivam20009@gmail.com"}]
12
12
  description = "Production-grade active RAG & cognitive memory engine with multi-tenancy, pgvector HNSW indexing, semantic deduplication, decay-based pruning, and token-aware context packing."
13
13
  readme = "README.md"
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="synapse-memory-engine",
5
- version="1.3.1",
5
+ version="1.4.0",
6
6
  description="Sovereign, active RAG & cognitive long-term memory engine for AI agents",
7
7
  author="Synapse Cognitive Systems",
8
8
  author_email="singhshivam20009@gmail.com",
@@ -9,8 +9,23 @@ from synapse_memory.core.decay_engine import DecayEngine
9
9
  from synapse_memory.integrations.langchain import SynapseLangChainMemory
10
10
  from synapse_memory.integrations.llamaindex import SynapseLlamaIndexMemory
11
11
  from synapse_memory.client import SynapseClient, SynapseError, CreateResult, QueryResult
12
+ from synapse_memory.runtime import (
13
+ RunPolicy,
14
+ RunStatus,
15
+ RunEventType,
16
+ RunEvent,
17
+ MemoryEvidence,
18
+ SynapseRun,
19
+ RunGuardError,
20
+ RunStateError,
21
+ RunLimitExceeded,
22
+ RunStepLimitExceeded,
23
+ RunToolCallLimitExceeded,
24
+ RunDeadlineExceeded,
25
+ RepeatedToolCallError,
26
+ )
12
27
 
13
- __version__ = "1.3.1"
28
+ __version__ = "1.4.0"
14
29
  __api_compat_version__ = "v1"
15
30
 
16
31
  __all__ = [
@@ -28,4 +43,17 @@ __all__ = [
28
43
  "SynapseError",
29
44
  "CreateResult",
30
45
  "QueryResult",
46
+ "RunPolicy",
47
+ "RunStatus",
48
+ "RunEventType",
49
+ "RunEvent",
50
+ "MemoryEvidence",
51
+ "SynapseRun",
52
+ "RunGuardError",
53
+ "RunStateError",
54
+ "RunLimitExceeded",
55
+ "RunStepLimitExceeded",
56
+ "RunToolCallLimitExceeded",
57
+ "RunDeadlineExceeded",
58
+ "RepeatedToolCallError",
31
59
  ]
@@ -5,10 +5,10 @@ import datetime
5
5
  import hashlib
6
6
  import logging
7
7
  import threading
8
- from typing import List, Dict, Any, Optional, Tuple
9
- from pydantic import BaseModel, ConfigDict, Field
8
+ from typing import List, Dict, Any, Optional, Tuple, Literal
9
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
10
10
  import uvicorn
11
- from fastapi import FastAPI, HTTPException, status, Header, Request, Depends
11
+ from fastapi import FastAPI, HTTPException, status, Header, Request, Depends, Path
12
12
  from fastapi.middleware.cors import CORSMiddleware
13
13
 
14
14
  # Adjust path to enable absolute imports when running as standalone script
@@ -21,6 +21,10 @@ from synapse_memory.core.memory_manager import DEFAULT_MAX_RECORD_LIMIT, MemoryM
21
21
  from synapse_memory.core.knapsack_packer import KnapsackPacker
22
22
  from synapse_memory.core.decay_engine import DecayEngine
23
23
  from synapse_memory.core.hybrid_search import HybridSearch
24
+ from synapse_memory.runtime import RunEventType, RunStatus
25
+ from synapse_memory.core.run_trace_store import (
26
+ RunTraceStore, RunTraceError, RunNotFoundError, RunConflictError,
27
+ )
24
28
  from synapse_memory.core.auth import (
25
29
  SecurityManager,
26
30
  APIKeyRecord,
@@ -106,6 +110,7 @@ else:
106
110
  ACTIVE_BACKEND = "sqlite"
107
111
 
108
112
  manager = MemoryManager(store=store, embedder=embedder, max_record_limit=DEFAULT_MAX_RECORD_LIMIT)
113
+ run_trace_store = RunTraceStore(store)
109
114
  logger.info(
110
115
  f"MemoryManager capacity limit: {manager.max_record_limit} records "
111
116
  f"(override with SYNAPSE_MAX_RECORD_LIMIT; historical default is 50)"
@@ -233,7 +238,7 @@ def _rate_limit_principal(request: Request) -> str:
233
238
  app = FastAPI(
234
239
  title="SynapseMemory Enterprise Cognitive Gateway",
235
240
  description="Sovereign Active Long-Term Memory & Context Budgeting REST API with Multi-Tenancy and Database-Level RLS.",
236
- version="1.3.1"
241
+ version="1.4.0"
237
242
  )
238
243
 
239
244
 
@@ -244,9 +249,11 @@ async def add_request_id_and_rate_limit(request: Request, call_next): # type: i
244
249
  request_id = request.headers.get("X-Request-ID") or f"req_{_uuid.uuid4().hex[:12]}"
245
250
  start = time.time()
246
251
  # Rate-limit only mutating + heavy retrieval endpoints
247
- if request.url.path.startswith(("/ingest", "/api/memories", "/query", "/api/retrieval")):
252
+ if request.url.path.startswith(("/ingest", "/api/memories", "/query", "/api/retrieval", "/api/agent-runs")):
253
+ # Avoid unbounded per-run limiter buckets: all run/event URLs share one bucket.
254
+ rate_route = "/api/agent-runs" if request.url.path.startswith("/api/agent-runs") else request.url.path
248
255
  try:
249
- _check_rate_limit(f"{_rate_limit_principal(request)}:{request.url.path}")
256
+ _check_rate_limit(f"{_rate_limit_principal(request)}:{rate_route}")
250
257
  except HTTPException as e:
251
258
  from fastapi.responses import JSONResponse
252
259
 
@@ -455,6 +462,62 @@ class QueryResponse(SynapseAPIModel):
455
462
  active_storage_backend: str
456
463
 
457
464
 
465
+ class RunCreateRequest(SynapseAPIModel):
466
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
467
+ run_id: str = Field(..., min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$")
468
+ schema_version: Literal[1] = 1
469
+ tenant_id: Optional[str] = Field(
470
+ None, alias="tenantId", min_length=1, max_length=128,
471
+ pattern=r"^[A-Za-z0-9_\-\.]+$",
472
+ )
473
+
474
+
475
+ class MemoryEvidenceRequest(SynapseAPIModel):
476
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
477
+ memory_id: str = Field(..., min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$")
478
+ rank: int = Field(..., ge=1, le=10000)
479
+ score_components: Dict[str, float] = Field(default_factory=dict, max_length=8)
480
+ token_cost: int = Field(0, ge=0, le=100000)
481
+ packed: bool = False
482
+ created_at: Optional[float] = Field(None, ge=0, allow_inf_nan=False)
483
+ supersedes: Optional[str] = Field(None, min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$")
484
+ superseded_by: Optional[str] = Field(None, min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$")
485
+
486
+
487
+ class AgentRunEventRequest(SynapseAPIModel):
488
+ model_config = ConfigDict(populate_by_name=True, extra="forbid")
489
+ schema_version: Literal[1] = 1
490
+ event_id: str = Field(..., min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$")
491
+ run_id: str = Field(..., min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$")
492
+ event_type: RunEventType
493
+ timestamp: datetime.datetime
494
+ run_status: RunStatus
495
+ step_index: Optional[int] = Field(None, ge=1)
496
+ tool_call_index: Optional[int] = Field(None, ge=1)
497
+ tool_name: Optional[str] = Field(None, min_length=1, max_length=128, pattern=r"^[A-Za-z][A-Za-z0-9_.:-]*$")
498
+ tool_fingerprint: Optional[str] = Field(None, pattern=r"^[a-fA-F0-9]{64}$")
499
+ reason: Optional[str] = Field(None, min_length=1, max_length=128, pattern=r"^[a-z][a-z0-9_]*$")
500
+ error_type: Optional[str] = Field(None, min_length=1, max_length=128, pattern=r"^[A-Za-z][A-Za-z0-9_.:-]*$")
501
+ retrieval_id: Optional[str] = Field(None, min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$")
502
+ memory_evidence: List[MemoryEvidenceRequest] = Field(default_factory=list, max_length=100)
503
+
504
+ @field_validator("timestamp")
505
+ @classmethod
506
+ def timestamp_must_be_timezone_aware(cls, value: datetime.datetime) -> datetime.datetime:
507
+ if value.tzinfo is None or value.utcoffset() is None:
508
+ raise ValueError("timestamp must include a timezone")
509
+ return value
510
+
511
+ @model_validator(mode="after")
512
+ def retrieval_metadata_must_match_event_type(self):
513
+ if self.event_type is RunEventType.MEMORY_RETRIEVAL:
514
+ if not self.retrieval_id:
515
+ raise ValueError("memory.retrieval requires retrieval_id")
516
+ elif self.retrieval_id is not None or self.memory_evidence:
517
+ raise ValueError("retrieval metadata is only valid for memory.retrieval")
518
+ return self
519
+
520
+
458
521
  class FeedbackRequest(SynapseAPIModel):
459
522
  memory_id: str = Field(..., min_length=1, max_length=256, description="Memory node ID")
460
523
  feedback_type: str = Field(..., pattern=r"^(positive|negative)$", description="Must be either 'positive' (boost) or 'negative' (hallucination alert)")
@@ -791,6 +854,137 @@ def query_memory(
791
854
  )
792
855
 
793
856
 
857
+ @app.post("/api/agent-runs", tags=["Agent Reliability"])
858
+ def create_agent_run(
859
+ request: Request,
860
+ payload: RunCreateRequest,
861
+ principal: APIKeyRecord = Depends(require_scope("agent-runs:write")),
862
+ ):
863
+ """Create a tenant-bound run before streaming events (or before manual upload)."""
864
+ effective_tenant = resolve_tenant(principal, payload.tenant_id)
865
+ try:
866
+ result = run_trace_store.create_run(
867
+ effective_tenant, payload.run_id, schema_version=payload.schema_version,
868
+ )
869
+ except RunTraceError as exc:
870
+ raise HTTPException(status_code=503, detail="Agent trace storage unavailable.") from exc
871
+ except ValueError as exc:
872
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
873
+ return envelope(result, request)
874
+
875
+
876
+ @app.post("/api/agent-runs/{run_id}/events", tags=["Agent Reliability"])
877
+ def append_agent_run_event(
878
+ request: Request,
879
+ payload: AgentRunEventRequest,
880
+ run_id: str = Path(..., min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$"),
881
+ tenant_id: Optional[str] = None,
882
+ principal: APIKeyRecord = Depends(require_scope("agent-runs:write")),
883
+ ):
884
+ """Append one versioned event; retries with the same event ID are idempotent."""
885
+ if payload.run_id != run_id:
886
+ raise HTTPException(status_code=422, detail="event run_id does not match the path run_id.")
887
+ effective_tenant = resolve_tenant(principal, tenant_id)
888
+ event = payload.model_dump(exclude_none=True)
889
+ event["event_type"] = payload.event_type.value
890
+ event["run_status"] = payload.run_status.value
891
+ event["memory_evidence"] = [
892
+ item.model_dump(exclude_none=True) for item in payload.memory_evidence
893
+ ]
894
+ try:
895
+ result = run_trace_store.append_event(effective_tenant, run_id, event)
896
+ except RunNotFoundError as exc:
897
+ raise HTTPException(status_code=404, detail="Run not found for the authenticated tenant.") from exc
898
+ except RunConflictError as exc:
899
+ raise HTTPException(status_code=409, detail=str(exc)) from exc
900
+ except RunTraceError as exc:
901
+ raise HTTPException(status_code=503, detail="Agent trace storage unavailable.") from exc
902
+ except (ValueError, TypeError) as exc:
903
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
904
+ return envelope(result, request)
905
+
906
+
907
+ @app.get("/api/agent-runs", tags=["Agent Reliability"])
908
+ def list_agent_runs(
909
+ request: Request,
910
+ tenant_id: Optional[str] = None,
911
+ limit: int = 50,
912
+ offset: int = 0,
913
+ principal: APIKeyRecord = Depends(require_scope("agent-runs:read")),
914
+ ):
915
+ """List recent run summaries for the authenticated tenant."""
916
+ effective_tenant = resolve_tenant(principal, tenant_id)
917
+ try:
918
+ runs = run_trace_store.list_runs(effective_tenant, limit=limit, offset=offset)
919
+ except RunTraceError as exc:
920
+ raise HTTPException(status_code=503, detail="Agent trace storage unavailable.") from exc
921
+ except ValueError as exc:
922
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
923
+ return envelope({"runs": runs, "limit": limit, "offset": offset}, request)
924
+
925
+
926
+ @app.get("/api/agent-runs/{run_id}", tags=["Agent Reliability"])
927
+ def get_agent_run(
928
+ request: Request,
929
+ run_id: str = Path(..., min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$"),
930
+ tenant_id: Optional[str] = None,
931
+ principal: APIKeyRecord = Depends(require_scope("agent-runs:read")),
932
+ ):
933
+ """Fetch one run summary without returning event payloads by default."""
934
+ effective_tenant = resolve_tenant(principal, tenant_id)
935
+ try:
936
+ result = run_trace_store.get_run(effective_tenant, run_id)
937
+ except RunNotFoundError as exc:
938
+ raise HTTPException(status_code=404, detail="Run not found for the authenticated tenant.") from exc
939
+ except RunTraceError as exc:
940
+ raise HTTPException(status_code=503, detail="Agent trace storage unavailable.") from exc
941
+ except ValueError as exc:
942
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
943
+ return envelope(result, request)
944
+
945
+
946
+ @app.get("/api/agent-runs/{run_id}/events", tags=["Agent Reliability"])
947
+ def list_agent_run_events(
948
+ request: Request,
949
+ run_id: str = Path(..., min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$"),
950
+ tenant_id: Optional[str] = None,
951
+ limit: int = 100,
952
+ offset: int = 0,
953
+ principal: APIKeyRecord = Depends(require_scope("agent-runs:read")),
954
+ ):
955
+ """Read a bounded, chronological metadata-only event page."""
956
+ effective_tenant = resolve_tenant(principal, tenant_id)
957
+ try:
958
+ events = run_trace_store.list_events(
959
+ effective_tenant, run_id, limit=limit, offset=offset,
960
+ )
961
+ except RunNotFoundError as exc:
962
+ raise HTTPException(status_code=404, detail="Run not found for the authenticated tenant.") from exc
963
+ except RunTraceError as exc:
964
+ raise HTTPException(status_code=503, detail="Agent trace storage unavailable.") from exc
965
+ except ValueError as exc:
966
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
967
+ return envelope({"run_id": run_id, "events": events, "limit": limit, "offset": offset}, request)
968
+
969
+
970
+ @app.delete("/api/agent-runs/{run_id}", tags=["Agent Reliability"])
971
+ def delete_agent_run(
972
+ request: Request,
973
+ run_id: str = Path(..., min_length=1, max_length=128, pattern=r"^[A-Za-z0-9._:-]+$"),
974
+ tenant_id: Optional[str] = None,
975
+ principal: APIKeyRecord = Depends(require_scope("agent-runs:delete")),
976
+ ):
977
+ """Delete a run and its events for data lifecycle/retention workflows."""
978
+ effective_tenant = resolve_tenant(principal, tenant_id)
979
+ try:
980
+ deleted = run_trace_store.delete_run(effective_tenant, run_id)
981
+ except RunTraceError as exc:
982
+ raise HTTPException(status_code=503, detail="Agent trace storage unavailable.") from exc
983
+ except ValueError as exc:
984
+ raise HTTPException(status_code=422, detail=str(exc)) from exc
985
+ return envelope({"run_id": run_id, "status": "DELETED" if deleted else "NOT_FOUND"}, request)
986
+
987
+
794
988
  @app.post("/feedback", tags=["Reinforcement"])
795
989
  def submit_feedback(
796
990
  request: Request,
@@ -7,6 +7,7 @@ retryable (429/5xx) failures, and the canonical {success, data} envelope.
7
7
  from __future__ import annotations
8
8
 
9
9
  import time
10
+ import re
10
11
  from email.utils import parsedate_to_datetime
11
12
  from datetime import datetime, timezone
12
13
  from dataclasses import dataclass
@@ -16,7 +17,7 @@ import requests
16
17
 
17
18
  __all__ = ["SynapseClient", "SynapseError", "CreateResult", "QueryResult"]
18
19
 
19
- SDK_VERSION = "1.3.1"
20
+ SDK_VERSION = "1.4.0"
20
21
  API_COMPAT_VERSION = "v1"
21
22
 
22
23
 
@@ -193,6 +194,55 @@ class SynapseClient:
193
194
  def feedback(self, memory_id: str, feedback_type: str) -> Dict[str, Any]:
194
195
  return self._request("POST", "/feedback", {"memory_id": memory_id, "feedback_type": feedback_type})
195
196
 
197
+ def create_agent_run(self, run_id: str) -> Dict[str, Any]:
198
+ """Create (idempotently) a tenant-bound persistent run trace."""
199
+ if not isinstance(run_id, str) or not re.fullmatch(r"[A-Za-z0-9._:-]{1,128}", run_id):
200
+ raise ValueError("run_id must match [A-Za-z0-9._:-] and be 1–128 characters")
201
+ return self._request(
202
+ "POST", "/api/agent-runs", {"run_id": run_id, "schema_version": 1}, retry_safe=True
203
+ )
204
+
205
+ def append_agent_run_event(self, run_id: str, event: Any) -> Dict[str, Any]:
206
+ """Append one versioned event. Stable event IDs make retry safe."""
207
+ if not isinstance(run_id, str) or not re.fullmatch(r"[A-Za-z0-9._:-]{1,128}", run_id):
208
+ raise ValueError("run_id must match [A-Za-z0-9._:-] and be 1–128 characters")
209
+ if hasattr(event, "to_dict") and callable(event.to_dict):
210
+ payload = event.to_dict()
211
+ elif isinstance(event, dict):
212
+ payload = dict(event)
213
+ else:
214
+ raise TypeError("event must be a RunEvent or JSON-compatible event dictionary")
215
+ if payload.get("run_id") != run_id:
216
+ raise ValueError("event run_id does not match run_id")
217
+ if not isinstance(payload.get("event_id"), str) or not payload["event_id"]:
218
+ raise ValueError("event must include a stable event_id")
219
+ return self._request(
220
+ "POST", f"/api/agent-runs/{run_id}/events", payload, retry_safe=True
221
+ )
222
+
223
+ def upload_agent_run(self, run: Any) -> Dict[str, Any]:
224
+ """Persist an in-memory SynapseRun after it finishes.
225
+
226
+ The API appends events in the run's original order. Re-uploading the same
227
+ run is idempotent because create-run and event IDs are stable. A partial
228
+ upload can be retried by calling this method again.
229
+ """
230
+ run_id = getattr(run, "run_id", None)
231
+ events = getattr(run, "events", None)
232
+ if not isinstance(run_id, str) or not run_id or events is None:
233
+ raise TypeError("run must expose run_id and events (for example, SynapseRun)")
234
+ summary = self.create_agent_run(run_id)
235
+ uploaded = []
236
+ for event in events:
237
+ uploaded.append(self.append_agent_run_event(run_id, event))
238
+ return {
239
+ "run": summary,
240
+ "events_submitted": len(uploaded),
241
+ "events_inserted": sum(1 for result in uploaded if not result.get("duplicate", False)),
242
+ "events_deduplicated": sum(1 for result in uploaded if result.get("duplicate", False)),
243
+ "last_sequence_number": uploaded[-1]["sequence_number"] if uploaded else 0,
244
+ }
245
+
196
246
  def health(self) -> Dict[str, Any]:
197
247
  return self._request("GET", "/health")
198
248
 
@@ -98,6 +98,9 @@ _KNOWN_SCOPES = frozenset({
98
98
  "memories:write",
99
99
  "memories:delete",
100
100
  "memories:list",
101
+ "agent-runs:read",
102
+ "agent-runs:write",
103
+ "agent-runs:delete",
101
104
  })
102
105
 
103
106
 
@@ -1,5 +1,6 @@
1
1
  from typing import List, Dict, Any, Optional
2
2
  import logging
3
+ import math
3
4
 
4
5
  logger = logging.getLogger("KnapsackPacker")
5
6
 
@@ -62,9 +63,18 @@ class KnapsackPacker:
62
63
 
63
64
  n = len(memories)
64
65
  weights = [max(1, int(m.get("token_cost", 1))) for m in memories]
65
- values = [max(0, int(float(m.get("relevance_score", 0.0)) * 1000)) for m in memories]
66
-
67
- dp = [0] * (budget + 1)
66
+ values: List[float] = []
67
+ for memory in memories:
68
+ score = float(memory.get("relevance_score", 0.0))
69
+ # Non-finite values are not useful selection signals. Treat them as
70
+ # zero rather than allowing NaN/inf to poison DP comparisons.
71
+ values.append(max(0.0, score) if math.isfinite(score) else 0.0)
72
+
73
+ # Preserve score precision. Hybrid RRF scores are commonly around
74
+ # 0.01-0.03, with rank-to-rank differences below 0.001. Converting to
75
+ # int(score * 1000) collapses distinct scores into ties (or zero), so
76
+ # the chosen set can change when every score is rescaled.
77
+ dp = [0.0] * (budget + 1)
68
78
  items_included: List[List[int]] = [[] for _ in range(budget + 1)]
69
79
 
70
80
  for i in range(n):