reactifact 0.9.0__tar.gz → 0.9.1__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 (214) hide show
  1. {reactifact-0.9.0/reactifact.egg-info → reactifact-0.9.1}/PKG-INFO +1 -1
  2. {reactifact-0.9.0 → reactifact-0.9.1}/pyproject.toml +1 -1
  3. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/__init__.py +1 -1
  4. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/tracer.py +152 -78
  5. {reactifact-0.9.0 → reactifact-0.9.1/reactifact.egg-info}/PKG-INFO +1 -1
  6. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_tracing.py +77 -1
  7. {reactifact-0.9.0 → reactifact-0.9.1}/LICENSE +0 -0
  8. {reactifact-0.9.0 → reactifact-0.9.1}/README.md +0 -0
  9. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/__main__.py +0 -0
  10. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/_extras.py +0 -0
  11. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/agent_tool.py +0 -0
  12. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/agents.py +0 -0
  13. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/artifacts.py +0 -0
  14. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/branching.py +0 -0
  15. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/budget.py +0 -0
  16. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/chat.py +0 -0
  17. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/checkpoints.py +0 -0
  18. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/cli/__init__.py +0 -0
  19. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/cli/branch.py +0 -0
  20. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/cli/common.py +0 -0
  21. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/cli/context.py +0 -0
  22. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/cli/graph.py +0 -0
  23. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/cli/replay.py +0 -0
  24. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/cli/scenario.py +0 -0
  25. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/cli/trace.py +0 -0
  26. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/commit.py +0 -0
  27. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/commit_log.py +0 -0
  28. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/consume.py +0 -0
  29. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/context.py +0 -0
  30. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/context_builder.py +0 -0
  31. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/effects.py +0 -0
  32. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/eval.py +0 -0
  33. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/events.py +0 -0
  34. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/interrupt.py +0 -0
  35. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/llm_agent.py +0 -0
  36. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/mcp/__init__.py +0 -0
  37. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/mcp/client.py +0 -0
  38. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/mcp/oauth.py +0 -0
  39. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/mcp/server.py +0 -0
  40. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/native_tool_use.py +0 -0
  41. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/operations.py +0 -0
  42. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/patches.py +0 -0
  43. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/produce.py +0 -0
  44. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/prompts.py +0 -0
  45. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/__init__.py +0 -0
  46. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/_retry.py +0 -0
  47. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/anthropic.py +0 -0
  48. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/azure.py +0 -0
  49. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/cerebras.py +0 -0
  50. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/chat.py +0 -0
  51. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/contracts.py +0 -0
  52. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/deepseek.py +0 -0
  53. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/fake.py +0 -0
  54. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/fireworks.py +0 -0
  55. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/gemini.py +0 -0
  56. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/github_models.py +0 -0
  57. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/groq.py +0 -0
  58. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/image.py +0 -0
  59. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/mistral.py +0 -0
  60. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/nvidia.py +0 -0
  61. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/ollama.py +0 -0
  62. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/openai.py +0 -0
  63. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/openrouter.py +0 -0
  64. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/perplexity.py +0 -0
  65. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/qwen.py +0 -0
  66. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/speech.py +0 -0
  67. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/together.py +0 -0
  68. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/video.py +0 -0
  69. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/xai.py +0 -0
  70. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/providers/zai.py +0 -0
  71. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/py.typed +0 -0
  72. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/__init__.py +0 -0
  73. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/cleanup.py +0 -0
  74. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/identity.py +0 -0
  75. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/inputs.py +0 -0
  76. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/memory.py +0 -0
  77. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/plan_execute.py +0 -0
  78. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/reflection.py +0 -0
  79. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/resolve.py +0 -0
  80. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/rollback.py +0 -0
  81. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/search.py +0 -0
  82. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/skills.py +0 -0
  83. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/status.py +0 -0
  84. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/supervisor.py +0 -0
  85. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/recipes/text.py +0 -0
  86. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/relations.py +0 -0
  87. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/replay.py +0 -0
  88. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/resources.py +0 -0
  89. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/runtime.py +0 -0
  90. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/scheduler.py +0 -0
  91. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/session.py +0 -0
  92. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/sources.py +0 -0
  93. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/streaming.py +0 -0
  94. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/structured.py +0 -0
  95. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/testing/__init__.py +0 -0
  96. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/testing/assertions.py +0 -0
  97. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/testing/exceptions.py +0 -0
  98. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/testing/fault.py +0 -0
  99. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/testing/lab.py +0 -0
  100. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/testing/mock.py +0 -0
  101. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/testing/record.py +0 -0
  102. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/testing/registry.py +0 -0
  103. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tool_use.py +0 -0
  104. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tools.py +0 -0
  105. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/__init__.py +0 -0
  106. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/_otlp.py +0 -0
  107. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/langfuse.py +0 -0
  108. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/models.py +0 -0
  109. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/otlp.py +0 -0
  110. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/postgres.py +0 -0
  111. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/store.py +0 -0
  112. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/templates/ui.html +0 -0
  113. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/templates/ui_run.html +0 -0
  114. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/tracing/web.py +0 -0
  115. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/triggers.py +0 -0
  116. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/verify.py +0 -0
  117. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/viz.py +0 -0
  118. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact/web.py +0 -0
  119. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact.egg-info/SOURCES.txt +0 -0
  120. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact.egg-info/dependency_links.txt +0 -0
  121. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact.egg-info/entry_points.txt +0 -0
  122. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact.egg-info/requires.txt +0 -0
  123. {reactifact-0.9.0 → reactifact-0.9.1}/reactifact.egg-info/top_level.txt +0 -0
  124. {reactifact-0.9.0 → reactifact-0.9.1}/setup.cfg +0 -0
  125. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_absent_consume.py +0 -0
  126. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_adaptive.py +0 -0
  127. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_agent_tool.py +0 -0
  128. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_anthropic_provider.py +0 -0
  129. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_artifacts.py +0 -0
  130. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_backbone.py +0 -0
  131. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_branching.py +0 -0
  132. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_budget.py +0 -0
  133. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_canonical_ports.py +0 -0
  134. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_chat_web.py +0 -0
  135. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_checkpoint.py +0 -0
  136. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_checkpoints_concurrency.py +0 -0
  137. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_cli.py +0 -0
  138. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_cli_scenario.py +0 -0
  139. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_commit_log.py +0 -0
  140. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_concurrency.py +0 -0
  141. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_consumes_produces.py +0 -0
  142. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_context_builder.py +0 -0
  143. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_correlated_consume.py +0 -0
  144. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_debounce.py +0 -0
  145. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_devops.py +0 -0
  146. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_devops_web.py +0 -0
  147. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_effects.py +0 -0
  148. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_eval.py +0 -0
  149. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_forklab.py +0 -0
  150. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_forklab_web.py +0 -0
  151. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_friendly_api.py +0 -0
  152. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_gemini_provider.py +0 -0
  153. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_hitl.py +0 -0
  154. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_image_provider.py +0 -0
  155. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_incident_commander.py +0 -0
  156. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_invalidation.py +0 -0
  157. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_join_consume.py +0 -0
  158. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_knowledge.py +0 -0
  159. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_knowledge_web.py +0 -0
  160. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_llm_ladder.py +0 -0
  161. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_mcp.py +0 -0
  162. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_medic_lab.py +0 -0
  163. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_medic_lab_web.py +0 -0
  164. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_multisource.py +0 -0
  165. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_native_tool_use.py +0 -0
  166. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_openai_provider.py +0 -0
  167. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_patches.py +0 -0
  168. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_ports.py +0 -0
  169. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_produce_styles.py +0 -0
  170. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_produce_trigger.py +0 -0
  171. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_prompts.py +0 -0
  172. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_provider_auth.py +0 -0
  173. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_provider_retry.py +0 -0
  174. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_providers_integration.py +0 -0
  175. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_reacts_to.py +0 -0
  176. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_recipe_plan_execute.py +0 -0
  177. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_recipe_reflection.py +0 -0
  178. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_recipe_supervisor.py +0 -0
  179. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_recipes.py +0 -0
  180. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_recipes_inputs.py +0 -0
  181. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_recipes_memory.py +0 -0
  182. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_recipes_skills.py +0 -0
  183. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_relation_graph.py +0 -0
  184. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_relations.py +0 -0
  185. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_repair.py +0 -0
  186. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_repair_web.py +0 -0
  187. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_replay.py +0 -0
  188. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_research.py +0 -0
  189. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_resources.py +0 -0
  190. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_retry.py +0 -0
  191. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_runtime.py +0 -0
  192. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_runtime_errors.py +0 -0
  193. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_sessions.py +0 -0
  194. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_sources.py +0 -0
  195. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_sources_search.py +0 -0
  196. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_sources_vector.py +0 -0
  197. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_speech_provider.py +0 -0
  198. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_streaming.py +0 -0
  199. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_structured.py +0 -0
  200. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_testing_assertions.py +0 -0
  201. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_testing_lab.py +0 -0
  202. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_testing_mock.py +0 -0
  203. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_testing_registry.py +0 -0
  204. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_tools.py +0 -0
  205. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_vendor_factories.py +0 -0
  206. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_vendor_multimodal_factories.py +0 -0
  207. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_verify.py +0 -0
  208. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_video_provider.py +0 -0
  209. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_view.py +0 -0
  210. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_viz.py +0 -0
  211. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_web_source.py +0 -0
  212. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_workspace.py +0 -0
  213. {reactifact-0.9.0 → reactifact-0.9.1}/tests/test_workspace_with_sources.py +0 -0
  214. {reactifact-0.9.0 → reactifact-0.9.1}/tests/tests_checkpoints_sqlite.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactifact
3
- Version: 0.9.0
3
+ Version: 0.9.1
4
4
  Summary: Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/bzdvdn/reactifact
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "reactifact"
7
- version = "0.9.0"
7
+ version = "0.9.1"
8
8
  description = "Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -48,7 +48,7 @@ from .session import Session, SessionStore
48
48
  from .tools import FunctionTool, Tool, ToolOutput, tool
49
49
  from .triggers import Trigger
50
50
 
51
- __version__ = "0.9.0"
51
+ __version__ = "0.9.1"
52
52
 
53
53
  __all__ = [
54
54
  "Agent",
@@ -16,6 +16,7 @@ from __future__ import annotations
16
16
 
17
17
  import asyncio
18
18
  import json
19
+ import logging
19
20
  import time
20
21
  import uuid
21
22
  from collections.abc import AsyncIterator, Callable, Iterable
@@ -34,24 +35,52 @@ if TYPE_CHECKING:
34
35
  from ..events import Event
35
36
  from ..patches import Patch
36
37
 
38
+ logger = logging.getLogger(__name__)
39
+
37
40
  #: Up to what size to truncate artifact/response data in a trace.
38
41
  TRACE_TRUNCATE = 1500
39
42
 
40
43
 
44
+ def _swallow_tracing_error(where: str, exc: BaseException) -> None:
45
+ """Reports a tracing failure and lets the run continue (§54).
46
+
47
+ Observability is best-effort by contract: a sink that is unreachable
48
+ (Langfuse down, Postgres refusing connections), a custom `Tracer`
49
+ callback that raises, or a malformed payload must never abort the
50
+ business run it was only supposed to observe. Every tracer call site
51
+ funnels its exceptions here and carries on; the trace for that step is
52
+ simply dropped. `exc_info` keeps the failure debuggable in logs.
53
+ """
54
+ logger.warning(
55
+ "tracing step %r failed; run continues without it: %s", where, exc, exc_info=exc
56
+ )
57
+
58
+
41
59
  def _clip(value: str, limit: int = TRACE_TRUNCATE) -> str:
42
60
  if len(value) <= limit:
43
61
  return value
44
62
  return value[:limit] + "…"
45
63
 
46
64
 
47
- def _clip_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
48
- return [
49
- {
50
- "role": msg.get("role", ""),
51
- "content": _clip(str(msg.get("content") or "")),
52
- }
53
- for msg in messages
54
- ]
65
+ def _message_fields(message: Any) -> tuple[str, Any]:
66
+ """Reads role/content off a `Message` dataclass, dict, or pydantic-like object.
67
+
68
+ `LLMRequest.messages` is typed `list[Message]` (a plain dataclass without
69
+ `model_dump`), but providers/hand-rolled callers can also hand over dicts or
70
+ pydantic models — only these two fields are ever traced, so attribute access
71
+ covers all of them without assuming a serialization method.
72
+ """
73
+ if isinstance(message, dict):
74
+ return str(message.get("role", "")), message.get("content")
75
+ return str(getattr(message, "role", "")), getattr(message, "content", None)
76
+
77
+
78
+ def _clip_messages(messages: Iterable[Any]) -> list[dict[str, Any]]:
79
+ clipped: list[dict[str, Any]] = []
80
+ for message in messages:
81
+ role, content = _message_fields(message)
82
+ clipped.append({"role": role, "content": _clip(str(content or ""))})
83
+ return clipped
55
84
 
56
85
 
57
86
  class Tracer:
@@ -87,7 +116,10 @@ class Tracer:
87
116
 
88
117
  async def on_turn_end(self, trace: RunTrace) -> None:
89
118
  for sink in self.sinks:
90
- await sink.export(trace)
119
+ try:
120
+ await sink.export(trace)
121
+ except Exception as exc: # noqa: BLE001 — one bad sink must not fail the run
122
+ _swallow_tracing_error(f"{type(sink).__name__}.export", exc)
91
123
 
92
124
 
93
125
  class RecordingLLM(LLMProvider):
@@ -110,14 +142,33 @@ class RecordingLLM(LLMProvider):
110
142
  try:
111
143
  response = await self._inner.complete(request)
112
144
  except Exception as exc: # noqa: BLE001 — record and re-raise
113
- self._record(request, None, (time.monotonic() - started) * 1000, str(exc))
145
+ self._safe_record(
146
+ request, None, (time.monotonic() - started) * 1000, str(exc)
147
+ )
114
148
  raise
115
- self._record(request, response, (time.monotonic() - started) * 1000, None)
149
+ self._safe_record(request, response, (time.monotonic() - started) * 1000, None)
116
150
  return response
117
151
 
118
152
  def stream(self, request: LLMRequest) -> AsyncIterator[LLMResponseChunk]:
119
153
  return self._inner.stream(request)
120
154
 
155
+ def _safe_record(
156
+ self,
157
+ request: LLMRequest,
158
+ response: LLMResponse | None,
159
+ latency_ms: float,
160
+ error: str | None,
161
+ ) -> None:
162
+ """`_record`, isolated: losing one LLMCall must not fail the LLM call.
163
+
164
+ The error path matters most: if recording raised while handling the
165
+ provider's own exception, it would mask that original error.
166
+ """
167
+ try:
168
+ self._record(request, response, latency_ms, error)
169
+ except Exception as exc: # noqa: BLE001 — best-effort observability
170
+ _swallow_tracing_error("RecordingLLM._record", exc)
171
+
121
172
  def _record(
122
173
  self,
123
174
  request: LLMRequest,
@@ -130,12 +181,7 @@ class RecordingLLM(LLMProvider):
130
181
  agent=self._agent_of(),
131
182
  provider=self._provider,
132
183
  model=str(getattr(self._inner, "model", "") or ""),
133
- messages=_clip_messages(
134
- [
135
- m.model_dump() if hasattr(m, "model_dump") else m.__dict__
136
- for m in request.messages
137
- ]
138
- ),
184
+ messages=_clip_messages(request.messages),
139
185
  response=_clip(response.text) if response is not None else "",
140
186
  prompt_tokens=int(usage.get("prompt_tokens") or usage.get("prompt") or 0),
141
187
  completion_tokens=int(
@@ -157,15 +203,26 @@ class CompositeTracer:
157
203
  self, run_id: str, *, session_id: str, started_at: datetime
158
204
  ) -> None:
159
205
  for tracer in self.tracers:
160
- tracer.on_turn_begin(run_id, session_id=session_id, started_at=started_at)
206
+ try:
207
+ tracer.on_turn_begin(
208
+ run_id, session_id=session_id, started_at=started_at
209
+ )
210
+ except Exception as exc: # noqa: BLE001 — isolate one bad tracer
211
+ _swallow_tracing_error(f"{type(tracer).__name__}.on_turn_begin", exc)
161
212
 
162
213
  def on_span(self, span: AgentSpan) -> None:
163
214
  for tracer in self.tracers:
164
- tracer.on_span(span)
215
+ try:
216
+ tracer.on_span(span)
217
+ except Exception as exc: # noqa: BLE001 — isolate one bad tracer
218
+ _swallow_tracing_error(f"{type(tracer).__name__}.on_span", exc)
165
219
 
166
220
  async def on_turn_end(self, trace: RunTrace) -> None:
167
221
  for tracer in self.tracers:
168
- await tracer.on_turn_end(trace)
222
+ try:
223
+ await tracer.on_turn_end(trace)
224
+ except Exception as exc: # noqa: BLE001 — isolate one bad tracer
225
+ _swallow_tracing_error(f"{type(tracer).__name__}.on_turn_end", exc)
169
226
 
170
227
 
171
228
  class RunTracer:
@@ -272,44 +329,52 @@ class RunTracer:
272
329
  ]
273
330
 
274
331
  def write_refs(self, patch: Patch, writes: list[Write]) -> list[ArtifactRef]:
275
- ops_by_id: dict[str, tuple[str, Any | None]] = {}
276
- for op in patch.operations:
277
- artifact_id = getattr(op, "artifact_id", None)
278
- if artifact_id is None:
279
- continue
280
- if isinstance(op, Create):
281
- model: Any | None = op.data
282
- elif isinstance(op, Update):
283
- model = op.new_data
284
- else:
285
- model = None
286
- ops_by_id[artifact_id] = (op.to_dict().get("type", ""), model)
287
- return [
288
- self.artifact_ref(
289
- w.artifact_id,
290
- w.version,
291
- ops_by_id.get(w.artifact_id, ("", None))[0],
292
- ops_by_id.get(w.artifact_id, ("", None))[1],
293
- )
294
- for w in writes
295
- ]
332
+ try:
333
+ ops_by_id: dict[str, tuple[str, Any | None]] = {}
334
+ for op in patch.operations:
335
+ artifact_id = getattr(op, "artifact_id", None)
336
+ if artifact_id is None:
337
+ continue
338
+ if isinstance(op, Create):
339
+ model: Any | None = op.data
340
+ elif isinstance(op, Update):
341
+ model = op.new_data
342
+ else:
343
+ model = None
344
+ ops_by_id[artifact_id] = (op.to_dict().get("type", ""), model)
345
+ return [
346
+ self.artifact_ref(
347
+ w.artifact_id,
348
+ w.version,
349
+ ops_by_id.get(w.artifact_id, ("", None))[0],
350
+ ops_by_id.get(w.artifact_id, ("", None))[1],
351
+ )
352
+ for w in writes
353
+ ]
354
+ except Exception as exc: # noqa: BLE001 — best-effort observability
355
+ _swallow_tracing_error("write_refs", exc)
356
+ return []
296
357
 
297
358
  def relation_refs(self, patch: Patch) -> list[RelationRef]:
298
359
  """Provenance edges (`patch.link`) recorded for the span (§34)."""
299
- refs: list[RelationRef] = []
300
- for op in patch.operations:
301
- if not isinstance(op, Link):
302
- continue
303
- refs.append(
304
- RelationRef(
305
- source_id=op.artifact_id,
306
- relation=op.relation,
307
- target_id=op.target_id,
308
- source_type=self._type_name(op.artifact_id, self._context),
309
- target_type=self._type_name(op.target_id, self._context),
360
+ try:
361
+ refs: list[RelationRef] = []
362
+ for op in patch.operations:
363
+ if not isinstance(op, Link):
364
+ continue
365
+ refs.append(
366
+ RelationRef(
367
+ source_id=op.artifact_id,
368
+ relation=op.relation,
369
+ target_id=op.target_id,
370
+ source_type=self._type_name(op.artifact_id, self._context),
371
+ target_type=self._type_name(op.target_id, self._context),
372
+ )
310
373
  )
311
- )
312
- return refs
374
+ return refs
375
+ except Exception as exc: # noqa: BLE001 — best-effort observability
376
+ _swallow_tracing_error("relation_refs", exc)
377
+ return []
313
378
 
314
379
  # ---- per-turn lifecycle ----
315
380
 
@@ -319,9 +384,12 @@ class RunTracer:
319
384
  self.run_id = str(uuid.uuid4())
320
385
  self.spans = []
321
386
  self._trace_data_cache = {}
322
- self.tracer.on_turn_begin(
323
- self.run_id, session_id=session_id, started_at=datetime.now(UTC)
324
- )
387
+ try:
388
+ self.tracer.on_turn_begin(
389
+ self.run_id, session_id=session_id, started_at=datetime.now(UTC)
390
+ )
391
+ except Exception as exc: # noqa: BLE001 — best-effort observability
392
+ _swallow_tracing_error("on_turn_begin", exc)
325
393
 
326
394
  def record_span(
327
395
  self,
@@ -340,31 +408,37 @@ class RunTracer:
340
408
  """
341
409
  if self.tracer is None:
342
410
  return None
343
- span = AgentSpan(
344
- agent=agent.name,
345
- event_type=event.type.value,
346
- reads=self.read_refs(reads),
347
- latency_ms=latency_ms,
348
- llm_calls=self._pending_llm.pop(agent.name, []),
349
- error=f"{type(error).__name__}: {error}" if error is not None else None,
350
- started_at=datetime.now(UTC),
351
- )
352
- self.spans.append(span)
353
- self.tracer.on_span(span)
354
- return span
411
+ try:
412
+ span = AgentSpan(
413
+ agent=agent.name,
414
+ event_type=event.type.value,
415
+ reads=self.read_refs(reads),
416
+ latency_ms=latency_ms,
417
+ llm_calls=self._pending_llm.pop(agent.name, []),
418
+ error=f"{type(error).__name__}: {error}" if error is not None else None,
419
+ started_at=datetime.now(UTC),
420
+ )
421
+ self.spans.append(span)
422
+ self.tracer.on_span(span)
423
+ return span
424
+ except Exception as exc: # noqa: BLE001 — best-effort observability
425
+ _swallow_tracing_error("on_span", exc)
426
+ return None
355
427
 
356
428
  async def end_turn(
357
429
  self, *, session_id: str, duration_ms: float, outcome: str
358
430
  ) -> None:
359
431
  if self.tracer is None:
360
432
  return
361
- await self.tracer.on_turn_end(
362
- RunTrace(
363
- id=self.run_id,
364
- session_id=session_id,
365
- started_at=datetime.now(UTC),
366
- duration_ms=duration_ms,
367
- outcome=outcome,
368
- spans=self.spans,
369
- )
433
+ trace = RunTrace(
434
+ id=self.run_id,
435
+ session_id=session_id,
436
+ started_at=datetime.now(UTC),
437
+ duration_ms=duration_ms,
438
+ outcome=outcome,
439
+ spans=self.spans,
370
440
  )
441
+ try:
442
+ await self.tracer.on_turn_end(trace)
443
+ except Exception as exc: # noqa: BLE001 — best-effort observability
444
+ _swallow_tracing_error("on_turn_end", exc)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactifact
3
- Version: 0.9.0
3
+ Version: 0.9.1
4
4
  Summary: Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/bzdvdn/reactifact
@@ -3,11 +3,12 @@ import json
3
3
 
4
4
  from pydantic import BaseModel
5
5
  from reactifact import Agent, Consume, Context, Patch, Runtime, RuntimeResources
6
- from reactifact.providers import LLMProvider, LLMRequest, LLMResponse
6
+ from reactifact.providers import LLMProvider, LLMRequest, LLMResponse, Message
7
7
  from reactifact.tracing import (
8
8
  AgentSpan,
9
9
  ArtifactRef,
10
10
  LLMCall,
11
+ RecordingLLM,
11
12
  RelationRef,
12
13
  RunTrace,
13
14
  Tracer,
@@ -202,6 +203,81 @@ def test_composite_tracer_fans_out(tmp_path):
202
203
  assert run(store_b.query())["total"] == 1
203
204
 
204
205
 
206
+ class BoomSink:
207
+ """A sink that is always unreachable (e.g. a down Langfuse)."""
208
+
209
+ async def export(self, trace: RunTrace) -> None:
210
+ raise RuntimeError("langfuse is down")
211
+
212
+
213
+ class CapturingSink:
214
+ """Records the traces it received, to prove fan-out continued."""
215
+
216
+ def __init__(self) -> None:
217
+ self.traces: list[RunTrace] = []
218
+
219
+ async def export(self, trace: RunTrace) -> None:
220
+ self.traces.append(trace)
221
+
222
+
223
+ def test_failing_sink_does_not_break_the_run():
224
+ """An unreachable sink is skipped; the run and healthy sinks still proceed."""
225
+ ok = CapturingSink()
226
+ ctx = Context(resources=RuntimeResources(llm=ReplyLLM()))
227
+ runtime = Runtime(ctx, agents=[Greeter()], tracer=Tracer(sinks=[BoomSink(), ok]))
228
+ ctx.create(Question(text="привет"))
229
+
230
+ assert asyncio.run(runtime.arun()) >= 1
231
+ assert ctx.list_artifacts(Answer) # business outcome still produced
232
+ assert len(ok.traces) == 1 # the healthy sibling still got the trace
233
+
234
+
235
+ class BoomTracer(Tracer):
236
+ """A custom tracer whose every callback raises."""
237
+
238
+ def on_turn_begin(self, run_id, *, session_id, started_at) -> None:
239
+ raise RuntimeError("boom")
240
+
241
+ def on_span(self, span) -> None:
242
+ raise RuntimeError("boom")
243
+
244
+ async def on_turn_end(self, trace: RunTrace) -> None:
245
+ raise RuntimeError("boom")
246
+
247
+
248
+ def test_failing_custom_tracer_does_not_break_the_run():
249
+ ctx = Context(resources=RuntimeResources(llm=ReplyLLM()))
250
+ runtime = Runtime(ctx, agents=[Greeter()], tracer=BoomTracer())
251
+ ctx.create(Question(text="привет"))
252
+
253
+ assert asyncio.run(runtime.arun()) >= 1
254
+ assert ctx.list_artifacts(Answer)
255
+
256
+
257
+ def test_composite_tracer_isolates_a_failing_member():
258
+ ok = CapturingSink()
259
+ ctx = Context(resources=RuntimeResources(llm=ReplyLLM()))
260
+ runtime = Runtime(
261
+ ctx, agents=[Greeter()], tracer=[BoomTracer(), Tracer(sinks=[ok])]
262
+ )
263
+ ctx.create(Question(text="привет"))
264
+
265
+ assert asyncio.run(runtime.arun()) >= 1
266
+ assert len(ok.traces) == 1 # the second tracer still received the trace
267
+
268
+
269
+ def test_recording_llm_ignores_a_failing_recorder():
270
+ """A raising `on_call` must not fail the wrapped LLM call."""
271
+
272
+ def boom(call: LLMCall) -> None:
273
+ raise RuntimeError("tracer is down")
274
+
275
+ llm = RecordingLLM(ReplyLLM(), on_call=boom, agent_of=lambda: "agent")
276
+ response = asyncio.run(llm.complete(LLMRequest(messages=[Message.user("hi")])))
277
+
278
+ assert response.text == '{"text":"привет"}'
279
+
280
+
205
281
  def test_trace_store_migrates_old_schema(tmp_path):
206
282
  """Old DB without the llm_calls column — migrated without data loss."""
207
283
  import sqlite3
File without changes
File without changes