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.
Files changed (148) hide show
  1. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/PKG-INFO +1 -1
  2. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/pyproject.toml +1 -1
  3. {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
  4. {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
  5. {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
  6. {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
  7. {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
  8. generic_ml_cache_core-0.4.0/src/generic_ml_cache_core/application/domain/model/run/persistence_depth.py +36 -0
  9. {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
  10. {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
  11. {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
  12. {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
  13. {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
  14. {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
  15. {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
  16. {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
  17. {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
  18. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_client_run_request.py +1 -1
  19. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_execution_repository.py +25 -0
  20. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_ml_execution.py +12 -1
  21. generic_ml_cache_core-0.4.0/tests/test_persistence_depth.py +26 -0
  22. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_probe_command.py +1 -1
  23. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_run_api_execution_command.py +5 -2
  24. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_run_api_execution_service.py +33 -2
  25. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_run_managed_local_execution_command.py +9 -4
  26. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_run_managed_local_execution_service.py +146 -5
  27. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_run_passthrough_execution_command.py +4 -3
  28. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_run_passthrough_execution_service.py +24 -3
  29. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_sqlite_execution_repository.py +86 -0
  30. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/.gitignore +0 -0
  31. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/LICENSE +0 -0
  32. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/NOTICE +0 -0
  33. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/README.md +0 -0
  34. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/__init__.py +0 -0
  35. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/adapter/__init__.py +0 -0
  36. {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
  37. {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
  38. {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
  39. {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
  40. {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
  41. {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
  42. {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
  43. {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
  44. {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
  45. {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
  46. {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
  47. {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
  48. {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
  49. {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
  50. {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
  51. {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
  52. {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
  53. {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
  54. {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
  55. {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
  56. {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
  57. {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
  58. {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
  59. {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
  60. {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
  61. {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
  62. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/application/__init__.py +0 -0
  63. {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
  64. {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
  65. {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
  66. {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
  67. {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
  68. {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
  69. {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
  70. {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
  71. {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
  72. {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
  73. {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
  74. {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
  75. {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
  76. {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
  77. {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
  78. {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
  79. {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
  80. {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
  81. {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
  82. {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
  83. {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
  84. {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
  85. {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
  86. {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
  87. {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
  88. {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
  89. {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
  90. {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
  91. {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
  92. {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
  93. {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
  94. {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
  95. {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
  96. {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
  97. {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
  98. {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
  99. {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
  100. {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
  101. {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
  102. {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
  103. {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
  104. {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
  105. {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
  106. {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
  107. {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
  108. {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
  109. {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
  110. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/common/__init__.py +0 -0
  111. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/common/checksum.py +0 -0
  112. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/common/errors.py +0 -0
  113. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/src/generic_ml_cache_core/stream.py +0 -0
  114. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/conftest.py +0 -0
  115. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/fake_client.py +0 -0
  116. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_adapters.py +0 -0
  117. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_api_call_identity.py +0 -0
  118. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_api_client_port.py +0 -0
  119. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_artifact.py +0 -0
  120. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_blob_store_port.py +0 -0
  121. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_cache_mode.py +0 -0
  122. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_cacheability.py +0 -0
  123. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_call_identity_building.py +0 -0
  124. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_call_identity_serialization.py +0 -0
  125. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_checksum.py +0 -0
  126. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_client_run_result.py +0 -0
  127. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_client_runner_port.py +0 -0
  128. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_clock_port.py +0 -0
  129. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_composition.py +0 -0
  130. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_execution_failure.py +0 -0
  131. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_execution_kind.py +0 -0
  132. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_execution_state.py +0 -0
  133. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_file_content_fingerprint.py +0 -0
  134. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_file_fingerprint_port.py +0 -0
  135. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_filesystem_blob_store.py +0 -0
  136. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_journal_metrics.py +0 -0
  137. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_local_client_runner.py +0 -0
  138. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_managed_call_identity.py +0 -0
  139. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_message.py +0 -0
  140. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_message_fingerprinting.py +0 -0
  141. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_metrics_port.py +0 -0
  142. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_passthrough_call_identity.py +0 -0
  143. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_passthrough_client_runner.py +0 -0
  144. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_passthrough_runner_port.py +0 -0
  145. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_probe_report.py +0 -0
  146. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_probe_service.py +0 -0
  147. {generic_ml_cache_core-0.2.0 → generic_ml_cache_core-0.4.0}/tests/test_token_usage.py +0 -0
  148. {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.2.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.2.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 Artifact, ArtifactType
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=self._load_artifacts(connection, execution_id),
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 generated output an Artifact holds.
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, persist_output,
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): ``persist_output = False`` will be incompatible with async
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
- persist_output: bool = True
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 an output for a run that ended
36
- with ``succeeded``: never without ``persist_output``; a failure only with
37
- ``record_on_error``."""
38
- if not self.persist_output:
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
- persist_output: bool = True
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 an output for a run that ended
44
- with ``succeeded``: never without ``persist_output``; a failure only with
45
- ``record_on_error``."""
46
- if not self.persist_output:
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
- persist_output: bool = True
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 an output for a run that ended
31
- with ``succeeded``: never without ``persist_output``; a failure only with
32
- ``record_on_error``."""
33
- if not self.persist_output:
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
- artifacts=artifacts,
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
- from typing import Tuple
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"))]