digitalkin 1.0.0.dev2__tar.gz → 1.0.0.dev3__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 (217) hide show
  1. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/PKG-INFO +1 -1
  2. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/pyproject.toml +1 -1
  3. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/__version__.py +1 -1
  4. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/job_manager/base_job_manager.py +1 -88
  5. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/job_manager/single_job_manager.py +56 -200
  6. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/profiling/task_profiler.py +30 -0
  7. digitalkin-1.0.0.dev3/src/digitalkin/core/resilience/task_supervisor.py +37 -0
  8. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/task_manager/base_task_manager.py +0 -49
  9. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/task_manager/local_task_manager.py +7 -2
  10. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/task_manager/module_runner.py +43 -33
  11. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/task_manager/redis/proto_streams.py +18 -2
  12. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/task_manager/redis/redis_signal.py +3 -0
  13. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/task_manager/redis/redis_streams.py +2 -0
  14. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/task_manager/task_executor.py +34 -6
  15. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/task_manager/task_session.py +29 -37
  16. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/grpc_servers/gateway_constants.py +3 -5
  17. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/grpc_servers/gateway_servicer.py +132 -50
  18. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/grpc_servers/module_server.py +1 -7
  19. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/grpc_servers/module_servicer.py +3 -375
  20. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/grpc_servers/stream_error_codes.py +1 -2
  21. digitalkin-1.0.0.dev3/src/digitalkin/grpc_servers/stream_registry.py +247 -0
  22. digitalkin-1.0.0.dev3/src/digitalkin/grpc_servers/stream_session.py +54 -0
  23. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/grpc_servers/utils/grpc_client_wrapper.py +5 -0
  24. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/settings/gateway.py +6 -13
  25. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/modules/_base_module.py +54 -15
  26. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/communication/gateway_consumer.py +17 -2
  27. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/utils/conditional_schema.py +1 -3
  28. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin.egg-info/PKG-INFO +1 -1
  29. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin.egg-info/SOURCES.txt +1 -0
  30. digitalkin-1.0.0.dev2/src/digitalkin/grpc_servers/stream_registry.py +0 -261
  31. digitalkin-1.0.0.dev2/src/digitalkin/grpc_servers/stream_session.py +0 -124
  32. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/LICENSE +0 -0
  33. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/README.md +0 -0
  34. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/base_server/__init__.py +0 -0
  35. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/base_server/mock/__init__.py +0 -0
  36. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/base_server/mock/mock_pb2.py +0 -0
  37. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/base_server/mock/mock_pb2_grpc.py +0 -0
  38. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/base_server/server_async_insecure.py +0 -0
  39. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/base_server/server_async_secure.py +0 -0
  40. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/base_server/server_sync_insecure.py +0 -0
  41. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/base_server/server_sync_secure.py +0 -0
  42. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/bench_module/__init__.py +0 -0
  43. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/bench_module/echo_module.py +0 -0
  44. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/bench_module/models/__init__.py +0 -0
  45. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/bench_module/models/input.py +0 -0
  46. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/bench_module/models/output.py +0 -0
  47. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/bench_module/models/secret.py +0 -0
  48. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/bench_module/models/setup.py +0 -0
  49. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/bench_module/server.py +0 -0
  50. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/bench_module/triggers/__init__.py +0 -0
  51. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/bench_module/triggers/message_trigger.py +0 -0
  52. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/modules/__init__.py +0 -0
  53. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/modules/archetype_with_tools_module.py +0 -0
  54. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/modules/cpu_intensive_module.py +0 -0
  55. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/modules/dynamic_setup_module.py +0 -0
  56. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/modules/minimal_llm_module.py +0 -0
  57. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/modules/text_transform_module.py +0 -0
  58. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/monitoring/digitalkin_observability/__init__.py +0 -0
  59. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/monitoring/digitalkin_observability/http_server.py +0 -0
  60. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/monitoring/digitalkin_observability/interceptors.py +0 -0
  61. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/monitoring/digitalkin_observability/metrics.py +0 -0
  62. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/monitoring/digitalkin_observability/prometheus.py +0 -0
  63. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/monitoring/tests/test_metrics.py +0 -0
  64. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/redis_demo/client.py +0 -0
  65. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/redis_demo/echo_module.py +0 -0
  66. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/redis_demo/models/__init__.py +0 -0
  67. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/redis_demo/models/input.py +0 -0
  68. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/redis_demo/models/output.py +0 -0
  69. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/redis_demo/models/secret.py +0 -0
  70. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/redis_demo/models/setup.py +0 -0
  71. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/redis_demo/server.py +0 -0
  72. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/redis_demo/triggers/__init__.py +0 -0
  73. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/redis_demo/triggers/message_trigger.py +0 -0
  74. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/services/filesystem_module.py +0 -0
  75. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/examples/services/storage_module.py +0 -0
  76. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/setup.cfg +0 -0
  77. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/__init__.py +0 -0
  78. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/community/__init__.py +0 -0
  79. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/community/agno/__init__.py +0 -0
  80. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/community/agno/agno_adapter.py +0 -0
  81. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/community/agno/agui_tools.py +0 -0
  82. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/community/agno/hitl.py +0 -0
  83. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/__init__.py +0 -0
  84. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/common/__init__.py +0 -0
  85. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/common/factories.py +0 -0
  86. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/job_manager/__init__.py +0 -0
  87. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/profiling/__init__.py +0 -0
  88. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/profiling/step_timer.py +0 -0
  89. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/resilience/__init__.py +0 -0
  90. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/resilience/bulkhead.py +0 -0
  91. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/resilience/graceful_shutdown.py +0 -0
  92. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/resilience/session_reaper.py +0 -0
  93. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/resilience/watchdog.py +0 -0
  94. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/task_manager/__init__.py +0 -0
  95. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/task_manager/redis/__init__.py +0 -0
  96. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/task_manager/redis/instrumented.py +0 -0
  97. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/task_manager/redis/redis_checkpoint.py +0 -0
  98. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/task_manager/redis/redis_client.py +0 -0
  99. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/task_manager/redis/redis_idempotency.py +0 -0
  100. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/task_manager/redis/redis_state.py +0 -0
  101. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/task_manager/redis/shadow.py +0 -0
  102. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/task_manager/remote_task_manager.py +0 -0
  103. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/core/task_manager/task_wrapper.py +0 -0
  104. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/grpc_servers/__init__.py +0 -0
  105. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/grpc_servers/_base_server.py +0 -0
  106. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/grpc_servers/interceptors/__init__.py +0 -0
  107. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/grpc_servers/interceptors/circuit_breaker_interceptor.py +0 -0
  108. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/grpc_servers/utils/__init__.py +0 -0
  109. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/grpc_servers/utils/circuit_breaker.py +0 -0
  110. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/grpc_servers/utils/exceptions.py +0 -0
  111. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/grpc_servers/utils/grpc_error_handler.py +0 -0
  112. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/grpc_servers/utils/utility_schema_extender.py +0 -0
  113. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/logger.py +0 -0
  114. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/mixins/__init__.py +0 -0
  115. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/mixins/agui_mixin.py +0 -0
  116. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/mixins/base_mixin.py +0 -0
  117. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/mixins/cost_mixin.py +0 -0
  118. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/mixins/file_history_mixin.py +0 -0
  119. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/mixins/filesystem_mixin.py +0 -0
  120. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/mixins/logger_mixin.py +0 -0
  121. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/mixins/storage_mixin.py +0 -0
  122. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/__init__.py +0 -0
  123. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/core/__init__.py +0 -0
  124. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/core/job_manager_models.py +0 -0
  125. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/core/task_monitor.py +0 -0
  126. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/events/__init__.py +0 -0
  127. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/events/agent_events.py +0 -0
  128. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/grpc_servers/__init__.py +0 -0
  129. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/grpc_servers/models.py +0 -0
  130. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/grpc_servers/types.py +0 -0
  131. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/module/__init__.py +0 -0
  132. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/module/ag_ui.py +0 -0
  133. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/module/base_types.py +0 -0
  134. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/module/module.py +0 -0
  135. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/module/module_context.py +0 -0
  136. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/module/module_types.py +0 -0
  137. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/module/request_metadata.py +0 -0
  138. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/module/select_schema.py +0 -0
  139. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/module/setup_types.py +0 -0
  140. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/module/tool_cache.py +0 -0
  141. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/module/tool_reference.py +0 -0
  142. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/module/utility.py +0 -0
  143. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/services/__init__.py +0 -0
  144. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/services/cost.py +0 -0
  145. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/services/registry.py +0 -0
  146. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/services/storage.py +0 -0
  147. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/settings/__init__.py +0 -0
  148. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/settings/consumer.py +0 -0
  149. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/settings/profiling.py +0 -0
  150. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/settings/redis.py +0 -0
  151. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/settings/server/__init__.py +0 -0
  152. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/settings/server/channel.py +0 -0
  153. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/settings/server/grpc.py +0 -0
  154. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/settings/server/server.py +0 -0
  155. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/settings/utils/__init__.py +0 -0
  156. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/models/settings/utils/channel.py +0 -0
  157. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/modules/__init__.py +0 -0
  158. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/modules/archetype_module.py +0 -0
  159. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/modules/tool_module.py +0 -0
  160. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/modules/trigger_handler.py +0 -0
  161. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/modules/triggers/__init__.py +0 -0
  162. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/modules/triggers/healthcheck_ping_trigger.py +0 -0
  163. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/modules/triggers/healthcheck_services_trigger.py +0 -0
  164. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/modules/triggers/healthcheck_status_trigger.py +0 -0
  165. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/py.typed +0 -0
  166. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/__init__.py +0 -0
  167. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/base_strategy.py +0 -0
  168. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/communication/__init__.py +0 -0
  169. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/communication/communication_strategy.py +0 -0
  170. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/communication/default_communication.py +0 -0
  171. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/communication/grpc_communication.py +0 -0
  172. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/cost/__init__.py +0 -0
  173. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/cost/cost_strategy.py +0 -0
  174. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/cost/default_cost.py +0 -0
  175. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/cost/grpc_cost.py +0 -0
  176. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/filesystem/__init__.py +0 -0
  177. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/filesystem/default_filesystem.py +0 -0
  178. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/filesystem/filesystem_strategy.py +0 -0
  179. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/filesystem/grpc_filesystem.py +0 -0
  180. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/identity/__init__.py +0 -0
  181. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/identity/default_identity.py +0 -0
  182. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/identity/identity_strategy.py +0 -0
  183. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/registry/__init__.py +0 -0
  184. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/registry/default_registry.py +0 -0
  185. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/registry/exceptions.py +0 -0
  186. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/registry/grpc_registry.py +0 -0
  187. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/registry/registry_models.py +0 -0
  188. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/registry/registry_strategy.py +0 -0
  189. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/services_config.py +0 -0
  190. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/services_models.py +0 -0
  191. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/setup/__init__.py +0 -0
  192. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/setup/default_setup.py +0 -0
  193. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/setup/grpc_setup.py +0 -0
  194. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/setup/setup_strategy.py +0 -0
  195. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/storage/__init__.py +0 -0
  196. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/storage/default_storage.py +0 -0
  197. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/storage/grpc_storage.py +0 -0
  198. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/storage/storage_strategy.py +0 -0
  199. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/task_manager/__init__.py +0 -0
  200. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/task_manager/default_task_manager.py +0 -0
  201. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/task_manager/redis_task_manager.py +0 -0
  202. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/task_manager/task_manager_strategy.py +0 -0
  203. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/user_profile/__init__.py +0 -0
  204. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/user_profile/default_user_profile.py +0 -0
  205. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/user_profile/grpc_user_profile.py +0 -0
  206. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/services/user_profile/user_profile_strategy.py +0 -0
  207. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/utils/__init__.py +0 -0
  208. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/utils/arg_parser.py +0 -0
  209. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/utils/development_mode_action.py +0 -0
  210. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/utils/dynamic_schema.py +0 -0
  211. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/utils/llm_ready_schema.py +0 -0
  212. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/utils/package_discover.py +0 -0
  213. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/utils/proto_utils.py +0 -0
  214. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin/utils/schema_splitter.py +0 -0
  215. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin.egg-info/dependency_links.txt +0 -0
  216. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin.egg-info/requires.txt +0 -0
  217. {digitalkin-1.0.0.dev2 → digitalkin-1.0.0.dev3}/src/digitalkin.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: digitalkin
3
- Version: 1.0.0.dev2
3
+ Version: 1.0.0.dev3
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
@@ -37,7 +37,7 @@
37
37
  "pydantic-settings>=2.14.0",
38
38
  "redis[hiredis]>=7.4.0,<8",
39
39
  ]
40
- version = "1.0.0.dev2"
40
+ version = "1.0.0.dev3"
41
41
 
42
42
  [project.optional-dependencies]
43
43
  performance = [ "uvloop>=0.21" ]
@@ -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.0.dev2"
8
+ __version__ = "1.0.0.dev3"
@@ -1,8 +1,7 @@
1
1
  """Background module manager."""
2
2
 
3
3
  import abc
4
- from collections.abc import AsyncGenerator, Callable, Coroutine
5
- from contextlib import AbstractAsyncContextManager
4
+ from collections.abc import Callable, Coroutine
6
5
  from typing import Any, Generic
7
6
 
8
7
  from digitalkin.core.task_manager.base_task_manager import BaseTaskManager
@@ -79,18 +78,6 @@ class BaseJobManager(abc.ABC, Generic[InputModelT, OutputModelT, SetupModelT]):
79
78
  """
80
79
  await self._task_manager.create_task(task_id, mission_id, module, coro, **kwargs)
81
80
 
82
- async def clean_session(self, task_id: str, mission_id: str) -> bool:
83
- """Clean a task's session.
84
-
85
- Args:
86
- task_id: Unique identifier for the task.
87
- mission_id: Mission identifier.
88
-
89
- Returns:
90
- bool: True if the task was successfully cancelled, False otherwise.
91
- """
92
- return await self._task_manager.clean_session(task_id, mission_id)
93
-
94
81
  async def cancel_task(self, task_id: str, mission_id: str, timeout: float | None = None) -> bool:
95
82
  """Cancel a task.
96
83
 
@@ -164,48 +151,6 @@ class BaseJobManager(abc.ABC, Generic[InputModelT, OutputModelT, SetupModelT]):
164
151
 
165
152
  return callback_wrapper
166
153
 
167
- @abc.abstractmethod
168
- def generate_stream_consumer(
169
- self, job_id: str
170
- ) -> AbstractAsyncContextManager[AsyncGenerator[dict[str, Any], None]]:
171
- """Generate a stream consumer for the job's message stream.
172
-
173
- Args:
174
- job_id: The unique identifier of the job to filter messages for.
175
-
176
- Yields:
177
- dict[str, Any]: The messages from the associated module's stream.
178
- """
179
-
180
- @abc.abstractmethod
181
- async def create_module_instance_job(
182
- self,
183
- input_data: InputModelT,
184
- setup_data: SetupModelT,
185
- mission_id: str,
186
- setup_id: str,
187
- setup_version_id: str,
188
- request_metadata: dict[str, str] | None = None,
189
- job_id: str | None = None,
190
- tool_cache: Any = None,
191
- ) -> str:
192
- """Create and start a new job for the module's instance.
193
-
194
- Args:
195
- input_data: The input data required to start the job.
196
- setup_data: The setup configuration for the module.
197
- mission_id: The mission ID associated with the job.
198
- setup_id: The setup ID.
199
- setup_version_id: The setup version ID associated with the module.
200
- request_metadata: gRPC request metadata (headers) to forward to the module.
201
- job_id: Optional externally-provided job ID (e.g., Gateway's task_id).
202
- If None, a UUID is minted internally.
203
- tool_cache: Pre-resolved ToolCache to inject (skips per-request resolution).
204
-
205
- Returns:
206
- str: The unique identifier (job ID) of the created job.
207
- """
208
-
209
154
  @abc.abstractmethod
210
155
  async def generate_config_setup_module_response(self, job_id: str) -> SetupModelT | ModuleCodeModel:
211
156
  """Generate a stream consumer for a module's output data.
@@ -249,38 +194,6 @@ class BaseJobManager(abc.ABC, Generic[InputModelT, OutputModelT, SetupModelT]):
249
194
  Exception: If the module fails to start.
250
195
  """
251
196
 
252
- @abc.abstractmethod
253
- async def stop_module(self, job_id: str) -> bool:
254
- """Stop a running module job.
255
-
256
- Args:
257
- job_id: The unique identifier of the job to stop.
258
-
259
- Returns:
260
- bool: True if the job was successfully stopped, False if it does not exist.
261
- """
262
-
263
- @abc.abstractmethod
264
- async def wait_for_completion(self, job_id: str) -> None:
265
- """Wait for a task to complete.
266
-
267
- This method blocks until the specified job has reached a terminal state.
268
- SingleJobManager awaits the asyncio.Task directly.
269
-
270
- Args:
271
- job_id: The unique identifier of the job to wait for.
272
-
273
- Raises:
274
- KeyError: If the job_id is not found.
275
- """
276
-
277
- @abc.abstractmethod
278
- async def stop_all_modules(self) -> None:
279
- """Stop all currently running module jobs.
280
-
281
- This method ensures that all active jobs are gracefully terminated.
282
- """
283
-
284
197
  @abc.abstractmethod
285
198
  async def list_modules(self) -> dict[str, dict[str, Any]]:
286
199
  """List all modules along with their statuses.
@@ -11,7 +11,6 @@ from __future__ import annotations
11
11
  import asyncio
12
12
  import os
13
13
  import uuid
14
- from contextlib import asynccontextmanager
15
14
  from typing import TYPE_CHECKING, Any
16
15
 
17
16
  import grpc
@@ -26,7 +25,7 @@ from digitalkin.models.module.base_types import DataModel, InputModelT, OutputMo
26
25
  from digitalkin.models.module.module import ModuleCodeModel
27
26
 
28
27
  if TYPE_CHECKING:
29
- from collections.abc import AsyncGenerator, AsyncIterator, Callable
28
+ from collections.abc import Callable
30
29
 
31
30
  from digitalkin.core.task_manager.redis.redis_client import RedisClient
32
31
  from digitalkin.core.task_manager.redis.redis_streams import RedisStreamWriter
@@ -239,104 +238,8 @@ class SingleJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
239
238
  except asyncio.QueueFull:
240
239
  logger.warning("Queue full, rejecting new message", extra={"job_id": job_id})
241
240
 
242
- @asynccontextmanager
243
- async def generate_stream_consumer(self, job_id: str) -> AsyncIterator[AsyncGenerator[dict[str, Any], None]]:
244
- """Generate a stream consumer for a module's output data.
245
-
246
- This method creates an asynchronous generator that streams output data
247
- from a specific module job. If the module does not exist, it generates
248
- an error message.
249
-
250
- Args:
251
- job_id: The unique identifier of the job.
252
-
253
- Yields:
254
- AsyncGenerator: A stream of output data or error messages.
255
- """
256
- if (session := self.tasks_sessions.get(job_id, None)) is None:
257
-
258
- async def _error_gen() -> AsyncGenerator[ # noqa: RUF029
259
- dict[str, Any], None
260
- ]: # Async generator type required by caller even though body uses yield
261
- """Generate an error message for a non-existent module.
262
-
263
- Yields:
264
- AsyncGenerator: A generator yielding an error message.
265
- """
266
- yield {
267
- "error": {
268
- "error_message": f"Module {job_id} not found",
269
- "code": grpc.StatusCode.NOT_FOUND,
270
- }
271
- }
272
-
273
- yield _error_gen()
274
- return
275
-
276
- logger.debug("Session: %s with Module %s", job_id, session.module)
277
-
278
- async def _stream() -> AsyncGenerator[dict[str, Any], Any]:
279
- """Stream output data from the module with bounded blocking.
280
-
281
- Uses a 1-second timeout on queue.get() to periodically re-check
282
- termination flags, preventing indefinite hangs when the task crashes
283
- without producing output.
284
-
285
- Termination behavior:
286
- - cancelled: abort immediately (abnormal, discard remaining)
287
- - stream_closed / completed / failed: drain remaining queue items, then exit
288
-
289
- Yields:
290
- dict: Output data generated by the module.
291
- """
292
- while True:
293
- if session.cancelled:
294
- logger.debug("Stream cancelled for job %s", job_id)
295
- break
296
-
297
- # If no more output will be produced, drain remaining items and exit
298
- if session.stream_closed or session.status in {"completed", "failed"}:
299
- while not session.queue.empty():
300
- msg = session.queue.get_nowait()
301
- try:
302
- yield msg
303
- finally:
304
- session.queue.task_done()
305
- logger.debug(
306
- "Stream drained for job %s: status=%s, stream_closed=%s",
307
- job_id,
308
- session.status,
309
- session.stream_closed,
310
- )
311
- break
312
-
313
- try:
314
- msg = await asyncio.wait_for(session.queue.get(), timeout=0.25)
315
- except asyncio.TimeoutError:
316
- continue
317
-
318
- try:
319
- yield msg
320
- finally:
321
- session.queue.task_done()
322
-
323
- if session.cancelled:
324
- break
325
-
326
- try:
327
- yield _stream()
328
- finally:
329
- # Write EOS to Redis Stream if writer exists (cleanup on stream close)
330
- writer = self._stream_writers.pop(job_id, None) if self._stream_writers is not None else None
331
- if writer is not None:
332
- try:
333
- await writer.write_eos()
334
- except Exception:
335
- logger.warning("Redis stream EOS write failed", extra={"job_id": job_id})
336
-
337
- async def create_module_instance_job(
241
+ async def preload_instance(
338
242
  self,
339
- input_data: InputModelT,
340
243
  setup_data: SetupModelT,
341
244
  mission_id: str,
342
245
  setup_id: str,
@@ -345,28 +248,32 @@ class SingleJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
345
248
  job_id: str | None = None,
346
249
  tool_cache: Any = None,
347
250
  callback: Callable | None = None,
348
- ) -> str:
349
- """Create and start a new module job.
251
+ ) -> tuple[Any, str, Callable]:
252
+ """Phase 3.A: Build + warm a module instance without input.
253
+
254
+ Calls the factory, wires the redis task manager + callback, and
255
+ runs the module's ``prepare()`` (which is idempotent — the later
256
+ ``start()`` call short-circuits past it).
257
+
258
+ Designed so the dial-back orchestrator can pay LiteLLM/agno init
259
+ costs (~440 ms) in parallel with the consumer's first reply RTT.
350
260
 
351
261
  Args:
352
- input_data: The input data required to start the job.
353
262
  setup_data: The setup configuration for the module.
354
- mission_id: The mission ID associated with the job.
355
- setup_id: The setup ID associated with the module.
356
- setup_version_id: The setup Version ID associated with the module.
357
- request_metadata: gRPC request metadata (headers) to forward to the module.
358
- job_id: Optional externally-provided job ID (e.g., Gateway's task_id).
359
- tool_cache: Pre-resolved ToolCache to inject (skips per-request resolution).
360
- callback: Direct output callback (writes to Redis). If None, uses
361
- internal queue path via add_to_queue.
263
+ mission_id: Mission ID.
264
+ setup_id: Setup ID.
265
+ setup_version_id: Setup version ID.
266
+ request_metadata: gRPC request metadata (headers).
267
+ job_id: Optional externally-provided job ID.
268
+ tool_cache: Pre-resolved ToolCache.
269
+ callback: Direct output callback. If None, the in-memory
270
+ queue path is wired for the eventual ``run_preloaded``.
362
271
 
363
272
  Returns:
364
- str: The unique identifier (job ID) of the created job.
365
-
366
- Raises:
367
- Exception: If the module fails to start.
273
+ ``(module, job_id, callback)`` pass to ``run_preloaded``.
368
274
  """
369
275
  from digitalkin.core.profiling.step_timer import StepTimer
276
+ from digitalkin.services.task_manager.redis_task_manager import RedisTaskManager
370
277
 
371
278
  timer = StepTimer()
372
279
  job_id = job_id or str(uuid.uuid4())
@@ -381,9 +288,6 @@ class SingleJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
381
288
  )
382
289
  timer.mark("factory_create")
383
290
 
384
- # Redis-backed signal service for cross-process signal delivery
385
- from digitalkin.services.task_manager.redis_task_manager import RedisTaskManager
386
-
387
291
  module.context.task_manager = RedisTaskManager(self._redis_client)
388
292
  timer.mark("redis_task_manager")
389
293
 
@@ -394,97 +298,49 @@ class SingleJobManager(BaseJobManager[InputModelT, OutputModelT, SetupModelT]):
394
298
  callback = await self.job_specific_callback(self.add_to_queue, job_id)
395
299
  timer.mark("default_callback")
396
300
 
397
- await self.create_task(
398
- job_id,
399
- mission_id,
400
- module,
401
- module.start(input_data, setup_data, callback, done_callback=None), # type: ignore[arg-type]
402
- )
403
- timer.mark("create_task")
404
- timer.log("create_module_instance_job", task_id=job_id)
405
- logger.info("Managed task started: '%s'", job_id, extra={"task_id": job_id})
406
- return job_id
407
-
408
- async def clean_session(self, task_id: str, mission_id: str) -> bool:
409
- """Clean a task's session.
301
+ await module.prepare(setup_data, callback)
302
+ timer.mark("prepare")
303
+ timer.log("preload_instance", task_id=job_id)
304
+ return module, job_id, callback
410
305
 
411
- Args:
412
- task_id: Unique identifier for the task.
413
- mission_id: Mission identifier.
414
-
415
- Returns:
416
- bool: True if the task was successfully cleaned, False otherwise.
417
- """
418
- return await self._task_manager.clean_session(task_id, mission_id)
306
+ async def run_preloaded(
307
+ self,
308
+ module: Any,
309
+ job_id: str,
310
+ mission_id: str,
311
+ input_data: InputModelT,
312
+ setup_data: SetupModelT,
313
+ callback: Callable,
314
+ ) -> str:
315
+ """Phase 3.A: Run a pre-prepared module instance with input.
419
316
 
420
- async def stop_module(self, job_id: str) -> bool:
421
- """Stop a running module job.
317
+ ``module`` must come from :meth:`preload_instance`. Schedules
318
+ the run in the task manager and returns the job_id.
422
319
 
423
320
  Args:
424
- job_id: The unique identifier of the job to stop.
321
+ module: Pre-prepared module instance.
322
+ job_id: Job/task ID assigned by ``preload_instance``.
323
+ mission_id: Mission ID for task manager scoping.
324
+ input_data: The first input (the query) to feed ``run()``.
325
+ setup_data: The setup the instance was prepared with.
326
+ callback: Output callback (already attached to context).
425
327
 
426
328
  Returns:
427
- bool: True if the module was successfully stopped, False if it does not exist.
428
-
429
- Raises:
430
- Exception: If an error occurs while stopping the module.
329
+ The ``job_id`` (echoed for caller convenience).
431
330
  """
432
- logger.info("Stop module requested", extra={"job_id": job_id})
433
-
434
- logger.debug("debug:stop_module acquiring lock job_id=%s", job_id)
435
- async with self._lock:
436
- session = self.tasks_sessions.get(job_id)
437
-
438
- if not session:
439
- logger.warning("Session not found", extra={"job_id": job_id})
440
- return False
441
- try:
442
- await session.module.stop()
443
- await self.cancel_task(job_id, session.mission_id)
444
- logger.debug(
445
- "Module stopped successfully",
446
- extra={"job_id": job_id, "mission_id": session.mission_id},
447
- )
448
- except Exception:
449
- logger.exception("Error stopping module", extra={"job_id": job_id})
450
- raise
451
- else:
452
- return True
453
- finally:
454
- # Clean up stream writer if consumer never started
455
- if self._stream_writers is not None:
456
- writer = self._stream_writers.pop(job_id, None)
457
- if writer is not None:
458
- try:
459
- await writer.write_eos()
460
- except Exception:
461
- logger.debug("EOS write failed during stop", extra={"job_id": job_id})
462
-
463
- async def wait_for_completion(self, job_id: str) -> None:
464
- """Wait for a task to complete by awaiting its asyncio.Task.
465
-
466
- Idempotent — safe to call after the task has already been cleaned up
467
- (e.g. by deferred cleanup during signal cancellation).
331
+ from digitalkin.core.profiling.step_timer import StepTimer
468
332
 
469
- Args:
470
- job_id: The unique identifier of the job to wait for.
471
- """
472
- task = self._task_manager.tasks.get(job_id)
473
- if task is None:
474
- logger.debug("Task already cleaned up, skipping wait_for_completion", extra={"job_id": job_id})
475
- return
476
- await task
477
-
478
- async def stop_all_modules(self) -> None:
479
- """Stop all currently running module jobs."""
480
- # Snapshot job IDs while holding lock
481
- async with self._lock:
482
- job_ids = list(self.tasks_sessions.keys())
483
-
484
- # Release lock before calling stop_module (which has its own lock)
485
- if job_ids:
486
- stop_tasks = [self.stop_module(job_id) for job_id in job_ids]
487
- await asyncio.gather(*stop_tasks, return_exceptions=True)
333
+ timer = StepTimer()
334
+ await self.create_task(
335
+ job_id,
336
+ mission_id,
337
+ module,
338
+ module.start(input_data, setup_data, callback, done_callback=None),
339
+ )
340
+ timer.mark("create_task")
341
+ timer.log("run_preloaded", task_id=job_id)
342
+ logger.info("Managed task started: '%s'", job_id, extra={"task_id": job_id})
343
+ return job_id
488
344
 
489
345
  async def list_modules(self) -> dict[str, dict[str, Any]]:
490
346
  """List all modules along with their statuses.
@@ -10,6 +10,35 @@ from typing import Any
10
10
 
11
11
  from digitalkin.logger import logger
12
12
 
13
+ # Phase 7.C: rotate per-task profile files to avoid unbounded growth.
14
+ # Default keeps the most recent N profiles by mtime; configure via
15
+ # ``DIGITALKIN_PROFILER_KEEP_N``.
16
+ PROFILER_KEEP_N = int(os.environ.get("DIGITALKIN_PROFILER_KEEP_N", "100"))
17
+
18
+
19
+ def _rotate_profiles(output_dir: str, keep_n: int, suffixes: tuple[str, ...]) -> None:
20
+ """Trim ``output_dir`` to the most recent ``keep_n`` files by mtime.
21
+
22
+ Args:
23
+ output_dir: Directory containing profile files.
24
+ keep_n: Number of files to keep. ``<= 0`` disables rotation.
25
+ suffixes: File extensions to include in rotation (e.g. ``(".html",)``).
26
+ """
27
+ if keep_n <= 0:
28
+ return
29
+ try:
30
+ candidates = [p for p in Path(output_dir).iterdir() if p.is_file() and p.suffix in suffixes]
31
+ except OSError:
32
+ return
33
+ if len(candidates) <= keep_n:
34
+ return
35
+ candidates.sort(key=lambda p: p.stat().st_mtime, reverse=True)
36
+ for stale in candidates[keep_n:]:
37
+ try:
38
+ stale.unlink()
39
+ except OSError:
40
+ logger.debug("Profiler rotation: could not delete %s", stale)
41
+
13
42
 
14
43
  class ProfilerMode(str, Enum):
15
44
  """Profiler backend selection."""
@@ -121,6 +150,7 @@ class TaskProfiler:
121
150
  Path(path).write_text(self._profiler.output_html(), encoding="utf-8")
122
151
  logger.info("Pyinstrument profile saved: %s", path)
123
152
  logger.info("Pyinstrument summary:\n%s", self._profiler.output_text())
153
+ _rotate_profiles(self._output_dir, PROFILER_KEEP_N, (".html",))
124
154
 
125
155
  except Exception:
126
156
  logger.exception("Failed to stop/save profiler %s for task %s", self._mode.value, self._task_id)
@@ -0,0 +1,37 @@
1
+ """Tiny helper: log unhandled exceptions on fire-and-forget asyncio tasks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any
7
+
8
+ from digitalkin.logger import logger
9
+
10
+
11
+ def log_unhandled(task: asyncio.Task[Any]) -> None:
12
+ """Done-callback that logs uncaught exceptions on a fire-and-forget task.
13
+
14
+ Cancellation and clean exits are silent. Anything else is logged at
15
+ error level with the task name and traceback — this replaces asyncio's
16
+ opaque ``Task exception was never retrieved`` warning with an
17
+ actionable log line.
18
+
19
+ Usage:
20
+
21
+ task = asyncio.create_task(coro, name="my_daemon")
22
+ task.add_done_callback(log_unhandled)
23
+
24
+ Args:
25
+ task: The done asyncio task to inspect.
26
+ """
27
+ if task.cancelled():
28
+ return
29
+ exc = task.exception()
30
+ if exc is not None:
31
+ logger.error(
32
+ "Background task '%s' failed with %s: %s",
33
+ task.get_name(),
34
+ type(exc).__name__,
35
+ exc,
36
+ exc_info=exc,
37
+ )
@@ -46,7 +46,6 @@ class BaseTaskManager(ABC):
46
46
  self._active_slots = 0
47
47
  self._task_wait_timeout = float(os.environ.get("DIGITALKIN_TASK_WAIT_TIMEOUT", "30"))
48
48
  self._stream_drain_timeout = float(os.environ.get("DIGITALKIN_STREAM_DRAIN_TIMEOUT", "2.0"))
49
- self._cleanup_tasks: set[asyncio.Task] = set()
50
49
 
51
50
  # Admission queue: allows tasks to wait for a slot instead of being rejected.
52
51
  # Total in-system capacity = max_concurrent + max_queued.
@@ -278,49 +277,6 @@ class BaseTaskManager(ABC):
278
277
  self.tasks_sessions[task_id] = session
279
278
  return session
280
279
 
281
- def _register_auto_cleanup(self, task_id: str, mission_id: str) -> None:
282
- """Register a done callback on the supervisor task for deferred cleanup.
283
-
284
- When the supervisor finishes, waits for the stream consumer to drain
285
- (up to 60s), then runs idempotent cleanup. Safe if the servicer
286
- already cleaned up.
287
-
288
- Args:
289
- task_id: The ID of the task.
290
- mission_id: The ID of the mission.
291
- """
292
- supervisor = self.tasks.get(task_id)
293
- if supervisor is None:
294
- return
295
-
296
- def _on_done(_: asyncio.Task) -> None:
297
- t = asyncio.ensure_future(self._deferred_cleanup(task_id, mission_id))
298
- self._cleanup_tasks.add(t)
299
- t.add_done_callback(self._cleanup_tasks.discard)
300
-
301
- supervisor.add_done_callback(_on_done)
302
-
303
- async def _deferred_cleanup(self, task_id: str, mission_id: str) -> None:
304
- """Wait for stream drain then cleanup.
305
-
306
- Args:
307
- task_id: The ID of the task.
308
- mission_id: The ID of the mission.
309
- """
310
- session = self.tasks_sessions.get(task_id)
311
- if session is None:
312
- return
313
-
314
- try:
315
- await asyncio.wait_for(session._stream_closed.wait(), timeout=self._stream_drain_timeout) # noqa: SLF001
316
- except asyncio.TimeoutError:
317
- logger.warning(
318
- "Stream drain timeout, proceeding with cleanup",
319
- extra={"task_id": task_id, "mission_id": mission_id},
320
- )
321
-
322
- await self._cleanup_task(task_id, mission_id)
323
-
324
280
  @abstractmethod
325
281
  async def create_task(
326
282
  self,
@@ -599,11 +555,6 @@ class BaseTaskManager(ABC):
599
555
  cleanup_coros = [self._cleanup_task(task_id, mission_id) for task_id in remaining_sessions]
600
556
  await asyncio.gather(*cleanup_coros, return_exceptions=True)
601
557
 
602
- # Await any deferred cleanup tasks
603
- if self._cleanup_tasks:
604
- await asyncio.gather(*self._cleanup_tasks, return_exceptions=True)
605
- self._cleanup_tasks.clear()
606
-
607
558
  logger.info(
608
559
  "TaskManager shutdown completed, cancelled: %d, failed: %d",
609
560
  len(results) - len(failed_tasks),
@@ -62,15 +62,20 @@ class LocalTaskManager(BaseTaskManager):
62
62
  },
63
63
  )
64
64
 
65
- # Execute task using TaskExecutor
65
+ # Execute task using TaskExecutor; cleanup runs inside the
66
+ # supervisor's `finally` (folded from former _deferred_cleanup).
67
+ async def _finalize() -> None:
68
+ await self._cleanup_task(task_id, mission_id=mission_id)
69
+
66
70
  supervisor_task = await self._executor.execute_task(
67
71
  task_id,
68
72
  mission_id,
69
73
  coro,
70
74
  session,
75
+ on_finalize=_finalize,
76
+ stream_drain_timeout=self._stream_drain_timeout,
71
77
  )
72
78
  self.tasks[task_id] = supervisor_task
73
- self._register_auto_cleanup(task_id, mission_id)
74
79
 
75
80
  logger.info(
76
81
  "Local task created and started: '%s'",