digitalkin 1.0.3.dev1__tar.gz → 1.0.3.dev2__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 (261) hide show
  1. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/PKG-INFO +1 -1
  2. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/pyproject.toml +1 -1
  3. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/__version__.py +1 -1
  4. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/toolkits/registry/loader/action.py +65 -32
  5. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/module/__init__.py +8 -0
  6. digitalkin-1.0.3.dev2/src/digitalkin/models/module/loaded_tools.py +140 -0
  7. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/module/module_context.py +89 -5
  8. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/module/tool_cache.py +70 -10
  9. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/modules/_base_module.py +9 -2
  10. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin.egg-info/PKG-INFO +1 -1
  11. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin.egg-info/SOURCES.txt +1 -0
  12. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/LICENSE +0 -0
  13. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/README.md +0 -0
  14. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/base_server/__init__.py +0 -0
  15. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/base_server/mock/__init__.py +0 -0
  16. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/base_server/mock/mock_pb2.py +0 -0
  17. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/base_server/mock/mock_pb2_grpc.py +0 -0
  18. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/base_server/server_async_insecure.py +0 -0
  19. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/base_server/server_async_secure.py +0 -0
  20. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/base_server/server_sync_insecure.py +0 -0
  21. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/base_server/server_sync_secure.py +0 -0
  22. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/bench_module/__init__.py +0 -0
  23. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/bench_module/echo_module.py +0 -0
  24. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/bench_module/models/__init__.py +0 -0
  25. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/bench_module/models/input.py +0 -0
  26. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/bench_module/models/output.py +0 -0
  27. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/bench_module/models/secret.py +0 -0
  28. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/bench_module/models/setup.py +0 -0
  29. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/bench_module/server.py +0 -0
  30. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/bench_module/triggers/__init__.py +0 -0
  31. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/bench_module/triggers/message_trigger.py +0 -0
  32. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/modules/__init__.py +0 -0
  33. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/modules/archetype_with_tools_module.py +0 -0
  34. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/modules/cpu_intensive_module.py +0 -0
  35. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/modules/dynamic_setup_module.py +0 -0
  36. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/modules/minimal_llm_module.py +0 -0
  37. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/modules/text_transform_module.py +0 -0
  38. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/monitoring/digitalkin_observability/__init__.py +0 -0
  39. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/monitoring/digitalkin_observability/http_server.py +0 -0
  40. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/monitoring/digitalkin_observability/interceptors.py +0 -0
  41. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/monitoring/digitalkin_observability/metrics.py +0 -0
  42. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/monitoring/digitalkin_observability/prometheus.py +0 -0
  43. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/monitoring/tests/test_metrics.py +0 -0
  44. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/redis_demo/client.py +0 -0
  45. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/redis_demo/echo_module.py +0 -0
  46. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/redis_demo/models/__init__.py +0 -0
  47. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/redis_demo/models/input.py +0 -0
  48. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/redis_demo/models/output.py +0 -0
  49. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/redis_demo/models/secret.py +0 -0
  50. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/redis_demo/models/setup.py +0 -0
  51. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/redis_demo/server.py +0 -0
  52. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/redis_demo/triggers/__init__.py +0 -0
  53. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/redis_demo/triggers/message_trigger.py +0 -0
  54. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/services/filesystem_module.py +0 -0
  55. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/examples/services/storage_module.py +0 -0
  56. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/setup.cfg +0 -0
  57. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/__init__.py +0 -0
  58. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/__init__.py +0 -0
  59. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/__init__.py +0 -0
  60. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/agno_adapter.py +0 -0
  61. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/agui_tools.py +0 -0
  62. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/hitl.py +0 -0
  63. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/models.py +0 -0
  64. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/module_toolkit.py +0 -0
  65. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/toolkits/__init__.py +0 -0
  66. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/toolkits/base.py +0 -0
  67. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/toolkits/chat_history.py +0 -0
  68. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/toolkits/defaults.py +0 -0
  69. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/toolkits/registry/__init__.py +0 -0
  70. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/toolkits/registry/action.py +0 -0
  71. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/toolkits/registry/base.py +0 -0
  72. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/toolkits/registry/kins/__init__.py +0 -0
  73. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/toolkits/registry/kins/action.py +0 -0
  74. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/toolkits/registry/kins/kit.py +0 -0
  75. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/toolkits/registry/loader/__init__.py +0 -0
  76. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/toolkits/registry/loader/kit.py +0 -0
  77. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/toolkits/registry/services/__init__.py +0 -0
  78. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/toolkits/registry/services/action.py +0 -0
  79. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/toolkits/registry/services/kit.py +0 -0
  80. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/toolkits/registry/tools/__init__.py +0 -0
  81. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/toolkits/registry/tools/action.py +0 -0
  82. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/toolkits/registry/tools/kit.py +0 -0
  83. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/community/agno/toolkits/user_profile.py +0 -0
  84. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/__init__.py +0 -0
  85. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/common/__init__.py +0 -0
  86. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/common/factories.py +0 -0
  87. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/exceptions.py +0 -0
  88. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/job_manager/__init__.py +0 -0
  89. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/job_manager/base_job_manager.py +0 -0
  90. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/job_manager/single_job_manager.py +0 -0
  91. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/profiling/__init__.py +0 -0
  92. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/profiling/step_timer.py +0 -0
  93. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/profiling/task_profiler.py +0 -0
  94. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/resilience/__init__.py +0 -0
  95. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/resilience/bulkhead.py +0 -0
  96. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/resilience/task_supervisor.py +0 -0
  97. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/task_manager/__init__.py +0 -0
  98. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/task_manager/base_task_manager.py +0 -0
  99. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/task_manager/local_task_manager.py +0 -0
  100. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/task_manager/module_runner.py +0 -0
  101. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/task_manager/redis/__init__.py +0 -0
  102. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/task_manager/redis/proto_streams.py +0 -0
  103. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/task_manager/redis/redis_client.py +0 -0
  104. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/task_manager/redis/redis_idempotency.py +0 -0
  105. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/task_manager/redis/redis_signal.py +0 -0
  106. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/task_manager/redis/redis_state.py +0 -0
  107. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/task_manager/remote_task_manager.py +0 -0
  108. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/task_manager/task_executor.py +0 -0
  109. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/core/task_manager/task_session.py +0 -0
  110. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/exceptions.py +0 -0
  111. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/grpc_servers/__init__.py +0 -0
  112. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/grpc_servers/_base_server.py +0 -0
  113. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/grpc_servers/exceptions.py +0 -0
  114. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/grpc_servers/gateway_servicer.py +0 -0
  115. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/grpc_servers/interceptors/__init__.py +0 -0
  116. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/grpc_servers/interceptors/permission.py +0 -0
  117. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/grpc_servers/interceptors/request_ids.py +0 -0
  118. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/grpc_servers/m2m_call_registry.py +0 -0
  119. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/grpc_servers/module_server.py +0 -0
  120. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/grpc_servers/module_servicer.py +0 -0
  121. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/grpc_servers/stream_registry.py +0 -0
  122. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/grpc_servers/stream_session.py +0 -0
  123. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/grpc_servers/utils/__init__.py +0 -0
  124. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/grpc_servers/utils/circuit_breaker.py +0 -0
  125. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/grpc_servers/utils/grpc_client_wrapper.py +0 -0
  126. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/grpc_servers/utils/grpc_error_handler.py +0 -0
  127. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/grpc_servers/utils/utility_schema_extender.py +0 -0
  128. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/grpc_servers/utils/validators.py +0 -0
  129. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/logger.py +0 -0
  130. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/mixins/__init__.py +0 -0
  131. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/mixins/agui_mixin.py +0 -0
  132. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/mixins/base_mixin.py +0 -0
  133. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/mixins/cost_mixin.py +0 -0
  134. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/mixins/file_history_mixin.py +0 -0
  135. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/mixins/filesystem_mixin.py +0 -0
  136. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/mixins/logger_mixin.py +0 -0
  137. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/mixins/storage_mixin.py +0 -0
  138. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/__init__.py +0 -0
  139. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/core/__init__.py +0 -0
  140. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/core/job_manager_models.py +0 -0
  141. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/core/redis.py +0 -0
  142. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/core/task_monitor.py +0 -0
  143. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/events/__init__.py +0 -0
  144. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/events/agent_events.py +0 -0
  145. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/grpc_servers/__init__.py +0 -0
  146. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/grpc_servers/circuit_breaker.py +0 -0
  147. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/grpc_servers/m2m.py +0 -0
  148. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/grpc_servers/models.py +0 -0
  149. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/grpc_servers/stream_error_codes.py +0 -0
  150. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/grpc_servers/types.py +0 -0
  151. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/module/ag_ui.py +0 -0
  152. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/module/base_types.py +0 -0
  153. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/module/module.py +0 -0
  154. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/module/module_types.py +0 -0
  155. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/module/request_metadata.py +0 -0
  156. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/module/select_schema.py +0 -0
  157. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/module/setup_types.py +0 -0
  158. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/module/tool_reference.py +0 -0
  159. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/module/utility.py +0 -0
  160. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/services/__init__.py +0 -0
  161. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/services/cost.py +0 -0
  162. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/services/registry.py +0 -0
  163. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/services/services.py +0 -0
  164. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/services/storage.py +0 -0
  165. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/settings/__init__.py +0 -0
  166. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/settings/gateway.py +0 -0
  167. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/settings/grpc_client.py +0 -0
  168. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/settings/log.py +0 -0
  169. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/settings/module.py +0 -0
  170. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/settings/profiling.py +0 -0
  171. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/settings/queue.py +0 -0
  172. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/settings/redis.py +0 -0
  173. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/settings/registry.py +0 -0
  174. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/settings/resilience.py +0 -0
  175. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/settings/server/__init__.py +0 -0
  176. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/settings/server/channel.py +0 -0
  177. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/settings/server/grpc.py +0 -0
  178. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/settings/server/server.py +0 -0
  179. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/settings/server/servicer.py +0 -0
  180. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/settings/task_manager.py +0 -0
  181. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/settings/utils/__init__.py +0 -0
  182. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/settings/utils/channel.py +0 -0
  183. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/utils/__init__.py +0 -0
  184. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/models/utils/dynamic_schema.py +0 -0
  185. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/modules/__init__.py +0 -0
  186. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/modules/archetype_module.py +0 -0
  187. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/modules/tool_module.py +0 -0
  188. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/modules/trigger_handler.py +0 -0
  189. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/modules/triggers/__init__.py +0 -0
  190. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/modules/triggers/healthcheck_ping_trigger.py +0 -0
  191. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/modules/triggers/healthcheck_services_trigger.py +0 -0
  192. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/modules/triggers/healthcheck_status_trigger.py +0 -0
  193. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/py.typed +0 -0
  194. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/__init__.py +0 -0
  195. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/base_strategy.py +0 -0
  196. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/communication/__init__.py +0 -0
  197. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/communication/communication_strategy.py +0 -0
  198. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/communication/default_communication.py +0 -0
  199. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/communication/exceptions.py +0 -0
  200. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/communication/grpc_communication.py +0 -0
  201. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/cost/__init__.py +0 -0
  202. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/cost/cost_strategy.py +0 -0
  203. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/cost/default_cost.py +0 -0
  204. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/cost/exceptions.py +0 -0
  205. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/cost/grpc_cost.py +0 -0
  206. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/filesystem/__init__.py +0 -0
  207. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/filesystem/default_filesystem.py +0 -0
  208. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/filesystem/exceptions.py +0 -0
  209. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/filesystem/filesystem_strategy.py +0 -0
  210. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/filesystem/grpc_filesystem.py +0 -0
  211. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/identity/__init__.py +0 -0
  212. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/identity/default_identity.py +0 -0
  213. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/identity/identity_strategy.py +0 -0
  214. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/registry/__init__.py +0 -0
  215. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/registry/default_registry.py +0 -0
  216. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/registry/exceptions.py +0 -0
  217. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/registry/grpc_registry.py +0 -0
  218. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/registry/registry_models.py +0 -0
  219. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/registry/registry_strategy.py +0 -0
  220. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/secret/__init__.py +0 -0
  221. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/secret/default_secret.py +0 -0
  222. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/secret/exceptions.py +0 -0
  223. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/secret/grpc_secret.py +0 -0
  224. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/secret/secret_strategy.py +0 -0
  225. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/services_config.py +0 -0
  226. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/services_models.py +0 -0
  227. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/setup/__init__.py +0 -0
  228. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/setup/default_setup.py +0 -0
  229. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/setup/exceptions.py +0 -0
  230. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/setup/grpc_setup.py +0 -0
  231. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/setup/setup_strategy.py +0 -0
  232. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/storage/__init__.py +0 -0
  233. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/storage/default_storage.py +0 -0
  234. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/storage/exceptions.py +0 -0
  235. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/storage/grpc_storage.py +0 -0
  236. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/storage/storage_strategy.py +0 -0
  237. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/task_manager/__init__.py +0 -0
  238. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/task_manager/default_task_manager.py +0 -0
  239. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/task_manager/exceptions.py +0 -0
  240. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/task_manager/redis_task_manager.py +0 -0
  241. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/task_manager/task_manager_strategy.py +0 -0
  242. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/user_profile/__init__.py +0 -0
  243. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/user_profile/default_user_profile.py +0 -0
  244. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/user_profile/exceptions.py +0 -0
  245. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/user_profile/grpc_user_profile.py +0 -0
  246. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/services/user_profile/user_profile_strategy.py +0 -0
  247. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/utils/__init__.py +0 -0
  248. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/utils/arg_parser.py +0 -0
  249. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/utils/conditional_schema.py +0 -0
  250. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/utils/development_mode_action.py +0 -0
  251. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/utils/dynamic_schema.py +0 -0
  252. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/utils/exceptions.py +0 -0
  253. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/utils/llm_ready_schema.py +0 -0
  254. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/utils/package_discover.py +0 -0
  255. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/utils/proto_utils.py +0 -0
  256. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/utils/schema_splitter.py +0 -0
  257. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin/utils/setup_content_validator.py +0 -0
  258. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin.egg-info/dependency_links.txt +0 -0
  259. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin.egg-info/requires.txt +0 -0
  260. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/src/digitalkin.egg-info/top_level.txt +0 -0
  261. {digitalkin-1.0.3.dev1 → digitalkin-1.0.3.dev2}/tests/test_exceptions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: digitalkin
3
- Version: 1.0.3.dev1
3
+ Version: 1.0.3.dev2
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
@@ -41,7 +41,7 @@
41
41
  "pydantic>=2.12.4",
42
42
  "redis[hiredis]>=7.4.0,<9",
43
43
  ]
44
- version = "1.0.3.dev1"
44
+ version = "1.0.3.dev2"
45
45
 
46
46
  [project.optional-dependencies]
47
47
  agno = [ "agno>=2.6" ]
@@ -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.3.dev1"
8
+ __version__ = "1.0.3.dev2"
@@ -78,6 +78,62 @@ class LoadToolAction(LoadAction):
78
78
  action: Literal["tool"] = "tool"
79
79
  setup_id: str = Field(..., description="The tool's setup id (from a tools_manager search/get) to load.")
80
80
 
81
+ async def _duplicate_outcome(self, ctx: LoadActionCtx, module_id: str) -> LoadOutcome | None:
82
+ """Resolve the two "already there" cases, or ``None`` if this is a genuinely new load.
83
+
84
+ Runs BEFORE ``resolve_tool``: ``module_id`` is already known from the registry lookup, so
85
+ a repeat load costs no schema fetch. Reads ``base_tools`` — what is callable right now —
86
+ which after rehydration also covers a tool loaded on an earlier turn of this mission.
87
+
88
+ Args:
89
+ ctx: The load context carrying the live tool list and the module context.
90
+ module_id: The requested setup's module, from the registry lookup.
91
+
92
+ Returns:
93
+ An ``already_loaded`` success, a conflict failure, or ``None`` to continue loading.
94
+ """
95
+ # Imported here, not at module top: ModuleToolkit requires the optional agno dependency at
96
+ # import time, while this module must stay importable without it (same convention as the
97
+ # rest of community.agno).
98
+ from digitalkin.community.agno.module_toolkit import ModuleToolkit
99
+
100
+ loaded = [tool for tool in ctx.base_tools if isinstance(tool, ModuleToolkit)]
101
+ existing = next((tool for tool in loaded if tool.tool_module_info.setup_id == self.setup_id), None)
102
+ if existing is not None:
103
+ # Re-persist rather than just report: the id is only durable if an earlier turn's write
104
+ # actually landed, and that write is fail-soft. Without this, one failed persist makes
105
+ # the tool re-loadable forever but never durable — the model keeps getting "already
106
+ # loaded" and the record never appears. The upsert is a no-op when it did land.
107
+ # Setup-declared tools are excluded: they need no mission record.
108
+ if self.setup_id not in ctx.context.tool_cache.declared:
109
+ await ctx.context.persist_loaded_tool(self.setup_id)
110
+ info = existing.tool_module_info
111
+ name = info.tool_name or info.module_name or info.slug
112
+ # Name the callable functions (they ARE callable right now) so an already-loaded result
113
+ # is as verifiable as a fresh load, instead of an empty list reading as "nothing to call".
114
+ callable_names = sorted({*existing.functions, *existing.async_functions})
115
+ listed = f" You can now call: {', '.join(callable_names)}." if callable_names else ""
116
+ return LoadOutcome(
117
+ ok=True,
118
+ status="already_loaded",
119
+ tool_name=name,
120
+ loaded_functions=callable_names,
121
+ message=f"'{name}' is already loaded; call it directly.{listed}",
122
+ )
123
+ # A *different* setup of an already-loaded module cannot rebind — the live binding wins
124
+ # server-side, so appending it would only add duplicate tool names and confirm a change that
125
+ # never takes effect. Refuse explicitly instead of lying.
126
+ conflict = next((tool for tool in loaded if tool.tool_module_info.module_id == module_id), None)
127
+ if conflict is not None:
128
+ return LoadOutcome(
129
+ ok=False,
130
+ message=(
131
+ f"could not load setup {self.setup_id}: its tool module is already loaded via setup "
132
+ f"{conflict.tool_module_info.setup_id}, whose configuration stays in effect"
133
+ ),
134
+ )
135
+ return None
136
+
81
137
  async def execute(self, ctx: LoadActionCtx) -> LoadOutcome: # noqa: C901, PLR0911 — each return is a distinct, LLM-readable outcome
82
138
  """Resolve the setup into a ModuleToolkit and append it to the live tool list.
83
139
 
@@ -125,7 +181,11 @@ class LoadToolAction(LoadAction):
125
181
  ),
126
182
  )
127
183
 
128
- # Confirmed tool module — resolve it into a callable toolkit.
184
+ duplicate = await self._duplicate_outcome(ctx, setup.module_id)
185
+ if duplicate is not None:
186
+ return duplicate
187
+
188
+ # Confirmed tool module, not already loaded — resolve it into a callable toolkit.
129
189
  try:
130
190
  info = await ctx.context.resolve_tool(self.setup_id)
131
191
  except PermissionDeniedError:
@@ -140,42 +200,15 @@ class LoadToolAction(LoadAction):
140
200
  ok=False, message=f"could not load setup {self.setup_id}: this tool module exposes no callable tools"
141
201
  )
142
202
 
143
- # Imported here, not at module top: ModuleToolkit requires the optional agno dependency at
144
- # import time, while this module must stay importable without it (same convention as the
145
- # rest of community.agno).
146
203
  from digitalkin.community.agno.module_toolkit import ModuleToolkit
147
204
 
148
205
  name = info.tool_name or info.module_name or info.slug
149
- loaded = [tool for tool in ctx.base_tools if isinstance(tool, ModuleToolkit)]
150
- # Idempotent: re-loading the exact same setup is a no-op success. Still name the callable
151
- # functions (they ARE callable right now) so an already-loaded result is as verifiable as a
152
- # fresh load, instead of an empty ``loaded_functions`` that reads as "nothing to call".
153
- existing = next((tool for tool in loaded if tool.tool_module_info.setup_id == self.setup_id), None)
154
- if existing is not None:
155
- callable_names = sorted({*existing.functions, *existing.async_functions})
156
- listed = f" You can now call: {', '.join(callable_names)}." if callable_names else ""
157
- return LoadOutcome(
158
- ok=True,
159
- status="already_loaded",
160
- tool_name=name,
161
- loaded_functions=callable_names,
162
- message=f"'{name}' is already loaded; call it directly.{listed}",
163
- )
164
- # A *different* setup of an already-loaded module cannot rebind — the live binding wins
165
- # server-side, so appending it would only add duplicate tool names and confirm a change that
166
- # never takes effect. Refuse explicitly instead of lying.
167
- conflict = next((tool for tool in loaded if tool.tool_module_info.module_id == info.module_id), None)
168
- if conflict is not None:
169
- return LoadOutcome(
170
- ok=False,
171
- message=(
172
- f"could not load setup {self.setup_id}: its tool module is already loaded via setup "
173
- f"{conflict.tool_module_info.setup_id}, whose configuration stays in effect"
174
- ),
175
- )
176
-
177
206
  toolkit = ModuleToolkit(ctx.context, info)
178
207
  ctx.base_tools.append(toolkit)
208
+ # Persist the id so the load outlives this turn. ``resolve_tool`` only reached the
209
+ # mission-scoped ``dynamic`` cache layer, which is rebuilt from scratch on the next
210
+ # user message; without this the tool would have to be re-loaded every turn.
211
+ await ctx.context.persist_loaded_tool(self.setup_id)
179
212
  await ctx.notify("tool_loaded", {"setup_id": self.setup_id, "tool_name": name})
180
213
  # Name the now-callable functions so the model can verify and call them directly.
181
214
  callable_names = sorted({*toolkit.functions, *toolkit.async_functions})
@@ -1,5 +1,10 @@
1
1
  """Module model exports. Import ag_ui types from ``digitalkin.models.module.ag_ui``."""
2
2
 
3
+ from digitalkin.models.module.loaded_tools import (
4
+ LOADED_TOOLS_STORAGE_CONFIG,
5
+ LoadedToolRecord,
6
+ LoadedToolStore,
7
+ )
3
8
  from digitalkin.models.module.module_context import ModuleContext
4
9
  from digitalkin.models.module.module_types import (
5
10
  DataModel,
@@ -25,9 +30,12 @@ from digitalkin.models.module.utility import (
25
30
  )
26
31
 
27
32
  __all__ = [
33
+ "LOADED_TOOLS_STORAGE_CONFIG",
28
34
  "DataModel",
29
35
  "DataTrigger",
30
36
  "EndOfStreamOutput",
37
+ "LoadedToolRecord",
38
+ "LoadedToolStore",
31
39
  "ModuleContext",
32
40
  "RequestMetadata",
33
41
  "SelectSchema",
@@ -0,0 +1,140 @@
1
+ """Mission-scoped persistence for tools the agent loaded at runtime.
2
+
3
+ A dynamic tool load (``load_manager`` → :meth:`ModuleContext.resolve_tool`) has to
4
+ outlive the turn that made it: every user message builds a fresh module instance,
5
+ so the in-memory toolkit list and the ``dynamic`` layer of the
6
+ :class:`~digitalkin.models.module.tool_cache.ToolCache` are both gone by the next
7
+ message. This module owns the only thing that survives — the list of loaded
8
+ ``setup_id``s, written to the mission-scoped ``loaded_tools`` storage collection.
9
+
10
+ Only ids are stored, never resolved schemas: rehydration re-runs ``resolve_tool``,
11
+ which re-checks authorization and picks up any schema change, so a tool revoked or
12
+ altered between two turns is handled correctly instead of being replayed from a
13
+ stale snapshot.
14
+
15
+ Modules that want runtime tool loading must register the collection::
16
+
17
+ services_config_params = {"storage": {"config": {**LOADED_TOOLS_STORAGE_CONFIG}}}
18
+ """
19
+
20
+ from typing import ClassVar
21
+
22
+ from pydantic import BaseModel
23
+
24
+ from digitalkin.logger import logger
25
+ from digitalkin.services.storage.storage_strategy import StorageStrategy
26
+
27
+ _LOADED_TOOLS_COLLECTION = "loaded_tools"
28
+
29
+ # The storage service caps a list() at 100 records; ask for the cap rather than the
30
+ # default 20, so a heavily-loaded mission rehydrates completely.
31
+ _LIST_LIMIT = 100
32
+
33
+
34
+ class LoadedToolRecord(BaseModel):
35
+ """A tool the agent loaded at runtime during this mission."""
36
+
37
+ setup_id: str
38
+
39
+
40
+ LOADED_TOOLS_STORAGE_CONFIG: dict[str, type[BaseModel]] = {_LOADED_TOOLS_COLLECTION: LoadedToolRecord}
41
+ """Storage config fragment for the ``loaded_tools`` collection."""
42
+
43
+
44
+ class LoadedToolStore:
45
+ """Storage wrapper for the mission-scoped ``loaded_tools`` collection.
46
+
47
+ Every method is fail-soft and returns instead of raising: a module that never
48
+ registered the collection (see :data:`LOADED_TOOLS_STORAGE_CONFIG`) must degrade
49
+ to the old turn-scoped behaviour, not crash the run through the HITL runner.
50
+ Registration is checked before each call so a module that does not opt into runtime
51
+ tool loading pays no storage round-trip per turn.
52
+ """
53
+
54
+ COLLECTION: ClassVar[str] = _LOADED_TOOLS_COLLECTION
55
+
56
+ def __init__(self, storage: StorageStrategy) -> None:
57
+ """Initialize the store.
58
+
59
+ Args:
60
+ storage: The module's storage strategy. Records are written under the
61
+ default mission context, which is what scopes a load to one
62
+ conversation.
63
+ """
64
+ self._storage = storage
65
+
66
+ async def save(self, setup_id: str) -> bool:
67
+ """Record ``setup_id`` as loaded for this mission (idempotent).
68
+
69
+ Args:
70
+ setup_id: The loaded tool's registry setup id.
71
+
72
+ Returns:
73
+ ``True`` if the id is now persisted, ``False`` if persistence is
74
+ unavailable — the caller keeps the tool for the current turn either way.
75
+ """
76
+ try:
77
+ if self.COLLECTION not in self._storage.config:
78
+ logger.warning(
79
+ "LoadedToolStore: collection '%s' is not registered on this module "
80
+ "(add LOADED_TOOLS_STORAGE_CONFIG to services_config_params); "
81
+ "tool '%s' will not survive the current turn",
82
+ self.COLLECTION,
83
+ setup_id,
84
+ )
85
+ return False
86
+ await self._storage.upsert(
87
+ collection=self.COLLECTION,
88
+ record_id=setup_id,
89
+ data=LoadedToolRecord(setup_id=setup_id).model_dump(),
90
+ )
91
+ except Exception:
92
+ logger.warning(
93
+ "LoadedToolStore: could not persist loaded tool '%s'; it will not survive this turn",
94
+ setup_id,
95
+ exc_info=True,
96
+ )
97
+ return False
98
+ logger.info("LoadedToolStore: persisted loaded tool '%s'", setup_id)
99
+ return True
100
+
101
+ async def list_setup_ids(self) -> list[str]:
102
+ """List the tool setup ids loaded so far in this mission.
103
+
104
+ Returns:
105
+ The persisted setup ids, or an empty list if none exist or the
106
+ collection is not registered.
107
+ """
108
+ try:
109
+ # Checked first so a module that never opted into runtime tool loading pays no
110
+ # storage round-trip on every turn.
111
+ if self.COLLECTION not in self._storage.config:
112
+ return []
113
+ records = await self._storage.list(collection=self.COLLECTION, limit=_LIST_LIMIT)
114
+ except Exception:
115
+ logger.debug("LoadedToolStore: could not list loaded tools", exc_info=True)
116
+ return []
117
+ # ``StorageRecord.data`` holds the instance the strategy validated against the
118
+ # registered model, so this is a narrowing, not a parse. Anything else in the
119
+ # collection is someone else's record and is skipped rather than guessed at.
120
+ setup_ids = [
121
+ record.data.setup_id
122
+ for record in records
123
+ if isinstance(record.data, LoadedToolRecord) and record.data.setup_id
124
+ ]
125
+ if setup_ids:
126
+ logger.info("LoadedToolStore: %d loaded tool(s) to rehydrate: %s", len(setup_ids), setup_ids)
127
+ return setup_ids
128
+
129
+ async def forget(self, setup_id: str) -> None:
130
+ """Drop a persisted id that no longer resolves, so it is not retried every turn.
131
+
132
+ Args:
133
+ setup_id: The setup id to remove from the mission's loaded set.
134
+ """
135
+ try:
136
+ if self.COLLECTION not in self._storage.config:
137
+ return
138
+ await self._storage.remove(collection=self.COLLECTION, record_id=setup_id)
139
+ except Exception:
140
+ logger.debug("LoadedToolStore: could not forget '%s'", setup_id, exc_info=True)
@@ -1,5 +1,6 @@
1
1
  """Define the module context used in the triggers."""
2
2
 
3
+ import asyncio
3
4
  from collections.abc import AsyncGenerator, Callable
4
5
  from datetime import tzinfo
5
6
  from types import SimpleNamespace
@@ -8,7 +9,9 @@ from zoneinfo import ZoneInfo
8
9
 
9
10
  from google.protobuf import json_format
10
11
 
12
+ from digitalkin.grpc_servers.exceptions import PermissionDeniedError
11
13
  from digitalkin.logger import logger
14
+ from digitalkin.models.module.loaded_tools import LoadedToolStore
12
15
  from digitalkin.models.module.request_metadata import RequestMetadata
13
16
  from digitalkin.models.module.tool_cache import ToolCache, ToolDefinition, ToolModuleInfo
14
17
  from digitalkin.models.settings.module import get_module_settings
@@ -334,7 +337,7 @@ class ModuleContext:
334
337
  Returns:
335
338
  List of (ToolDefinition, async_generator_function) tuples. Empty if not found.
336
339
  """
337
- tool_module_info = self.tool_cache.entries.get(slug)
340
+ tool_module_info = self.tool_cache.get(slug)
338
341
  if not tool_module_info:
339
342
  return []
340
343
 
@@ -355,11 +358,14 @@ class ModuleContext:
355
358
  """Resolve a registry ``setup_id`` into a ``ToolModuleInfo`` and cache it.
356
359
 
357
360
  On-demand loader for a discovered tool. ``registry.get_setup`` always runs
358
- first — it is the permission gate, and the tool cache is shared across
359
- missions of the same agent setup, so a cache hit must never skip authz.
361
+ first — it is the permission gate, and the cache's ``declared`` layer is shared
362
+ across missions of the same agent setup, so a cache hit must never skip authz.
360
363
  The cache only short-circuits the module discovery + schema fetch.
361
364
  Permission denials propagate so callers can surface them distinctly.
362
365
 
366
+ The result lands in the cache's mission-scoped ``dynamic`` layer. Pair this with
367
+ :meth:`persist_loaded_tool` to make the load outlive the current turn.
368
+
363
369
  Args:
364
370
  setup_id: The registry setup id to load as an invocable tool.
365
371
 
@@ -376,7 +382,7 @@ class ModuleContext:
376
382
  "resolve_tool: setup '%s' not found or has no module", setup_id, extra=self.session.current_ids()
377
383
  )
378
384
  return None
379
- cached = self.tool_cache.entries.get(setup_id)
385
+ cached = self.tool_cache.get(setup_id)
380
386
  if cached is not None:
381
387
  logger.debug(
382
388
  "resolve_tool: cache hit for setup '%s' (authz re-checked)", setup_id, extra=self.session.current_ids()
@@ -401,7 +407,10 @@ class ModuleContext:
401
407
  )
402
408
  return None
403
409
  tool_info = await ToolModuleInfo.from_module_info(info, setup_id, setup.name, self.communication)
404
- self.tool_cache.add(tool_info)
410
+ # Mission-scoped layer, never ``declared``: the declared layer is the object the
411
+ # servicer shares with every other mission of this setup, so writing a runtime
412
+ # load there makes it reappear in unrelated conversations.
413
+ self.tool_cache.add_dynamic(tool_info)
405
414
  logger.info(
406
415
  "resolve_tool: resolved setup '%s' -> module '%s' (%d tools), cached",
407
416
  setup_id,
@@ -411,6 +420,81 @@ class ModuleContext:
411
420
  )
412
421
  return tool_info
413
422
 
423
+ async def persist_loaded_tool(self, setup_id: str) -> bool:
424
+ """Record a runtime-loaded tool so it survives into this mission's next turn.
425
+
426
+ Every user message builds a fresh module instance, so the ``dynamic`` cache layer
427
+ and the agent's tool list are both rebuilt from scratch. Persisting the id is what
428
+ turns a load into a property of the *conversation* rather than of the turn.
429
+
430
+ Args:
431
+ setup_id: The loaded tool's registry setup id.
432
+
433
+ Returns:
434
+ ``True`` if the id was persisted; ``False`` if the module never registered the
435
+ ``loaded_tools`` collection, in which case the load lasts only this turn.
436
+ """
437
+ return await LoadedToolStore(self.storage).save(setup_id)
438
+
439
+ async def rehydrate_loaded_tools(self) -> int:
440
+ """Re-resolve this mission's previously-loaded tools into the ``dynamic`` layer.
441
+
442
+ Called once per turn from ``BaseModule.prepare()``, before ``initialize()`` builds
443
+ the agent's toolkits. Ids are re-resolved rather than replayed from a snapshot, so
444
+ each turn re-checks authorization and picks up schema changes; an id that no longer
445
+ resolves — revoked, deleted — is dropped from the mission so it is not retried on
446
+ every subsequent message.
447
+
448
+ Returns:
449
+ The number of tools restored into the dynamic layer.
450
+ """
451
+ store = LoadedToolStore(self.storage)
452
+ setup_ids = [sid for sid in await store.list_setup_ids() if sid not in self.tool_cache.dynamic]
453
+ if not setup_ids:
454
+ return 0
455
+
456
+ # Resolve concurrently: each id costs a get_setup + discover + schema fetch, and this
457
+ # sits on the critical path of every user message.
458
+ results = await asyncio.gather(
459
+ *(self.resolve_tool(setup_id) for setup_id in setup_ids),
460
+ return_exceptions=True,
461
+ )
462
+
463
+ restored = 0
464
+ for setup_id, result in zip(setup_ids, results, strict=True):
465
+ if isinstance(result, PermissionDeniedError):
466
+ logger.warning(
467
+ "rehydrate_loaded_tools: access to '%s' was revoked; dropping it from the mission",
468
+ setup_id,
469
+ extra=self.session.current_ids(),
470
+ )
471
+ await store.forget(setup_id)
472
+ elif isinstance(result, BaseException):
473
+ # Transient (registry hiccup): keep the id so a later turn can retry.
474
+ logger.warning(
475
+ "rehydrate_loaded_tools: could not restore '%s': %s",
476
+ setup_id,
477
+ result,
478
+ extra=self.session.current_ids(),
479
+ )
480
+ elif result is None:
481
+ logger.warning(
482
+ "rehydrate_loaded_tools: '%s' no longer exists; dropping it from the mission",
483
+ setup_id,
484
+ extra=self.session.current_ids(),
485
+ )
486
+ await store.forget(setup_id)
487
+ else:
488
+ restored += 1
489
+
490
+ logger.info(
491
+ "rehydrate_loaded_tools: restored %d/%d loaded tool(s)",
492
+ restored,
493
+ len(setup_ids),
494
+ extra=self.session.current_ids(),
495
+ )
496
+ return restored
497
+
414
498
  @staticmethod
415
499
  def _create_single_tool_function(
416
500
  communication: CommunicationStrategy,
@@ -186,20 +186,79 @@ class ToolModuleInfo(ModuleInfo):
186
186
 
187
187
 
188
188
  class ToolCache(BaseModel):
189
- """Registry cache storing resolved tool references by setup field name."""
189
+ """Two-layer cache of resolved tool references, keyed by ``setup_id``.
190
190
 
191
- entries: dict[str, ToolModuleInfo] = Field(default_factory=dict)
191
+ The layers differ by *lifetime*, which is the whole point of the split:
192
+
193
+ - ``declared`` — the tools the setup itself selects, resolved once by
194
+ ``SetupModel.build_tool_cache``. The servicer keeps this layer alive per
195
+ ``setup_id`` and hands the same object to every mission of that setup.
196
+ - ``dynamic`` — tools the agent loaded at runtime via
197
+ ``ModuleContext.resolve_tool``. This layer is **mission-scoped**: it is
198
+ rebuilt per mission from the ``loaded_tools`` storage collection, so a load
199
+ persists across the turns of one conversation and never leaks into another
200
+ mission of the same setup.
201
+
202
+ Never write a runtime resolution into ``declared`` — doing so is what leaked
203
+ dynamically-loaded tools across missions before the split existed.
204
+ """
205
+
206
+ declared: dict[str, ToolModuleInfo] = Field(
207
+ default_factory=dict, description="Setup-selected tools; shared across missions of one setup."
208
+ )
209
+ dynamic: dict[str, ToolModuleInfo] = Field(
210
+ default_factory=dict, description="Runtime-loaded tools; scoped to a single mission."
211
+ )
212
+
213
+ @property
214
+ def entries(self) -> dict[str, ToolModuleInfo]:
215
+ """Merged read-only view of both layers, ``dynamic`` winning on conflict.
216
+
217
+ Returns:
218
+ A fresh dict — mutating it does not touch either layer (and must not:
219
+ ``declared`` is shared with every other mission of this setup).
220
+ """
221
+ return {**self.declared, **self.dynamic}
222
+
223
+ def mission_view(self, dynamic: dict[str, ToolModuleInfo] | None = None) -> "ToolCache":
224
+ """Build a per-mission view that shares this cache's ``declared`` entries.
225
+
226
+ The mapping is shallow-copied so a mission can never mutate the shared
227
+ declared layer, while the ``ToolModuleInfo`` values are reused as-is
228
+ (they are treated as immutable, and re-validating them per turn is not free).
229
+
230
+ Args:
231
+ dynamic: Pre-resolved runtime tools to seed the mission layer with.
232
+
233
+ Returns:
234
+ A new ``ToolCache`` whose ``dynamic`` layer is private to this mission.
235
+ """
236
+ return ToolCache.model_construct(declared=dict(self.declared), dynamic=dynamic or {})
192
237
 
193
238
  def add(self, tool_module_info: ToolModuleInfo) -> None:
194
- """Add a tool to the cache.
239
+ """Add a setup-declared tool to the ``declared`` layer.
240
+
241
+ Args:
242
+ tool_module_info: Resolved tool module information.
243
+ """
244
+ setup_id = tool_module_info.setup_id
245
+ self.declared[setup_id] = tool_module_info
246
+ logger.debug(
247
+ "Tool cached (declared): module_id=%s",
248
+ tool_module_info.module_id,
249
+ extra={"setup_id": setup_id},
250
+ )
251
+
252
+ def add_dynamic(self, tool_module_info: ToolModuleInfo) -> None:
253
+ """Add a runtime-loaded tool to the mission-scoped ``dynamic`` layer.
195
254
 
196
255
  Args:
197
256
  tool_module_info: Resolved tool module information.
198
257
  """
199
258
  setup_id = tool_module_info.setup_id
200
- self.entries[setup_id] = tool_module_info
259
+ self.dynamic[setup_id] = tool_module_info
201
260
  logger.debug(
202
- "Tool cached: module_id=%s",
261
+ "Tool cached (dynamic): module_id=%s",
203
262
  tool_module_info.module_id,
204
263
  extra={"setup_id": setup_id},
205
264
  )
@@ -208,7 +267,7 @@ class ToolCache(BaseModel):
208
267
  self,
209
268
  setup_id: str,
210
269
  ) -> ToolModuleInfo | None:
211
- """Get a tool from cache, optionally querying registry on miss.
270
+ """Get a tool from either layer, preferring the mission-scoped one.
212
271
 
213
272
  Args:
214
273
  setup_id: Field name to look up.
@@ -216,14 +275,15 @@ class ToolCache(BaseModel):
216
275
  Returns:
217
276
  ToolModuleInfo if found, None otherwise.
218
277
  """
219
- return self.entries.get(setup_id)
278
+ return self.dynamic.get(setup_id) or self.declared.get(setup_id)
220
279
 
221
280
  def clear(self) -> None:
222
- """Clear all cache entries."""
223
- self.entries.clear()
281
+ """Clear both layers."""
282
+ self.declared.clear()
283
+ self.dynamic.clear()
224
284
 
225
285
  def list_tools(self) -> list[str]:
226
- """List all cached tool names.
286
+ """List all cached tool names across both layers.
227
287
 
228
288
  Returns:
229
289
  List of setup field names in cache.
@@ -648,9 +648,16 @@ class BaseModule( # Module SDK base class requires many public methods # noqa:
648
648
  self.context.registry,
649
649
  self.context.communication,
650
650
  )
651
- if tool_cache.entries:
652
- self.context.tool_cache = tool_cache
651
+ # Installed unconditionally, even when the setup declares no tools: the
652
+ # mission view owns the ``dynamic`` layer, which is where runtime loads land.
653
+ # Gating on a non-empty declared layer used to leave the context holding a
654
+ # throwaway ToolCache, so a setup with no selected tools could never keep one.
655
+ self.context.tool_cache = tool_cache.mission_view()
653
656
  timer.mark("build_tool_cache")
657
+ # Restore the tools the agent loaded earlier in this mission, before
658
+ # initialize() builds the toolkits that have to expose them.
659
+ await self.context.rehydrate_loaded_tools()
660
+ timer.mark("rehydrate_loaded_tools")
654
661
 
655
662
  await self.initialize(self.context, setup_data)
656
663
  timer.mark("initialize")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: digitalkin
3
- Version: 1.0.3.dev1
3
+ Version: 1.0.3.dev2
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
@@ -148,6 +148,7 @@ src/digitalkin/models/grpc_servers/types.py
148
148
  src/digitalkin/models/module/__init__.py
149
149
  src/digitalkin/models/module/ag_ui.py
150
150
  src/digitalkin/models/module/base_types.py
151
+ src/digitalkin/models/module/loaded_tools.py
151
152
  src/digitalkin/models/module/module.py
152
153
  src/digitalkin/models/module/module_context.py
153
154
  src/digitalkin/models/module/module_types.py
File without changes