krons 0.2.1__tar.gz → 0.2.2__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 (267) hide show
  1. {krons-0.2.1 → krons-0.2.2}/PKG-INFO +1 -1
  2. {krons-0.2.1 → krons-0.2.2}/pyproject.toml +1 -1
  3. {krons-0.2.1 → krons-0.2.2}/src/krons/core/__init__.py +3 -0
  4. {krons-0.2.1 → krons-0.2.2}/src/krons/core/base/__init__.py +4 -0
  5. krons-0.2.2/src/krons/core/base/log.py +32 -0
  6. {krons-0.2.1 → krons-0.2.2}/src/krons/session/session.py +114 -1
  7. krons-0.2.2/tests/core/test_log.py +44 -0
  8. krons-0.2.2/tests/session/test_session_logging.py +337 -0
  9. {krons-0.2.1 → krons-0.2.2}/.github/workflows/release.yml +0 -0
  10. {krons-0.2.1 → krons-0.2.2}/.gitignore +0 -0
  11. {krons-0.2.1 → krons-0.2.2}/.python-version +0 -0
  12. {krons-0.2.1 → krons-0.2.2}/CLAUDE.md +0 -0
  13. {krons-0.2.1 → krons-0.2.2}/LICENSE +0 -0
  14. {krons-0.2.1 → krons-0.2.2}/README.md +0 -0
  15. {krons-0.2.1 → krons-0.2.2}/cookbooks/007_fan_out_in.py +0 -0
  16. {krons-0.2.1 → krons-0.2.2}/cookbooks/test_conduct.py +0 -0
  17. {krons-0.2.1 → krons-0.2.2}/src/krons/__init__.py +0 -0
  18. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/__init__.py +0 -0
  19. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/mcps/__init__.py +0 -0
  20. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/mcps/loader.py +0 -0
  21. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/mcps/wrapper.py +0 -0
  22. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/message/__init__.py +0 -0
  23. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/message/action.py +0 -0
  24. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/message/assistant.py +0 -0
  25. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/message/common.py +0 -0
  26. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/message/instruction.py +0 -0
  27. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/message/prepare_msg.py +0 -0
  28. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/message/role.py +0 -0
  29. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/message/system.py +0 -0
  30. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/operations/__init__.py +0 -0
  31. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/operations/act.py +0 -0
  32. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/operations/generate.py +0 -0
  33. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/operations/llm_reparse.py +0 -0
  34. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/operations/operate.py +0 -0
  35. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/operations/parse.py +0 -0
  36. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/operations/react.py +0 -0
  37. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/operations/specs.py +0 -0
  38. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/operations/structure.py +0 -0
  39. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/operations/utils.py +0 -0
  40. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/providers/__init__.py +0 -0
  41. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/providers/anthropic_messages.py +0 -0
  42. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/providers/claude_code.py +0 -0
  43. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/providers/gemini.py +0 -0
  44. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/providers/match.py +0 -0
  45. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/providers/oai_chat.py +0 -0
  46. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/third_party/__init__.py +0 -0
  47. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/third_party/anthropic_models.py +0 -0
  48. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/third_party/claude_code.py +0 -0
  49. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/third_party/gemini_models.py +0 -0
  50. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/third_party/openai_models.py +0 -0
  51. {krons-0.2.1 → krons-0.2.2}/src/krons/agent/tool.py +0 -0
  52. {krons-0.2.1 → krons-0.2.2}/src/krons/core/base/broadcaster.py +0 -0
  53. {krons-0.2.1 → krons-0.2.2}/src/krons/core/base/element.py +0 -0
  54. {krons-0.2.1 → krons-0.2.2}/src/krons/core/base/event.py +0 -0
  55. {krons-0.2.1 → krons-0.2.2}/src/krons/core/base/eventbus.py +0 -0
  56. {krons-0.2.1 → krons-0.2.2}/src/krons/core/base/flow.py +0 -0
  57. {krons-0.2.1 → krons-0.2.2}/src/krons/core/base/graph.py +0 -0
  58. {krons-0.2.1 → krons-0.2.2}/src/krons/core/base/node.py +0 -0
  59. {krons-0.2.1 → krons-0.2.2}/src/krons/core/base/pile.py +0 -0
  60. {krons-0.2.1 → krons-0.2.2}/src/krons/core/base/processor.py +0 -0
  61. {krons-0.2.1 → krons-0.2.2}/src/krons/core/base/progression.py +0 -0
  62. {krons-0.2.1 → krons-0.2.2}/src/krons/core/specs/__init__.py +0 -0
  63. {krons-0.2.1 → krons-0.2.2}/src/krons/core/specs/adapters/__init__.py +0 -0
  64. {krons-0.2.1 → krons-0.2.2}/src/krons/core/specs/adapters/_utils.py +0 -0
  65. {krons-0.2.1 → krons-0.2.2}/src/krons/core/specs/adapters/dataclass_field.py +0 -0
  66. {krons-0.2.1 → krons-0.2.2}/src/krons/core/specs/adapters/factory.py +0 -0
  67. {krons-0.2.1 → krons-0.2.2}/src/krons/core/specs/adapters/pydantic_adapter.py +0 -0
  68. {krons-0.2.1 → krons-0.2.2}/src/krons/core/specs/adapters/sql_ddl.py +0 -0
  69. {krons-0.2.1 → krons-0.2.2}/src/krons/core/specs/catalog/__init__.py +0 -0
  70. {krons-0.2.1 → krons-0.2.2}/src/krons/core/specs/catalog/_audit.py +0 -0
  71. {krons-0.2.1 → krons-0.2.2}/src/krons/core/specs/catalog/_common.py +0 -0
  72. {krons-0.2.1 → krons-0.2.2}/src/krons/core/specs/catalog/_content.py +0 -0
  73. {krons-0.2.1 → krons-0.2.2}/src/krons/core/specs/catalog/_enforcement.py +0 -0
  74. {krons-0.2.1 → krons-0.2.2}/src/krons/core/specs/factory.py +0 -0
  75. {krons-0.2.1 → krons-0.2.2}/src/krons/core/specs/operable.py +0 -0
  76. {krons-0.2.1 → krons-0.2.2}/src/krons/core/specs/protocol.py +0 -0
  77. {krons-0.2.1 → krons-0.2.2}/src/krons/core/specs/spec.py +0 -0
  78. {krons-0.2.1 → krons-0.2.2}/src/krons/core/types/__init__.py +0 -0
  79. {krons-0.2.1 → krons-0.2.2}/src/krons/core/types/_sentinel.py +0 -0
  80. {krons-0.2.1 → krons-0.2.2}/src/krons/core/types/base.py +0 -0
  81. {krons-0.2.1 → krons-0.2.2}/src/krons/core/types/db_types.py +0 -0
  82. {krons-0.2.1 → krons-0.2.2}/src/krons/core/types/identity.py +0 -0
  83. {krons-0.2.1 → krons-0.2.2}/src/krons/errors.py +0 -0
  84. {krons-0.2.1 → krons-0.2.2}/src/krons/protocols.py +0 -0
  85. {krons-0.2.1 → krons-0.2.2}/src/krons/py.typed +0 -0
  86. {krons-0.2.1 → krons-0.2.2}/src/krons/resource/__init__.py +0 -0
  87. {krons-0.2.1 → krons-0.2.2}/src/krons/resource/backend.py +0 -0
  88. {krons-0.2.1 → krons-0.2.2}/src/krons/resource/endpoint.py +0 -0
  89. {krons-0.2.1 → krons-0.2.2}/src/krons/resource/hook.py +0 -0
  90. {krons-0.2.1 → krons-0.2.2}/src/krons/resource/imodel.py +0 -0
  91. {krons-0.2.1 → krons-0.2.2}/src/krons/resource/registry.py +0 -0
  92. {krons-0.2.1 → krons-0.2.2}/src/krons/resource/utilities/__init__.py +0 -0
  93. {krons-0.2.1 → krons-0.2.2}/src/krons/resource/utilities/header_factory.py +0 -0
  94. {krons-0.2.1 → krons-0.2.2}/src/krons/resource/utilities/rate_limited_executor.py +0 -0
  95. {krons-0.2.1 → krons-0.2.2}/src/krons/resource/utilities/rate_limiter.py +0 -0
  96. {krons-0.2.1 → krons-0.2.2}/src/krons/resource/utilities/resilience.py +0 -0
  97. {krons-0.2.1 → krons-0.2.2}/src/krons/resource/utilities/token_calculator.py +0 -0
  98. {krons-0.2.1 → krons-0.2.2}/src/krons/session/__init__.py +0 -0
  99. {krons-0.2.1 → krons-0.2.2}/src/krons/session/constraints.py +0 -0
  100. {krons-0.2.1 → krons-0.2.2}/src/krons/session/exchange.py +0 -0
  101. {krons-0.2.1 → krons-0.2.2}/src/krons/session/message.py +0 -0
  102. {krons-0.2.1 → krons-0.2.2}/src/krons/session/registry.py +0 -0
  103. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/__init__.py +0 -0
  104. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/_function_arg_parser.py +0 -0
  105. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/_hash.py +0 -0
  106. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/_json_dump.py +0 -0
  107. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/_lazy_init.py +0 -0
  108. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/_pythonic_function_call.py +0 -0
  109. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/_to_list.py +0 -0
  110. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/_to_num.py +0 -0
  111. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/_utils.py +0 -0
  112. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/concurrency/__init__.py +0 -0
  113. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/concurrency/_async_call.py +0 -0
  114. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/concurrency/_cancel.py +0 -0
  115. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/concurrency/_errors.py +0 -0
  116. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/concurrency/_patterns.py +0 -0
  117. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/concurrency/_primitives.py +0 -0
  118. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/concurrency/_priority_queue.py +0 -0
  119. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/concurrency/_resource_tracker.py +0 -0
  120. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/concurrency/_run_async.py +0 -0
  121. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/concurrency/_task.py +0 -0
  122. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/concurrency/_utils.py +0 -0
  123. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/display.py +0 -0
  124. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/fuzzy/__init__.py +0 -0
  125. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/fuzzy/_extract_json.py +0 -0
  126. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/fuzzy/_fuzzy_json.py +0 -0
  127. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/fuzzy/_fuzzy_match.py +0 -0
  128. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/fuzzy/_string_similarity.py +0 -0
  129. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/fuzzy/_to_dict.py +0 -0
  130. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/schemas/__init__.py +0 -0
  131. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/schemas/_breakdown_pydantic_annotation.py +0 -0
  132. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/schemas/_formatter.py +0 -0
  133. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/schemas/_minimal_yaml.py +0 -0
  134. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/schemas/_typescript.py +0 -0
  135. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/sql/__init__.py +0 -0
  136. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/sql/_sql_validation.py +0 -0
  137. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/validators/__init__.py +0 -0
  138. {krons-0.2.1 → krons-0.2.2}/src/krons/utils/validators/_validate_image_url.py +0 -0
  139. {krons-0.2.1 → krons-0.2.2}/src/krons/work/__init__.py +0 -0
  140. {krons-0.2.1 → krons-0.2.2}/src/krons/work/engine.py +0 -0
  141. {krons-0.2.1 → krons-0.2.2}/src/krons/work/form.py +0 -0
  142. {krons-0.2.1 → krons-0.2.2}/src/krons/work/operations/__init__.py +0 -0
  143. {krons-0.2.1 → krons-0.2.2}/src/krons/work/operations/builder.py +0 -0
  144. {krons-0.2.1 → krons-0.2.2}/src/krons/work/operations/context.py +0 -0
  145. {krons-0.2.1 → krons-0.2.2}/src/krons/work/operations/flow.py +0 -0
  146. {krons-0.2.1 → krons-0.2.2}/src/krons/work/operations/node.py +0 -0
  147. {krons-0.2.1 → krons-0.2.2}/src/krons/work/operations/registry.py +0 -0
  148. {krons-0.2.1 → krons-0.2.2}/src/krons/work/report.py +0 -0
  149. {krons-0.2.1 → krons-0.2.2}/src/krons/work/rules/__init__.py +0 -0
  150. {krons-0.2.1 → krons-0.2.2}/src/krons/work/rules/common/__init__.py +0 -0
  151. {krons-0.2.1 → krons-0.2.2}/src/krons/work/rules/common/boolean.py +0 -0
  152. {krons-0.2.1 → krons-0.2.2}/src/krons/work/rules/common/choice.py +0 -0
  153. {krons-0.2.1 → krons-0.2.2}/src/krons/work/rules/common/mapping.py +0 -0
  154. {krons-0.2.1 → krons-0.2.2}/src/krons/work/rules/common/model.py +0 -0
  155. {krons-0.2.1 → krons-0.2.2}/src/krons/work/rules/common/number.py +0 -0
  156. {krons-0.2.1 → krons-0.2.2}/src/krons/work/rules/common/string.py +0 -0
  157. {krons-0.2.1 → krons-0.2.2}/src/krons/work/rules/registry.py +0 -0
  158. {krons-0.2.1 → krons-0.2.2}/src/krons/work/rules/rule.py +0 -0
  159. {krons-0.2.1 → krons-0.2.2}/src/krons/work/rules/validator.py +0 -0
  160. {krons-0.2.1 → krons-0.2.2}/src/krons/work/worker.py +0 -0
  161. {krons-0.2.1 → krons-0.2.2}/tests/__init__.py +0 -0
  162. {krons-0.2.1 → krons-0.2.2}/tests/agent/__init__.py +0 -0
  163. {krons-0.2.1 → krons-0.2.2}/tests/agent/test_mcps_unit.py +0 -0
  164. {krons-0.2.1 → krons-0.2.2}/tests/agent/test_message.py +0 -0
  165. {krons-0.2.1 → krons-0.2.2}/tests/agent/test_providers_e2e.py +0 -0
  166. {krons-0.2.1 → krons-0.2.2}/tests/agent/test_providers_unit.py +0 -0
  167. {krons-0.2.1 → krons-0.2.2}/tests/agent/test_tool_unit.py +0 -0
  168. {krons-0.2.1 → krons-0.2.2}/tests/conftest.py +0 -0
  169. {krons-0.2.1 → krons-0.2.2}/tests/core/__init__.py +0 -0
  170. {krons-0.2.1 → krons-0.2.2}/tests/core/test_broadcaster.py +0 -0
  171. {krons-0.2.1 → krons-0.2.2}/tests/core/test_element.py +0 -0
  172. {krons-0.2.1 → krons-0.2.2}/tests/core/test_error_paths.py +0 -0
  173. {krons-0.2.1 → krons-0.2.2}/tests/core/test_event.py +0 -0
  174. {krons-0.2.1 → krons-0.2.2}/tests/core/test_event_status_race.py +0 -0
  175. {krons-0.2.1 → krons-0.2.2}/tests/core/test_eventbus.py +0 -0
  176. {krons-0.2.1 → krons-0.2.2}/tests/core/test_flow.py +0 -0
  177. {krons-0.2.1 → krons-0.2.2}/tests/core/test_flow_edge_cases.py +0 -0
  178. {krons-0.2.1 → krons-0.2.2}/tests/core/test_graph.py +0 -0
  179. {krons-0.2.1 → krons-0.2.2}/tests/core/test_graph_event_loop.py +0 -0
  180. {krons-0.2.1 → krons-0.2.2}/tests/core/test_node.py +0 -0
  181. {krons-0.2.1 → krons-0.2.2}/tests/core/test_pile.py +0 -0
  182. {krons-0.2.1 → krons-0.2.2}/tests/core/test_pile_edge_cases.py +0 -0
  183. {krons-0.2.1 → krons-0.2.2}/tests/core/test_processor.py +0 -0
  184. {krons-0.2.1 → krons-0.2.2}/tests/core/test_processor_security.py +0 -0
  185. {krons-0.2.1 → krons-0.2.2}/tests/core/test_progression.py +0 -0
  186. {krons-0.2.1 → krons-0.2.2}/tests/core/test_progression_edge_cases.py +0 -0
  187. {krons-0.2.1 → krons-0.2.2}/tests/core/test_serialization_roundtrip.py +0 -0
  188. {krons-0.2.1 → krons-0.2.2}/tests/core/test_thread_safety_stress.py +0 -0
  189. {krons-0.2.1 → krons-0.2.2}/tests/operations/__init__.py +0 -0
  190. {krons-0.2.1 → krons-0.2.2}/tests/operations/test_builder.py +0 -0
  191. {krons-0.2.1 → krons-0.2.2}/tests/operations/test_flow.py +0 -0
  192. {krons-0.2.1 → krons-0.2.2}/tests/operations/test_node.py +0 -0
  193. {krons-0.2.1 → krons-0.2.2}/tests/operations/test_op_flow.py +0 -0
  194. {krons-0.2.1 → krons-0.2.2}/tests/operations/test_op_node.py +0 -0
  195. {krons-0.2.1 → krons-0.2.2}/tests/operations/test_registry.py +0 -0
  196. {krons-0.2.1 → krons-0.2.2}/tests/resource/__init__.py +0 -0
  197. {krons-0.2.1 → krons-0.2.2}/tests/resource/test_backend.py +0 -0
  198. {krons-0.2.1 → krons-0.2.2}/tests/resource/test_endpoint.py +0 -0
  199. {krons-0.2.1 → krons-0.2.2}/tests/resource/test_hook.py +0 -0
  200. {krons-0.2.1 → krons-0.2.2}/tests/resource/test_imodel.py +0 -0
  201. {krons-0.2.1 → krons-0.2.2}/tests/resource/test_registry.py +0 -0
  202. {krons-0.2.1 → krons-0.2.2}/tests/resource/utilities/__init__.py +0 -0
  203. {krons-0.2.1 → krons-0.2.2}/tests/resource/utilities/test_header_factory.py +0 -0
  204. {krons-0.2.1 → krons-0.2.2}/tests/resource/utilities/test_rate_limiter.py +0 -0
  205. {krons-0.2.1 → krons-0.2.2}/tests/resource/utilities/test_resilience.py +0 -0
  206. {krons-0.2.1 → krons-0.2.2}/tests/resource/utilities/test_resilience_retry.py +0 -0
  207. {krons-0.2.1 → krons-0.2.2}/tests/rules/__init__.py +0 -0
  208. {krons-0.2.1 → krons-0.2.2}/tests/rules/test_base.py +0 -0
  209. {krons-0.2.1 → krons-0.2.2}/tests/rules/test_builtin.py +0 -0
  210. {krons-0.2.1 → krons-0.2.2}/tests/rules/test_registry.py +0 -0
  211. {krons-0.2.1 → krons-0.2.2}/tests/rules/test_rule_params_edge_cases.py +0 -0
  212. {krons-0.2.1 → krons-0.2.2}/tests/rules/test_rules_comprehensive.py +0 -0
  213. {krons-0.2.1 → krons-0.2.2}/tests/rules/test_validator.py +0 -0
  214. {krons-0.2.1 → krons-0.2.2}/tests/session/__init__.py +0 -0
  215. {krons-0.2.1 → krons-0.2.2}/tests/session/test_exchange.py +0 -0
  216. {krons-0.2.1 → krons-0.2.2}/tests/session/test_message.py +0 -0
  217. {krons-0.2.1 → krons-0.2.2}/tests/session/test_message_edge_cases.py +0 -0
  218. {krons-0.2.1 → krons-0.2.2}/tests/session/test_session.py +0 -0
  219. {krons-0.2.1 → krons-0.2.2}/tests/session/test_session_edge_cases.py +0 -0
  220. {krons-0.2.1 → krons-0.2.2}/tests/specs/__init__.py +0 -0
  221. {krons-0.2.1 → krons-0.2.2}/tests/specs/test_catalog.py +0 -0
  222. {krons-0.2.1 → krons-0.2.2}/tests/specs/test_factory.py +0 -0
  223. {krons-0.2.1 → krons-0.2.2}/tests/test_protocols.py +0 -0
  224. {krons-0.2.1 → krons-0.2.2}/tests/types/__init__.py +0 -0
  225. {krons-0.2.1 → krons-0.2.2}/tests/types/conftest.py +0 -0
  226. {krons-0.2.1 → krons-0.2.2}/tests/types/spec_adapters/__init__.py +0 -0
  227. {krons-0.2.1 → krons-0.2.2}/tests/types/spec_adapters/test_adapters_py311.py +0 -0
  228. {krons-0.2.1 → krons-0.2.2}/tests/types/spec_adapters/test_protocol.py +0 -0
  229. {krons-0.2.1 → krons-0.2.2}/tests/types/spec_adapters/test_pydantic_field.py +0 -0
  230. {krons-0.2.1 → krons-0.2.2}/tests/types/spec_adapters/test_sql_ddl_specs.py +0 -0
  231. {krons-0.2.1 → krons-0.2.2}/tests/types/test_db_types.py +0 -0
  232. {krons-0.2.1 → krons-0.2.2}/tests/types/test_identity.py +0 -0
  233. {krons-0.2.1 → krons-0.2.2}/tests/types/test_model.py +0 -0
  234. {krons-0.2.1 → krons-0.2.2}/tests/types/test_operable.py +0 -0
  235. {krons-0.2.1 → krons-0.2.2}/tests/types/test_sentinel.py +0 -0
  236. {krons-0.2.1 → krons-0.2.2}/tests/types/test_spec.py +0 -0
  237. {krons-0.2.1 → krons-0.2.2}/tests/types/test_types.py +0 -0
  238. {krons-0.2.1 → krons-0.2.2}/tests/types/test_types_py311.py +0 -0
  239. {krons-0.2.1 → krons-0.2.2}/tests/utils/__init__.py +0 -0
  240. {krons-0.2.1 → krons-0.2.2}/tests/utils/concurrency/__init__.py +0 -0
  241. {krons-0.2.1 → krons-0.2.2}/tests/utils/concurrency/test_async_call.py +0 -0
  242. {krons-0.2.1 → krons-0.2.2}/tests/utils/concurrency/test_cancel.py +0 -0
  243. {krons-0.2.1 → krons-0.2.2}/tests/utils/concurrency/test_errors.py +0 -0
  244. {krons-0.2.1 → krons-0.2.2}/tests/utils/concurrency/test_patterns.py +0 -0
  245. {krons-0.2.1 → krons-0.2.2}/tests/utils/concurrency/test_primitives.py +0 -0
  246. {krons-0.2.1 → krons-0.2.2}/tests/utils/concurrency/test_priority_queue.py +0 -0
  247. {krons-0.2.1 → krons-0.2.2}/tests/utils/concurrency/test_run_async.py +0 -0
  248. {krons-0.2.1 → krons-0.2.2}/tests/utils/concurrency/test_task.py +0 -0
  249. {krons-0.2.1 → krons-0.2.2}/tests/utils/concurrency/test_utils.py +0 -0
  250. {krons-0.2.1 → krons-0.2.2}/tests/utils/test_extract_json.py +0 -0
  251. {krons-0.2.1 → krons-0.2.2}/tests/utils/test_fuzzy_json.py +0 -0
  252. {krons-0.2.1 → krons-0.2.2}/tests/utils/test_fuzzy_match.py +0 -0
  253. {krons-0.2.1 → krons-0.2.2}/tests/utils/test_hash.py +0 -0
  254. {krons-0.2.1 → krons-0.2.2}/tests/utils/test_json_dump.py +0 -0
  255. {krons-0.2.1 → krons-0.2.2}/tests/utils/test_lazy_init.py +0 -0
  256. {krons-0.2.1 → krons-0.2.2}/tests/utils/test_sql_validation.py +0 -0
  257. {krons-0.2.1 → krons-0.2.2}/tests/utils/test_string_similarity.py +0 -0
  258. {krons-0.2.1 → krons-0.2.2}/tests/utils/test_to_dict.py +0 -0
  259. {krons-0.2.1 → krons-0.2.2}/tests/utils/test_to_list.py +0 -0
  260. {krons-0.2.1 → krons-0.2.2}/tests/utils/test_to_num.py +0 -0
  261. {krons-0.2.1 → krons-0.2.2}/tests/utils/test_utils.py +0 -0
  262. {krons-0.2.1 → krons-0.2.2}/tests/work/__init__.py +0 -0
  263. {krons-0.2.1 → krons-0.2.2}/tests/work/test_engine.py +0 -0
  264. {krons-0.2.1 → krons-0.2.2}/tests/work/test_form.py +0 -0
  265. {krons-0.2.1 → krons-0.2.2}/tests/work/test_report.py +0 -0
  266. {krons-0.2.1 → krons-0.2.2}/tests/work/test_worker.py +0 -0
  267. {krons-0.2.1 → krons-0.2.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: krons
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Spec-based composable framework for building type-safe systems
5
5
  Project-URL: Homepage, https://github.com/khive-ai/krons
6
6
  Project-URL: Repository, https://github.com/khive-ai/krons
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "krons"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Spec-based composable framework for building type-safe systems"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -21,6 +21,7 @@ _LAZY_IMPORTS: dict[str, tuple[str, str]] = {
21
21
  "PERSISTABLE_NODE_REGISTRY": ("krons.core.base", "PERSISTABLE_NODE_REGISTRY"),
22
22
  # Classes
23
23
  "Broadcaster": ("krons.core.base", "Broadcaster"),
24
+ "DataLoggerConfig": ("krons.core.base", "DataLoggerConfig"),
24
25
  "Edge": ("krons.core.base", "Edge"),
25
26
  "EdgeCondition": ("krons.core.base", "EdgeCondition"),
26
27
  "Element": ("krons.core.base", "Element"),
@@ -75,6 +76,7 @@ if TYPE_CHECKING:
75
76
  NODE_REGISTRY,
76
77
  PERSISTABLE_NODE_REGISTRY,
77
78
  Broadcaster,
79
+ DataLoggerConfig,
78
80
  Edge,
79
81
  EdgeCondition,
80
82
  Element,
@@ -103,6 +105,7 @@ __all__ = [
103
105
  "PERSISTABLE_NODE_REGISTRY",
104
106
  # classes
105
107
  "Broadcaster",
108
+ "DataLoggerConfig",
106
109
  "Edge",
107
110
  "EdgeCondition",
108
111
  "Element",
@@ -26,6 +26,8 @@ _LAZY_IMPORTS: dict[str, tuple[str, str]] = {
26
26
  "Edge": ("krons.core.base.graph", "Edge"),
27
27
  "EdgeCondition": ("krons.core.base.graph", "EdgeCondition"),
28
28
  "Graph": ("krons.core.base.graph", "Graph"),
29
+ # log
30
+ "DataLoggerConfig": ("krons.core.base.log", "DataLoggerConfig"),
29
31
  # node
30
32
  "NODE_REGISTRY": ("krons.core.base.node", "NODE_REGISTRY"),
31
33
  "PERSISTABLE_NODE_REGISTRY": ("krons.core.base.node", "PERSISTABLE_NODE_REGISTRY"),
@@ -77,6 +79,7 @@ if TYPE_CHECKING:
77
79
  from .eventbus import EventBus, Handler
78
80
  from .flow import Flow
79
81
  from .graph import Edge, EdgeCondition, Graph
82
+ from .log import DataLoggerConfig
80
83
  from .node import (
81
84
  NODE_REGISTRY,
82
85
  PERSISTABLE_NODE_REGISTRY,
@@ -97,6 +100,7 @@ __all__ = [
97
100
  "PERSISTABLE_NODE_REGISTRY",
98
101
  # classes
99
102
  "Broadcaster",
103
+ "DataLoggerConfig",
100
104
  "Edge",
101
105
  "EdgeCondition",
102
106
  "Element",
@@ -0,0 +1,32 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Logging configuration for Session message persistence.
5
+
6
+ Provides DataLoggerConfig for configuring automatic message dumps.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+ from typing import Literal
13
+
14
+ from pydantic import Field
15
+
16
+ from krons.core.types import HashableModel
17
+
18
+ __all__ = ("DataLoggerConfig",)
19
+
20
+
21
+ class DataLoggerConfig(HashableModel):
22
+ """Configuration for Session message persistence.
23
+
24
+ Attributes:
25
+ persist_dir: Directory for dump files.
26
+ extension: Output format (.json array or .jsonl newline-delimited).
27
+ auto_save_on_exit: Register atexit handler on Session creation.
28
+ """
29
+
30
+ persist_dir: str | Path = Field(default="./logs")
31
+ extension: Literal[".json", ".jsonl"] = Field(default=".jsonl")
32
+ auto_save_on_exit: bool = Field(default=True)
@@ -9,12 +9,16 @@ Branch is a named message progression with capability/resource access control.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import atexit
12
13
  import contextlib
13
14
  from collections.abc import AsyncGenerator, Iterable
15
+ from pathlib import Path
14
16
  from typing import Any, Literal
17
+
18
+ from krons.core.base.log import DataLoggerConfig
15
19
  from uuid import UUID
16
20
 
17
- from pydantic import Field, model_validator
21
+ from pydantic import Field, PrivateAttr, model_validator
18
22
 
19
23
  from krons.core import Element, Flow, Pile, Progression
20
24
  from krons.core.types import HashableModel, Unset, UnsetType, not_sentinel
@@ -59,6 +63,10 @@ class SessionConfig(HashableModel):
59
63
  default_gen_model: str | None = None
60
64
  default_parse_model: str | None = None
61
65
  auto_create_default_branch: bool = True
66
+ log_config: DataLoggerConfig | None = Field(
67
+ default=None,
68
+ description="DataLoggerConfig for auto-logging (None = disabled)",
69
+ )
62
70
 
63
71
 
64
72
  class Session(Element):
@@ -71,6 +79,9 @@ class Session(Element):
71
79
  config: SessionConfig = Field(default_factory=SessionConfig)
72
80
  default_branch_id: UUID | None = None
73
81
 
82
+ _registered_atexit: bool = PrivateAttr(default=False)
83
+ _dump_count: int = PrivateAttr(default=0)
84
+
74
85
  @model_validator(mode="after")
75
86
  def _validate_default_branch(self) -> Session:
76
87
  """Auto-create default branch and register built-in operations."""
@@ -83,6 +94,15 @@ class Session(Element):
83
94
  )
84
95
  self.set_default_branch(default_branch_name)
85
96
 
97
+ # Register atexit handler if configured
98
+ if (
99
+ self.config.log_config is not None
100
+ and self.config.log_config.auto_save_on_exit
101
+ and not self._registered_atexit
102
+ ):
103
+ atexit.register(self._save_at_exit)
104
+ self._registered_atexit = True
105
+
86
106
  # Register built-in operations (lazy import avoids circular)
87
107
  from krons.agent.operations import (
88
108
  generate,
@@ -386,6 +406,99 @@ class Session(Element):
386
406
  async for result in handler(params, ctx):
387
407
  yield result
388
408
 
409
+ def dump_messages(self, clear: bool = False) -> Path | None:
410
+ """Sync dump all session messages to file.
411
+
412
+ Args:
413
+ clear: Clear messages after dump (default False).
414
+
415
+ Returns:
416
+ Path to written file, or None if no log_config or no messages.
417
+ """
418
+ from krons.utils import create_path, json_dumpb, json_lines_iter
419
+ from krons.utils.concurrency import run_async
420
+
421
+ if self.config.log_config is None or len(self.messages) == 0:
422
+ return None
423
+
424
+ cfg = self.config.log_config
425
+ self._dump_count += 1
426
+
427
+ filepath = run_async(
428
+ create_path(
429
+ directory=cfg.persist_dir,
430
+ filename=f"session_{str(self.id)[:8]}_{self._dump_count}",
431
+ extension=cfg.extension,
432
+ timestamp=True,
433
+ file_exist_ok=True,
434
+ )
435
+ )
436
+
437
+ items = [msg.to_dict(mode="json") for msg in self.messages]
438
+
439
+ std_path = Path(filepath)
440
+ if cfg.extension == ".jsonl":
441
+ with std_path.open("wb") as f:
442
+ for chunk in json_lines_iter(items, safe_fallback=True):
443
+ f.write(chunk)
444
+ else:
445
+ data = json_dumpb(items, safe_fallback=True)
446
+ std_path.write_bytes(data)
447
+
448
+ if clear:
449
+ self.communications.items.clear()
450
+
451
+ return std_path
452
+
453
+ async def adump_messages(self, clear: bool = False) -> Path | None:
454
+ """Async dump all session messages to file with lock protection.
455
+
456
+ Args:
457
+ clear: Clear messages after dump (default False).
458
+
459
+ Returns:
460
+ Path to written file, or None if no log_config or no messages.
461
+ """
462
+ from krons.utils import create_path, json_dumpb, json_lines_iter
463
+
464
+ if self.config.log_config is None or len(self.messages) == 0:
465
+ return None
466
+
467
+ cfg = self.config.log_config
468
+
469
+ async with self.messages:
470
+ self._dump_count += 1
471
+
472
+ filepath = await create_path(
473
+ directory=cfg.persist_dir,
474
+ filename=f"session_{str(self.id)[:8]}_{self._dump_count}",
475
+ extension=cfg.extension,
476
+ timestamp=True,
477
+ file_exist_ok=True,
478
+ )
479
+
480
+ items = [msg.to_dict(mode="json") for msg in self.messages]
481
+
482
+ if cfg.extension == ".jsonl":
483
+ content = b"".join(json_lines_iter(items, safe_fallback=True))
484
+ await filepath.write_bytes(content)
485
+ else:
486
+ data = json_dumpb(items, safe_fallback=True)
487
+ await filepath.write_bytes(data)
488
+
489
+ if clear:
490
+ self.communications.items.clear()
491
+
492
+ return Path(filepath)
493
+
494
+ def _save_at_exit(self) -> None:
495
+ """atexit callback. Dumps messages synchronously. Errors are suppressed."""
496
+ if len(self.messages) > 0:
497
+ try:
498
+ self.dump_messages(clear=False)
499
+ except Exception:
500
+ pass # Silent failure during interpreter shutdown
501
+
389
502
  def _resolve_branch(self, branch: Branch | UUID | str | None) -> Branch:
390
503
  """Resolve to Branch, falling back to default. Raises if neither available."""
391
504
  if branch is not None:
@@ -0,0 +1,44 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Tests for krons.core.base.log - DataLoggerConfig."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ from krons.core.base.log import DataLoggerConfig
13
+
14
+
15
+ class TestDataLoggerConfig:
16
+ """Tests for DataLoggerConfig."""
17
+
18
+ def test_defaults(self):
19
+ """Config should have sensible defaults."""
20
+ config = DataLoggerConfig()
21
+ assert config.persist_dir == "./logs"
22
+ assert config.extension == ".jsonl"
23
+ assert config.auto_save_on_exit is True
24
+
25
+ def test_custom_values(self):
26
+ """Config should accept custom values."""
27
+ config = DataLoggerConfig(
28
+ persist_dir="/tmp/custom_logs",
29
+ extension=".json",
30
+ auto_save_on_exit=False,
31
+ )
32
+ assert config.persist_dir == "/tmp/custom_logs"
33
+ assert config.extension == ".json"
34
+ assert config.auto_save_on_exit is False
35
+
36
+ def test_extension_validation(self):
37
+ """Config should only accept .json or .jsonl."""
38
+ with pytest.raises(Exception):
39
+ DataLoggerConfig(extension=".csv")
40
+
41
+ def test_persist_dir_as_path(self):
42
+ """Config should accept Path object for persist_dir."""
43
+ config = DataLoggerConfig(persist_dir=Path("/tmp/logs"))
44
+ assert config.persist_dir == Path("/tmp/logs")
@@ -0,0 +1,337 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Tests for Session message persistence."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+
10
+ import pytest
11
+
12
+ from krons.core.base.log import DataLoggerConfig
13
+ from krons.session import Session, SessionConfig
14
+ from krons.session.message import Message
15
+
16
+
17
+ class TestSessionMessageDump:
18
+ """Tests for Session.dump_messages and adump_messages."""
19
+
20
+ def test_dump_messages_no_config(self):
21
+ """dump_messages should return None when log_config is None."""
22
+ session = Session()
23
+ assert session.dump_messages() is None
24
+
25
+ def test_dump_messages_no_messages(self, tmp_path):
26
+ """dump_messages should return None when no messages."""
27
+ config = SessionConfig(
28
+ log_config=DataLoggerConfig(
29
+ persist_dir=str(tmp_path),
30
+ auto_save_on_exit=False,
31
+ )
32
+ )
33
+ session = Session(config=config)
34
+ # Clear default branch messages if any
35
+ session.communications.items.clear()
36
+ assert session.dump_messages() is None
37
+
38
+ def test_dump_messages_jsonl(self, tmp_path):
39
+ """dump_messages should write JSONL file."""
40
+ config = SessionConfig(
41
+ log_config=DataLoggerConfig(
42
+ persist_dir=str(tmp_path),
43
+ extension=".jsonl",
44
+ auto_save_on_exit=False,
45
+ )
46
+ )
47
+ session = Session(config=config)
48
+
49
+ msg1 = Message(content={"role": "user", "text": "Hello"})
50
+ msg2 = Message(content={"role": "assistant", "text": "Hi there"})
51
+ session.add_message(msg1)
52
+ session.add_message(msg2)
53
+
54
+ filepath = session.dump_messages()
55
+
56
+ assert filepath is not None
57
+ assert filepath.exists()
58
+ assert filepath.suffix == ".jsonl"
59
+
60
+ lines = filepath.read_text().strip().split("\n")
61
+ assert len(lines) == 2
62
+
63
+ first = json.loads(lines[0])
64
+ assert first["content"]["role"] == "user"
65
+
66
+ def test_dump_messages_json(self, tmp_path):
67
+ """dump_messages should write JSON array file."""
68
+ config = SessionConfig(
69
+ log_config=DataLoggerConfig(
70
+ persist_dir=str(tmp_path),
71
+ extension=".json",
72
+ auto_save_on_exit=False,
73
+ )
74
+ )
75
+ session = Session(config=config)
76
+
77
+ msg = Message(content={"test": "data"})
78
+ session.add_message(msg)
79
+
80
+ filepath = session.dump_messages()
81
+
82
+ assert filepath is not None
83
+ assert filepath.suffix == ".json"
84
+
85
+ data = json.loads(filepath.read_bytes())
86
+ assert isinstance(data, list)
87
+ assert len(data) == 1
88
+ assert data[0]["content"]["test"] == "data"
89
+
90
+ def test_dump_messages_no_clear_by_default(self, tmp_path):
91
+ """dump_messages should NOT clear messages by default."""
92
+ config = SessionConfig(
93
+ log_config=DataLoggerConfig(
94
+ persist_dir=str(tmp_path),
95
+ auto_save_on_exit=False,
96
+ )
97
+ )
98
+ session = Session(config=config)
99
+
100
+ msg = Message(content={"keep": "me"})
101
+ session.add_message(msg)
102
+
103
+ session.dump_messages()
104
+ assert len(session.messages) == 1
105
+
106
+ def test_dump_messages_with_clear(self, tmp_path):
107
+ """dump_messages(clear=True) should clear messages."""
108
+ config = SessionConfig(
109
+ log_config=DataLoggerConfig(
110
+ persist_dir=str(tmp_path),
111
+ auto_save_on_exit=False,
112
+ )
113
+ )
114
+ session = Session(config=config)
115
+
116
+ msg = Message(content={"delete": "me"})
117
+ session.add_message(msg)
118
+
119
+ session.dump_messages(clear=True)
120
+ assert len(session.messages) == 0
121
+
122
+ def test_dump_creates_directory(self, tmp_path):
123
+ """dump_messages should create persist_dir if needed."""
124
+ nested = tmp_path / "sub" / "dir"
125
+ config = SessionConfig(
126
+ log_config=DataLoggerConfig(
127
+ persist_dir=str(nested),
128
+ auto_save_on_exit=False,
129
+ )
130
+ )
131
+ session = Session(config=config)
132
+
133
+ msg = Message(content={"test": 1})
134
+ session.add_message(msg)
135
+
136
+ filepath = session.dump_messages()
137
+ assert filepath is not None
138
+ assert nested.exists()
139
+
140
+ def test_atexit_registration(self):
141
+ """Session should register atexit when configured."""
142
+ config = SessionConfig(
143
+ log_config=DataLoggerConfig(auto_save_on_exit=True)
144
+ )
145
+ session = Session(config=config)
146
+ assert session._registered_atexit is True
147
+
148
+ def test_no_atexit_when_disabled(self):
149
+ """Session should not register atexit when disabled."""
150
+ config = SessionConfig(
151
+ log_config=DataLoggerConfig(auto_save_on_exit=False)
152
+ )
153
+ session = Session(config=config)
154
+ assert session._registered_atexit is False
155
+
156
+ def test_no_atexit_when_no_config(self):
157
+ """Session should not register atexit when no log_config."""
158
+ session = Session()
159
+ assert session._registered_atexit is False
160
+
161
+ def test_multiple_dumps(self, tmp_path):
162
+ """Multiple dumps should create separate files."""
163
+ config = SessionConfig(
164
+ log_config=DataLoggerConfig(
165
+ persist_dir=str(tmp_path),
166
+ auto_save_on_exit=False,
167
+ )
168
+ )
169
+ session = Session(config=config)
170
+
171
+ msg1 = Message(content={"batch": 1})
172
+ session.add_message(msg1)
173
+ p1 = session.dump_messages()
174
+
175
+ msg2 = Message(content={"batch": 2})
176
+ session.add_message(msg2)
177
+ p2 = session.dump_messages()
178
+
179
+ assert p1 != p2
180
+ assert p1.exists()
181
+ assert p2.exists()
182
+
183
+ def test_save_at_exit_with_messages(self, tmp_path):
184
+ """_save_at_exit should dump messages when present."""
185
+ config = SessionConfig(
186
+ log_config=DataLoggerConfig(
187
+ persist_dir=str(tmp_path),
188
+ auto_save_on_exit=False,
189
+ )
190
+ )
191
+ session = Session(config=config)
192
+
193
+ msg = Message(content={"atexit": "test"})
194
+ session.add_message(msg)
195
+
196
+ # Call directly to test the method
197
+ session._save_at_exit()
198
+
199
+ files = list(tmp_path.glob("*.jsonl"))
200
+ assert len(files) == 1
201
+
202
+ def test_save_at_exit_no_messages(self, tmp_path):
203
+ """_save_at_exit should not create file when no messages."""
204
+ config = SessionConfig(
205
+ log_config=DataLoggerConfig(
206
+ persist_dir=str(tmp_path),
207
+ auto_save_on_exit=False,
208
+ )
209
+ )
210
+ session = Session(config=config)
211
+ session.communications.items.clear()
212
+
213
+ session._save_at_exit()
214
+
215
+ files = list(tmp_path.glob("*"))
216
+ assert len(files) == 0
217
+
218
+ def test_save_at_exit_suppresses_errors(self, tmp_path):
219
+ """_save_at_exit should suppress exceptions silently."""
220
+ config = SessionConfig(
221
+ log_config=DataLoggerConfig(
222
+ persist_dir="/nonexistent/readonly/path",
223
+ auto_save_on_exit=False,
224
+ )
225
+ )
226
+ session = Session(config=config)
227
+
228
+ msg = Message(content={"test": 1})
229
+ session.add_message(msg)
230
+
231
+ # Should not raise even with invalid path
232
+ session._save_at_exit() # No exception = pass
233
+
234
+
235
+ class TestSessionMessageDumpAsync:
236
+ """Tests for Session.adump_messages."""
237
+
238
+ @pytest.mark.anyio
239
+ async def test_adump_messages_jsonl(self, tmp_path):
240
+ """adump_messages should write JSONL file."""
241
+ config = SessionConfig(
242
+ log_config=DataLoggerConfig(
243
+ persist_dir=str(tmp_path),
244
+ extension=".jsonl",
245
+ auto_save_on_exit=False,
246
+ )
247
+ )
248
+ session = Session(config=config)
249
+
250
+ msg = Message(content={"async": True})
251
+ session.add_message(msg)
252
+
253
+ filepath = await session.adump_messages()
254
+
255
+ assert filepath is not None
256
+ assert filepath.exists()
257
+
258
+ lines = filepath.read_text().strip().split("\n")
259
+ first = json.loads(lines[0])
260
+ assert first["content"]["async"] is True
261
+
262
+ @pytest.mark.anyio
263
+ async def test_adump_messages_json(self, tmp_path):
264
+ """adump_messages should write JSON array file."""
265
+ config = SessionConfig(
266
+ log_config=DataLoggerConfig(
267
+ persist_dir=str(tmp_path),
268
+ extension=".json",
269
+ auto_save_on_exit=False,
270
+ )
271
+ )
272
+ session = Session(config=config)
273
+
274
+ msg = Message(content={"format": "json"})
275
+ session.add_message(msg)
276
+
277
+ filepath = await session.adump_messages()
278
+
279
+ assert filepath is not None
280
+ data = json.loads(filepath.read_bytes())
281
+ assert data[0]["content"]["format"] == "json"
282
+
283
+ @pytest.mark.anyio
284
+ async def test_adump_no_config(self):
285
+ """adump_messages should return None when no log_config."""
286
+ session = Session()
287
+ assert await session.adump_messages() is None
288
+
289
+ @pytest.mark.anyio
290
+ async def test_adump_no_messages(self, tmp_path):
291
+ """adump_messages should return None when no messages."""
292
+ config = SessionConfig(
293
+ log_config=DataLoggerConfig(
294
+ persist_dir=str(tmp_path),
295
+ auto_save_on_exit=False,
296
+ )
297
+ )
298
+ session = Session(config=config)
299
+ session.communications.items.clear()
300
+
301
+ assert await session.adump_messages() is None
302
+
303
+ @pytest.mark.anyio
304
+ async def test_adump_with_clear(self, tmp_path):
305
+ """adump_messages(clear=True) should clear messages."""
306
+ config = SessionConfig(
307
+ log_config=DataLoggerConfig(
308
+ persist_dir=str(tmp_path),
309
+ auto_save_on_exit=False,
310
+ )
311
+ )
312
+ session = Session(config=config)
313
+
314
+ msg = Message(content={"clear": "me"})
315
+ session.add_message(msg)
316
+
317
+ await session.adump_messages(clear=True)
318
+ assert len(session.messages) == 0
319
+
320
+ @pytest.mark.anyio
321
+ async def test_adump_creates_directory(self, tmp_path):
322
+ """adump_messages should create persist_dir if needed."""
323
+ nested = tmp_path / "async" / "logs"
324
+ config = SessionConfig(
325
+ log_config=DataLoggerConfig(
326
+ persist_dir=str(nested),
327
+ auto_save_on_exit=False,
328
+ )
329
+ )
330
+ session = Session(config=config)
331
+
332
+ msg = Message(content={"test": 1})
333
+ session.add_message(msg)
334
+
335
+ filepath = await session.adump_messages()
336
+ assert filepath is not None
337
+ assert nested.exists()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes