digitalkin 1.0.5.dev5__tar.gz → 1.0.5.dev6__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 (270) hide show
  1. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/PKG-INFO +5 -3
  2. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/pyproject.toml +5 -3
  3. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/__version__.py +1 -1
  4. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/profiling/task_profiler.py +2 -2
  5. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/task_manager/module_runner.py +7 -1
  6. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/task_manager/redis/redis_client.py +1 -1
  7. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/task_manager/redis/redis_idempotency.py +6 -4
  8. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/task_manager/task_executor.py +4 -4
  9. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/task_manager/task_session.py +5 -5
  10. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/grpc_servers/gateway_servicer.py +86 -27
  11. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/grpc_servers/interceptors/validation.py +18 -3
  12. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/grpc_servers/module_server.py +0 -2
  13. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/module/setup_types.py +1 -1
  14. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/server/grpc.py +7 -0
  15. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/setup/default_setup.py +4 -4
  16. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/setup/setup_strategy.py +2 -2
  17. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/storage/default_storage.py +6 -8
  18. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/storage/storage_strategy.py +3 -3
  19. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin.egg-info/PKG-INFO +5 -3
  20. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin.egg-info/requires.txt +4 -2
  21. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/LICENSE +0 -0
  22. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/README.md +0 -0
  23. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/base_server/__init__.py +0 -0
  24. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/base_server/mock/__init__.py +0 -0
  25. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/base_server/mock/mock_pb2.py +0 -0
  26. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/base_server/mock/mock_pb2_grpc.py +0 -0
  27. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/base_server/server_async_insecure.py +0 -0
  28. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/base_server/server_async_secure.py +0 -0
  29. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/base_server/server_sync_insecure.py +0 -0
  30. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/base_server/server_sync_secure.py +0 -0
  31. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/bench_module/__init__.py +0 -0
  32. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/bench_module/echo_module.py +0 -0
  33. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/bench_module/models/__init__.py +0 -0
  34. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/bench_module/models/input.py +0 -0
  35. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/bench_module/models/output.py +0 -0
  36. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/bench_module/models/secret.py +0 -0
  37. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/bench_module/models/setup.py +0 -0
  38. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/bench_module/server.py +0 -0
  39. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/bench_module/triggers/__init__.py +0 -0
  40. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/bench_module/triggers/message_trigger.py +0 -0
  41. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/modules/__init__.py +0 -0
  42. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/modules/archetype_with_tools_module.py +0 -0
  43. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/modules/cpu_intensive_module.py +0 -0
  44. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/modules/dynamic_setup_module.py +0 -0
  45. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/modules/minimal_llm_module.py +0 -0
  46. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/modules/text_transform_module.py +0 -0
  47. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/monitoring/digitalkin_observability/__init__.py +0 -0
  48. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/monitoring/digitalkin_observability/http_server.py +0 -0
  49. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/monitoring/digitalkin_observability/interceptors.py +0 -0
  50. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/monitoring/digitalkin_observability/metrics.py +0 -0
  51. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/monitoring/digitalkin_observability/prometheus.py +0 -0
  52. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/monitoring/tests/test_metrics.py +0 -0
  53. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/redis_demo/client.py +0 -0
  54. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/redis_demo/echo_module.py +0 -0
  55. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/redis_demo/models/__init__.py +0 -0
  56. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/redis_demo/models/input.py +0 -0
  57. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/redis_demo/models/output.py +0 -0
  58. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/redis_demo/models/secret.py +0 -0
  59. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/redis_demo/models/setup.py +0 -0
  60. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/redis_demo/server.py +0 -0
  61. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/redis_demo/triggers/__init__.py +0 -0
  62. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/redis_demo/triggers/message_trigger.py +0 -0
  63. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/services/filesystem_module.py +0 -0
  64. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/examples/services/storage_module.py +0 -0
  65. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/setup.cfg +0 -0
  66. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/__init__.py +0 -0
  67. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/__init__.py +0 -0
  68. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/__init__.py +0 -0
  69. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/agno_adapter.py +0 -0
  70. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/agui_tools.py +0 -0
  71. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/hitl.py +0 -0
  72. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/models.py +0 -0
  73. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/module_toolkit.py +0 -0
  74. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/toolkits/__init__.py +0 -0
  75. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/toolkits/base.py +0 -0
  76. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/toolkits/chat_history.py +0 -0
  77. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/toolkits/defaults.py +0 -0
  78. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/toolkits/registry/__init__.py +0 -0
  79. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/toolkits/registry/action.py +0 -0
  80. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/toolkits/registry/base.py +0 -0
  81. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/toolkits/registry/kins/__init__.py +0 -0
  82. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/toolkits/registry/kins/action.py +0 -0
  83. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/toolkits/registry/kins/kit.py +0 -0
  84. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/toolkits/registry/loader/__init__.py +0 -0
  85. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/toolkits/registry/loader/action.py +0 -0
  86. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/toolkits/registry/loader/kit.py +0 -0
  87. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/toolkits/registry/services/__init__.py +0 -0
  88. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/toolkits/registry/services/action.py +0 -0
  89. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/toolkits/registry/services/kit.py +0 -0
  90. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/toolkits/registry/tools/__init__.py +0 -0
  91. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/toolkits/registry/tools/action.py +0 -0
  92. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/toolkits/registry/tools/kit.py +0 -0
  93. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/community/agno/toolkits/user_profile.py +0 -0
  94. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/__init__.py +0 -0
  95. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/common/__init__.py +0 -0
  96. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/common/factories.py +0 -0
  97. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/exceptions.py +0 -0
  98. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/job_manager/__init__.py +0 -0
  99. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/job_manager/base_job_manager.py +0 -0
  100. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/job_manager/single_job_manager.py +0 -0
  101. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/profiling/__init__.py +0 -0
  102. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/profiling/step_timer.py +0 -0
  103. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/resilience/__init__.py +0 -0
  104. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/resilience/bulkhead.py +0 -0
  105. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/resilience/task_supervisor.py +0 -0
  106. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/task_manager/__init__.py +0 -0
  107. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/task_manager/base_task_manager.py +0 -0
  108. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/task_manager/local_task_manager.py +0 -0
  109. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/task_manager/redis/__init__.py +0 -0
  110. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/task_manager/redis/proto_streams.py +0 -0
  111. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/task_manager/redis/redis_signal.py +0 -0
  112. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/core/task_manager/remote_task_manager.py +0 -0
  113. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/exceptions.py +0 -0
  114. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/grpc_servers/__init__.py +0 -0
  115. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/grpc_servers/_base_server.py +0 -0
  116. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/grpc_servers/exceptions.py +0 -0
  117. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/grpc_servers/interceptors/__init__.py +0 -0
  118. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/grpc_servers/interceptors/permission.py +0 -0
  119. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/grpc_servers/interceptors/request_ids.py +0 -0
  120. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/grpc_servers/m2m_call_registry.py +0 -0
  121. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/grpc_servers/module_servicer.py +0 -0
  122. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/grpc_servers/stream_registry.py +0 -0
  123. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/grpc_servers/stream_session.py +0 -0
  124. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/grpc_servers/utils/__init__.py +0 -0
  125. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/grpc_servers/utils/circuit_breaker.py +0 -0
  126. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/grpc_servers/utils/grpc_client_wrapper.py +0 -0
  127. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/grpc_servers/utils/grpc_error_handler.py +0 -0
  128. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/grpc_servers/utils/utility_schema_extender.py +0 -0
  129. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/grpc_servers/utils/validators.py +0 -0
  130. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/logger.py +0 -0
  131. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/mixins/__init__.py +0 -0
  132. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/mixins/agui_mixin.py +0 -0
  133. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/mixins/base_mixin.py +0 -0
  134. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/mixins/cost_mixin.py +0 -0
  135. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/mixins/file_history_mixin.py +0 -0
  136. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/mixins/filesystem_mixin.py +0 -0
  137. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/mixins/logger_mixin.py +0 -0
  138. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/mixins/storage_mixin.py +0 -0
  139. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/__init__.py +0 -0
  140. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/core/__init__.py +0 -0
  141. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/core/job_manager_models.py +0 -0
  142. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/core/redis.py +0 -0
  143. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/core/task_monitor.py +0 -0
  144. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/events/__init__.py +0 -0
  145. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/events/agent_events.py +0 -0
  146. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/grpc_servers/__init__.py +0 -0
  147. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/grpc_servers/circuit_breaker.py +0 -0
  148. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/grpc_servers/m2m.py +0 -0
  149. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/grpc_servers/models.py +0 -0
  150. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/grpc_servers/stream_error_codes.py +0 -0
  151. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/grpc_servers/types.py +0 -0
  152. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/module/__init__.py +0 -0
  153. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/module/ag_ui.py +0 -0
  154. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/module/base_types.py +0 -0
  155. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/module/knowledge_files.py +0 -0
  156. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/module/loaded_tools.py +0 -0
  157. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/module/module.py +0 -0
  158. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/module/module_context.py +0 -0
  159. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/module/module_types.py +0 -0
  160. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/module/request_metadata.py +0 -0
  161. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/module/select_schema.py +0 -0
  162. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/module/tool_cache.py +0 -0
  163. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/module/tool_reference.py +0 -0
  164. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/module/utility.py +0 -0
  165. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/services/__init__.py +0 -0
  166. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/services/cost.py +0 -0
  167. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/services/filesystem.py +0 -0
  168. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/services/registry.py +0 -0
  169. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/services/services.py +0 -0
  170. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/services/storage.py +0 -0
  171. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/__init__.py +0 -0
  172. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/certificate.py +0 -0
  173. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/client/__init__.py +0 -0
  174. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/client/channel.py +0 -0
  175. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/client/circuit_breaker.py +0 -0
  176. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/client/client.py +0 -0
  177. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/client/grpc.py +0 -0
  178. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/gateway.py +0 -0
  179. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/log.py +0 -0
  180. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/module.py +0 -0
  181. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/profiling.py +0 -0
  182. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/queue.py +0 -0
  183. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/redis.py +0 -0
  184. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/registry.py +0 -0
  185. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/resilience.py +0 -0
  186. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/server/__init__.py +0 -0
  187. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/server/channel.py +0 -0
  188. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/server/server.py +0 -0
  189. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/server/servicer.py +0 -0
  190. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/task_manager.py +0 -0
  191. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/utils/__init__.py +0 -0
  192. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/settings/utils/channel.py +0 -0
  193. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/utils/__init__.py +0 -0
  194. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/models/utils/dynamic_schema.py +0 -0
  195. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/modules/__init__.py +0 -0
  196. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/modules/_base_module.py +0 -0
  197. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/modules/archetype_module.py +0 -0
  198. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/modules/tool_module.py +0 -0
  199. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/modules/trigger_handler.py +0 -0
  200. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/modules/triggers/__init__.py +0 -0
  201. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/modules/triggers/healthcheck_ping_trigger.py +0 -0
  202. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/modules/triggers/healthcheck_services_trigger.py +0 -0
  203. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/modules/triggers/healthcheck_status_trigger.py +0 -0
  204. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/py.typed +0 -0
  205. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/__init__.py +0 -0
  206. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/base_strategy.py +0 -0
  207. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/communication/__init__.py +0 -0
  208. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/communication/communication_strategy.py +0 -0
  209. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/communication/default_communication.py +0 -0
  210. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/communication/exceptions.py +0 -0
  211. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/communication/grpc_communication.py +0 -0
  212. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/cost/__init__.py +0 -0
  213. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/cost/cost_strategy.py +0 -0
  214. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/cost/default_cost.py +0 -0
  215. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/cost/exceptions.py +0 -0
  216. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/cost/grpc_cost.py +0 -0
  217. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/filesystem/__init__.py +0 -0
  218. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/filesystem/default_filesystem.py +0 -0
  219. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/filesystem/exceptions.py +0 -0
  220. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/filesystem/filesystem_strategy.py +0 -0
  221. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/filesystem/grpc_filesystem.py +0 -0
  222. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/identity/__init__.py +0 -0
  223. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/identity/default_identity.py +0 -0
  224. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/identity/identity_strategy.py +0 -0
  225. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/registry/__init__.py +0 -0
  226. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/registry/default_registry.py +0 -0
  227. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/registry/exceptions.py +0 -0
  228. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/registry/grpc_registry.py +0 -0
  229. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/registry/registry_strategy.py +0 -0
  230. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/secret/__init__.py +0 -0
  231. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/secret/default_secret.py +0 -0
  232. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/secret/exceptions.py +0 -0
  233. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/secret/grpc_secret.py +0 -0
  234. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/secret/secret_strategy.py +0 -0
  235. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/services_config.py +0 -0
  236. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/services_models.py +0 -0
  237. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/setup/__init__.py +0 -0
  238. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/setup/exceptions.py +0 -0
  239. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/setup/grpc_setup.py +0 -0
  240. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/storage/__init__.py +0 -0
  241. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/storage/exceptions.py +0 -0
  242. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/storage/grpc_storage.py +0 -0
  243. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/task_manager/__init__.py +0 -0
  244. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/task_manager/default_task_manager.py +0 -0
  245. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/task_manager/exceptions.py +0 -0
  246. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/task_manager/redis_task_manager.py +0 -0
  247. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/task_manager/task_manager_strategy.py +0 -0
  248. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/user_profile/__init__.py +0 -0
  249. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/user_profile/default_user_profile.py +0 -0
  250. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/user_profile/exceptions.py +0 -0
  251. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/user_profile/grpc_user_profile.py +0 -0
  252. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/services/user_profile/user_profile_strategy.py +0 -0
  253. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/utils/__init__.py +0 -0
  254. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/utils/arg_parser.py +0 -0
  255. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/utils/conditional_schema.py +0 -0
  256. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/utils/development_mode_action.py +0 -0
  257. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/utils/dynamic_schema.py +0 -0
  258. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/utils/env_manager.py +0 -0
  259. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/utils/env_template.py +0 -0
  260. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/utils/exceptions.py +0 -0
  261. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/utils/json_structure.py +0 -0
  262. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/utils/llm_ready_schema.py +0 -0
  263. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/utils/package_discover.py +0 -0
  264. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/utils/proto_utils.py +0 -0
  265. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/utils/schema_splitter.py +0 -0
  266. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin/utils/setup_content_validator.py +0 -0
  267. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin.egg-info/SOURCES.txt +0 -0
  268. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin.egg-info/dependency_links.txt +0 -0
  269. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/src/digitalkin.egg-info/top_level.txt +0 -0
  270. {digitalkin-1.0.5.dev5 → digitalkin-1.0.5.dev6}/tests/test_exceptions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: digitalkin
3
- Version: 1.0.5.dev5
3
+ Version: 1.0.5.dev6
4
4
  Summary: SDK to build kin used in DigitalKin
5
5
  Author-email: "DigitalKin.ai" <contact@digitalkin.ai>
6
6
  License: Attribution-NonCommercial-ShareAlike 4.0 International
@@ -454,16 +454,18 @@ Description-Content-Type: text/markdown
454
454
  License-File: LICENSE
455
455
  Requires-Dist: ag-ui-protocol==0.1.21.dev1787220377
456
456
  Requires-Dist: agentic-mesh-protocol==1.2.0
457
- Requires-Dist: agno<3,>=2.8.0
457
+ Requires-Dist: agno<4,>=3.1.1
458
458
  Requires-Dist: anyio>=4.13.0
459
+ Requires-Dist: cel-python>=0.5
459
460
  Requires-Dist: grpcio-health-checking==1.82.1
460
461
  Requires-Dist: grpcio-reflection==1.82.1
461
462
  Requires-Dist: grpcio-status==1.82.1
463
+ Requires-Dist: protovalidate<2,>=1.2.0
462
464
  Requires-Dist: pydantic-settings>=2.14.1
463
465
  Requires-Dist: pydantic>=2.12.4
464
466
  Requires-Dist: redis[hiredis]<9,>=7.4.0
465
467
  Provides-Extra: agno
466
- Requires-Dist: agno>=2.6; extra == "agno"
468
+ Requires-Dist: agno<4,>=3.1.1; extra == "agno"
467
469
  Provides-Extra: performance
468
470
  Requires-Dist: uvloop>=0.21; extra == "performance"
469
471
  Provides-Extra: profiling
@@ -32,19 +32,21 @@
32
32
  # because it is a dated prerelease; move to >=0.1.21 once that ships stable.
33
33
  "ag-ui-protocol==0.1.21.dev1787220377",
34
34
  "agentic-mesh-protocol==1.2.0",
35
- "agno>=2.8.0,<3",
35
+ "agno>=3.1.1,<4",
36
36
  "anyio>=4.13.0",
37
+ "cel-python>=0.5",
37
38
  "grpcio-health-checking==1.82.1",
38
39
  "grpcio-reflection==1.82.1",
39
40
  "grpcio-status==1.82.1",
41
+ "protovalidate>=1.2.0,<2",
40
42
  "pydantic-settings>=2.14.1",
41
43
  "pydantic>=2.12.4",
42
44
  "redis[hiredis]>=7.4.0,<9",
43
45
  ]
44
- version = "1.0.5.dev5"
46
+ version = "1.0.5.dev6"
45
47
 
46
48
  [project.optional-dependencies]
47
- agno = [ "agno>=2.6" ]
49
+ agno = [ "agno>=3.1.1,<4" ]
48
50
  performance = [ "uvloop>=0.21" ]
49
51
  profiling = [
50
52
  "pyinstrument>=5.1.2",
@@ -5,4 +5,4 @@ from importlib.metadata import PackageNotFoundError, version
5
5
  try:
6
6
  __version__ = version("digitalkin")
7
7
  except PackageNotFoundError:
8
- __version__ = "1.0.5.dev5"
8
+ __version__ = "1.0.5.dev6"
@@ -1,6 +1,6 @@
1
1
  """Per-task profiling wrapper for VizTracer, Yappi, and Pyinstrument."""
2
2
 
3
- import datetime
3
+ import datetime as dt
4
4
  import io
5
5
  import os
6
6
  from pathlib import Path
@@ -101,7 +101,7 @@ class TaskProfiler:
101
101
  return
102
102
 
103
103
  try: # ruff: ignore[too-many-statements-in-try-clause]
104
- timestamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%S%f")
104
+ timestamp = dt.datetime.now(dt.timezone.utc).strftime("%Y%m%dT%H%M%S%f")
105
105
  base = f"{self._task_id}_{timestamp}"
106
106
 
107
107
  if self._mode == ProfilerMode.VIZTRACER:
@@ -151,9 +151,13 @@ class ModuleRunner:
151
151
  last_write_ns = time.monotonic_ns()
152
152
  data = output_data.model_dump(mode="json")
153
153
  if data.get("root", {}).get("protocol") == "stream.end":
154
+ if eos_written:
155
+ # TODO(validate): EOS-ONCE a stream gets a single end-of-stream marker
156
+ logger.info("[VALIDATE EOS-ONCE] duplicate stream.end skipped", extra=log_extra)
157
+ return
154
158
  t_eos_write_start = time.perf_counter_ns()
155
- eos_written = True
156
159
  await self._redis_client.xadd(stream_key, {"eos": b"true"})
160
+ eos_written = True
157
161
  await self._redis_client.expire(stream_key, stream_settings.redis_stream_ttl)
158
162
  await self._redis_client.expire(idem_key, stream_settings.redis_stream_ttl)
159
163
  # TODO(validate): IDEM-TTL-EOS idem TTL drops to redis_stream_ttl once EOS is written
@@ -372,6 +376,8 @@ class ModuleRunner:
372
376
  logger.warning(
373
377
  "[VALIDATE RUNNER-LEAK] task never created; stopping preloaded instance", extra=log_extra
374
378
  )
379
+ # Only a failure path gets here, and on_fatal already wrote stream.error + EOS.
380
+ eos_written = True
375
381
  try:
376
382
  await module.stop(cancel_reason)
377
383
  finally:
@@ -331,7 +331,7 @@ class RedisClient: # ruff: ignore[too-many-public-methods]
331
331
  """
332
332
  return await self._client.decr(name)
333
333
 
334
- async def eval(self, script: str, keys: list[str], args: list[str]) -> int | str | bytes | None:
334
+ async def eval(self, script: str, keys: list[str], args: list[str]) -> Any:
335
335
  """Execute a Lua script on Redis.
336
336
 
337
337
  Args:
@@ -33,11 +33,12 @@ class RedisIdempotency:
33
33
  """
34
34
  self._redis_client = redis_client
35
35
 
36
- async def claim(self, task_id: str, instance_id: str) -> ClaimResult:
36
+ async def claim(self, task_id: str, instance_id: str, mission_id: str) -> ClaimResult:
37
37
  """Atomically claim execution of ``task_id``.
38
38
 
39
- ``CLAIMED`` on the first claim; ``RECLAIMED`` if ``instance_id``
40
- already owns it (same replica retrying, TTL left untouched); ``TAKEN``
39
+ The claim stores ``{instance_id}|{mission_id}`` so ``SendSignal`` can tie a
40
+ cancel to the owning mission. ``CLAIMED`` on the first claim; ``RECLAIMED``
41
+ if the same owner already holds it (same replica retrying, TTL left untouched); ``TAKEN``
41
42
  if another replica owns it. The GET/SET is a single Lua eval so concurrent
42
43
  callers can never both win. The script returns the ``ClaimResult``
43
44
  integer value (0/1/2) — a Redis integer reply.
@@ -45,6 +46,7 @@ class RedisIdempotency:
45
46
  Args:
46
47
  task_id: Task whose execution is being claimed.
47
48
  instance_id: Stable per-process identifier of the claimer.
49
+ mission_id: Mission that owns the task.
48
50
 
49
51
  Returns:
50
52
  The claim outcome.
@@ -63,7 +65,7 @@ class RedisIdempotency:
63
65
  raw = await self._redis_client.eval(
64
66
  script,
65
67
  [f"idem:{task_id}"],
66
- [instance_id, str(get_redis_settings().idem_ttl)],
68
+ [f"{instance_id}|{mission_id}", str(get_redis_settings().idem_ttl)],
67
69
  )
68
70
  value = raw.decode() if isinstance(raw, bytes) else raw
69
71
  # An unexpected nil reply is treated as TAKEN so we never double-execute.
@@ -9,7 +9,7 @@ hard cancel that only changes the recorded reason.
9
9
  """
10
10
 
11
11
  import asyncio
12
- import datetime
12
+ import datetime as dt
13
13
  from collections.abc import Awaitable, Callable, Coroutine
14
14
  from typing import Any
15
15
 
@@ -82,7 +82,7 @@ class TaskExecutor:
82
82
  async def _run() -> None:
83
83
  nonlocal started
84
84
  started = True
85
- session.started_at = datetime.datetime.now(datetime.timezone.utc)
85
+ session.started_at = dt.datetime.now(dt.timezone.utc)
86
86
  try:
87
87
  await session.set_status("running")
88
88
  await coro
@@ -100,7 +100,7 @@ class TaskExecutor:
100
100
  logger.exception("Task failed: '%s'", task_id, extra=ids)
101
101
  finally:
102
102
  coro.close()
103
- session.completed_at = datetime.datetime.now(datetime.timezone.utc)
103
+ session.completed_at = dt.datetime.now(dt.timezone.utc)
104
104
  session.close_stream()
105
105
 
106
106
  duration = (
@@ -118,7 +118,7 @@ class TaskExecutor:
118
118
  await _finalize()
119
119
 
120
120
  async def _early_cancel() -> None:
121
- session.completed_at = datetime.datetime.now(datetime.timezone.utc)
121
+ session.completed_at = dt.datetime.now(dt.timezone.utc)
122
122
  await _cancelled()
123
123
  session.close_stream()
124
124
  await _finalize()
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- import datetime
6
+ import datetime as dt
7
7
  import time
8
8
  import traceback
9
9
  from typing import TYPE_CHECKING
@@ -30,9 +30,9 @@ class TaskSession:
30
30
  task_id: str
31
31
  mission_id: str
32
32
 
33
- created_at: datetime.datetime
34
- started_at: datetime.datetime | None
35
- completed_at: datetime.datetime | None
33
+ created_at: dt.datetime
34
+ started_at: dt.datetime | None
35
+ completed_at: dt.datetime | None
36
36
 
37
37
  is_cancelled: asyncio.Event
38
38
  cancellation_reason: CancellationReason
@@ -72,7 +72,7 @@ class TaskSession:
72
72
  self.task_id = task_id
73
73
  self.mission_id = mission_id
74
74
 
75
- self.created_at = datetime.datetime.now(datetime.timezone.utc)
75
+ self.created_at = dt.datetime.now(dt.timezone.utc)
76
76
  self.started_at = None
77
77
  self.completed_at = None
78
78
 
@@ -166,7 +166,7 @@ class GatewayServicer:
166
166
  """
167
167
  await context.abort(grpc.StatusCode.UNIMPLEMENTED, "AssociateTask is served by the backend")
168
168
 
169
- async def StartStream( # ruff: ignore[too-many-return-statements, too-many-locals, too-many-statements]
169
+ async def StartStream( # ruff: ignore[complex-structure, too-many-return-statements, too-many-locals, too-many-statements]
170
170
  self,
171
171
  request: Any,
172
172
  context: grpc.aio.ServicerContext,
@@ -225,7 +225,7 @@ class GatewayServicer:
225
225
  # consumer), so a re-issued StartStream for an already-claimed/running task is
226
226
  # REFUSED — one task_id maps to exactly one dial.
227
227
  try:
228
- claim = await self._idempotency.claim(task_id, SharedRedisListener.PROCESS_ID)
228
+ claim = await self._idempotency.claim(task_id, SharedRedisListener.PROCESS_ID, request.mission_id)
229
229
  except RedisError:
230
230
  return gateway_dto_pb2.StartStreamResponse(accepted=False, task_id=task_id)
231
231
  timer.mark("idempotency_claim")
@@ -260,18 +260,36 @@ class GatewayServicer:
260
260
  stream_key = f"task:{task_id}:stream"
261
261
  # A fresh claim owns the task: drop any stream/cursor left by a released failed dial so a
262
262
  # retry never replays its stream.error + eos, and arm a TTL before the first output.
263
+ cancel_key = f"cancel:{task_id}"
263
264
  pipe = self._redis_client.pipeline()
264
265
  pipe.delete(stream_key, f"task:{task_id}:cursor")
265
266
  pipe.xadd(stream_key, {"pb": start_info.SerializeToString(), "seq": "0"})
266
267
  pipe.expire(stream_key, get_gateway_settings().stream.redis_stream_initial_ttl)
268
+ pipe.get(cancel_key)
267
269
  try:
268
- stale, _, _ = await pipe.execute()
270
+ stale, _, _, tombstone = await pipe.execute()
269
271
  except RedisError:
270
272
  # Claimed + registered but the stream couldn't be seeded: undo both so a retry re-runs.
271
273
  with contextlib.suppress(RedisError):
272
274
  await self._idempotency.release(task_id)
273
275
  await self._registry.unregister(task_id)
274
276
  return gateway_dto_pb2.StartStreamResponse(accepted=False, task_id=task_id)
277
+ # A pre-start cancel binds the task unless another mission sent it (a mission-less one binds).
278
+ if tombstone and tombstone.decode() != request.mission_id:
279
+ # Compare-and-delete: a cancel the owner sends meanwhile overwrites the value and is kept.
280
+ with contextlib.suppress(RedisError):
281
+ dropped = await self._redis_client.eval(
282
+ "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end return 0",
283
+ [cancel_key],
284
+ [tombstone.decode()],
285
+ )
286
+ # TODO(validate): CANCEL-FOREIGN a cancel tombstone from another mission is dropped at StartStream
287
+ logger.warning(
288
+ "[VALIDATE CANCEL-FOREIGN] cancel tombstone from mission=%r dropped=%s",
289
+ tombstone,
290
+ bool(dropped),
291
+ extra=log_extra,
292
+ )
275
293
  timer.mark("xadd_stream_start")
276
294
  # TODO(validate): SEED-TTL stream.start seed carries a TTL and drops stale keys
277
295
  logger.info("[VALIDATE SEED-TTL] stream.start seeded with TTL; stale keys dropped=%s", stale, extra=log_extra)
@@ -428,9 +446,10 @@ class GatewayServicer:
428
446
 
429
447
  if first_msg.data and len(first_msg.data.fields) > 0:
430
448
  # TODO(validate): NO-INPUT follow-up data is dropped when there is no input consumer
431
- logger.warning(
432
- "[VALIDATE NO-INPUT] Stream first message data dropped (no input consumer): %s",
433
- first_msg.data,
449
+ logger.info(
450
+ "[VALIDATE NO-INPUT] Stream first message data is not used yet; dropped bytes=%d keys=%s",
451
+ first_msg.data.ByteSize(),
452
+ list(first_msg.data.fields),
434
453
  extra={"task_id": task_id},
435
454
  )
436
455
 
@@ -470,9 +489,10 @@ class GatewayServicer:
470
489
  break
471
490
  if msg.data and len(msg.data.fields) > 0:
472
491
  # TODO(validate): NO-INPUT follow-up data is dropped when there is no input consumer
473
- logger.warning(
474
- "[VALIDATE NO-INPUT] Stream follow-up dropped (no input consumer): %s",
475
- msg.data,
492
+ logger.info(
493
+ "[VALIDATE NO-INPUT] Stream follow-up input is not used yet; dropped bytes=%d keys=%s",
494
+ msg.data.ByteSize(),
495
+ list(msg.data.fields),
476
496
  extra={"task_id": task_id},
477
497
  )
478
498
  except asyncio.CancelledError:
@@ -480,7 +500,7 @@ class GatewayServicer:
480
500
  except Exception:
481
501
  logger.exception("Peer upstream reader error: task_id=%s", task_id)
482
502
 
483
- async def SendSignal(
503
+ async def SendSignal( # ruff: ignore[complex-structure, too-many-return-statements]
484
504
  self,
485
505
  request: Any,
486
506
  context: grpc.aio.ServicerContext, # ruff: ignore[unused-method-argument]
@@ -555,22 +575,46 @@ class GatewayServicer:
555
575
  timer.mark("validate_task_id")
556
576
  last_mark = "validate_task_id"
557
577
 
558
- # The task may run on any replica, or not be registered yet: always publish, and
559
- # leave a durable tombstone the ModuleRunner checks around task registration.
560
- pipe = self._redis_client.pipeline()
561
- pipe.exists(f"idem:{task_id}")
562
- pipe.set(f"cancel:{task_id}", "1", ex=get_gateway_settings().stream.redis_stream_initial_ttl)
563
- pipe.publish(
564
- f"signal_ch:{task_id}",
565
- json.dumps({
566
- "action": "cancel",
567
- "task_id": task_id,
568
- "published_at_ns": time.time_ns(),
569
- }),
578
+ # The task may run on any replica, or not be registered yet: publish, and leave a
579
+ # durable tombstone (the caller's mission) the ModuleRunner checks around registration.
580
+ # A claimed task (``idem`` = ``{process}|{mission}``) only accepts its own mission's cancel;
581
+ # a caller without ``x-mission-id`` is let through and logged (CANCEL-NO-MISSION).
582
+ mission_id = RequestContext.current().get("mission_id", "")
583
+ idem_exists, receivers = await self._redis_client.eval(
584
+ "local owner = redis.call('GET', KEYS[1])\n"
585
+ "local sep = owner and string.find(owner, '|', 1, true)\n"
586
+ "if sep and ARGV[1] ~= '' and string.sub(owner, sep + 1) ~= ARGV[1] then\n"
587
+ " return {-1, 0}\n"
588
+ "end\n"
589
+ "redis.call('SET', KEYS[2], ARGV[1], 'EX', tonumber(ARGV[2]))\n"
590
+ "return {owner and 1 or 0, redis.call('PUBLISH', KEYS[3], ARGV[3])}",
591
+ [f"idem:{task_id}", f"cancel:{task_id}", f"signal_ch:{task_id}"],
592
+ [
593
+ mission_id,
594
+ str(get_gateway_settings().stream.redis_stream_initial_ttl),
595
+ json.dumps({"action": "cancel", "task_id": task_id, "published_at_ns": time.time_ns()}),
596
+ ],
570
597
  )
571
- idem_exists, _, receivers = await pipe.execute()
572
598
  timer.mark("redis_publish")
573
599
  last_mark = "redis_publish"
600
+ if idem_exists == -1:
601
+ # TODO(validate): CANCEL-OWNER a cancel from another mission is refused and leaves no tombstone
602
+ logger.warning(
603
+ "[VALIDATE CANCEL-OWNER] SendSignal_failed: failure=Forbidden caller_mission=%r "
604
+ "does not own the task; nothing published signal=%s task_id=%s",
605
+ mission_id,
606
+ signal,
607
+ task_id,
608
+ extra=log_extra,
609
+ )
610
+ return gateway_dto_pb2.SendSignalResponse(success=False, task_id=task_id)
611
+ if not mission_id:
612
+ # TODO(validate): CANCEL-NO-MISSION a cancel without x-mission-id skips the owner check
613
+ logger.warning(
614
+ "[VALIDATE CANCEL-NO-MISSION] cancel without x-mission-id accepted unchecked task_id=%s",
615
+ task_id,
616
+ extra=log_extra,
617
+ )
574
618
  # TODO(validate): SIGNAL-PUB SendSignal publishes without the local registry
575
619
  logger.info(
576
620
  "[VALIDATE SIGNAL-PUB] SendSignal published: signal=%s idem_exists=%s receivers=%s task_id=%s",
@@ -582,7 +626,8 @@ class GatewayServicer:
582
626
  )
583
627
  if not idem_exists:
584
628
  logger.warning(
585
- "[gateway] SendSignal_failed: failure=TaskNotFound at_step=%s elapsed_ms=%.2f signal=%s task_id=%s",
629
+ "[gateway] SendSignal_failed: failure=TaskNotFound at_step=%s elapsed_ms=%.2f signal=%s "
630
+ "task_id=%s; tombstone kept for a later StartStream",
586
631
  last_mark,
587
632
  timer.elapsed_now_ms(),
588
633
  signal,
@@ -600,6 +645,19 @@ class GatewayServicer:
600
645
  )
601
646
  return gateway_dto_pb2.SendSignalResponse(success=True, task_id=task_id)
602
647
 
648
+ except RedisError as exc:
649
+ # The response has no reason field: the log is the only place the cause is recorded.
650
+ logger.error(
651
+ "[gateway] SendSignal_failed: failure=RedisUnavailable (%s) at_step=%s elapsed_ms=%.2f signal=%s "
652
+ "task_id=%s; signal not delivered",
653
+ type(exc).__name__,
654
+ last_mark,
655
+ timer.elapsed_now_ms(),
656
+ signal,
657
+ task_id,
658
+ extra=log_extra,
659
+ )
660
+ return gateway_dto_pb2.SendSignalResponse(success=False, task_id=task_id)
603
661
  except Exception as exc:
604
662
  logger.warning(
605
663
  "[gateway] SendSignal_failed: failure=%s at_step=%s elapsed_ms=%.2f signal=%s task_id=%s",
@@ -1114,9 +1172,10 @@ class GatewayServicer:
1114
1172
  first = False
1115
1173
  continue
1116
1174
  # TODO(validate): NO-INPUT follow-up data is dropped when there is no input consumer
1117
- logger.warning(
1118
- "[VALIDATE NO-INPUT] Dial-back follow-up dropped (no input consumer): %s",
1119
- upstream.data,
1175
+ logger.info(
1176
+ "[VALIDATE NO-INPUT] Dial-back follow-up input is not used yet; dropped bytes=%d keys=%s",
1177
+ upstream.data.ByteSize(),
1178
+ list(upstream.data.fields),
1120
1179
  extra=log_extra,
1121
1180
  )
1122
1181
  except grpc.aio.AioRpcError as exc:
@@ -2,13 +2,18 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import asyncio
5
6
  import inspect
6
7
  from typing import TYPE_CHECKING, Any
7
8
 
9
+ import celpy
8
10
  import grpc
9
11
  import grpc.aio
10
12
  import protovalidate
11
13
 
14
+ from digitalkin.logger import logger
15
+ from digitalkin.models.settings.server.server import get_server_settings
16
+
12
17
  if TYPE_CHECKING:
13
18
  from collections.abc import AsyncIterator, Awaitable, Callable
14
19
 
@@ -16,6 +21,8 @@ if TYPE_CHECKING:
16
21
  class ValidationServerInterceptor(grpc.aio.ServerInterceptor):
17
22
  """Reject a unary request that breaks its ``buf.validate`` rules with ``INVALID_ARGUMENT``.
18
23
 
24
+ A validator failure (rule compilation or evaluation) aborts with ``INTERNAL``.
25
+
19
26
  Request-streaming RPCs pass through untouched: the gateway ``Stream`` reports
20
27
  failures in-band as ``stream.error`` and never aborts. Synchronous handlers
21
28
  (e.g. the ``grpc_health`` ``HealthServicer``) also pass through untouched:
@@ -23,7 +30,7 @@ class ValidationServerInterceptor(grpc.aio.ServerInterceptor):
23
30
  would bypass by awaiting their plain return value.
24
31
  """
25
32
 
26
- async def intercept_service( # ruff: ignore[no-self-use]
33
+ async def intercept_service( # ruff: ignore[no-self-use, complex-structure]
27
34
  self,
28
35
  continuation: Callable[[grpc.HandlerCallDetails], Awaitable[grpc.RpcMethodHandler[Any, Any] | None]],
29
36
  handler_call_details: grpc.HandlerCallDetails,
@@ -46,13 +53,21 @@ class ValidationServerInterceptor(grpc.aio.ServerInterceptor):
46
53
 
47
54
  async def _validate(request: Any, context: grpc.aio.ServicerContext) -> None:
48
55
  try:
49
- protovalidate.validate(request)
56
+ # Validation costs ~0.3 ms/KiB of CPU: large requests leave the event loop.
57
+ if request.ByteSize() > get_server_settings().grpc.validation_offload_bytes:
58
+ await asyncio.to_thread(protovalidate.validate, request)
59
+ else:
60
+ protovalidate.validate(request)
50
61
  except protovalidate.ValidationError as e:
51
62
  details = "; ".join(
52
63
  f"{'.'.join(el.field_name for el in v.proto.field.elements) or v.proto.rule_id}: {v.proto.message}"
53
- for v in e.violations
64
+ for v in e.violations[:10]
54
65
  )
55
66
  await context.abort(grpc.StatusCode.INVALID_ARGUMENT, f"invalid {request.DESCRIPTOR.name}: {details}")
67
+ except (protovalidate.CompilationError, celpy.CELEvalError):
68
+ # A rule that fails to compile or evaluate is a server defect; keep its text out of the reply.
69
+ logger.exception("Request validation could not run for %s", request.DESCRIPTOR.full_name)
70
+ await context.abort(grpc.StatusCode.INTERNAL, f"could not validate {request.DESCRIPTOR.name}")
56
71
 
57
72
  if handler.response_streaming:
58
73
  stream_behavior: Any = handler.unary_stream
@@ -134,8 +134,6 @@ class ModuleServer(BaseServer):
134
134
  service_descriptor=gateway_service_pb2.DESCRIPTOR, # type: ignore[arg-type] # protobuf FileDescriptor; see above
135
135
  )
136
136
 
137
- logger.info("GatewayServicer + ModuleRunner registered (Redis: %s)", redis_url)
138
-
139
137
  listener = SharedRedisListener.singleton_or_none()
140
138
  if listener is not None:
141
139
  listener.set_cache_invalidator(self._handle_cache_invalidation)
@@ -251,7 +251,7 @@ class SetupModel(BaseModel, Generic[SetupModelT]):
251
251
  return results
252
252
 
253
253
  @classmethod
254
- async def _refresh_annotation(cls, annotation: "type | None") -> "type | None":
254
+ async def _refresh_annotation(cls, annotation: Any) -> "type | None":
255
255
  """Refresh dynamic fields in an annotation, handling Unions and single models.
256
256
 
257
257
  Args:
@@ -20,6 +20,7 @@ class GrpcServerSettings(BaseSettings):
20
20
  max_send_message_length (NonNegativeInt): Maximum message size the server can send, in bytes.
21
21
  max_pings_without_data (NonNegativeInt): Maximum number of pings the server allows without receiving any data.
22
22
  keepalive_permit_without_calls (bool): Allow clients to send keepalive pings even when there are no active RPCs.
23
+ validation_offload_bytes (NonNegativeInt): Request size above which ``buf.validate`` runs in a worker thread.
23
24
 
24
25
  """
25
26
 
@@ -74,6 +75,12 @@ class GrpcServerSettings(BaseSettings):
74
75
  "alive through proxies and detecting dead clients.",
75
76
  alias="SERVER_GRPC_OPTIONS_KEEPALIVE_PERMIT_WITHOUT_CALLS",
76
77
  )
78
+ validation_offload_bytes: NonNegativeInt = Field(
79
+ 96 * 1024,
80
+ description="Request size above which buf.validate runs in a worker thread instead of on the event "
81
+ "loop. Validation costs ~0.3 ms/KiB, so the default keeps inline validation under ~30 ms.",
82
+ alias="SERVER_GRPC_VALIDATION_OFFLOAD_BYTES",
83
+ )
77
84
 
78
85
  @property
79
86
  def options(self) -> list[tuple[str, Any]]:
@@ -1,6 +1,6 @@
1
1
  """In-memory setup strategy mirroring the SetupService protocol."""
2
2
 
3
- import datetime
3
+ import datetime as dt
4
4
  import secrets
5
5
  import string
6
6
  from typing import Any
@@ -187,7 +187,7 @@ class DefaultSetup(SetupStrategy):
187
187
  documentation=setup_dict.get("documentation") or "",
188
188
  content=setup_dict.get("content") or {},
189
189
  structure=JsonStructure.clip(setup_dict.get("structure") or {}),
190
- creation_date=datetime.datetime.now(datetime.timezone.utc),
190
+ creation_date=dt.datetime.now(dt.timezone.utc),
191
191
  ),
192
192
  )
193
193
  except ValidationError as e:
@@ -239,7 +239,7 @@ class DefaultSetup(SetupStrategy):
239
239
  documentation=setup_dict.get("documentation") or "",
240
240
  content=content,
241
241
  structure=JsonStructure.clip(setup_dict.get("structure") or {}),
242
- creation_date=datetime.datetime.now(datetime.timezone.utc),
242
+ creation_date=dt.datetime.now(dt.timezone.utc),
243
243
  )
244
244
  history.append(version)
245
245
  if setup_dict.get("set_as_current", True):
@@ -315,7 +315,7 @@ class DefaultSetup(SetupStrategy):
315
315
  documentation=setup_dict.get("documentation") or "",
316
316
  content=content,
317
317
  structure=JsonStructure.clip(setup_dict.get("structure") or {}),
318
- creation_date=datetime.datetime.now(datetime.timezone.utc),
318
+ creation_date=dt.datetime.now(dt.timezone.utc),
319
319
  )
320
320
  self.versions.setdefault(setup.id, [setup.current_setup_version]).append(version)
321
321
  if setup_dict.get("set_as_current"):
@@ -1,6 +1,6 @@
1
1
  """This module contains the abstract base class for setup strategies."""
2
2
 
3
- import datetime
3
+ import datetime as dt
4
4
  from abc import ABC, abstractmethod
5
5
  from typing import Any
6
6
 
@@ -31,7 +31,7 @@ class SetupVersionData(BaseModel):
31
31
  documentation: str = ""
32
32
  content: dict[str, Any]
33
33
  structure: dict[str, str] | None = None
34
- creation_date: datetime.datetime = Field(validation_alias=AliasChoices("creation_date", "created_at"))
34
+ creation_date: dt.datetime = Field(validation_alias=AliasChoices("creation_date", "created_at"))
35
35
 
36
36
 
37
37
  class SetupVersionPage(BaseModel):
@@ -1,6 +1,6 @@
1
1
  """This module implements the default storage strategy."""
2
2
 
3
- import datetime
3
+ import datetime as dt
4
4
  import json
5
5
  import tempfile
6
6
  import uuid
@@ -37,7 +37,7 @@ class DefaultStorage(StorageStrategy):
37
37
  Raises:
38
38
  TypeError: If the object is not serializable
39
39
  """
40
- if isinstance(o, datetime.datetime):
40
+ if isinstance(o, dt.datetime):
41
41
  return o.isoformat()
42
42
  msg = f"Type {o.__class__.__name__} not serializable"
43
43
  raise TypeError(msg)
@@ -69,10 +69,8 @@ class DefaultStorage(StorageStrategy):
69
69
  data=data_model,
70
70
  data_type=DataType[rd["data_type"]],
71
71
  visibility=Visibility[rd["visibility"]] if rd.get("visibility") else Visibility.UNSPECIFIED,
72
- creation_date=datetime.datetime.fromisoformat(rd["creation_date"])
73
- if rd.get("creation_date")
74
- else None,
75
- update_date=datetime.datetime.fromisoformat(rd["update_date"]) if rd.get("update_date") else None,
72
+ creation_date=dt.datetime.fromisoformat(rd["creation_date"]) if rd.get("creation_date") else None,
73
+ update_date=dt.datetime.fromisoformat(rd["update_date"]) if rd.get("update_date") else None,
76
74
  )
77
75
  out[key] = rec
78
76
  except Exception:
@@ -126,7 +124,7 @@ class DefaultStorage(StorageStrategy):
126
124
  if key in self.storage:
127
125
  msg = f"Document {key!r} already exists"
128
126
  raise ValueError(msg)
129
- now = datetime.datetime.now(datetime.timezone.utc)
127
+ now = dt.datetime.now(dt.timezone.utc)
130
128
  record.creation_date = now
131
129
  record.update_date = now
132
130
  record.storage_id = f"storage:{uuid.uuid4()}"
@@ -183,7 +181,7 @@ class DefaultStorage(StorageStrategy):
183
181
  rec.data = data
184
182
  if visibility is not Visibility.UNSPECIFIED:
185
183
  rec.visibility = visibility
186
- rec.update_date = datetime.datetime.now(datetime.timezone.utc)
184
+ rec.update_date = dt.datetime.now(dt.timezone.utc)
187
185
  self._save_to_file()
188
186
  logger.debug("Modified %s", key)
189
187
  return rec
@@ -1,7 +1,7 @@
1
1
  """This module contains the abstract base class for storage strategies."""
2
2
 
3
3
  import asyncio
4
- import datetime
4
+ import datetime as dt
5
5
  from abc import ABC, abstractmethod
6
6
  from typing import Any, Literal, TypeGuard
7
7
  from uuid import uuid4
@@ -26,8 +26,8 @@ class StorageRecord(BaseModel):
26
26
  description="Read-access scope of this record (UNSPECIFIED = storage-service default)",
27
27
  )
28
28
  data: BaseModel = Field(..., description="The typed payload of this record")
29
- creation_date: datetime.datetime | None = Field(default=None, description="When this record was first created")
30
- update_date: datetime.datetime | None = Field(default=None, description="When this record was last modified")
29
+ creation_date: dt.datetime | None = Field(default=None, description="When this record was first created")
30
+ update_date: dt.datetime | None = Field(default=None, description="When this record was last modified")
31
31
  storage_id: str = Field(
32
32
  default="", description="Service-assigned `storage:<id>` for this record; empty until stored"
33
33
  )