generic-ml-cache-core 0.2.0__tar.gz → 0.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.
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/PKG-INFO +1 -1
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/pyproject.toml +1 -1
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/persistence/in_memory_execution_repository.py +31 -1
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/persistence/sqlite_execution_repository.py +81 -2
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/execution/artifact.py +28 -1
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/execution/ml_execution.py +17 -1
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/run/client_run_request.py +1 -1
- generic_ml_cache_core-0.4.0/src/generic_ml_cache_core/application/domain/model/run/persistence_depth.py +36 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/port/inbound/run_api_execution_command.py +8 -7
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/port/inbound/run_managed_local_execution_command.py +7 -5
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/port/inbound/run_passthrough_execution_command.py +6 -5
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/port/out/execution_repository_port.py +21 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/usecase/cached_ml_execution_service.py +84 -1
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/usecase/journal_events.py +4 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/usecase/run_api_execution_service.py +11 -1
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/usecase/run_managed_local_execution_service.py +21 -1
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/usecase/run_passthrough_execution_service.py +11 -1
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_client_run_request.py +1 -1
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_execution_repository.py +25 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_ml_execution.py +12 -1
- generic_ml_cache_core-0.4.0/tests/test_persistence_depth.py +26 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_probe_command.py +1 -1
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_run_api_execution_command.py +5 -2
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_run_api_execution_service.py +33 -2
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_run_managed_local_execution_command.py +9 -4
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_run_managed_local_execution_service.py +146 -5
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_run_passthrough_execution_command.py +4 -3
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_run_passthrough_execution_service.py +24 -3
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_sqlite_execution_repository.py +86 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/.gitignore +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/LICENSE +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/NOTICE +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/README.md +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/inbound/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/inbound/composition.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/api/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/api/stub_api_client_adapter.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/client/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/client/claude.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/client/codex.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/client/cursor.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/client/discover.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/client/isolation.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/client/local_client_runner.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/client/passthrough_client_runner.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/client/prime_directive.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/client/registry.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/clock/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/clock/system_clock.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/fingerprint/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/fingerprint/filesystem_file_fingerprint.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/metrics/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/metrics/access_registry.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/metrics/journal_metrics.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/persistence/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/persistence/call_identity_serialization.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/storage/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/out/storage/filesystem_blob_store.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/client_status.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/execution/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/execution/execution_failure.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/execution/execution_kind.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/execution/execution_state.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/identity/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/identity/api_call_identity.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/identity/call_identity.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/identity/managed_call_identity.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/identity/passthrough_call_identity.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/model_info.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/model_listing.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/parsed_output.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/probe/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/probe/probe_report.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/probe/probe_status.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/run/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/run/cache_mode.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/run/client_run_result.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/run/message.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/usage/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/usage/token_usage.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/model/usage/usage.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/service/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/service/cacheability.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/domain/service/message_fingerprinting.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/port/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/port/inbound/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/port/inbound/probe_command.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/port/inbound/probe_use_case.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/port/inbound/run_api_execution_use_case.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/port/inbound/run_managed_local_execution_use_case.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/port/inbound/run_passthrough_execution_use_case.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/port/out/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/port/out/api_client_port.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/port/out/base.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/port/out/blob_store_port.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/port/out/client_runner_port.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/port/out/clock_port.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/port/out/file_fingerprint_port.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/port/out/metrics_port.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/port/out/passthrough_runner_port.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/usecase/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/usecase/call_identity_building.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/usecase/probe_service.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/common/__init__.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/common/checksum.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/common/errors.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/stream.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/conftest.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/fake_client.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_adapters.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_api_call_identity.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_api_client_port.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_artifact.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_blob_store_port.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_cache_mode.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_cacheability.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_call_identity_building.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_call_identity_serialization.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_checksum.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_client_run_result.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_client_runner_port.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_clock_port.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_composition.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_execution_failure.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_execution_kind.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_execution_state.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_file_content_fingerprint.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_file_fingerprint_port.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_filesystem_blob_store.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_journal_metrics.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_local_client_runner.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_managed_call_identity.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_message.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_message_fingerprinting.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_metrics_port.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_passthrough_call_identity.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_passthrough_client_runner.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_passthrough_runner_port.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_probe_report.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_probe_service.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_token_usage.py +0 -0
- {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_usage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: generic-ml-cache-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Hexagonal core library for generic-ml-cache: domain, use cases, ports, and the default outbound adapters (SQLite repo, blob store, local clients, API). Stateless; inject the data source. Zero runtime deps.
|
|
5
5
|
Project-URL: Homepage, https://github.com/danielslobozian/generic-ml-cache
|
|
6
6
|
Project-URL: Repository, https://github.com/danielslobozian/generic-ml-cache
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "generic-ml-cache-core"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Hexagonal core library for generic-ml-cache: domain, use cases, ports, and the default outbound adapters (SQLite repo, blob store, local clients, API). Stateless; inject the data source. Zero runtime deps."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -5,8 +5,12 @@
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
from dataclasses import replace
|
|
8
|
-
from typing import Dict, List, Optional
|
|
8
|
+
from typing import Dict, List, Optional, Set
|
|
9
9
|
|
|
10
|
+
from generic_ml_cache_core.application.domain.model.execution.artifact import (
|
|
11
|
+
INPUT_ARTIFACT_TYPES,
|
|
12
|
+
Artifact,
|
|
13
|
+
)
|
|
10
14
|
from generic_ml_cache_core.application.domain.model.execution.execution_state import ExecutionState
|
|
11
15
|
from generic_ml_cache_core.application.domain.model.execution.ml_execution import MlExecution
|
|
12
16
|
from generic_ml_cache_core.application.port.out.clock_port import ClockPort
|
|
@@ -30,6 +34,7 @@ class InMemoryExecutionRepository(ExecutionRepositoryPort):
|
|
|
30
34
|
def __init__(self, clock: ClockPort) -> None:
|
|
31
35
|
self._clock = clock
|
|
32
36
|
self._by_key: Dict[str, List[MlExecution]] = {}
|
|
37
|
+
self._tags_by_key: Dict[str, Set[str]] = {}
|
|
33
38
|
|
|
34
39
|
def find_current(self, execution_key: str) -> Optional[MlExecution]:
|
|
35
40
|
for execution in self._by_key.get(execution_key, []):
|
|
@@ -51,6 +56,31 @@ class InMemoryExecutionRepository(ExecutionRepositoryPort):
|
|
|
51
56
|
prior.superseded_at = superseded_at
|
|
52
57
|
history.append(stored)
|
|
53
58
|
|
|
59
|
+
def add_tags(self, execution_key: str, tags: List[str]) -> None:
|
|
60
|
+
# Tags the key's current execution; a no-op when there is none.
|
|
61
|
+
if not tags or self.find_current(execution_key) is None:
|
|
62
|
+
return
|
|
63
|
+
self._tags_by_key.setdefault(execution_key, set()).update(tags)
|
|
64
|
+
|
|
65
|
+
def tags_for(self, execution_key: str) -> List[str]:
|
|
66
|
+
if self.find_current(execution_key) is None:
|
|
67
|
+
return []
|
|
68
|
+
return sorted(self._tags_by_key.get(execution_key, set()))
|
|
69
|
+
|
|
70
|
+
def add_input_artifacts(self, execution_key: str, artifacts: List[Artifact]) -> None:
|
|
71
|
+
# Back-fill the input onto the key's current execution; idempotent and a
|
|
72
|
+
# no-op when there is none or it already carries input.
|
|
73
|
+
if not artifacts:
|
|
74
|
+
return
|
|
75
|
+
for execution in self._by_key.get(execution_key, []):
|
|
76
|
+
if not self._is_servable(execution):
|
|
77
|
+
continue
|
|
78
|
+
if any(a.artifact_type in INPUT_ARTIFACT_TYPES for a in execution.artifacts):
|
|
79
|
+
return
|
|
80
|
+
execution.artifacts.extend(replace(a, content=None) for a in artifacts)
|
|
81
|
+
execution.input_persisted = True
|
|
82
|
+
return
|
|
83
|
+
|
|
54
84
|
@staticmethod
|
|
55
85
|
def _is_servable(execution: MlExecution) -> bool:
|
|
56
86
|
"""A servable execution is the current cached answer: a persisted success
|
|
@@ -16,7 +16,11 @@ from generic_ml_cache_core.adapter.out.persistence.call_identity_serialization i
|
|
|
16
16
|
deserialize_identity,
|
|
17
17
|
serialize_identity,
|
|
18
18
|
)
|
|
19
|
-
from generic_ml_cache_core.application.domain.model.execution.artifact import
|
|
19
|
+
from generic_ml_cache_core.application.domain.model.execution.artifact import (
|
|
20
|
+
INPUT_ARTIFACT_TYPES,
|
|
21
|
+
Artifact,
|
|
22
|
+
ArtifactType,
|
|
23
|
+
)
|
|
20
24
|
from generic_ml_cache_core.application.domain.model.identity.call_identity import CallIdentity
|
|
21
25
|
from generic_ml_cache_core.application.domain.model.execution.execution_failure import (
|
|
22
26
|
ExecutionFailure,
|
|
@@ -33,6 +37,9 @@ from generic_ml_cache_core.application.port.out.execution_repository_port import
|
|
|
33
37
|
|
|
34
38
|
_DB_NAME = "executions.sqlite3"
|
|
35
39
|
|
|
40
|
+
#: stored string values of the input artifact types, for the idempotency check.
|
|
41
|
+
_INPUT_TYPE_VALUES = tuple(t.value for t in INPUT_ARTIFACT_TYPES)
|
|
42
|
+
|
|
36
43
|
|
|
37
44
|
@dataclass(frozen=True)
|
|
38
45
|
class ExecutionSummary:
|
|
@@ -86,6 +93,11 @@ CREATE TABLE IF NOT EXISTS token_usage (
|
|
|
86
93
|
cost_usd REAL,
|
|
87
94
|
raw_json TEXT NOT NULL
|
|
88
95
|
);
|
|
96
|
+
CREATE TABLE IF NOT EXISTS execution_tags (
|
|
97
|
+
execution_id INTEGER NOT NULL,
|
|
98
|
+
tag TEXT NOT NULL,
|
|
99
|
+
UNIQUE(execution_id, tag)
|
|
100
|
+
);
|
|
89
101
|
"""
|
|
90
102
|
|
|
91
103
|
|
|
@@ -288,6 +300,70 @@ class SqliteExecutionRepository(ExecutionRepositoryPort):
|
|
|
288
300
|
),
|
|
289
301
|
)
|
|
290
302
|
|
|
303
|
+
# -- tags (a separate annotation; never rewrites an execution) --------
|
|
304
|
+
|
|
305
|
+
@staticmethod
|
|
306
|
+
def _current_execution_id(connection: sqlite3.Connection, execution_key: str) -> Optional[int]:
|
|
307
|
+
row = connection.execute(
|
|
308
|
+
"SELECT id FROM executions WHERE execution_key = ? AND state = ? "
|
|
309
|
+
"AND output_persisted = 1 AND superseded_at IS NULL ORDER BY id DESC LIMIT 1",
|
|
310
|
+
(execution_key, ExecutionState.SUCCESS.value),
|
|
311
|
+
).fetchone()
|
|
312
|
+
return int(row[0]) if row is not None else None
|
|
313
|
+
|
|
314
|
+
def add_tags(self, execution_key: str, tags: List[str]) -> None:
|
|
315
|
+
if not tags:
|
|
316
|
+
return
|
|
317
|
+
connection = self._connect()
|
|
318
|
+
try:
|
|
319
|
+
execution_id = self._current_execution_id(connection, execution_key)
|
|
320
|
+
if execution_id is None:
|
|
321
|
+
return
|
|
322
|
+
for tag in tags:
|
|
323
|
+
connection.execute(
|
|
324
|
+
"INSERT OR IGNORE INTO execution_tags (execution_id, tag) VALUES (?, ?)",
|
|
325
|
+
(execution_id, tag),
|
|
326
|
+
)
|
|
327
|
+
connection.commit()
|
|
328
|
+
finally:
|
|
329
|
+
connection.close()
|
|
330
|
+
|
|
331
|
+
def tags_for(self, execution_key: str) -> List[str]:
|
|
332
|
+
connection = self._connect()
|
|
333
|
+
try:
|
|
334
|
+
execution_id = self._current_execution_id(connection, execution_key)
|
|
335
|
+
if execution_id is None:
|
|
336
|
+
return []
|
|
337
|
+
rows = connection.execute(
|
|
338
|
+
"SELECT tag FROM execution_tags WHERE execution_id = ? ORDER BY tag",
|
|
339
|
+
(execution_id,),
|
|
340
|
+
).fetchall()
|
|
341
|
+
return [tag for (tag,) in rows]
|
|
342
|
+
finally:
|
|
343
|
+
connection.close()
|
|
344
|
+
|
|
345
|
+
def add_input_artifacts(self, execution_key: str, artifacts: List[Artifact]) -> None:
|
|
346
|
+
if not artifacts:
|
|
347
|
+
return
|
|
348
|
+
connection = self._connect()
|
|
349
|
+
try:
|
|
350
|
+
execution_id = self._current_execution_id(connection, execution_key)
|
|
351
|
+
if execution_id is None:
|
|
352
|
+
return
|
|
353
|
+
# Idempotent: skip if this execution already carries input artifacts.
|
|
354
|
+
placeholders = ",".join("?" * len(_INPUT_TYPE_VALUES))
|
|
355
|
+
already = connection.execute(
|
|
356
|
+
f"SELECT 1 FROM artifacts WHERE execution_id = ? "
|
|
357
|
+
f"AND artifact_type IN ({placeholders}) LIMIT 1",
|
|
358
|
+
(execution_id, *_INPUT_TYPE_VALUES),
|
|
359
|
+
).fetchone()
|
|
360
|
+
if already is not None:
|
|
361
|
+
return
|
|
362
|
+
self._insert_artifacts(connection, execution_id, artifacts)
|
|
363
|
+
connection.commit()
|
|
364
|
+
finally:
|
|
365
|
+
connection.close()
|
|
366
|
+
|
|
291
367
|
# -- reconstruction ---------------------------------------------------
|
|
292
368
|
|
|
293
369
|
def _load_execution(self, connection: sqlite3.Connection, row: tuple) -> MlExecution:
|
|
@@ -302,12 +378,15 @@ class SqliteExecutionRepository(ExecutionRepositoryPort):
|
|
|
302
378
|
failure_message,
|
|
303
379
|
failure_exit_code,
|
|
304
380
|
) = row
|
|
381
|
+
artifacts = self._load_artifacts(connection, execution_id)
|
|
305
382
|
return MlExecution(
|
|
306
383
|
call_identity=self._load_identity(connection, execution_key),
|
|
307
384
|
execution_state=ExecutionState(state),
|
|
308
385
|
execution_kind=ExecutionKind(kind),
|
|
309
386
|
output_persisted=bool(output_persisted),
|
|
310
|
-
artifacts
|
|
387
|
+
# Derived, not a column: input is persisted iff INPUT_* artifacts exist.
|
|
388
|
+
input_persisted=any(a.artifact_type in INPUT_ARTIFACT_TYPES for a in artifacts),
|
|
389
|
+
artifacts=artifacts,
|
|
311
390
|
token_usage=self._load_token_usage(connection, execution_id),
|
|
312
391
|
failure=(
|
|
313
392
|
ExecutionFailure(
|
|
@@ -13,7 +13,15 @@ _BINARY = "binary"
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class ArtifactType(enum.Enum):
|
|
16
|
-
"""The kind of
|
|
16
|
+
"""The kind of document an Artifact holds.
|
|
17
|
+
|
|
18
|
+
The ``STDOUT``/``STDERR``/``OUTPUT_FILE`` types are an execution's *output*,
|
|
19
|
+
stored whenever caching is on. The ``INPUT_*`` types are the *input* sent to
|
|
20
|
+
the client — and are stored only at ``DATASET`` persistence depth, to build a
|
|
21
|
+
queryable ``(input, output)`` corpus. Each execution kind keeps its own input
|
|
22
|
+
shape: managed-local uses ``INPUT_CONTEXT``/``INPUT_PROMPT``/``INPUT_SYSTEM``,
|
|
23
|
+
the API kind a single ``INPUT_MESSAGES`` (the JSON message list), and
|
|
24
|
+
passthrough a single ``INPUT_ARGS`` (the JSON native-argument list).
|
|
17
25
|
|
|
18
26
|
RAW_USAGE is reserved for a later step (the raw client usage block stored as
|
|
19
27
|
its own artifact); today raw usage still rides on TokenUsage.
|
|
@@ -22,6 +30,11 @@ class ArtifactType(enum.Enum):
|
|
|
22
30
|
STDOUT = "stdout"
|
|
23
31
|
STDERR = "stderr"
|
|
24
32
|
OUTPUT_FILE = "output_file"
|
|
33
|
+
INPUT_CONTEXT = "input_context"
|
|
34
|
+
INPUT_PROMPT = "input_prompt"
|
|
35
|
+
INPUT_SYSTEM = "input_system"
|
|
36
|
+
INPUT_MESSAGES = "input_messages"
|
|
37
|
+
INPUT_ARGS = "input_args"
|
|
25
38
|
|
|
26
39
|
|
|
27
40
|
@dataclass(frozen=True)
|
|
@@ -76,3 +89,17 @@ class Artifact:
|
|
|
76
89
|
def is_hydrated(self) -> bool:
|
|
77
90
|
"""True when the artifact's bytes are materialised in memory."""
|
|
78
91
|
return self.content is not None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
#: The artifact types that make up an execution's persisted *input* (DATASET
|
|
95
|
+
#: depth). A single place so consumers can tell input apart from output without
|
|
96
|
+
#: re-listing the members.
|
|
97
|
+
INPUT_ARTIFACT_TYPES = frozenset(
|
|
98
|
+
{
|
|
99
|
+
ArtifactType.INPUT_CONTEXT,
|
|
100
|
+
ArtifactType.INPUT_PROMPT,
|
|
101
|
+
ArtifactType.INPUT_SYSTEM,
|
|
102
|
+
ArtifactType.INPUT_MESSAGES,
|
|
103
|
+
ArtifactType.INPUT_ARGS,
|
|
104
|
+
}
|
|
105
|
+
)
|
|
@@ -6,7 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
8
|
from datetime import datetime
|
|
9
|
-
from typing import List, Optional
|
|
9
|
+
from typing import Iterable, List, Optional
|
|
10
10
|
|
|
11
11
|
from generic_ml_cache_core.application.domain.model.execution.artifact import Artifact
|
|
12
12
|
from generic_ml_cache_core.application.domain.model.identity.call_identity import CallIdentity
|
|
@@ -29,13 +29,29 @@ class MlExecution:
|
|
|
29
29
|
cache-currency axis (None = current, set = stale); executions are append-only
|
|
30
30
|
per call identity. ``artifacts`` may be dehydrated (refs only) or hydrated
|
|
31
31
|
(bytes materialised).
|
|
32
|
+
|
|
33
|
+
``output_persisted`` is set whenever caching is on (CACHE/DATASET);
|
|
34
|
+
``input_persisted`` is set only at DATASET depth, when the input is also kept
|
|
35
|
+
(as ``INPUT_*`` artifacts) to form a ``(input, output)`` corpus.
|
|
32
36
|
"""
|
|
33
37
|
|
|
34
38
|
call_identity: CallIdentity
|
|
35
39
|
execution_state: ExecutionState
|
|
36
40
|
execution_kind: ExecutionKind
|
|
37
41
|
output_persisted: bool
|
|
42
|
+
input_persisted: bool = False
|
|
38
43
|
artifacts: List[Artifact] = field(default_factory=list)
|
|
39
44
|
token_usage: Optional[TokenUsage] = None
|
|
40
45
|
failure: Optional[ExecutionFailure] = None
|
|
41
46
|
superseded_at: Optional[datetime] = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def normalize_tags(raw_tags: Iterable[str]) -> List[str]:
|
|
50
|
+
"""Normalise user-supplied tags: trim, drop blanks, de-duplicate, sort.
|
|
51
|
+
|
|
52
|
+
Tags are metadata, never part of the cache key. Normalising at the boundary
|
|
53
|
+
keeps stored tags deterministic (the same set in any input order compares
|
|
54
|
+
equal) without interpreting their meaning — they are stored verbatim
|
|
55
|
+
otherwise.
|
|
56
|
+
"""
|
|
57
|
+
return sorted({tag.strip() for tag in raw_tags if tag and tag.strip()})
|
|
@@ -13,7 +13,7 @@ class ClientRunRequest:
|
|
|
13
13
|
"""The DTO the use case constructs and passes to ClientRunnerPort.
|
|
14
14
|
|
|
15
15
|
Carries only what the client runner needs to launch the client. The
|
|
16
|
-
command's gmlcache-specific policy fields (cache_mode,
|
|
16
|
+
command's gmlcache-specific policy fields (cache_mode, persistence_depth,
|
|
17
17
|
scan_trust) do not appear here — they are the use case's concern, not
|
|
18
18
|
the client runner's.
|
|
19
19
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Daniel Slobozian
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""PersistenceDepth."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PersistenceDepth(Enum):
|
|
11
|
+
"""How much of an execution is kept on disk — a single ordered choice.
|
|
12
|
+
|
|
13
|
+
Each level is a superset of the one below, so the degenerate "input stored
|
|
14
|
+
without output" state is unrepresentable:
|
|
15
|
+
|
|
16
|
+
- ``METER`` -- metadata/usage only. The call runs and is recorded, but no
|
|
17
|
+
output is stored, so it is never replayed (a usage/observability mode).
|
|
18
|
+
- ``CACHE`` -- ``METER`` plus the output: stored and replayed on a hit. The
|
|
19
|
+
default, and today's behaviour.
|
|
20
|
+
- ``DATASET`` -- ``CACHE`` plus the input: replayed and retained as a labelled
|
|
21
|
+
``(input, output)`` pair.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
METER = "meter"
|
|
25
|
+
CACHE = "cache"
|
|
26
|
+
DATASET = "dataset"
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def stores_output(self) -> bool:
|
|
30
|
+
"""Whether this depth keeps the output (``CACHE`` and ``DATASET``)."""
|
|
31
|
+
return self in (PersistenceDepth.CACHE, PersistenceDepth.DATASET)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def stores_input(self) -> bool:
|
|
35
|
+
"""Whether this depth keeps the input (``DATASET`` only)."""
|
|
36
|
+
return self is PersistenceDepth.DATASET
|
|
@@ -8,6 +8,7 @@ from dataclasses import dataclass, field
|
|
|
8
8
|
from typing import List
|
|
9
9
|
|
|
10
10
|
from generic_ml_cache_core.application.domain.model.run.cache_mode import CacheMode
|
|
11
|
+
from generic_ml_cache_core.application.domain.model.run.persistence_depth import PersistenceDepth
|
|
11
12
|
from generic_ml_cache_core.application.domain.model.run.message import Message
|
|
12
13
|
|
|
13
14
|
|
|
@@ -19,8 +20,8 @@ class RunApiExecutionCommand:
|
|
|
19
20
|
files or scan folders), so there are no input-file, allow-path, grant, or
|
|
20
21
|
scan-trust fields. An API call is always cacheable.
|
|
21
22
|
|
|
22
|
-
Note (future): ``
|
|
23
|
-
execution — an async call must store its output so the caller can retrieve it
|
|
23
|
+
Note (future): the ``METER`` depth (storing no output) will be incompatible with
|
|
24
|
+
async execution — an async call must store its output so the caller can retrieve it
|
|
24
25
|
by id later. Async is not built yet, so nothing enforces it here.
|
|
25
26
|
"""
|
|
26
27
|
|
|
@@ -28,13 +29,13 @@ class RunApiExecutionCommand:
|
|
|
28
29
|
model: str
|
|
29
30
|
messages: List[Message] = field(default_factory=list)
|
|
30
31
|
cache_mode: CacheMode = CacheMode.CACHE
|
|
31
|
-
|
|
32
|
+
persistence_depth: PersistenceDepth = PersistenceDepth.CACHE
|
|
32
33
|
record_on_error: bool = False
|
|
33
34
|
|
|
34
35
|
def should_persist(self, succeeded: bool) -> bool:
|
|
35
|
-
"""Whether this command's policy stores
|
|
36
|
-
with ``succeeded``: never
|
|
37
|
-
``record_on_error``."""
|
|
38
|
-
if not self.
|
|
36
|
+
"""Whether this command's policy stores the output for a run that ended
|
|
37
|
+
with ``succeeded``: never below ``CACHE`` depth (``METER`` stores nothing);
|
|
38
|
+
a failure only with ``record_on_error``."""
|
|
39
|
+
if not self.persistence_depth.stores_output:
|
|
39
40
|
return False
|
|
40
41
|
return succeeded or self.record_on_error
|
|
@@ -8,6 +8,7 @@ from dataclasses import dataclass, field
|
|
|
8
8
|
from typing import List, Optional
|
|
9
9
|
|
|
10
10
|
from generic_ml_cache_core.application.domain.model.run.cache_mode import CacheMode
|
|
11
|
+
from generic_ml_cache_core.application.domain.model.run.persistence_depth import PersistenceDepth
|
|
11
12
|
from generic_ml_cache_core.application.domain.service.cacheability import is_call_uncacheable
|
|
12
13
|
|
|
13
14
|
|
|
@@ -32,17 +33,18 @@ class RunManagedLocalExecutionCommand:
|
|
|
32
33
|
client_args: List[str] = field(default_factory=list)
|
|
33
34
|
grants: List[str] = field(default_factory=list)
|
|
34
35
|
cache_mode: CacheMode = CacheMode.CACHE
|
|
35
|
-
|
|
36
|
+
persistence_depth: PersistenceDepth = PersistenceDepth.CACHE
|
|
36
37
|
record_on_error: bool = False
|
|
38
|
+
tags: List[str] = field(default_factory=list)
|
|
37
39
|
|
|
38
40
|
@property
|
|
39
41
|
def is_uncacheable(self) -> bool:
|
|
40
42
|
return is_call_uncacheable(self.allow_paths, self.scan_trust)
|
|
41
43
|
|
|
42
44
|
def should_persist(self, succeeded: bool) -> bool:
|
|
43
|
-
"""Whether this command's policy stores
|
|
44
|
-
with ``succeeded``: never
|
|
45
|
-
``record_on_error``."""
|
|
46
|
-
if not self.
|
|
45
|
+
"""Whether this command's policy stores the output for a run that ended
|
|
46
|
+
with ``succeeded``: never below ``CACHE`` depth (``METER`` stores nothing);
|
|
47
|
+
a failure only with ``record_on_error``."""
|
|
48
|
+
if not self.persistence_depth.stores_output:
|
|
47
49
|
return False
|
|
48
50
|
return succeeded or self.record_on_error
|
|
@@ -8,6 +8,7 @@ from dataclasses import dataclass, field
|
|
|
8
8
|
from typing import List
|
|
9
9
|
|
|
10
10
|
from generic_ml_cache_core.application.domain.model.run.cache_mode import CacheMode
|
|
11
|
+
from generic_ml_cache_core.application.domain.model.run.persistence_depth import PersistenceDepth
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
@dataclass(frozen=True)
|
|
@@ -23,13 +24,13 @@ class RunPassthroughExecutionCommand:
|
|
|
23
24
|
client: str
|
|
24
25
|
native_args: List[str] = field(default_factory=list)
|
|
25
26
|
cache_mode: CacheMode = CacheMode.CACHE
|
|
26
|
-
|
|
27
|
+
persistence_depth: PersistenceDepth = PersistenceDepth.CACHE
|
|
27
28
|
record_on_error: bool = False
|
|
28
29
|
|
|
29
30
|
def should_persist(self, succeeded: bool) -> bool:
|
|
30
|
-
"""Whether this command's policy stores
|
|
31
|
-
with ``succeeded``: never
|
|
32
|
-
``record_on_error``."""
|
|
33
|
-
if not self.
|
|
31
|
+
"""Whether this command's policy stores the output for a run that ended
|
|
32
|
+
with ``succeeded``: never below ``CACHE`` depth (``METER`` stores nothing);
|
|
33
|
+
a failure only with ``record_on_error``."""
|
|
34
|
+
if not self.persistence_depth.stores_output:
|
|
34
35
|
return False
|
|
35
36
|
return succeeded or self.record_on_error
|
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
8
|
from typing import List, Optional
|
|
9
9
|
|
|
10
|
+
from generic_ml_cache_core.application.domain.model.execution.artifact import Artifact
|
|
10
11
|
from generic_ml_cache_core.application.domain.model.execution.ml_execution import MlExecution
|
|
11
12
|
|
|
12
13
|
|
|
@@ -38,3 +39,23 @@ class ExecutionRepositoryPort(ABC):
|
|
|
38
39
|
"""Append a new execution. If it is a servable success, atomically
|
|
39
40
|
supersede the prior current execution for the same key — the supersession
|
|
40
41
|
happens here, where atomicity belongs, never in the caller."""
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def add_tags(self, execution_key: str, tags: List[str]) -> None:
|
|
45
|
+
"""Attach ``tags`` to the current execution for ``execution_key``,
|
|
46
|
+
idempotently — already-present tags are left untouched, new ones added.
|
|
47
|
+
A separate annotation layer: this never rewrites the execution record,
|
|
48
|
+
and is a no-op if there is no current execution for the key."""
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def tags_for(self, execution_key: str) -> List[str]:
|
|
52
|
+
"""Return the tags on the current execution for ``execution_key``, sorted;
|
|
53
|
+
empty if none (or no current execution)."""
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def add_input_artifacts(self, execution_key: str, artifacts: List[Artifact]) -> None:
|
|
57
|
+
"""Attach input ``artifacts`` to the current execution for ``execution_key``,
|
|
58
|
+
back-filling the input side of the corpus when a DATASET-depth call hits an
|
|
59
|
+
entry that has none yet. Idempotent — a no-op if the current execution
|
|
60
|
+
already carries input, or if there is no current execution. Like tags, this
|
|
61
|
+
enriches an existing entry without rewriting its output."""
|
|
@@ -10,6 +10,7 @@ from typing import List, Optional, Protocol, Tuple
|
|
|
10
10
|
|
|
11
11
|
from generic_ml_cache_core.application.domain.model.execution.artifact import Artifact, ArtifactType
|
|
12
12
|
from generic_ml_cache_core.application.domain.model.run.cache_mode import CacheMode
|
|
13
|
+
from generic_ml_cache_core.application.domain.model.run.persistence_depth import PersistenceDepth
|
|
13
14
|
from generic_ml_cache_core.application.domain.model.identity.call_identity import CallIdentity
|
|
14
15
|
from generic_ml_cache_core.application.domain.model.run.client_run_result import ClientRunResult
|
|
15
16
|
from generic_ml_cache_core.application.domain.model.execution.execution_kind import ExecutionKind
|
|
@@ -31,6 +32,7 @@ class CacheableExecutionCommand(Protocol):
|
|
|
31
32
|
persistence policy. The kind-specific fields are read through hooks."""
|
|
32
33
|
|
|
33
34
|
cache_mode: CacheMode
|
|
35
|
+
persistence_depth: PersistenceDepth
|
|
34
36
|
|
|
35
37
|
def should_persist(self, succeeded: bool) -> bool: ...
|
|
36
38
|
|
|
@@ -63,6 +65,11 @@ class CachedMlExecutionService(ABC):
|
|
|
63
65
|
if command.cache_mode is CacheMode.OFFLINE:
|
|
64
66
|
return self._serve_offline(command, execution_key)
|
|
65
67
|
|
|
68
|
+
if not command.persistence_depth.stores_output:
|
|
69
|
+
# METER: never replays — always run, store nothing, but record whether
|
|
70
|
+
# the call *would* have hit a stored entry (would-be hit/miss).
|
|
71
|
+
return self._run_metered(command, call_identity, execution_key)
|
|
72
|
+
|
|
66
73
|
if command.cache_mode is CacheMode.CACHE:
|
|
67
74
|
current_execution = self._repository.find_current(execution_key)
|
|
68
75
|
if current_execution is not None:
|
|
@@ -93,6 +100,19 @@ class CachedMlExecutionService(ABC):
|
|
|
93
100
|
"""Whether this command cannot be cached. Default: always cacheable."""
|
|
94
101
|
return False
|
|
95
102
|
|
|
103
|
+
def _execution_tags(self, command: CacheableExecutionCommand) -> List[str]:
|
|
104
|
+
"""User-supplied tags to attach to executions this service records.
|
|
105
|
+
Metadata only — never part of the key. Default: none."""
|
|
106
|
+
return []
|
|
107
|
+
|
|
108
|
+
def _apply_tags(self, execution_key: str, command: CacheableExecutionCommand) -> None:
|
|
109
|
+
"""Attach the command's tags to the current execution for this key,
|
|
110
|
+
idempotently (a no-op when there are none). Tags are a separate
|
|
111
|
+
annotation: adding one never rewrites the execution record."""
|
|
112
|
+
tags = self._execution_tags(command)
|
|
113
|
+
if tags:
|
|
114
|
+
self._repository.add_tags(execution_key, tags)
|
|
115
|
+
|
|
96
116
|
# -- resolution paths -------------------------------------------------
|
|
97
117
|
|
|
98
118
|
def _serve_offline(self, command: CacheableExecutionCommand, execution_key: str) -> MlExecution:
|
|
@@ -107,8 +127,23 @@ class CachedMlExecutionService(ABC):
|
|
|
107
127
|
) -> MlExecution:
|
|
108
128
|
hydrated_execution = self._hydrate(current_execution)
|
|
109
129
|
self._record_event(journal_events.HIT, execution_key, command)
|
|
130
|
+
self._apply_tags(execution_key, command)
|
|
131
|
+
self._accumulate_input(command, execution_key, current_execution)
|
|
110
132
|
return hydrated_execution
|
|
111
133
|
|
|
134
|
+
def _accumulate_input(
|
|
135
|
+
self, command: CacheableExecutionCommand, execution_key: str, current_execution: MlExecution
|
|
136
|
+
) -> None:
|
|
137
|
+
"""If the user now wants the input kept (DATASET) and this entry doesn't yet
|
|
138
|
+
carry it, back-fill it onto the existing entry — the input is in the command,
|
|
139
|
+
so no re-run is needed. Mirrors how tags accumulate on a hit; the user
|
|
140
|
+
changing their mind to enrich the stored data is their decision."""
|
|
141
|
+
if not command.persistence_depth.stores_input or current_execution.input_persisted:
|
|
142
|
+
return
|
|
143
|
+
input_artifacts = self._build_input_artifacts(command, store=True)
|
|
144
|
+
if input_artifacts:
|
|
145
|
+
self._repository.add_input_artifacts(execution_key, input_artifacts)
|
|
146
|
+
|
|
112
147
|
def _run_uncacheable(
|
|
113
148
|
self, command: CacheableExecutionCommand, call_identity: CallIdentity, execution_key: str
|
|
114
149
|
) -> MlExecution:
|
|
@@ -127,22 +162,50 @@ class CachedMlExecutionService(ABC):
|
|
|
127
162
|
client_run_result = self._run_client(command)
|
|
128
163
|
should_store = allow_store and command.should_persist(client_run_result.succeeded)
|
|
129
164
|
artifacts = self._build_artifacts(client_run_result, store=should_store)
|
|
165
|
+
# Input rides on a stored output (DATASET is a superset of CACHE): only
|
|
166
|
+
# capture it when the output is being stored and the depth keeps input.
|
|
167
|
+
store_input = should_store and command.persistence_depth.stores_input
|
|
168
|
+
input_artifacts = self._build_input_artifacts(command, store=store_input)
|
|
130
169
|
execution = MlExecution(
|
|
131
170
|
call_identity=call_identity,
|
|
132
171
|
execution_state=client_run_result.outcome(),
|
|
133
172
|
execution_kind=self._execution_kind(),
|
|
134
173
|
output_persisted=should_store,
|
|
135
|
-
|
|
174
|
+
input_persisted=bool(input_artifacts),
|
|
175
|
+
artifacts=artifacts + input_artifacts,
|
|
136
176
|
token_usage=client_run_result.token_usage,
|
|
137
177
|
failure=client_run_result.failure(),
|
|
138
178
|
)
|
|
139
179
|
if should_store:
|
|
140
180
|
self._repository.save(execution)
|
|
141
181
|
self._record_event(journal_events.RECORD, execution_key, command)
|
|
182
|
+
self._apply_tags(execution_key, command)
|
|
142
183
|
else:
|
|
143
184
|
self._record_event(journal_events.RUN, execution_key, command)
|
|
144
185
|
return execution
|
|
145
186
|
|
|
187
|
+
def _run_metered(
|
|
188
|
+
self, command: CacheableExecutionCommand, call_identity: CallIdentity, execution_key: str
|
|
189
|
+
) -> MlExecution:
|
|
190
|
+
"""METER depth: always run and store nothing, but journal whether a stored
|
|
191
|
+
entry existed — so usage analytics can report would-be hit/miss ("you'd
|
|
192
|
+
have saved N runs") without the cache ever serving or storing anything."""
|
|
193
|
+
would_hit = self._repository.find_current(execution_key) is not None
|
|
194
|
+
client_run_result = self._run_client(command)
|
|
195
|
+
execution = MlExecution(
|
|
196
|
+
call_identity=call_identity,
|
|
197
|
+
execution_state=client_run_result.outcome(),
|
|
198
|
+
execution_kind=self._execution_kind(),
|
|
199
|
+
output_persisted=False,
|
|
200
|
+
input_persisted=False,
|
|
201
|
+
artifacts=self._build_artifacts(client_run_result, store=False),
|
|
202
|
+
token_usage=client_run_result.token_usage,
|
|
203
|
+
failure=client_run_result.failure(),
|
|
204
|
+
)
|
|
205
|
+
event = journal_events.WOULD_HIT if would_hit else journal_events.WOULD_MISS
|
|
206
|
+
self._record_event(event, execution_key, command)
|
|
207
|
+
return execution
|
|
208
|
+
|
|
146
209
|
# -- artifacts --------------------------------------------------------
|
|
147
210
|
|
|
148
211
|
def _build_artifacts(self, client_run_result: ClientRunResult, store: bool) -> List[Artifact]:
|
|
@@ -174,6 +237,26 @@ class CachedMlExecutionService(ABC):
|
|
|
174
237
|
self._blob_store.put(blob_key, content_bytes)
|
|
175
238
|
return Artifact.from_content(artifact_type, blob_key, content_bytes, name=artifact_name)
|
|
176
239
|
|
|
240
|
+
def _build_input_artifacts(
|
|
241
|
+
self, command: CacheableExecutionCommand, store: bool
|
|
242
|
+
) -> List[Artifact]:
|
|
243
|
+
"""The input documents to keep at DATASET depth, content-addressed like
|
|
244
|
+
any artifact. Empty when ``store`` is false (below DATASET, or nothing was
|
|
245
|
+
stored) or when the kind has no recordable input."""
|
|
246
|
+
if not store:
|
|
247
|
+
return []
|
|
248
|
+
return [
|
|
249
|
+
self._store_artifact(artifact_type, name, content_bytes, store=True)
|
|
250
|
+
for (artifact_type, name, content_bytes) in self._input_parts(command)
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
def _input_parts(
|
|
254
|
+
self, command: CacheableExecutionCommand
|
|
255
|
+
) -> List[Tuple[ArtifactType, Optional[str], bytes]]:
|
|
256
|
+
"""The ``(type, name, bytes)`` input documents this kind would persist at
|
|
257
|
+
DATASET depth. Default: none — a kind whose input is not recorded."""
|
|
258
|
+
return []
|
|
259
|
+
|
|
177
260
|
def _hydrate(self, execution: MlExecution) -> MlExecution:
|
|
178
261
|
hydrated_artifacts = [self._hydrate_artifact(artifact) for artifact in execution.artifacts]
|
|
179
262
|
return replace(execution, artifacts=hydrated_artifacts)
|
|
@@ -17,3 +17,7 @@ RECORD = "record"
|
|
|
17
17
|
MISS = "miss"
|
|
18
18
|
#: a fresh real call ran but was not stored (uncacheable, or a non-persisted/failed run)
|
|
19
19
|
RUN = "run"
|
|
20
|
+
#: a METER call ran (never replays) and a stored entry existed — it *would* have hit
|
|
21
|
+
WOULD_HIT = "would_hit"
|
|
22
|
+
#: a METER call ran (never replays) and no stored entry existed — it *would* have missed
|
|
23
|
+
WOULD_MISS = "would_miss"
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
import json
|
|
8
|
+
from typing import List, Optional, Tuple
|
|
8
9
|
|
|
10
|
+
from generic_ml_cache_core.application.domain.model.execution.artifact import ArtifactType
|
|
9
11
|
from generic_ml_cache_core.application.domain.model.identity.api_call_identity import (
|
|
10
12
|
ApiCallIdentity,
|
|
11
13
|
)
|
|
@@ -67,3 +69,11 @@ class RunApiExecutionService(CachedMlExecutionService, RunApiExecutionUseCase):
|
|
|
67
69
|
def _journal_fields(self, command: RunApiExecutionCommand) -> Tuple[str, str, str]:
|
|
68
70
|
# The provider plays the role of "client" in the journal; no effort concept.
|
|
69
71
|
return command.provider, command.model, ""
|
|
72
|
+
|
|
73
|
+
def _input_parts(
|
|
74
|
+
self, command: RunApiExecutionCommand
|
|
75
|
+
) -> List[Tuple[ArtifactType, Optional[str], bytes]]:
|
|
76
|
+
# The API call's input is its message list; keep it as one JSON artifact so
|
|
77
|
+
# the (role, content) structure survives into the exported corpus.
|
|
78
|
+
payload = json.dumps([{"role": m.role, "content": m.content} for m in command.messages])
|
|
79
|
+
return [(ArtifactType.INPUT_MESSAGES, None, payload.encode("utf-8"))]
|