pycontextdb 0.4.0__tar.gz → 0.4.1__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.
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/CHANGELOG.md +7 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/PKG-INFO +1 -1
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/__init__.py +1 -1
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/client.py +48 -17
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/dynamics/trust.py +31 -7
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/docs/memory_evolution.md +17 -6
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/pyproject.toml +1 -1
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_memory_evolution.py +184 -1
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/.github/CODEOWNERS +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/.github/copilot-instructions.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/.github/pull_request_template.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/.github/workflows/ci.yml +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/.github/workflows/publish.yml +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/.gitignore +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/.pre-commit-config.yaml +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/COMMERCIAL.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/CONTRIBUTING.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/LICENSE +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/NOTICE +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/OPEN_CORE.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/README.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/ai-tools/.cursorrules +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/ai-tools/CLAUDE.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/ai-tools/README.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/ai-tools/llms-full.txt +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/ai-tools/llms.txt +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/assets/README.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/assets/architecture.png +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/assets/benchmarks.png +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/assets/hero.png +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/assets/performance.png +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/assets/problem.png +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/assets/quickstart.png +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/benchmarks/__init__.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/benchmarks/run_benchmarks.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/benchmarks/trust_bakeoff.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/agents/__init__.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/agents/memory_bus.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/agents/rl_manager.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/cli.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/connectors/__init__.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/connectors/base.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/connectors/postgres.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/core/__init__.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/core/clock.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/core/config.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/core/exceptions.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/core/models.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/core/policy.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/core/slots.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/dynamics/__init__.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/dynamics/evolution.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/dynamics/formation.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/dynamics/retrieval.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/dynamics/salience.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/graphs/__init__.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/graphs/base.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/graphs/causal.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/graphs/entity.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/graphs/semantic.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/graphs/temporal.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/integrations/__init__.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/integrations/act.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/integrations/autogen.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/integrations/crewai.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/integrations/langchain.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/integrations/livekit.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/integrations/openai_tools.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/integrations/pipecat.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/integrations/prompting.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/mcp.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/memory/__init__.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/memory/experiential.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/memory/factual.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/memory/working.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/pool.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/privacy/__init__.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/privacy/audit.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/privacy/injection.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/privacy/pii_detector.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/privacy/retention.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/py.typed +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/serve.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/store/__init__.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/store/base.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/store/factory.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/store/pg_sql.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/store/postgres_store.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/store/sqlite_store.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/store/vector_index.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/utils/__init__.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/utils/embeddings.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/utils/llm.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/contextdb/utils/migrations.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/docs/api.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/docs/architecture.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/docs/connectors.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/docs/index.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/docs/migrations.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/docs/multi_tenant.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/docs/postgres.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/docs/prompts.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/docs/quickstart.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/docs/serve.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/docs/trust_policy.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/examples/customer_support.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/examples/multi_agent_team.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/examples/phone_agent.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/examples/research_assistant.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/legal/CLA-entity.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/legal/CLA-individual.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/legal/signed/README.md +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/mkdocs.yml +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/scripts/check_open_core_boundary.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/scripts/check_release.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/__init__.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/conftest.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/evals/__init__.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/evals/test_trust_model.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/integration/__init__.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/integration/test_client.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/integration/test_langchain_integration.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/pg_util.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/test_version.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/__init__.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_audit.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_config.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_connectors.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_decision_scope.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_embeddings.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_evolution.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_formation.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_graphs.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_host_api.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_init.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_memory_bus.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_models.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_open_core_boundary.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_pg_sql.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_pii_detector.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_postgres_concurrency.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_postgres_connector.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_postgres_memory_evolution.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_retention.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_retrieval.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_scoped_id_access.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_serve.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_store.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_trust_policy.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_vector_index.py +0 -0
- {pycontextdb-0.4.0 → pycontextdb-0.4.1}/tests/unit/test_vector_scope.py +0 -0
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.4.1 — 2026-08-24
|
|
6
|
+
|
|
7
|
+
- NOOP audit entries now carry the existing memory ID and canonical slot
|
|
8
|
+
identifiers when a duplicate, explicit target, or unambiguous slot resolves
|
|
9
|
+
to a current memory. Ambiguous slot-only NOOPs fail closed, while all NOOPs
|
|
10
|
+
remain content-free and leave the memory version unchanged.
|
|
11
|
+
|
|
5
12
|
## 0.4.0 — 2026-08-23
|
|
6
13
|
|
|
7
14
|
- Explicit deterministic `add` / `update` / `delete` / `noop` memory
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pycontextdb
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: The unified context layer for AI agents — memory you can act on without treating wishes as facts
|
|
5
5
|
Project-URL: Homepage, https://github.com/atomsai/contextdb
|
|
6
6
|
Project-URL: Documentation, https://contextdb.dev
|
|
@@ -639,13 +639,26 @@ class ContextDB:
|
|
|
639
639
|
attribute=attribute,
|
|
640
640
|
)
|
|
641
641
|
if self._audit is not None:
|
|
642
|
+
details: dict[str, object] = {
|
|
643
|
+
"operation": EvolutionOperation.NOOP.value,
|
|
644
|
+
"reason": safe_reason,
|
|
645
|
+
}
|
|
646
|
+
verified_slot = (
|
|
647
|
+
canonicalize_slot(
|
|
648
|
+
verified.entity_key,
|
|
649
|
+
verified.attribute_key,
|
|
650
|
+
)
|
|
651
|
+
if verified is not None
|
|
652
|
+
else None
|
|
653
|
+
)
|
|
654
|
+
if verified_slot is not None:
|
|
655
|
+
details["entity"] = verified_slot.entity
|
|
656
|
+
details["attribute"] = verified_slot.attribute
|
|
642
657
|
await self._audit.log(
|
|
643
658
|
operation="NOOP",
|
|
659
|
+
memory_id=verified.id if verified is not None else None,
|
|
644
660
|
user_id=uid,
|
|
645
|
-
details=
|
|
646
|
-
"operation": EvolutionOperation.NOOP.value,
|
|
647
|
-
"reason": safe_reason,
|
|
648
|
-
},
|
|
661
|
+
details=details,
|
|
649
662
|
)
|
|
650
663
|
token = await store.consistency_token()
|
|
651
664
|
return MemoryEvolutionResult(
|
|
@@ -890,12 +903,20 @@ class ContextDB:
|
|
|
890
903
|
target = await self._evolution_target(
|
|
891
904
|
target_memory_id,
|
|
892
905
|
user_id=user_id,
|
|
893
|
-
require_current=
|
|
906
|
+
require_current=True,
|
|
894
907
|
)
|
|
895
908
|
|
|
896
909
|
slot = canonicalize_slot(entity, attribute)
|
|
897
|
-
|
|
898
|
-
|
|
910
|
+
has_slot_reference = bool(entity and entity.strip()) and bool(
|
|
911
|
+
attribute and attribute.strip()
|
|
912
|
+
)
|
|
913
|
+
if has_slot_reference and slot is None:
|
|
914
|
+
raise EvolutionTargetRequiredError(
|
|
915
|
+
"NOOP entity/attribute must resolve to a slot"
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
if target is not None:
|
|
919
|
+
if slot is not None:
|
|
899
920
|
target_slot = canonicalize_slot(
|
|
900
921
|
target.entity_key,
|
|
901
922
|
target.attribute_key,
|
|
@@ -907,17 +928,27 @@ class ContextDB:
|
|
|
907
928
|
raise EvolutionOperationConflictError(
|
|
908
929
|
"target memory and requested entity/attribute identify different slots"
|
|
909
930
|
)
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
931
|
+
return target
|
|
932
|
+
|
|
933
|
+
if slot is None:
|
|
934
|
+
return None
|
|
935
|
+
|
|
936
|
+
rows = await self._require_store().list_by_slot(
|
|
937
|
+
slot.entity,
|
|
938
|
+
slot.attribute,
|
|
939
|
+
user_id=user_id,
|
|
940
|
+
)
|
|
941
|
+
current_at = self.clock()
|
|
942
|
+
current = [row for row in rows if row.is_valid_at(current_at)]
|
|
943
|
+
if not current:
|
|
944
|
+
raise EvolutionTargetNotFoundError(
|
|
945
|
+
f"{slot.entity}/{slot.attribute}"
|
|
914
946
|
)
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
return target
|
|
947
|
+
if len(current) != 1:
|
|
948
|
+
raise EvolutionOperationConflictError(
|
|
949
|
+
"slot NOOP is ambiguous; pass target_memory_id"
|
|
950
|
+
)
|
|
951
|
+
return current[0]
|
|
921
952
|
|
|
922
953
|
async def _evolve_delete_transactional(
|
|
923
954
|
self,
|
|
@@ -225,8 +225,11 @@ class TrustEngine:
|
|
|
225
225
|
speakers = list(candidate.corroborated_by)
|
|
226
226
|
if incoming_speaker in speakers:
|
|
227
227
|
await self._log_noop(
|
|
228
|
+
candidate.id,
|
|
228
229
|
user_id,
|
|
229
230
|
reason="same_speaker_same_value",
|
|
231
|
+
entity=item.entity_key,
|
|
232
|
+
attribute=item.attribute_key,
|
|
230
233
|
)
|
|
231
234
|
return candidate, "noop"
|
|
232
235
|
speakers.append(incoming_speaker)
|
|
@@ -495,7 +498,13 @@ class TrustEngine:
|
|
|
495
498
|
speakers = list(candidate.corroborated_by)
|
|
496
499
|
if incoming_speaker in speakers:
|
|
497
500
|
reason = "same_speaker_same_value"
|
|
498
|
-
await self._log_noop(
|
|
501
|
+
await self._log_noop(
|
|
502
|
+
candidate.id,
|
|
503
|
+
user_id,
|
|
504
|
+
reason=reason,
|
|
505
|
+
entity=slot.entity,
|
|
506
|
+
attribute=slot.attribute,
|
|
507
|
+
)
|
|
499
508
|
return EvolutionWrite(
|
|
500
509
|
applied_operation=EvolutionOperation.NOOP,
|
|
501
510
|
outcome=EvolutionOutcome.NOOP,
|
|
@@ -543,7 +552,13 @@ class TrustEngine:
|
|
|
543
552
|
speakers = list(candidate.corroborated_by)
|
|
544
553
|
if incoming_speaker in speakers:
|
|
545
554
|
reason = "same_speaker_same_value"
|
|
546
|
-
await self._log_noop(
|
|
555
|
+
await self._log_noop(
|
|
556
|
+
candidate.id,
|
|
557
|
+
user_id,
|
|
558
|
+
reason=reason,
|
|
559
|
+
entity=slot.entity,
|
|
560
|
+
attribute=slot.attribute,
|
|
561
|
+
)
|
|
547
562
|
return EvolutionWrite(
|
|
548
563
|
applied_operation=EvolutionOperation.NOOP,
|
|
549
564
|
outcome=EvolutionOutcome.NOOP,
|
|
@@ -768,17 +783,26 @@ class TrustEngine:
|
|
|
768
783
|
|
|
769
784
|
async def _log_noop(
|
|
770
785
|
self,
|
|
786
|
+
memory_id: str,
|
|
771
787
|
user_id: str | None,
|
|
772
788
|
*,
|
|
773
789
|
reason: str,
|
|
790
|
+
entity: str | None,
|
|
791
|
+
attribute: str | None,
|
|
774
792
|
) -> None:
|
|
775
|
-
"""Append
|
|
793
|
+
"""Append scoped, content-free NOOP lineage without mutating memory."""
|
|
776
794
|
if self.audit is not None:
|
|
795
|
+
details: dict[str, object] = {
|
|
796
|
+
"operation": EvolutionOperation.NOOP.value,
|
|
797
|
+
"reason": reason,
|
|
798
|
+
}
|
|
799
|
+
slot = canonicalize_slot(entity, attribute)
|
|
800
|
+
if slot is not None:
|
|
801
|
+
details["entity"] = slot.entity
|
|
802
|
+
details["attribute"] = slot.attribute
|
|
777
803
|
await self.audit.log(
|
|
778
804
|
operation="NOOP",
|
|
805
|
+
memory_id=memory_id,
|
|
779
806
|
user_id=user_id,
|
|
780
|
-
details=
|
|
781
|
-
"operation": EvolutionOperation.NOOP.value,
|
|
782
|
-
"reason": reason,
|
|
783
|
-
},
|
|
807
|
+
details=details,
|
|
784
808
|
)
|
|
@@ -33,9 +33,10 @@ canonicalized, and injection-shaped content is demoted.
|
|
|
33
33
|
|
|
34
34
|
For an empty slot, ADD stores a new memory. If the same speaker repeats the
|
|
35
35
|
same canonical slot value, the applied operation is NOOP: no row is inserted
|
|
36
|
-
or updated and the memory version does not advance. The audit entry
|
|
37
|
-
only the fixed operation and reason
|
|
38
|
-
|
|
36
|
+
or updated and the memory version does not advance. The audit entry points to
|
|
37
|
+
the existing memory and contains only the fixed operation and reason plus its
|
|
38
|
+
canonical entity and attribute identifiers. A new independent speaker
|
|
39
|
+
asserting the same value corroborates the current memory and applies as UPDATE.
|
|
39
40
|
|
|
40
41
|
ADD fails with `EvolutionOperationConflictError` when the slot already holds a
|
|
41
42
|
different value. Use UPDATE to correct an occupied slot.
|
|
@@ -83,9 +84,19 @@ the broader `forget_user()` and `verify_forgotten()` contracts.
|
|
|
83
84
|
|
|
84
85
|
## NOOP
|
|
85
86
|
|
|
86
|
-
NOOP requires a nonempty reason of at most 256 characters.
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
NOOP requires a nonempty reason of at most 256 characters. An optional
|
|
88
|
+
`target_memory_id` must resolve to a current memory in the caller's scope. A
|
|
89
|
+
slot-only reference resolves only when exactly one current occupant exists;
|
|
90
|
+
an ambiguous contested slot raises `EvolutionOperationConflictError` and
|
|
91
|
+
requires an explicit target. The resolved memory is returned and its ID is
|
|
92
|
+
attached to the audit entry. A NOOP without a target or slot remains valid,
|
|
93
|
+
returns no memory, and has no audit `memory_id`.
|
|
94
|
+
|
|
95
|
+
The reason is passed through the configured PII processor and bounded to 256
|
|
96
|
+
characters before the content-free `NOOP` audit entry is appended. When a
|
|
97
|
+
memory resolves to a slot, the details include only the operation, processed
|
|
98
|
+
reason, and canonical entity and attribute identifiers. They never include
|
|
99
|
+
memory content, old or new values, raw user IDs, or PII annotation originals.
|
|
89
100
|
|
|
90
101
|
NOOP does not change memory rows, vectors, or the memory version. Its result
|
|
91
102
|
token therefore reports the same memory version visible before the operation
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pycontextdb"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "The unified context layer for AI agents — memory you can act on without treating wishes as facts"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -181,10 +181,12 @@ async def test_explicit_add_same_speaker_is_true_noop(
|
|
|
181
181
|
assert [row.id for row in rows_after] == [row.id for row in rows_before]
|
|
182
182
|
assert client.audit is not None
|
|
183
183
|
noop = [entry for entry in await client.audit.get_history() if entry.operation == "NOOP"][-1]
|
|
184
|
-
assert noop.memory_id
|
|
184
|
+
assert noop.memory_id == first.memory.id
|
|
185
185
|
assert noop.details == {
|
|
186
186
|
"operation": "noop",
|
|
187
187
|
"reason": "same_speaker_same_value",
|
|
188
|
+
"entity": "profile",
|
|
189
|
+
"attribute": "color",
|
|
188
190
|
}
|
|
189
191
|
|
|
190
192
|
|
|
@@ -374,6 +376,174 @@ async def test_target_ids_are_scoped_and_missing_is_indistinguishable(
|
|
|
374
376
|
)
|
|
375
377
|
assert await client._require_store().get_raw(alice.memory.id) is not None
|
|
376
378
|
|
|
379
|
+
version = (await client.consistency_token()).memory_version
|
|
380
|
+
for target in (alice.memory.id, "missing-memory"):
|
|
381
|
+
with pytest.raises(EvolutionTargetNotFoundError):
|
|
382
|
+
await client.factual.evolve(
|
|
383
|
+
"noop",
|
|
384
|
+
target_memory_id=target,
|
|
385
|
+
noop_reason="already checked",
|
|
386
|
+
user_id="bob",
|
|
387
|
+
)
|
|
388
|
+
assert (await client.consistency_token()).memory_version == version
|
|
389
|
+
assert client.audit is not None
|
|
390
|
+
assert not [
|
|
391
|
+
entry
|
|
392
|
+
for entry in await client.audit.get_history(user_id="bob")
|
|
393
|
+
if entry.operation == "NOOP"
|
|
394
|
+
]
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
async def test_targeted_noop_returns_lineage_without_private_audit_values(
|
|
398
|
+
client: ContextDB,
|
|
399
|
+
) -> None:
|
|
400
|
+
raw_content = "The profile color is blue for SSN 123-45-6789"
|
|
401
|
+
raw_user_id = "alice-private-user"
|
|
402
|
+
added = await client.factual.evolve(
|
|
403
|
+
"add",
|
|
404
|
+
raw_content,
|
|
405
|
+
source="user_stated",
|
|
406
|
+
entity="Profile",
|
|
407
|
+
attribute="Color",
|
|
408
|
+
user_id=raw_user_id,
|
|
409
|
+
)
|
|
410
|
+
assert added.memory is not None
|
|
411
|
+
assert added.memory.pii_annotations
|
|
412
|
+
version = added.consistency_token.memory_version
|
|
413
|
+
|
|
414
|
+
result = await client.factual.evolve(
|
|
415
|
+
"noop",
|
|
416
|
+
target_memory_id=added.memory.id,
|
|
417
|
+
user_id=raw_user_id,
|
|
418
|
+
noop_reason="duplicate reported by reviewer@example.com",
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
assert result.memory is not None
|
|
422
|
+
assert result.memory.id == added.memory.id
|
|
423
|
+
assert result.consistency_token.memory_version == version
|
|
424
|
+
assert result.noop_reason == "duplicate reported by [EMAIL]"
|
|
425
|
+
assert client.audit is not None
|
|
426
|
+
entry = [
|
|
427
|
+
entry
|
|
428
|
+
for entry in await client.audit.get_history(memory_id=added.memory.id)
|
|
429
|
+
if entry.operation == "NOOP"
|
|
430
|
+
][-1]
|
|
431
|
+
assert entry.memory_id == added.memory.id
|
|
432
|
+
assert entry.details == {
|
|
433
|
+
"operation": "noop",
|
|
434
|
+
"reason": "duplicate reported by [EMAIL]",
|
|
435
|
+
"entity": "profile",
|
|
436
|
+
"attribute": "color",
|
|
437
|
+
}
|
|
438
|
+
serialized_details = str(entry.details)
|
|
439
|
+
for private_value in (
|
|
440
|
+
raw_content,
|
|
441
|
+
"123-45-6789",
|
|
442
|
+
"reviewer@example.com",
|
|
443
|
+
raw_user_id,
|
|
444
|
+
added.memory.content,
|
|
445
|
+
added.memory.pii_annotations[0].original,
|
|
446
|
+
):
|
|
447
|
+
assert private_value not in serialized_details
|
|
448
|
+
assert {"content", "old_value", "new_value", "user_id", "pii_annotations"}.isdisjoint(
|
|
449
|
+
entry.details
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
async def test_slot_noop_returns_unambiguous_current_memory(
|
|
454
|
+
client: ContextDB,
|
|
455
|
+
) -> None:
|
|
456
|
+
added = await client.factual.evolve(
|
|
457
|
+
"add",
|
|
458
|
+
"The user has a peanut allergy",
|
|
459
|
+
source="user_stated",
|
|
460
|
+
entity="Customer",
|
|
461
|
+
attribute="Allergies",
|
|
462
|
+
user_id="alice",
|
|
463
|
+
)
|
|
464
|
+
assert added.memory is not None
|
|
465
|
+
version = added.consistency_token.memory_version
|
|
466
|
+
|
|
467
|
+
result = await client.factual.evolve(
|
|
468
|
+
"noop",
|
|
469
|
+
entity="CUSTOMER",
|
|
470
|
+
attribute="ALLERGIES",
|
|
471
|
+
user_id="alice",
|
|
472
|
+
noop_reason="already verified",
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
assert result.memory is not None
|
|
476
|
+
assert result.memory.id == added.memory.id
|
|
477
|
+
assert result.consistency_token.memory_version == version
|
|
478
|
+
assert client.audit is not None
|
|
479
|
+
entry = [
|
|
480
|
+
entry
|
|
481
|
+
for entry in await client.audit.get_history(memory_id=added.memory.id)
|
|
482
|
+
if entry.operation == "NOOP"
|
|
483
|
+
][-1]
|
|
484
|
+
assert entry.details == {
|
|
485
|
+
"operation": "noop",
|
|
486
|
+
"reason": "already verified",
|
|
487
|
+
"entity": "user",
|
|
488
|
+
"attribute": "allergy",
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
async def test_slot_noop_rejects_ambiguous_contested_slot(
|
|
493
|
+
client: ContextDB,
|
|
494
|
+
) -> None:
|
|
495
|
+
first = await client.factual.evolve(
|
|
496
|
+
"add",
|
|
497
|
+
"The meeting is at 3pm",
|
|
498
|
+
source="user_stated",
|
|
499
|
+
entity="meeting",
|
|
500
|
+
attribute="time",
|
|
501
|
+
user_id="alice",
|
|
502
|
+
)
|
|
503
|
+
assert first.memory is not None
|
|
504
|
+
other = contextdb.init(
|
|
505
|
+
config=client.config,
|
|
506
|
+
session_id="independent-session",
|
|
507
|
+
)
|
|
508
|
+
try:
|
|
509
|
+
contested = await other.factual.evolve(
|
|
510
|
+
"update",
|
|
511
|
+
"The meeting is at 4pm",
|
|
512
|
+
source="user_stated",
|
|
513
|
+
entity="meeting",
|
|
514
|
+
attribute="time",
|
|
515
|
+
user_id="alice",
|
|
516
|
+
)
|
|
517
|
+
assert contested.outcome == EvolutionOutcome.CONTESTED
|
|
518
|
+
version = contested.consistency_token.memory_version
|
|
519
|
+
assert other.audit is not None
|
|
520
|
+
noops_before = [
|
|
521
|
+
entry
|
|
522
|
+
for entry in await other.audit.get_history(user_id="alice")
|
|
523
|
+
if entry.operation == "NOOP"
|
|
524
|
+
]
|
|
525
|
+
|
|
526
|
+
with pytest.raises(EvolutionOperationConflictError, match="ambiguous"):
|
|
527
|
+
await other.factual.evolve(
|
|
528
|
+
"noop",
|
|
529
|
+
entity="meeting",
|
|
530
|
+
attribute="time",
|
|
531
|
+
user_id="alice",
|
|
532
|
+
noop_reason="already handled",
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
assert (await other.consistency_token()).memory_version == version
|
|
536
|
+
noops_after = [
|
|
537
|
+
entry
|
|
538
|
+
for entry in await other.audit.get_history(user_id="alice")
|
|
539
|
+
if entry.operation == "NOOP"
|
|
540
|
+
]
|
|
541
|
+
assert [entry.id for entry in noops_after] == [
|
|
542
|
+
entry.id for entry in noops_before
|
|
543
|
+
]
|
|
544
|
+
finally:
|
|
545
|
+
await other.close()
|
|
546
|
+
|
|
377
547
|
|
|
378
548
|
async def test_explicit_noop_is_pii_safe_and_does_not_mutate_store(
|
|
379
549
|
client: ContextDB,
|
|
@@ -454,3 +624,16 @@ async def test_existing_add_return_type_and_duplicate_id_are_compatible(
|
|
|
454
624
|
assert isinstance(duplicate, MemoryItem)
|
|
455
625
|
assert duplicate.id == first.id
|
|
456
626
|
assert (await client.consistency_token()).memory_version == version
|
|
627
|
+
assert client.audit is not None
|
|
628
|
+
noop = [
|
|
629
|
+
entry
|
|
630
|
+
for entry in await client.audit.get_history(memory_id=first.id)
|
|
631
|
+
if entry.operation == "NOOP"
|
|
632
|
+
][-1]
|
|
633
|
+
assert noop.memory_id == first.id
|
|
634
|
+
assert noop.details == {
|
|
635
|
+
"operation": "noop",
|
|
636
|
+
"reason": "same_speaker_same_value",
|
|
637
|
+
"entity": "profile",
|
|
638
|
+
"attribute": "color",
|
|
639
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|