atomicmemory 1.0.1__tar.gz → 1.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.
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/.gitignore +6 -0
- atomicmemory-1.1.0/CHANGELOG.md +46 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/PKG-INFO +49 -4
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/README.md +47 -3
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/__init__.py +26 -0
- atomicmemory-1.1.0/atomicmemory/_version.py +7 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/client/async_memory_client.py +91 -11
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/client/memory_client.py +42 -11
- atomicmemory-1.1.0/atomicmemory/contract/__init__.py +18 -0
- atomicmemory-1.1.0/atomicmemory/contract/v1.py +400 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/memory/__init__.py +26 -0
- atomicmemory-1.1.0/atomicmemory/memory/capability_profiles.py +79 -0
- atomicmemory-1.1.0/atomicmemory/memory/meta_fact_filter.py +122 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/memory/registry.py +2 -2
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/memory/service.py +121 -20
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/memory/types.py +42 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/async_handle_impl.py +10 -7
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/async_provider.py +18 -4
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/config.py +7 -2
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/handle.py +2 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/handle_impl.py +28 -8
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/mappers.py +33 -1
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/provider.py +41 -5
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/scope_mapper.py +28 -5
- atomicmemory-1.1.0/contract/CONTRACT.md +114 -0
- atomicmemory-1.1.0/contract/VENDORED.json +8 -0
- atomicmemory-1.1.0/contract/v1/capabilities-descriptor.schema.json +8 -0
- atomicmemory-1.1.0/contract/v1/conformance/capabilities-descriptor.json +20 -0
- atomicmemory-1.1.0/contract/v1/conformance/ingest-text.json +18 -0
- atomicmemory-1.1.0/contract/v1/conformance/ingest-verbatim.json +19 -0
- atomicmemory-1.1.0/contract/v1/conformance/manifest.json +12 -0
- atomicmemory-1.1.0/contract/v1/conformance/search-with-retrieval-receipt.json +39 -0
- atomicmemory-1.1.0/contract/v1/ingest-input.schema.json +8 -0
- atomicmemory-1.1.0/contract/v1/provider-contract.schema.json +336 -0
- atomicmemory-1.1.0/contract/v1/search-result-page.schema.json +8 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/examples/async_pipeline.py +1 -1
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/examples/basic_ingest_search.py +2 -2
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/pyproject.toml +3 -1
- atomicmemory-1.1.0/tests/_lifecycle_fakes.py +153 -0
- atomicmemory-1.1.0/tests/client/test_client_lifecycle.py +312 -0
- atomicmemory-1.1.0/tests/contract/_schema_registry.py +66 -0
- atomicmemory-1.1.0/tests/contract/test_codec.py +207 -0
- atomicmemory-1.1.0/tests/contract/test_conformance.py +142 -0
- atomicmemory-1.1.0/tests/contract/test_vendored_tree.py +101 -0
- atomicmemory-1.1.0/tests/memory/test_capability_profiles.py +60 -0
- atomicmemory-1.1.0/tests/memory/test_meta_fact_filter.py +80 -0
- atomicmemory-1.1.0/tests/memory/test_service_lifecycle.py +188 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_async_provider.py +101 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_handle_base.py +98 -0
- atomicmemory-1.1.0/tests/providers/atomicmemory/test_ingest_content_class.py +50 -0
- atomicmemory-1.1.0/tests/providers/atomicmemory/test_integration.py +89 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_mappers.py +57 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_provider.py +90 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_scope_mapper.py +33 -0
- atomicmemory-1.1.0/tests/search/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/test_package_imports.py +1 -1
- atomicmemory-1.1.0/tests/test_version_consistency.py +26 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/uv.lock +445 -1
- atomicmemory-1.0.1/CHANGELOG.md +0 -25
- atomicmemory-1.0.1/atomicmemory/_version.py +0 -3
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/LICENSE +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/client/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/client/atomic_memory_client.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/core/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/core/errors.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/core/events.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/core/logging.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/core/retry.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/core/validation.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/embeddings/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/embeddings/base.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/embeddings/sentence_transformers.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/kv_cache/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/kv_cache/adapter.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/kv_cache/memory_storage.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/kv_cache/sqlite_storage.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/memory/filters.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/memory/pipeline.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/memory/provider.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/agents.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/audit.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/config_handle.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/http.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/lessons.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/lifecycle.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/path.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/hindsight/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/hindsight/async_provider.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/hindsight/config.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/hindsight/http.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/hindsight/mappers.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/hindsight/provider.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/mem0/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/mem0/async_provider.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/mem0/config.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/mem0/http.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/mem0/mappers.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/mem0/provider.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/py.typed +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/search/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/search/chunking.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/search/ranking.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/search/semantic_search.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/search/similarity.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/storage/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/storage/_mapping.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/storage/async_client.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/storage/client.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/storage/errors.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/storage/types.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/utils/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/utils/environment.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/examples/local_search.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/client/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/client/test_async_memory_client.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/client/test_atomic_memory_client.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/client/test_memory_client.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/conftest.py +0 -0
- {atomicmemory-1.0.1/tests/core → atomicmemory-1.1.0/tests/contract}/__init__.py +0 -0
- {atomicmemory-1.0.1/tests/embeddings → atomicmemory-1.1.0/tests/core}/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/core/test_errors.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/core/test_events.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/core/test_retry.py +0 -0
- {atomicmemory-1.0.1/tests/kv_cache → atomicmemory-1.1.0/tests/embeddings}/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/embeddings/test_sentence_transformers.py +0 -0
- {atomicmemory-1.0.1/tests/memory → atomicmemory-1.1.0/tests/kv_cache}/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/kv_cache/test_memory_storage.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/kv_cache/test_sqlite_storage.py +0 -0
- {atomicmemory-1.0.1/tests/providers → atomicmemory-1.1.0/tests/memory}/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/memory/test_filters.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/memory/test_provider_base.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/memory/test_registry.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/memory/test_service.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/memory/test_types.py +0 -0
- {atomicmemory-1.0.1/tests/providers/atomicmemory → atomicmemory-1.1.0/tests/providers}/__init__.py +0 -0
- {atomicmemory-1.0.1/tests/providers/mem0 → atomicmemory-1.1.0/tests/providers/atomicmemory}/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_handle_agents.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_handle_audit.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_handle_config.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_handle_lessons.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_handle_lifecycle.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_http.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_path.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/hindsight/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/hindsight/test_async_provider.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/hindsight/test_integration.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/hindsight/test_mappers.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/hindsight/test_provider.py +0 -0
- {atomicmemory-1.0.1/tests/search → atomicmemory-1.1.0/tests/providers/mem0}/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/mem0/test_mappers.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/mem0/test_provider.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/search/test_chunking.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/search/test_semantic_search.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/search/test_similarity.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/storage/__init__.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/storage/test_async_storage_client.py +0 -0
- {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/storage/test_storage_client.py +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `atomicmemory` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [1.1.0] - 2026-06-09
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `atomicmemory.contract.v1`: a wire codec for the v1 provider contract's deliberately mixed-case encoding (`Memory.createdAt`/`updatedAt` and `SearchResult.rankingScore` are camelCase on the wire; `version_id`, `observed_at`, and retrieval-receipt fields are snake_case). Encode/decode helpers cover `Memory`, `Provenance`, `SearchResult`, `SearchResultPage`, `SearchRequest`, and ingest payloads (`IngestInput`, `IngestResult`). Dates follow the contract's ISO-8601 UTC millisecond `Z` form (`_to_iso_z`, equivalent to TS `toISOString()`). Naive datetimes in encode paths are assumed UTC. `encode_ingest_input` fails closed on the Python-ahead `content_class` field (no place in the v1 `additionalProperties: false` schemas; TS contract alignment is a recorded follow-up). Explicit-null `version_id` in `SearchResult` normalizes to absent on re-encode, matching the TS optional declaration. `encode_search_request` uses `by_alias=True` so Python-keyword-safe combinator field names (`and_`/`or_`/`not_`) emit their wire aliases; a recursive `_jsonify` walk converts any `datetime` operands in filter trees to the toISOString form. In-process models and provider mappers are unchanged.
|
|
13
|
+
- Vendored the TS SDK's versioned v1 wire contract (JSON Schemas, cross-provider conformance corpus, and CONTRACT.md) under `contract/`, with explicit provenance in `contract/VENDORED.json` and a documented refresh script (`scripts/refresh_contract.py`, never run in CI). A pytest conformance harness proves corpus fixtures decode into the Python models (directly for snake-on-wire types, through the codec for the mixed-case search response) and that SDK emissions validate against the vendored draft-2020-12 schemas, with the TS suite's negative cases mirrored against both schemas and Pydantic. The `capabilities-descriptor` case is schema-only (no Python model in this release — recorded follow-up).
|
|
14
|
+
- `atomicmemory.contract` re-exports `v1` as a specialty import surface; deliberately not re-exported from the package root to keep the root namespace focused on the core provider API.
|
|
15
|
+
- `AsyncProviderFactory` now accepts factories that return an `Awaitable[AsyncProviderRegistration]`, enabling lazy or async provider construction during `AsyncMemoryService.initialize()`.
|
|
16
|
+
- `MemoryService.initialize()` and `AsyncMemoryService.initialize()` raise `ConfigError` when the configured default provider has no registered factory, making a misconfigured default an immediate, explicit error rather than a silent no-op.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `content_class` is now accepted on **every** ingest mode (`text`, `messages`, and `verbatim`), not just `verbatim`, and is forwarded to core for all modes. Extraction-based ingests (`text`/`messages`) can now satisfy a core running the default `RAW_CONTENT_POLICY=reject`. Still never defaulted — omitting it leaves the field off the wire and a reject-policy core fails closed.
|
|
20
|
+
- Both clients' `initialize()` is now concurrency-safe and idempotent: concurrent callers share a single initialization run (the first caller's registry wins), and the completed outcome — success or failure — is captured in loop-independent state for `AsyncMemoryClient`.
|
|
21
|
+
- A failed `initialize()` is sticky: retrying re-raises the original error from any caller; resolve the cause and construct a new client rather than retrying on the same instance.
|
|
22
|
+
- `AsyncMemoryClient.initialize()` shields each waiter from cancellation so that one waiter's timeout or cancellation never cancels the shared run for other concurrent callers.
|
|
23
|
+
- `AsyncMemoryClient.close()` during a pending initialization cancels the shared run; staged providers are torn down by the service's atomic-initialize cleanup, any concurrent `initialize()` waiter receives `CancelledError`, and the client ends in the not-initialized state without recording a sticky error.
|
|
24
|
+
- Both `MemoryService` and `AsyncMemoryService` stage provider registrations atomically: factories and provider `initialize()` calls run against a local staging area, and the maps are replaced only after every provider succeeds; on any failure, already-staged providers are torn down best-effort before the original error re-raises.
|
|
25
|
+
- `MemoryService.close()` and `AsyncMemoryService.close()` are best-effort: every provider gets a chance to close regardless of earlier failures, maps are cleared in a `finally` block, and the first failure is re-raised after all providers have been given the chance to close.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- `atomicmemory.__version__` reported `1.0.0` while package metadata said `1.0.1`; all version sources now agree at `1.1.0`, guarded by a regression test that will fail if they drift again.
|
|
29
|
+
|
|
30
|
+
## [1.0.1] - 2026-05-14
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- Version bump for public package publication after internal-to-public repository sync.
|
|
34
|
+
|
|
35
|
+
## [1.0.0]
|
|
36
|
+
|
|
37
|
+
Initial public stable release.
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
- `AtomicMemoryClient` and `AsyncAtomicMemoryClient` as the primary public client surfaces.
|
|
41
|
+
- Memory ingestion, search, package, get, list, and delete support.
|
|
42
|
+
- AtomicMemory, Mem0, and Hindsight provider adapters.
|
|
43
|
+
- Typed AtomicMemory namespace handles for lifecycle, audit, lessons, agents, and runtime config.
|
|
44
|
+
- Direct artifact storage client with pointer and managed artifact workflows.
|
|
45
|
+
- Local embedding, semantic search, and KV cache helpers.
|
|
46
|
+
- Pydantic models, typed exceptions, and `py.typed` marker for downstream type checkers.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: atomicmemory
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Python client SDK for AtomicMemory memory and artifact storage.
|
|
5
5
|
Project-URL: Homepage, https://github.com/atomicstrata/atomicmemory-python
|
|
6
6
|
Project-URL: Repository, https://github.com/atomicstrata/atomicmemory-python
|
|
@@ -25,6 +25,7 @@ Requires-Dist: httpx>=0.27
|
|
|
25
25
|
Requires-Dist: numpy>=1.26
|
|
26
26
|
Requires-Dist: pydantic>=2.7
|
|
27
27
|
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: jsonschema[format]>=4.21; extra == 'dev'
|
|
28
29
|
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
29
30
|
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
30
31
|
Requires-Dist: pytest-mock>=3.12; extra == 'dev'
|
|
@@ -70,13 +71,13 @@ pip install 'atomicmemory[embeddings]' # + sentence-transformers for local
|
|
|
70
71
|
|
|
71
72
|
## Quick start
|
|
72
73
|
|
|
73
|
-
Prerequisite: start `atomicmemory-core` first. Follow the [Core Quickstart](https://docs.atomicstrata.ai/quickstart) if you do not already have a backend at `http://localhost:
|
|
74
|
+
Prerequisite: start `atomicmemory-core` first. Follow the [Core Quickstart](https://docs.atomicstrata.ai/quickstart) if you do not already have a backend at `http://localhost:17350`.
|
|
74
75
|
|
|
75
76
|
```python
|
|
76
77
|
from atomicmemory import AtomicMemoryClient
|
|
77
78
|
|
|
78
79
|
with AtomicMemoryClient({
|
|
79
|
-
"apiUrl": "http://localhost:
|
|
80
|
+
"apiUrl": "http://localhost:17350",
|
|
80
81
|
"apiKey": "server-api-key",
|
|
81
82
|
"userId": "demo",
|
|
82
83
|
}) as client:
|
|
@@ -110,7 +111,7 @@ from atomicmemory import AsyncAtomicMemoryClient
|
|
|
110
111
|
|
|
111
112
|
async def main() -> None:
|
|
112
113
|
async with AsyncAtomicMemoryClient({
|
|
113
|
-
"apiUrl": "http://localhost:
|
|
114
|
+
"apiUrl": "http://localhost:17350",
|
|
114
115
|
"apiKey": "server-api-key",
|
|
115
116
|
"userId": "demo",
|
|
116
117
|
}) as client:
|
|
@@ -169,6 +170,50 @@ The `client.storage` namespace mirrors the TypeScript SDK's direct storage API:
|
|
|
169
170
|
|
|
170
171
|
Every storage request sends `Authorization: Bearer <apiKey>` and `X-AtomicMemory-User-Id`. The SDK never sends the legacy `?user_id=` URL parameter.
|
|
171
172
|
|
|
173
|
+
## v1 wire contract
|
|
174
|
+
|
|
175
|
+
`atomicmemory.contract.v1` is the wire codec for the v1 provider-contract encoding. The wire form is deliberately mixed-case — `Memory.createdAt`/`updatedAt` and `SearchResult.rankingScore` are camelCase; `version_id`, `observed_at`, and retrieval-receipt fields are snake_case — as pinned by the vendored `contract/CONTRACT.md`. This module is the only place that mapping lives; in-process models and provider mappers are unchanged.
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from atomicmemory.contract import v1
|
|
179
|
+
|
|
180
|
+
# decode a wire search response (e.g. from a cross-SDK provider call)
|
|
181
|
+
wire_page = {
|
|
182
|
+
"results": [
|
|
183
|
+
{
|
|
184
|
+
"memory": {
|
|
185
|
+
"id": "mem_1",
|
|
186
|
+
"content": "I prefer aisle seats on flights.",
|
|
187
|
+
"scope": {"user": "demo"},
|
|
188
|
+
"kind": "fact",
|
|
189
|
+
"createdAt": "2026-05-30T12:00:00.000Z",
|
|
190
|
+
},
|
|
191
|
+
"score": 0.91,
|
|
192
|
+
"rankingScore": 0.87,
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
"retrieval": {
|
|
196
|
+
"embedding_model": "text-embedding-x",
|
|
197
|
+
"embedding_model_version": "1",
|
|
198
|
+
"embedding_dimensions": 1536,
|
|
199
|
+
"query_text": "deploy gate",
|
|
200
|
+
"candidate_ids": ["mem_1"],
|
|
201
|
+
"trace_id": "trace-1",
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
page = v1.decode_search_result_page(wire_page)
|
|
206
|
+
for hit in page.results:
|
|
207
|
+
print(hit.memory.content, hit.score) # snake_case in-process models
|
|
208
|
+
|
|
209
|
+
# re-encode to the exact v1 wire form (millisecond-precision UTC datetimes)
|
|
210
|
+
wire_out = v1.encode_search_result_page(page)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Two behaviors to know: naive datetimes passed to encode functions are assumed UTC (bare `astimezone()` would shift by the host's UTC offset); `encode_ingest_input` rejects models carrying `content_class` with a clear error because the v1 schemas have `additionalProperties: false` and no such field — this is a Python-ahead field pending TS contract alignment.
|
|
214
|
+
|
|
215
|
+
This is NOT the AtomicMemory core HTTP API. That boundary stays in the provider mappers. The import path is `atomicmemory.contract` — deliberately not re-exported from the package root to keep the root namespace focused on the core provider API.
|
|
216
|
+
|
|
172
217
|
## Development
|
|
173
218
|
|
|
174
219
|
```bash
|
|
@@ -32,13 +32,13 @@ pip install 'atomicmemory[embeddings]' # + sentence-transformers for local
|
|
|
32
32
|
|
|
33
33
|
## Quick start
|
|
34
34
|
|
|
35
|
-
Prerequisite: start `atomicmemory-core` first. Follow the [Core Quickstart](https://docs.atomicstrata.ai/quickstart) if you do not already have a backend at `http://localhost:
|
|
35
|
+
Prerequisite: start `atomicmemory-core` first. Follow the [Core Quickstart](https://docs.atomicstrata.ai/quickstart) if you do not already have a backend at `http://localhost:17350`.
|
|
36
36
|
|
|
37
37
|
```python
|
|
38
38
|
from atomicmemory import AtomicMemoryClient
|
|
39
39
|
|
|
40
40
|
with AtomicMemoryClient({
|
|
41
|
-
"apiUrl": "http://localhost:
|
|
41
|
+
"apiUrl": "http://localhost:17350",
|
|
42
42
|
"apiKey": "server-api-key",
|
|
43
43
|
"userId": "demo",
|
|
44
44
|
}) as client:
|
|
@@ -72,7 +72,7 @@ from atomicmemory import AsyncAtomicMemoryClient
|
|
|
72
72
|
|
|
73
73
|
async def main() -> None:
|
|
74
74
|
async with AsyncAtomicMemoryClient({
|
|
75
|
-
"apiUrl": "http://localhost:
|
|
75
|
+
"apiUrl": "http://localhost:17350",
|
|
76
76
|
"apiKey": "server-api-key",
|
|
77
77
|
"userId": "demo",
|
|
78
78
|
}) as client:
|
|
@@ -131,6 +131,50 @@ The `client.storage` namespace mirrors the TypeScript SDK's direct storage API:
|
|
|
131
131
|
|
|
132
132
|
Every storage request sends `Authorization: Bearer <apiKey>` and `X-AtomicMemory-User-Id`. The SDK never sends the legacy `?user_id=` URL parameter.
|
|
133
133
|
|
|
134
|
+
## v1 wire contract
|
|
135
|
+
|
|
136
|
+
`atomicmemory.contract.v1` is the wire codec for the v1 provider-contract encoding. The wire form is deliberately mixed-case — `Memory.createdAt`/`updatedAt` and `SearchResult.rankingScore` are camelCase; `version_id`, `observed_at`, and retrieval-receipt fields are snake_case — as pinned by the vendored `contract/CONTRACT.md`. This module is the only place that mapping lives; in-process models and provider mappers are unchanged.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from atomicmemory.contract import v1
|
|
140
|
+
|
|
141
|
+
# decode a wire search response (e.g. from a cross-SDK provider call)
|
|
142
|
+
wire_page = {
|
|
143
|
+
"results": [
|
|
144
|
+
{
|
|
145
|
+
"memory": {
|
|
146
|
+
"id": "mem_1",
|
|
147
|
+
"content": "I prefer aisle seats on flights.",
|
|
148
|
+
"scope": {"user": "demo"},
|
|
149
|
+
"kind": "fact",
|
|
150
|
+
"createdAt": "2026-05-30T12:00:00.000Z",
|
|
151
|
+
},
|
|
152
|
+
"score": 0.91,
|
|
153
|
+
"rankingScore": 0.87,
|
|
154
|
+
}
|
|
155
|
+
],
|
|
156
|
+
"retrieval": {
|
|
157
|
+
"embedding_model": "text-embedding-x",
|
|
158
|
+
"embedding_model_version": "1",
|
|
159
|
+
"embedding_dimensions": 1536,
|
|
160
|
+
"query_text": "deploy gate",
|
|
161
|
+
"candidate_ids": ["mem_1"],
|
|
162
|
+
"trace_id": "trace-1",
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
page = v1.decode_search_result_page(wire_page)
|
|
167
|
+
for hit in page.results:
|
|
168
|
+
print(hit.memory.content, hit.score) # snake_case in-process models
|
|
169
|
+
|
|
170
|
+
# re-encode to the exact v1 wire form (millisecond-precision UTC datetimes)
|
|
171
|
+
wire_out = v1.encode_search_result_page(page)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Two behaviors to know: naive datetimes passed to encode functions are assumed UTC (bare `astimezone()` would shift by the host's UTC offset); `encode_ingest_input` rejects models carrying `content_class` with a clear error because the v1 schemas have `additionalProperties: false` and no such field — this is a Python-ahead field pending TS contract alignment.
|
|
175
|
+
|
|
176
|
+
This is NOT the AtomicMemory core HTTP API. That boundary stays in the provider mappers. The import path is `atomicmemory.contract` — deliberately not re-exported from the package root to keep the root namespace focused on the core provider API.
|
|
177
|
+
|
|
134
178
|
## Development
|
|
135
179
|
|
|
136
180
|
```bash
|
|
@@ -23,11 +23,25 @@ from atomicmemory.core.errors import (
|
|
|
23
23
|
RateLimitError,
|
|
24
24
|
ValidationError,
|
|
25
25
|
)
|
|
26
|
+
from atomicmemory.memory.capability_profiles import (
|
|
27
|
+
CapabilityGap,
|
|
28
|
+
CapabilityProfile,
|
|
29
|
+
capability_gaps,
|
|
30
|
+
satisfies_profile,
|
|
31
|
+
)
|
|
26
32
|
from atomicmemory.memory.filters import FieldFilter, FieldFilterOp, FilterExpr
|
|
33
|
+
from atomicmemory.memory.meta_fact_filter import (
|
|
34
|
+
DEFAULT_META_FACT_PATTERNS,
|
|
35
|
+
MetaFactFilterConfig,
|
|
36
|
+
filter_meta_facts,
|
|
37
|
+
is_meta_fact,
|
|
38
|
+
resolve_meta_fact_patterns,
|
|
39
|
+
)
|
|
27
40
|
from atomicmemory.memory.types import (
|
|
28
41
|
Capabilities,
|
|
29
42
|
CapabilitiesExtensions,
|
|
30
43
|
CapabilitiesRequiredScope,
|
|
44
|
+
ContentClass,
|
|
31
45
|
ContextPackage,
|
|
32
46
|
GraphEdge,
|
|
33
47
|
GraphNode,
|
|
@@ -52,6 +66,7 @@ from atomicmemory.memory.types import (
|
|
|
52
66
|
PackageRequest,
|
|
53
67
|
Profile,
|
|
54
68
|
Provenance,
|
|
69
|
+
RetrievalReceipt,
|
|
55
70
|
Scope,
|
|
56
71
|
SearchRequest,
|
|
57
72
|
SearchResult,
|
|
@@ -87,6 +102,7 @@ from atomicmemory.storage import (
|
|
|
87
102
|
)
|
|
88
103
|
|
|
89
104
|
__all__ = [
|
|
105
|
+
"DEFAULT_META_FACT_PATTERNS",
|
|
90
106
|
"ArtifactHead",
|
|
91
107
|
"ArtifactInUseError",
|
|
92
108
|
"ArtifactMetadata",
|
|
@@ -103,7 +119,10 @@ __all__ = [
|
|
|
103
119
|
"Capabilities",
|
|
104
120
|
"CapabilitiesExtensions",
|
|
105
121
|
"CapabilitiesRequiredScope",
|
|
122
|
+
"CapabilityGap",
|
|
123
|
+
"CapabilityProfile",
|
|
106
124
|
"ConfigError",
|
|
125
|
+
"ContentClass",
|
|
107
126
|
"ContextPackage",
|
|
108
127
|
"DeleteArtifactOptions",
|
|
109
128
|
"DeleteArtifactPolicy",
|
|
@@ -133,6 +152,7 @@ __all__ = [
|
|
|
133
152
|
"Message",
|
|
134
153
|
"MessageIngest",
|
|
135
154
|
"MessageRole",
|
|
155
|
+
"MetaFactFilterConfig",
|
|
136
156
|
"NetworkError",
|
|
137
157
|
"NotInitializedError",
|
|
138
158
|
"PackageFormat",
|
|
@@ -146,6 +166,7 @@ __all__ = [
|
|
|
146
166
|
"PutManagedInput",
|
|
147
167
|
"PutPointerInput",
|
|
148
168
|
"RateLimitError",
|
|
169
|
+
"RetrievalReceipt",
|
|
149
170
|
"Scope",
|
|
150
171
|
"SearchRequest",
|
|
151
172
|
"SearchResult",
|
|
@@ -163,4 +184,9 @@ __all__ = [
|
|
|
163
184
|
"VerificationResult",
|
|
164
185
|
"VerifyArtifactOptions",
|
|
165
186
|
"__version__",
|
|
187
|
+
"capability_gaps",
|
|
188
|
+
"filter_meta_facts",
|
|
189
|
+
"is_meta_fact",
|
|
190
|
+
"resolve_meta_fact_patterns",
|
|
191
|
+
"satisfies_profile",
|
|
166
192
|
]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""AsyncMemoryClient — async facade for the V3 memory layer.
|
|
2
2
|
|
|
3
|
+
Port of `atomicmemory-sdk/src/client/memory-client.ts` (async variant).
|
|
3
4
|
Mirrors :class:`atomicmemory.client.memory_client.MemoryClient` with
|
|
4
5
|
``async def`` for every I/O method and a ``__aenter__`` / ``__aexit__``
|
|
5
6
|
context manager. Dict coercion + Pydantic-error wrapping is identical
|
|
@@ -8,6 +9,8 @@ to the sync client.
|
|
|
8
9
|
|
|
9
10
|
from __future__ import annotations
|
|
10
11
|
|
|
12
|
+
import asyncio
|
|
13
|
+
import contextlib
|
|
11
14
|
from dataclasses import dataclass
|
|
12
15
|
from types import TracebackType
|
|
13
16
|
from typing import Any
|
|
@@ -17,11 +20,13 @@ import atomicmemory.providers.atomicmemory
|
|
|
17
20
|
import atomicmemory.providers.hindsight
|
|
18
21
|
import atomicmemory.providers.mem0 # noqa: F401
|
|
19
22
|
from atomicmemory.client.memory_client import (
|
|
23
|
+
MemoryProviderConfigs,
|
|
20
24
|
_coerce_ingest,
|
|
21
25
|
_coerce_list_request,
|
|
22
26
|
_coerce_package,
|
|
23
27
|
_coerce_ref,
|
|
24
28
|
_coerce_search,
|
|
29
|
+
_pick_first_provider_key,
|
|
25
30
|
)
|
|
26
31
|
from atomicmemory.core.errors import ConfigError, NotInitializedError
|
|
27
32
|
from atomicmemory.memory.provider import BaseAsyncMemoryProvider
|
|
@@ -42,8 +47,6 @@ from atomicmemory.memory.types import (
|
|
|
42
47
|
)
|
|
43
48
|
from atomicmemory.providers.atomicmemory.async_handle_impl import AsyncAtomicMemoryHandle
|
|
44
49
|
|
|
45
|
-
MemoryProviderConfigs = dict[str, Any]
|
|
46
|
-
|
|
47
50
|
|
|
48
51
|
@dataclass
|
|
49
52
|
class AsyncProviderStatus:
|
|
@@ -62,7 +65,7 @@ class AsyncMemoryClient:
|
|
|
62
65
|
|
|
63
66
|
Example:
|
|
64
67
|
>>> async with AsyncMemoryClient(
|
|
65
|
-
... providers={"atomicmemory": {"api_url": "http://localhost:
|
|
68
|
+
... providers={"atomicmemory": {"api_url": "http://localhost:17350"}}
|
|
66
69
|
... ) as memory:
|
|
67
70
|
... await memory.initialize()
|
|
68
71
|
... await memory.ingest({"mode": "text", "content": "hi", "scope": {"user": "u1"}})
|
|
@@ -88,18 +91,82 @@ class AsyncMemoryClient:
|
|
|
88
91
|
)
|
|
89
92
|
)
|
|
90
93
|
self._initialized = False
|
|
94
|
+
self._init_error: Exception | None = None
|
|
95
|
+
self._init_task: asyncio.Task[None] | None = None
|
|
91
96
|
|
|
92
97
|
async def initialize(self, registry: AsyncProviderRegistry | None = None) -> None:
|
|
98
|
+
"""Initialize all configured providers. Idempotent and concurrency-safe.
|
|
99
|
+
|
|
100
|
+
Concurrent calls on one event loop share a single initialization run
|
|
101
|
+
(the first call's ``registry`` wins). The COMPLETED outcome — success
|
|
102
|
+
or the original failure — is captured into loop-independent state, so
|
|
103
|
+
a failed initialization is sticky from any loop: retrying re-raises
|
|
104
|
+
the original error; construct a new client after resolving the cause.
|
|
105
|
+
An instance is bound to the event loop of its first ``initialize()``
|
|
106
|
+
while initialization is still PENDING — awaiting a pending run from a
|
|
107
|
+
different loop is unsupported. ``close()`` after a SUCCESSFUL
|
|
108
|
+
lifecycle returns the client to the uninitialized state.
|
|
109
|
+
"""
|
|
93
110
|
if self._initialized:
|
|
94
111
|
return
|
|
95
|
-
|
|
112
|
+
if self._init_error is not None:
|
|
113
|
+
raise self._init_error
|
|
114
|
+
if self._init_task is None:
|
|
115
|
+
self._init_task = asyncio.ensure_future(self._run_initialize(registry))
|
|
116
|
+
self._init_task.add_done_callback(_mark_retrieved)
|
|
117
|
+
task = self._init_task
|
|
118
|
+
try:
|
|
119
|
+
# shield: cancelling ONE waiter (e.g. wait_for timeout) must not
|
|
120
|
+
# cancel the shared run for everyone — promises aren't cancellable
|
|
121
|
+
# in TS, so unshielded awaiting would NOT be lifecycle parity.
|
|
122
|
+
await asyncio.shield(task)
|
|
123
|
+
finally:
|
|
124
|
+
if task.done():
|
|
125
|
+
self._init_task = None
|
|
126
|
+
|
|
127
|
+
async def _run_initialize(self, registry: AsyncProviderRegistry | None) -> None:
|
|
128
|
+
"""Execute the shared initialization run; capture errors into sticky state.
|
|
129
|
+
|
|
130
|
+
CancelledError is BaseException and never caught here, so cancellation
|
|
131
|
+
never becomes sticky. A cancelled task's ``_init_task`` slot is cleared
|
|
132
|
+
by a surviving waiter's ``finally`` once the task is done, or by
|
|
133
|
+
``close()``; either path lets a later call start fresh.
|
|
134
|
+
"""
|
|
135
|
+
try:
|
|
136
|
+
await self._service.initialize(registry if registry is not None else default_async_registry)
|
|
137
|
+
except Exception as exc:
|
|
138
|
+
self._init_error = exc
|
|
139
|
+
raise
|
|
96
140
|
self._initialized = True
|
|
97
141
|
|
|
98
142
|
async def close(self) -> None:
|
|
143
|
+
"""Close providers; safe to call multiple times.
|
|
144
|
+
|
|
145
|
+
Closing while an initialization is PENDING cancels that run: staged
|
|
146
|
+
providers are torn down by the service's atomic-initialize cleanup,
|
|
147
|
+
any concurrent initialize() waiter receives CancelledError, and the
|
|
148
|
+
client ends not-initialized (no sticky error is recorded for
|
|
149
|
+
cancellation). After a SUCCESSFUL lifecycle, close() returns the
|
|
150
|
+
client to the uninitialized state. A FAILED initialization remains
|
|
151
|
+
sticky — close() does not reset it.
|
|
152
|
+
"""
|
|
153
|
+
task = self._init_task
|
|
154
|
+
if task is not None:
|
|
155
|
+
if not task.done():
|
|
156
|
+
task.cancel()
|
|
157
|
+
with contextlib.suppress(Exception, asyncio.CancelledError):
|
|
158
|
+
await task
|
|
159
|
+
# Always clear, even when already done: a run whose waiters were
|
|
160
|
+
# all cancelled leaves a stale DONE task behind, and a later
|
|
161
|
+
# initialize() awaiting it would resolve instantly WITHOUT
|
|
162
|
+
# re-running — silently leaving the client uninitialized.
|
|
163
|
+
self._init_task = None
|
|
99
164
|
if not self._initialized:
|
|
100
165
|
return
|
|
101
|
-
|
|
102
|
-
|
|
166
|
+
try:
|
|
167
|
+
await self._service.close()
|
|
168
|
+
finally:
|
|
169
|
+
self._initialized = False
|
|
103
170
|
|
|
104
171
|
async def __aenter__(self) -> AsyncMemoryClient:
|
|
105
172
|
return self
|
|
@@ -117,6 +184,7 @@ class AsyncMemoryClient:
|
|
|
117
184
|
return await self._service.ingest(_coerce_ingest(input))
|
|
118
185
|
|
|
119
186
|
async def ingest_direct(self, input: IngestInput | dict[str, Any]) -> IngestResult:
|
|
187
|
+
"""Identical to :meth:`ingest`; preserved for wrapper-subclass parity with TS."""
|
|
120
188
|
self._assert_initialized()
|
|
121
189
|
return await self._service.ingest(_coerce_ingest(input))
|
|
122
190
|
|
|
@@ -125,6 +193,7 @@ class AsyncMemoryClient:
|
|
|
125
193
|
return await self._service.search(_coerce_search(request))
|
|
126
194
|
|
|
127
195
|
async def search_direct(self, request: SearchRequest | dict[str, Any]) -> SearchResultPage:
|
|
196
|
+
"""Identical to :meth:`search`; preserved for wrapper-subclass parity with TS."""
|
|
128
197
|
self._assert_initialized()
|
|
129
198
|
return await self._service.search(_coerce_search(request))
|
|
130
199
|
|
|
@@ -133,6 +202,7 @@ class AsyncMemoryClient:
|
|
|
133
202
|
return await self._service.package(_coerce_package(request))
|
|
134
203
|
|
|
135
204
|
async def package_direct(self, request: PackageRequest | dict[str, Any]) -> ContextPackage:
|
|
205
|
+
"""Identical to :meth:`package`; preserved for wrapper-subclass parity with TS."""
|
|
136
206
|
self._assert_initialized()
|
|
137
207
|
return await self._service.package(_coerce_package(request))
|
|
138
208
|
|
|
@@ -181,6 +251,11 @@ class AsyncMemoryClient:
|
|
|
181
251
|
|
|
182
252
|
@property
|
|
183
253
|
def atomicmemory(self) -> AsyncAtomicMemoryHandle | None:
|
|
254
|
+
"""Typed access to AtomicMemory-specific routes.
|
|
255
|
+
|
|
256
|
+
Returns ``None`` when the client is not yet initialized or the
|
|
257
|
+
``atomicmemory`` provider was not configured.
|
|
258
|
+
"""
|
|
184
259
|
if not self._initialized:
|
|
185
260
|
return None
|
|
186
261
|
if "atomicmemory" not in self._service.get_configured_providers():
|
|
@@ -196,8 +271,13 @@ class AsyncMemoryClient:
|
|
|
196
271
|
raise NotInitializedError("AsyncMemoryClient is not initialized. Call await client.initialize() first.")
|
|
197
272
|
|
|
198
273
|
|
|
199
|
-
def
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
274
|
+
def _mark_retrieved(task: asyncio.Task[None]) -> None:
|
|
275
|
+
"""Retrieve the task's exception so asyncio never logs 'never retrieved'.
|
|
276
|
+
|
|
277
|
+
A run whose waiters were all cancelled fails unobserved; without this
|
|
278
|
+
callback asyncio would log "Task exception was never retrieved" at GC.
|
|
279
|
+
Correctness is unchanged: waiters still see errors through the shield,
|
|
280
|
+
and stickiness is recorded by ``_run_initialize`` itself.
|
|
281
|
+
"""
|
|
282
|
+
if not task.cancelled():
|
|
283
|
+
task.exception()
|
|
@@ -9,6 +9,7 @@ code. Async users get the same surface via
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import threading
|
|
12
13
|
from dataclasses import dataclass
|
|
13
14
|
from types import TracebackType
|
|
14
15
|
from typing import Any
|
|
@@ -127,7 +128,7 @@ class MemoryClient:
|
|
|
127
128
|
"""Sync entry point for the V3 memory API.
|
|
128
129
|
|
|
129
130
|
Example:
|
|
130
|
-
>>> with MemoryClient(providers={"atomicmemory": {"api_url": "http://localhost:
|
|
131
|
+
>>> with MemoryClient(providers={"atomicmemory": {"api_url": "http://localhost:17350"}}) as memory:
|
|
131
132
|
... memory.initialize()
|
|
132
133
|
... memory.ingest({"mode": "text", "content": "hi", "scope": {"user": "u1"}})
|
|
133
134
|
"""
|
|
@@ -151,24 +152,54 @@ class MemoryClient:
|
|
|
151
152
|
)
|
|
152
153
|
)
|
|
153
154
|
self._initialized = False
|
|
155
|
+
self._init_lock = threading.Lock()
|
|
156
|
+
self._init_error: Exception | None = None
|
|
154
157
|
|
|
155
158
|
# ------------------------------------------------------------------
|
|
156
159
|
# Lifecycle
|
|
157
160
|
# ------------------------------------------------------------------
|
|
158
161
|
|
|
159
162
|
def initialize(self, registry: ProviderRegistry | None = None) -> None:
|
|
160
|
-
"""Initialize all configured providers. Idempotent.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
"""Initialize all configured providers. Idempotent and thread-safe.
|
|
164
|
+
|
|
165
|
+
Concurrent and subsequent calls share a single initialization run
|
|
166
|
+
(the first call's ``registry`` wins; later arguments are ignored).
|
|
167
|
+
A FAILED initialization is sticky: retrying re-raises the original
|
|
168
|
+
error — resolve the cause and construct a new client. A successful
|
|
169
|
+
lifecycle stays re-openable: ``close()`` returns the client to the
|
|
170
|
+
uninitialized state. Factories must not call back into this client
|
|
171
|
+
instance; the non-reentrant lock would deadlock.
|
|
172
|
+
"""
|
|
173
|
+
with self._init_lock:
|
|
174
|
+
if self._initialized:
|
|
175
|
+
return
|
|
176
|
+
if self._init_error is not None:
|
|
177
|
+
raise self._init_error
|
|
178
|
+
try:
|
|
179
|
+
self._service.initialize(registry if registry is not None else default_registry)
|
|
180
|
+
except Exception as exc:
|
|
181
|
+
# Sticky failures are real initialization errors ONLY —
|
|
182
|
+
# KeyboardInterrupt/SystemExit propagate without poisoning the client.
|
|
183
|
+
self._init_error = exc
|
|
184
|
+
raise
|
|
185
|
+
self._initialized = True
|
|
165
186
|
|
|
166
187
|
def close(self) -> None:
|
|
167
|
-
"""Close every initialized provider; safe to call multiple times.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
188
|
+
"""Close every initialized provider; safe to call multiple times.
|
|
189
|
+
|
|
190
|
+
A pending sync initialize holds the lock, so close() blocks until
|
|
191
|
+
initialization finishes, including any network I/O it performs —
|
|
192
|
+
deterministic by construction. A client that never initialized
|
|
193
|
+
successfully is unaffected: close() is a no-op and the sticky
|
|
194
|
+
initialization error is preserved — construct a new client.
|
|
195
|
+
"""
|
|
196
|
+
with self._init_lock:
|
|
197
|
+
if not self._initialized:
|
|
198
|
+
return
|
|
199
|
+
try:
|
|
200
|
+
self._service.close()
|
|
201
|
+
finally:
|
|
202
|
+
self._initialized = False
|
|
172
203
|
|
|
173
204
|
def __enter__(self) -> MemoryClient:
|
|
174
205
|
return self
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""atomicmemory.contract — v1 provider-contract wire codec.
|
|
2
|
+
|
|
3
|
+
Re-exports the public codec functions from :mod:`atomicmemory.contract.v1`.
|
|
4
|
+
Import the submodule directly for IDE discoverability:
|
|
5
|
+
|
|
6
|
+
from atomicmemory.contract import v1
|
|
7
|
+
|
|
8
|
+
page = v1.decode_search_result_page(wire_dict)
|
|
9
|
+
wire = v1.encode_search_result_page(page)
|
|
10
|
+
|
|
11
|
+
This package is a specialty surface; it is deliberately NOT re-exported from
|
|
12
|
+
the ``atomicmemory`` package root to keep the root namespace focused on the
|
|
13
|
+
core provider API.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from atomicmemory.contract import v1
|
|
17
|
+
|
|
18
|
+
__all__ = ["v1"]
|